Parent
#32
What to build
The AgentAdapter interprets its own chunk stream, so opencode's event vocabulary stops leaking
into the runtime. Today runtime/agent-step.ts string-matches CUSTOM + opencode.session-id to
populate AgentStepFinished.sessionId, and string-matches structured-output.complete — which is
not a provider capability at all, but @tanstack/ai-opencode's text adapter simulating
structured output by injecting JSON Schema into the prompt and re-parsing its own final message
(ADR 0001 §1).
After this ticket, the adapter yields the opaque chunk and a normalized signal drawn from a
small closed union Factory owns — session identifier, structured-output value, run error. The
runtime records those signals and never again matches a vendor event name. The union stays tiny by
rule: a member exists only when Factory has a field for it (AgentStepFinished.sessionId,
.output, .error), not as a general capability model for agents.
Chunks themselves do not change shape. ADR 0003 §2 stands: AgentChunk carries AG-UI verbatim, the
SPA keeps folding it with StreamProcessor from @tanstack/ai/client, and no stored event needs
migrating.
The observable payoff is that a second adapter — claude-code, codex, a raw provider loop — becomes
a normal amount of work instead of requiring it to emit opencode's exact CUSTOM event names. The
corpus replay adapter is the proof: it should be able to supply signals without replaying opencode
chunk shapes.
ADR 0012 was written ahead of this ticket — see docs/adr/0012-agent-runtime-seam.md §1, §2
and §5 for the boundary this implements, including why the adapter contract stays plain async
(AsyncIterable, not Stream.Stream).
Acceptance criteria
Blocked by
None (can start immediately).
Parent
#32
What to build
The
AgentAdapterinterprets its own chunk stream, so opencode's event vocabulary stops leakinginto the runtime. Today
runtime/agent-step.tsstring-matchesCUSTOM+opencode.session-idtopopulate
AgentStepFinished.sessionId, and string-matchesstructured-output.complete— which isnot a provider capability at all, but
@tanstack/ai-opencode's text adapter simulatingstructured output by injecting JSON Schema into the prompt and re-parsing its own final message
(ADR 0001 §1).
After this ticket, the adapter yields the opaque chunk and a normalized signal drawn from a
small closed union Factory owns — session identifier, structured-output value, run error. The
runtime records those signals and never again matches a vendor event name. The union stays tiny by
rule: a member exists only when Factory has a field for it (
AgentStepFinished.sessionId,.output,.error), not as a general capability model for agents.Chunks themselves do not change shape. ADR 0003 §2 stands:
AgentChunkcarries AG-UI verbatim, theSPA keeps folding it with
StreamProcessorfrom@tanstack/ai/client, and no stored event needsmigrating.
The observable payoff is that a second adapter — claude-code, codex, a raw provider loop — becomes
a normal amount of work instead of requiring it to emit opencode's exact CUSTOM event names. The
corpus replay adapter is the proof: it should be able to supply signals without replaying opencode
chunk shapes.
ADR 0012 was written ahead of this ticket — see
docs/adr/0012-agent-runtime-seam.md§1, §2and §5 for the boundary this implements, including why the adapter contract stays plain async
(
AsyncIterable, notStream.Stream).Acceptance criteria
AgentAdapteryields opaque chunks plus a normalized signal union; the union's members are documented against the event fields they populateopencode.session-id,structured-output.complete) appears anywhere undersrc/runtime/outsideopencode-adapter.tsAgentStepFinished.sessionId,.outputand.errorare populated exactly as before for a live opencode runfinalTextre-parse) still works — this ticket changes where the value comes from, not the fallbackAsyncIterable), authorable without Effectbun run checkpassesBlocked by
None (can start immediately).