Skip to main content
The mobile app picks an engine from a factory that is handed the store engines write through, so the in-process engine records a turn to the same session the chat list reads. Everything above the seam is written against AgentEnginePort and nothing else.

Quick Start

1

RegistryDeps requires a persistence

persistence is required. It is passed only into the in-process engine’s factory.
2

enginesFor passes persistence to the in-process engine only

The remote engine deliberately does not receive it.
3

Build engines from the session

AppDeps.engines is a factory (persistence) => EngineChoice[]. The composition root builds it from the session, so an engine cannot exist without the store it writes through.
4

Run a turn

run returns an AsyncIterable, so for await gives free backpressure and one cancellation path.
5

Answer an approval

Approval is a reverse channel while the stream is live, so it is a method, not an event.

Who Persists

Two engines, two owners of the write. The remote engine deliberately does not take persistence: the server it connects to owns the write and is the only thing that can report authoritative indices for its own store.
A picker omits an engine whose prerequisites are absent rather than offering it and then failing. createInProcess is optional — when it is not supplied, only the remote engine is offered.

Where a Completed Turn Is Written

Each engine owns its own write, and reports end.userIndex from the store it wrote to. The in-process engine records the turn and reports the indices it actually wrote, which is what makes end.userIndex real:
A turn answered by the default remote-http engine does not currently mirror into the local mobile session. The remote server owns that transcript and is the only thing that can report authoritative indices for its own store. Local mirroring is tracked separately.

What a Recorded Turn Returns

session.record(prompt, answer) writes the user message and the answer together, then returns the indices into the persisted array. A null return means the save failed — the turn is on screen but not on disk.
The user message is written when the turn succeeds, not when the user pressed send. A turn that never completes must not leave a dangling user message with no reply under it.

The Port

AgentEnginePort is the entire agent-framework coupling.

The Three Engines

Three implementations pass the same conformance suite, which is what makes “swappable” a fact.

Conformance

Passing engines/src/conformance.ts is the definition of implementing the seam.
The suite also asserts the negative direction: an engine declaring approvals: false must never emit an approval_request. Every unsupported scenario is printed on each run, so a contract that quietly shrinks is visible rather than silently green.
praisonai-ts declares 5 of 11 events unsupported because upstream Agent.streamEvents() emits only text / finish / error. See src/praisonai-mobile/docs/gaps.md.

When To Pick Which Engine


Common Patterns

An engine whose prerequisites are absent is omitted, not offered and then failed.
selectEngine names the available engines when an id is unknown, so a missing engine is an honest message rather than a crash.

Best Practices

Obtaining the engine list requires the persistence argument. Do not construct engines directly — the type makes the wiring impossible to forget.
RegistryDeps.persistence has no default. The in-process engine’s end.userIndex is only real because it records through this store — without it, the turn is on screen and not on disk.
The remote server owns its own store and reports its own indices. Writing a second copy locally reintroduces divergence between screen position and disk position.
The session already knows which conversation is open. Taking direction from the run request would let an in-flight turn write into whichever chat the user has since navigated to.
When record returns null the write failed. Null travels to the UI as “do not offer Fork or Delete”, because those affordances would address a message that does not exist. Index 0 is valid, so a falsy check is a trap.
capabilities is a property, not a method, so the UI decides what to render before the first token arrives.
Declaring a gap in the unsupported map is honest; faking it hides a defect the conformance suite exists to catch.
decide and cancel return false for an unknown id — reporting success for an id the engine never issued is a lie the UI cannot detect.

Architecture

Boot order and the engines factory shape.

Overview

Native navigation and the retained chat screen.

Capabilities & Gaps

What each engine can and cannot report.

Shell & Adapters

The keyboard snapshot and the pinch-zoom guard.

Protocol

The 11 events every engine speaks.

Capabilities & Gaps

What each engine emits today.