Repository navigation
refactor(runtime): move chunk interpretation into the agent adapter - #54
Merged
FreshlyBrewedCode merged 4 commits intoOct 2, 2026
Conversation
FreshlyBrewedCode
added this pull request to stack #58
September 20, 2026 13:00
FreshlyBrewedCode
force-pushed
the
35-move-chunk-interpretation-into-adapter
branch
from
September 20, 2026 13:15
64e78cc to
2336bab
Compare
This was referenced Sep 22, 2026
FreshlyBrewedCode
force-pushed
the
35-move-chunk-interpretation-into-adapter
branch
from
September 23, 2026 07:09
2336bab to
8bf6c49
Compare
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
Prove the runtime consumes adapter signals rather than matching opencode chunk shapes, and rename the misleading non-string RUN_ERROR test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
FreshlyBrewedCode
force-pushed
the
35-move-chunk-interpretation-into-adapter
branch
from
October 2, 2026 09:41
8bf6c49 to
fe59cb3
Compare
FreshlyBrewedCode
added a commit
that referenced
this pull request
Oct 2, 2026
…56) * test(runtime): pin AgentStepFinished field population from signals end to end Run-level regression tests for issue #35: session/structured-output/ error signals land on AgentStepFinished.sessionId/.output/.error, and the two-tier structured-output resolution (signal value, then finalText re-parse) is unchanged. * feat(runtime): add Effect composition root and agent runtime service closes #36 - Introduce AgentRuntime context service and AgentRuntimeLayer - Resolve adapter from context in agent-step.ts instead of threading it - Add agent.adapter to factory.config.ts with opencode default - Pass ManagedRuntime through daemon/http/runs/scheduler/cli - Remove adapter field from ServerOptions, StartTrackedRunOptions, DispatchEnv - Update tests to provide agent runtime layer * fix(runtime): reconcile replay adapter signal API with runtime naming * fix(runtime): preserve usage accounting and domain-error messages on composition root * fix(cli): restore the effect/unstable/cli entrypoint after a bad merge Commit a507d1e's conflict resolution against 33-parse-cli-with-effect-cli silently reverted PR #52: src/cli.ts's import.meta.main block regressed to the pre-#52 hand-rolled USAGE/parseFlags/usageError/parseArgs parser, even though src/cli-commands.ts's factoryCommand (the effect/unstable/cli command tree) and its tests kept passing in isolation — so CI stayed green while the shipped binary silently lost generated help, typed flag validation, and --wizard/--completions. Restore the base branch's entrypoint (import { factoryCommand } from "./cli-commands"; Command.run(factoryCommand, ...)) while keeping this PR's actual new work intact: the ManagedRuntime/AgentRuntimeLayer composition root in runCli, which resolves the adapter from factory.config.ts (falling back to opencodeAdapter) and threads a ManagedRuntime into startRun. Also restore `prepareWorkspace: options.clone !== undefined`, which the same bad merge had silently dropped from runCli's startRun call. cli-commands.ts's runCommand was hardcoding `adapter: opencodeAdapter` on every `factory run` invocation, which bypassed runCli's config-driven adapter resolution and defeated issue #36's "runtime selectable from factory.config.ts" criterion for the direct-run path. Drop that override so runCli's own fallback (options.adapter ?? config.agent.adapter ?? opencodeAdapter) decides. Add tests that exercise src/cli.ts's actual import.meta.main entrypoint (the same path bin/factory.js runs in production), not just factoryCommand in isolation: a static check that the file contains no hand-rolled parser, and spawned-process checks that --help renders effect/unstable/cli's generated help and that `serve --port abc` is rejected by the typed Int flag. Without these, this class of regression can pass CI again undetected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(server): pass the agent runtime to the prepareWorkspace tests Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(runtime): restore lower-layer behavior and docs under the agent runtime The composition-root layer had been rebuilt over a bad merge: it dropped token usage from AgentStepFinished, deleted doc comments and tests from the layers below, and reworked unrelated code. Re-derive it from the base so only the adapter-to-runtime plumbing differs: - restore `usage` on AgentStepFinished (completed, failed and cancelled) and pin it end to end through startRun for the completed and cancelled paths - restore the deleted module/field/contract doc comments in run.ts and runs.ts, the RunCancelledSignal describe block, and the #54 agent-step signal tests - buildAgentStepEffect stays synchronous; its effect requires AgentRuntime and resolves the adapter when it runs, so startRun is synchronous again and forks the step on the managed runtime (`runtime.runFork`) - dispose the managed runtime: runCli disposes it after the run, the daemon handle gains `stop()` (scheduler, server, runtime), and `factory serve` calls it on SIGINT/SIGTERM - the opencode default lives only in AgentRuntimeLayer; config leaves `agent.adapter` unset unless a project picks one Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #32 · Closes #35
runtime/agent-step.tsstring-matchedCUSTOM+opencode.session-idto populateAgentStepFinished.sessionId, and string-matchedstructured-output.complete— which isn't aprovider capability at all, but
@tanstack/ai-opencode's text adapter simulating structuredoutput 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.streamnow yieldsAgentAdapterYield({ chunk, signal? }) instead of a bareunknownchunk.AgentSignalis a small closed union —sessionId,structuredOutput,runError— documented against the exactAgentStepFinishedfields each one populates.opencode-adapter.tsgainsextractOpencodeSignal, the only place undersrc/runtime/leftthat string-matches
opencode.session-idandstructured-output.complete; it also folds inRUN_ERRORdetection, whichagent-step.tsused to do itself.agent-step.tsdrops all vendor string-matching: it now just switches onyieldItem.signal._tagto populate
partial.sessionId/structuredOutput/runError, and keeps interpreting onlythe standard AG-UI
TEXT_MESSAGE_*/RUN_FINISHEDtypes forfinalTextaccumulation and usage.replay/adapter.ts's corpus replayer andcreateSlowFakeAdapterboth updated to the new yieldshape; the corpus replayer reuses
extractOpencodeSignalso recorded opencode traces keepproducing the same signals without re-deriving the logic.
runtime/agent-step.test.ts(signal-to-field wiring, chunk passthrough, no-signalcase),
runtime/opencode-adapter.test.ts(extractOpencodeSignalunit coverage including theguarded/negative cases), and a corpus round-trip assertion in
replay/adapter.test.tsthatchecks signal extraction end-to-end against a real recorded trace.
Notes for reviewers
RUN_ERRORis a standard AG-UI type, not opencode-specific, but its detection now lives insideextractOpencodeSignalalongside the two vendor-specific signals — every adapter owns detectingall three signal kinds for its own stream, per ADR 0012 §2, rather than the runtime special-casing
one AG-UI type generically.
extractOpencodeSignalfromopencode-adapter.tsrather thanduplicating 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, thenfinalTextre-parse)in
runtime/run.tsis untouched — this ticket only changes where the tier-1 value comes from.opencode.session-id,structured-output.complete) still appear inagent-step.test.tsandopencode-adapter.test.tsas literal test fixtures for the extractionlogic — that's expected; no interpretation logic outside
opencode-adapter.tsmatches on them.Verification
extractOpencodeSignal), signal-to-field wiring inbuildAgentStepEffect, and a real-corpus round trip confirming the recorded session-id tracestill yields the expected signal and chunk count.
checkjob (format, lint, typecheck,bun test) passed on this PR; I did not additionallyrun it locally. There's no CI coverage of a live opencode run, so
AgentStepFinished.sessionId/.output/.errorparity for a live run is established by code inspection (the moved logic inextractOpencodeSignalis byte-for-byte equivalent to whatagent-step.tsused to do) ratherthan an end-to-end run.
Stack
33-parse-cli-with-effect-cli(issue Parse CLI arguments with effect/unstable/cli #33)34-domain-errors-as-tagged-errors(issue Represent domain failures as Schema.TaggedError #34)35-move-chunk-interpretation-into-adapter(issue Move chunk interpretation into the agent adapter #35) — this PR37-move-headless-permissions(issue Move headless permission setup out of the workspace allocator #37)36-agent-runtime-service(issue Add an Effect composition root and make the agent runtime a service #36)38-move-singletons-into-layers(issue Move the daemon's remaining singletons into layers #38)Stack created with GitHub Stacks CLI • Give Feedback 💬