model_call_requested carried three duplications that dominated turn-file
size (measured on a real 647KB two-step turn): the system prompt (28KB)
and the 38-tool snapshot (40KB) repeated per call despite being byte-
identical to turn_created.agent.resolved, and messages re-inlining the
whole current-turn transcript (230KB re-stating a 207KB tool result).
ModelRequest is now a list of references into the turn's own events —
call 0: [{context}?, {input}]; call N: [{assistant: N-1}, ...that
batch's toolResults in source order]; re-issue after an interruption:
[]. Every referenced byte exists exactly once in the file; the measured
turn drops to ~285KB and each further model call costs ~200 bytes. The
reducer's ordering invariant got stricter: reference lists are matched
exactly against the transcript.
Debuggability is now byte-for-byte at the wire level: ResolvedModel
gains encodeMessages (the structural->wire conversion — user-message
context weaving, attachment rendering, tool-result enveloping, i.e.
convertFromMessages), and composeModelRequest rebuilds the exact
provider payload (resolved system prompt + wrapped tools + materialized
prefix + resolved refs, encoded). The loop transmits exactly the
composer's output, so the durable file plus the composer reproduce what
the model received — pinned by a property test asserting composed ==
sent for every call, and a 2KB size guard on request events. A bridge
test demonstrates the woven wire form (no raw userMessageContext).
Breaking for existing dev turn files (same schemaVersion, pre-release):
wipe ~/.rowboat/storage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five bridges adapt existing app code to the runtime seams, each unit-
tested against injected fakes:
- RealAgentResolver: loadAgent + dynamic builders -> immutable
ResolvedAgent. Model precedence override > agent > app default; tool
descriptors from BuiltinTools (zod -> JSON schema) and MCP
attachments (toolId schemes builtin:/mcp:server:tool); ask-human as
the async requiresHuman tool. System prompt composed via
composeSystemInstructions — extracted verbatim from streamAgent so
old and new runtimes share one implementation — driven by the new
opaque RequestedAgent.overrides.composition (session-sticky inputs
preserve provider prefix caching; per-message context stays on
UserMessage.userMessageContext as today).
- RealModelRegistry: models.json -> live AI SDK model; one streamText
step (stopWhen: stepCountIs(1)) normalized into deltas, durable step
events, and the completed assistant message.
- RealToolRegistry: execTool dispatch (builtins + MCP) with per-call
abortRegistry bracketing and tool-output-stream -> durable progress.
- RealPermissionChecker: getToolPermissionMetadata rules (command
allowlist, workspace file boundaries); session grants deferred.
- RealPermissionClassifier: classifyToolPermissions with conversation
context now threaded through the classifier batch.
Interface refinements (doc updated): ToolExecutionContext gains
turnId/toolCallId; classifier takes a batch with turnId + messages;
TurnRuntime dep renamed bus -> lifecycleBus and SessionsImpl bus ->
sessionBus for strict-PROXY DI. Container registers the whole stack
(FS repos rooted at WorkDir/storage); the legacy agentRuntime
registration became lazy to survive the new import-order cycle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Turn runtime spec amended after review: context references with a
context resolver, recovery that re-issues interrupted model calls and
continues after interrupted sync tools, turn_failed.code with
model-call-limit, default maxModelCalls 20.
New session layer spec: append-only session JSONL, turn-file-first
write ordering, in-memory write-through index with startup scan,
reject-while-busy sendMessage, dedicated ask-human endpoint, deferred
v2 shapes for queueing/steering/grants/compaction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>