mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
feat(x): reference-based model requests + wire-form composer
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>
This commit is contained in:
parent
a58493b545
commit
3822bf53da
18 changed files with 524 additions and 227 deletions
|
|
@ -479,11 +479,15 @@ Every AI SDK `streamText` invocation performs exactly one model step. Tool
|
|||
execution is controlled manually by the turn loop.
|
||||
|
||||
```ts
|
||||
type ModelRequestMessageRef =
|
||||
| { kind: "context" } // the inline context block
|
||||
| { kind: "input" } // the turn's user message
|
||||
| { kind: "assistant"; modelCallIndex: number } // → model_call_completed
|
||||
| { kind: "toolResult"; toolCallId: string }; // → tool_result
|
||||
|
||||
interface ModelRequest {
|
||||
systemPrompt: string;
|
||||
contextRef?: { previousTurnId: string };
|
||||
messages: ConversationMessage[];
|
||||
tools: ToolDescriptor[];
|
||||
contextRef?: { previousTurnId: string }; // call 0 only (cross-turn prefix)
|
||||
messages: ModelRequestMessageRef[]; // what is NEW since the previous call
|
||||
parameters: Record<string, JsonValue>;
|
||||
}
|
||||
|
||||
|
|
@ -497,24 +501,26 @@ interface ModelCallRequested extends BaseTurnEvent {
|
|||
`modelCallIndex` starts at `0` and increments monotonically for each primary
|
||||
agent model call in the turn.
|
||||
|
||||
`request` records the exact model input with one substitution: when the
|
||||
turn's context is a reference, the resolved prefix is not re-inlined.
|
||||
`contextRef` repeats the reference and `messages` contains only current-turn
|
||||
messages — the turn input, this turn's prior assistant responses, and ordered
|
||||
tool results — captured byte-for-byte as assembled immediately before
|
||||
provider invocation. When the turn's context is inline, `contextRef` is
|
||||
absent and `messages` begins with that inline context. The full request is
|
||||
deterministically materializable because referenced turns are immutable; a
|
||||
`materializeRequest(turnId, modelCallIndex)` debug utility composes resolver
|
||||
output with the persisted messages.
|
||||
`request` is a list of references into the turn's own events: every
|
||||
referenced byte exists exactly once in the file. A request records only what
|
||||
is new since the previous model call — call 0 is `[{context}?, {input}]`
|
||||
(context only when inline and nonempty; cross-turn prefixes ride
|
||||
`contextRef`); call N is `[{assistant: N-1}, …that batch's toolResults in
|
||||
source order]`; a re-issued call after an interruption is `[]`. The system
|
||||
prompt and tool set are not repeated: they are byte-identical to
|
||||
`turn_created.agent.resolved` by construction.
|
||||
|
||||
Within-turn duplication is deliberate: each request restates the current
|
||||
turn's accumulated messages even though they also exist as earlier events in
|
||||
the same file. The persisted `messages` are the ground truth of what the loop
|
||||
actually assembled and sent — including any assembly bugs — which a derived
|
||||
reconstruction would mask.
|
||||
The exact provider payload is rebuilt by the request composer
|
||||
(`composeModelRequest` in core): resolved system prompt, wrapped tool
|
||||
definitions, the materialized cross-turn prefix, and every request's
|
||||
references resolved against the turn's events, all passed through the model
|
||||
bridge's `encodeMessages` (the deterministic structural→wire conversion —
|
||||
user-message context weaving, attachment rendering, tool-result enveloping).
|
||||
The loop transmits exactly the composer's output, and debugging/audit calls
|
||||
the same function, so the durable file plus the composer reproduce the wire
|
||||
bytes and the two views cannot diverge.
|
||||
|
||||
`request` excludes credentials, auth headers, functions, model objects, and
|
||||
Requests exclude credentials, auth headers, functions, model objects, and
|
||||
transport objects.
|
||||
|
||||
The name `requested` is intentional. The event proves durable intent, not that
|
||||
|
|
@ -1526,7 +1532,11 @@ but the first turn implementation does not enforce it.
|
|||
- Any event after a terminal turn event.
|
||||
- Mutation of immutable turn definition data.
|
||||
- `model_call_requested.contextRef` values inconsistent with the turn
|
||||
definition's context.
|
||||
definition's context, or present on a non-initial model call.
|
||||
- Request references that do not match the transcript: call 0 must be
|
||||
`[{context}?, {input}]`; call N must reference the previous completed
|
||||
call's response and its batch's tool results in source order (or nothing,
|
||||
after an interrupted call).
|
||||
|
||||
The reducer validates `context` structurally but treats it as opaque; it
|
||||
never resolves references (section 6.6).
|
||||
|
|
@ -1755,8 +1765,8 @@ tests for:
|
|||
closure results.
|
||||
- A reference to a missing or corrupt turn file is an infrastructure error
|
||||
and appends nothing.
|
||||
- `materializeRequest` output equals resolver output plus persisted
|
||||
current-turn request messages.
|
||||
- The composed request equals what the loop sent (byte-for-byte property:
|
||||
durable file + composer reproduce the provider payload).
|
||||
- The reducer never resolves references.
|
||||
|
||||
## 28. Suggested module layout
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue