Skip to content

refactor(runtime): move chunk interpretation into the agent adapter - #54

Open
FreshlyBrewedCode wants to merge 2 commits into
34-domain-errors-as-tagged-errorsfrom
35-move-chunk-interpretation-into-adapter
Open

FreshlyBrewedCode wants to merge 2 commits into
34-domain-errors-as-tagged-errorsfrom
35-move-chunk-interpretation-into-adapter

Conversation

@FreshlyBrewedCode

@FreshlyBrewedCode FreshlyBrewedCode commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

Part of #32 · Closes #35

runtime/agent-step.ts string-matched CUSTOM + opencode.session-id to populate
AgentStepFinished.sessionId, and string-matched structured-output.complete — which isn't a
provider capability at all, but @tanstack/ai-opencode's text adapter simulating structured
output by re-parsing its own final message (ADR 0001 §1). That left opencode's event vocabulary
leaking into the runtime, so a second adapter (claude-code, codex, a raw provider loop) would have
had to emit opencode's exact CUSTOM event names just to surface a session id or a structured
output. ADR 0012 §1/§2 draws the seam: the adapter interprets its own stream, the runtime consumes
a normalized signal.

What changed

  • AgentAdapter.stream now yields AgentAdapterYield ({ chunk, signal? }) instead of a bare
    unknown chunk. AgentSignal is a small closed union — sessionId, structuredOutput,
    runError — documented against the exact AgentStepFinished fields each one populates.
  • opencode-adapter.ts gains extractOpencodeSignal, the only place under src/runtime/ left
    that string-matches opencode.session-id and structured-output.complete; it also folds in
    RUN_ERROR detection, which agent-step.ts used to do itself.
  • agent-step.ts drops all vendor string-matching: it now just switches on yieldItem.signal._tag
    to populate partial.sessionId / structuredOutput / runError, and keeps interpreting only
    the standard AG-UI TEXT_MESSAGE_*/RUN_FINISHED types for finalText accumulation and usage.
  • replay/adapter.ts's corpus replayer and createSlowFakeAdapter both updated to the new yield
    shape; the corpus replayer reuses extractOpencodeSignal so recorded opencode traces keep
    producing the same signals without re-deriving the logic.
  • New tests: runtime/agent-step.test.ts (signal-to-field wiring, chunk passthrough, no-signal
    case), runtime/opencode-adapter.test.ts (extractOpencodeSignal unit coverage including the
    guarded/negative cases), and a corpus round-trip assertion in replay/adapter.test.ts that
    checks signal extraction end-to-end against a real recorded trace.

Notes for reviewers

  • RUN_ERROR is a standard AG-UI type, not opencode-specific, but its detection now lives inside
    extractOpencodeSignal alongside the two vendor-specific signals — every adapter owns detecting
    all three signal kinds for its own stream, per ADR 0012 §2, rather than the runtime special-casing
    one AG-UI type generically.
  • The corpus replay adapter imports extractOpencodeSignal from opencode-adapter.ts rather than
    duplicating the matching logic. That's a deliberate coupling: the recorded corpora are opencode
    chunks, so replaying them faithfully means reusing the real interpretation, not reimplementing it.
    A future non-opencode adapter is not obligated to imitate opencode chunk shapes at all — it would
    supply signals through its own stream implementation.
  • resolveOutput's two-tier structured-output extraction (event value, then finalText re-parse)
    in runtime/run.ts is untouched — this ticket only changes where the tier-1 value comes from.
  • Vendor event-name strings (opencode.session-id, structured-output.complete) still appear in
    agent-step.test.ts and opencode-adapter.test.ts as literal test fixtures for the extraction
    logic — that's expected; no interpretation logic outside opencode-adapter.ts matches on them.

Verification

  • Added unit tests cover signal extraction (extractOpencodeSignal), signal-to-field wiring in
    buildAgentStepEffect, and a real-corpus round trip confirming the recorded session-id trace
    still yields the expected signal and chunk count.
  • CI's check job (format, lint, typecheck, bun test) passed on this PR; I did not additionally
    run it locally. There's no CI coverage of a live opencode run, so AgentStepFinished.sessionId/
    .output/.error parity for a live run is established by code inspection (the moved logic in
    extractOpencodeSignal is byte-for-byte equivalent to what agent-step.ts used to do) rather
    than an end-to-end run.

Stack

  1. refactor(cli): replace hand-rolled parsers with effect/unstable/cli #52 — 33-parse-cli-with-effect-cli (issue Parse CLI arguments with effect/unstable/cli #33)
  2. refactor(errors): represent domain failures as Schema.TaggedError #53 — 34-domain-errors-as-tagged-errors (issue Represent domain failures as Schema.TaggedError #34)
  3. refactor(runtime): move chunk interpretation into the agent adapter #54 — 35-move-chunk-interpretation-into-adapter (issue Move chunk interpretation into the agent adapter #35) — this PR
  4. refactor(runtime): move headless permission setup into the agent adapter #55 — 37-move-headless-permissions (issue Move headless permission setup out of the workspace allocator #37)
  5. feat(runtime): add Effect composition root and agent runtime service #56 — 36-agent-runtime-service (issue Add an Effect composition root and make the agent runtime a service #36)
  6. refactor(daemon): move singletons into per-daemon layers #57 — 38-move-singletons-into-layers (issue Move the daemon's remaining singletons into layers #38)

Stack created with GitHub Stacks CLI • Give Feedback 💬

ADR 0012 §2: AgentAdapter now yields AgentAdapterYield items (opaque chunk
plus optional AgentSignal). The normalized signal union has three members —
sessionId, structuredOutput, runError — each documented against the
AgentStepFinished field it populates.

The opencode adapter extracts signals via extractOpencodeSignal(), the only
place in src/runtime/ that string-matches vendor event names
(opencode.session-id, structured-output.complete). The runtime consumes
signals directly via a switch on _tag and never matches vendor strings.

The corpus replay adapter reuses extractOpencodeSignal() to supply signals
from recorded opencode traces. Existing corpus files replay without
modification. The slow fake adapter wraps raw chunks in { chunk } items.

The adapter contract remains plain async (AsyncIterable), authorable
without Effect (ADR 0012 §5).
- agent-step.test.ts: signal-based adapter yields are consumed correctly
  (sessionId, structuredOutput, runError signals; onChunk receives raw
  chunk; chunks without signals pass through)
- opencode-adapter.test.ts: extractOpencodeSignal() handles all cases
  (vendor CUSTOM chunks, RUN_ERROR, non-matching chunks, malformed values)
- adapter.test.ts: corpus replay yields AgentAdapterYield items with
  signals extracted from recorded opencode chunks
@FreshlyBrewedCode
FreshlyBrewedCode force-pushed the 35-move-chunk-interpretation-into-adapter branch from 2336bab to 8bf6c49 Compare September 23, 2026 07:09

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Move chunk interpretation into the agent adapter

1 participant