mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
feat(x): compact string request refs + turn inspector CLI
Two debuggability follow-ups from reviewing real turn files:
- Request refs are now compact strings — 'context' | 'input' |
'assistant:<index>' | 'toolResult:<toolCallId>' — so a raw JSONL line
reads naturally: {"messages": ["assistant:0", "toolResult:toolu_…"]}.
Same exact-match reducer invariants, via parseRequestRef.
- npm run inspect-turn -- <turnId|path> [callIndex] [--full]: prints,
per model call, the EXACT provider payload (resolved system prompt,
tool list, wire-form messages with user-message context woven in, and
the response/failure) — rebuilt by the same composer the loop sends
through. This is the missing viewer for the derived-not-duplicated
request design: the file stores facts once; the inspector shows what
the model actually received.
Breaking for turn files written since the previous commit (dev only):
wipe ~/.rowboat/storage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0ab09d4d47
commit
97169be93e
10 changed files with 261 additions and 135 deletions
|
|
@ -479,11 +479,12 @@ 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
|
||||
// Compact string refs, so raw JSONL reads naturally:
|
||||
// "context" the inline context block
|
||||
// "input" the turn's user message
|
||||
// "assistant:<index>" → that model_call_completed's message
|
||||
// "toolResult:<toolCallId>" → that tool_result
|
||||
type ModelRequestMessageRef = string;
|
||||
|
||||
interface ModelRequest {
|
||||
contextRef?: { previousTurnId: string }; // call 0 only (cross-turn prefix)
|
||||
|
|
@ -503,10 +504,11 @@ agent model call in the turn.
|
|||
|
||||
`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}]`
|
||||
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
|
||||
`contextRef`); call N is `["assistant:N-1", …that batch's
|
||||
"toolResult:<id>" refs 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
||||
"dev": "tsc -w -p tsconfig.build.json",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"inspect-turn": "node dist/turns/inspect-cli.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/claude-agent-acp": "^0.39.0",
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ function turnLog(
|
|||
turnId,
|
||||
ts: TS,
|
||||
modelCallIndex: 0,
|
||||
request: { messages: [{ kind: 'input' as const }], parameters: {} },
|
||||
request: { messages: ['input'], parameters: {} },
|
||||
};
|
||||
switch (status) {
|
||||
case "idle":
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ function completedTurnLog(
|
|||
...(Array.isArray(context) ? {} : { contextRef: context }),
|
||||
messages:
|
||||
Array.isArray(context) && context.length > 0
|
||||
? [{ kind: "context" as const }, { kind: "input" as const }]
|
||||
: [{ kind: "input" as const }],
|
||||
? ["context", "input"]
|
||||
: ["input"],
|
||||
parameters: {},
|
||||
},
|
||||
},
|
||||
|
|
@ -138,7 +138,7 @@ function limitFailedTurnLog(turnId: string): TEvent[] {
|
|||
ts,
|
||||
modelCallIndex: 0,
|
||||
request: {
|
||||
messages: [{ kind: "input" as const }],
|
||||
messages: ["input"],
|
||||
parameters: {},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function requested(turnId = TURN_ID): z.infer<typeof TurnEvent> {
|
|||
ts: "2026-07-02T10:00:01Z",
|
||||
modelCallIndex: 0,
|
||||
request: {
|
||||
messages: [{ kind: "input" }],
|
||||
messages: ["input"],
|
||||
parameters: {},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
105
apps/x/packages/core/src/turns/inspect-cli.ts
Normal file
105
apps/x/packages/core/src/turns/inspect-cli.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// Turn inspector: prints, per model call, the EXACT provider payload the
|
||||
// loop sent — rebuilt from the durable file by the same composer the loop
|
||||
// transmits through (compose-model-request.ts). This is where the woven
|
||||
// wire-form messages (user-message context, attachments, tool-result
|
||||
// envelopes) are visible; the file itself stores only structural facts and
|
||||
// references.
|
||||
//
|
||||
// Usage:
|
||||
// npm run inspect-turn -- <turnId | path/to/turn.jsonl> [modelCallIndex] [--full]
|
||||
//
|
||||
// --full prints the entire system prompt and untruncated message contents.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
reduceTurn,
|
||||
type JsonValue,
|
||||
type TurnEvent,
|
||||
} from "@x/shared/dist/turns.js";
|
||||
import type { z } from "zod";
|
||||
import { convertFromMessages } from "../agents/runtime.js";
|
||||
import { WorkDir } from "../config/config.js";
|
||||
import { composeModelRequest } from "./compose-model-request.js";
|
||||
import { TurnRepoContextResolver } from "./context-resolver.js";
|
||||
import { FSTurnRepo } from "./fs-repo.js";
|
||||
|
||||
function usage(): never {
|
||||
console.error(
|
||||
"usage: inspect-turn <turnId | path/to/turn.jsonl> [modelCallIndex] [--full]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function clip(text: string, full: boolean, limit = 400): string {
|
||||
if (full || text.length <= limit) return text;
|
||||
return `${text.slice(0, limit)}… [${text.length} chars total; pass --full]`;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2).filter((a) => a !== "--full");
|
||||
const full = process.argv.includes("--full");
|
||||
const target = args[0];
|
||||
if (!target) usage();
|
||||
const onlyIndex = args[1] !== undefined ? Number(args[1]) : undefined;
|
||||
|
||||
const turnsRootDir = path.join(WorkDir, "storage", "turns");
|
||||
const repo = new FSTurnRepo({ turnsRootDir });
|
||||
const turnId = target.endsWith(".jsonl")
|
||||
? path.basename(target, ".jsonl")
|
||||
: target;
|
||||
|
||||
let events: Array<z.infer<typeof TurnEvent>>;
|
||||
if (target.endsWith(".jsonl") && fs.existsSync(target)) {
|
||||
// Direct path: parse in place (still strict via the reducer below).
|
||||
events = fs
|
||||
.readFileSync(target, "utf8")
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line) as z.infer<typeof TurnEvent>);
|
||||
} else {
|
||||
events = await repo.read(turnId);
|
||||
}
|
||||
const state = reduceTurn(events);
|
||||
const resolver = new TurnRepoContextResolver({ turnRepo: repo });
|
||||
const prefix = await resolver.resolve(state.definition.context);
|
||||
const encode = (messages: Parameters<typeof convertFromMessages>[0]) =>
|
||||
convertFromMessages(messages) as unknown as JsonValue[];
|
||||
|
||||
console.log(`turn ${turnId}`);
|
||||
console.log(
|
||||
`agent ${state.definition.agent.resolved.agentId} model ${state.definition.agent.resolved.model.provider}/${state.definition.agent.resolved.model.model} calls ${state.modelCalls.length}`,
|
||||
);
|
||||
|
||||
for (const call of state.modelCalls) {
|
||||
if (onlyIndex !== undefined && call.index !== onlyIndex) continue;
|
||||
const composed = composeModelRequest(state, call.index, prefix, encode);
|
||||
console.log(`\n━━ model call ${call.index} ━━ (as sent to the provider)`);
|
||||
console.log(`system (${composed.systemPrompt.length} chars): ${clip(composed.systemPrompt, full)}`);
|
||||
console.log(
|
||||
`tools (${composed.tools.length}): ${composed.tools.map((t) => t.name).join(", ")}`,
|
||||
);
|
||||
console.log(`messages (${composed.messages.length}):`);
|
||||
for (const message of composed.messages) {
|
||||
const m = message as { role?: string; content?: unknown };
|
||||
const content =
|
||||
typeof m.content === "string"
|
||||
? m.content
|
||||
: JSON.stringify(m.content);
|
||||
console.log(` [${m.role}] ${clip(content, full)}`);
|
||||
}
|
||||
if (call.error !== undefined) {
|
||||
console.log(` → failed: ${call.error}`);
|
||||
} else if (call.response !== undefined) {
|
||||
const response =
|
||||
typeof call.response.content === "string"
|
||||
? call.response.content
|
||||
: JSON.stringify(call.response.content);
|
||||
console.log(` → response (${call.finishReason}): ${clip(response, full)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -3,7 +3,6 @@ import type { z } from "zod";
|
|||
import {
|
||||
MODEL_CALL_LIMIT_ERROR_CODE,
|
||||
type JsonValue,
|
||||
type ModelRequestMessageRef,
|
||||
type ResolvedAgent,
|
||||
type ToolDescriptor,
|
||||
type TurnEvent,
|
||||
|
|
@ -422,7 +421,7 @@ describe("plain model response (26.1)", () => {
|
|||
const request = log[1];
|
||||
expect(request).toMatchObject({
|
||||
modelCallIndex: 0,
|
||||
request: { messages: [{ kind: "input" }] },
|
||||
request: { messages: ["input"] },
|
||||
});
|
||||
// The model received the composed payload: resolved system prompt,
|
||||
// snapshot tools, encoded messages.
|
||||
|
|
@ -525,11 +524,11 @@ describe("mixed sync and async tools (26.2)", () => {
|
|||
? secondRequest.request.messages
|
||||
: [],
|
||||
).toEqual([
|
||||
{ kind: "assistant", modelCallIndex: 0 },
|
||||
{ kind: "toolResult", toolCallId: "A" },
|
||||
{ kind: "toolResult", toolCallId: "B" },
|
||||
{ kind: "toolResult", toolCallId: "C" },
|
||||
{ kind: "toolResult", toolCallId: "D" },
|
||||
"assistant:0",
|
||||
"toolResult:A",
|
||||
"toolResult:B",
|
||||
"toolResult:C",
|
||||
"toolResult:D",
|
||||
]);
|
||||
// The live model call saw the results in source order.
|
||||
expect(
|
||||
|
|
@ -1166,7 +1165,7 @@ describe("crash recovery (26.7)", () => {
|
|||
|
||||
function seedRequested(
|
||||
index: number,
|
||||
refs: z.infer<typeof ModelRequestMessageRef>[] = [{ kind: "input" }],
|
||||
refs: string[] = ["input"],
|
||||
): z.infer<typeof TurnEvent> {
|
||||
return {
|
||||
type: "model_call_requested",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import {
|
|||
type JsonValue,
|
||||
type ModelCallFailed,
|
||||
type ModelRequest,
|
||||
type ModelRequestMessageRef,
|
||||
assistantRef,
|
||||
toolResultRef,
|
||||
type ToolCallState,
|
||||
type ToolDescriptor,
|
||||
type ToolInvocationRequested,
|
||||
|
|
@ -856,25 +857,22 @@ class TurnAdvance {
|
|||
const index = this.state.modelCalls.length;
|
||||
const context = this.definition.context;
|
||||
const isRef = !Array.isArray(context);
|
||||
let refs: Array<z.infer<typeof ModelRequestMessageRef>>;
|
||||
let refs: string[];
|
||||
if (index === 0) {
|
||||
refs =
|
||||
!isRef && context.length > 0
|
||||
? [{ kind: "context" }, { kind: "input" }]
|
||||
: [{ kind: "input" }];
|
||||
? ["context", "input"]
|
||||
: ["input"];
|
||||
} else {
|
||||
const previous = this.state.modelCalls[index - 1];
|
||||
refs =
|
||||
previous.response !== undefined
|
||||
? [
|
||||
{ kind: "assistant", modelCallIndex: previous.index },
|
||||
assistantRef(previous.index),
|
||||
...this.state.toolCalls
|
||||
.filter((tc) => tc.modelCallIndex === previous.index)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((tc) => ({
|
||||
kind: "toolResult" as const,
|
||||
toolCallId: tc.toolCallId,
|
||||
})),
|
||||
.map((tc) => toolResultRef(tc.toolCallId)),
|
||||
]
|
||||
: []; // re-issue after an interrupted call adds nothing new
|
||||
}
|
||||
|
|
|
|||
|
|
@ -346,13 +346,13 @@ function syncToolSequence(): TEvent[] {
|
|||
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
||||
return [
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
permResolved("tc1", "allow"),
|
||||
invocation("tc1"),
|
||||
result("tc1", "echo"),
|
||||
requested(1, [{ kind: "assistant", modelCallIndex: 0 }, { kind: "toolResult", toolCallId: "tc1" }]),
|
||||
requested(1, ["assistant:0", "toolResult:tc1"]),
|
||||
completed(1, assistantText("done")),
|
||||
turnCompletedEv(),
|
||||
];
|
||||
|
|
@ -393,7 +393,7 @@ describe("plain completion", () => {
|
|||
it("reduces a plain model response to a completed turn", () => {
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
stepEvent(0),
|
||||
completed(0, assistantText("done")),
|
||||
turnCompletedEv(),
|
||||
|
|
@ -418,7 +418,7 @@ describe("plain completion", () => {
|
|||
it("leaves usage fields undefined when never reported", () => {
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, assistantText("a"), { inputTokens: 5 }),
|
||||
]);
|
||||
expect(state.usage).toEqual({ inputTokens: 5 });
|
||||
|
|
@ -434,7 +434,7 @@ describe("tool execution", () => {
|
|||
);
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
]);
|
||||
expect(state.toolCalls).toHaveLength(2);
|
||||
|
|
@ -457,7 +457,7 @@ describe("tool execution", () => {
|
|||
it("leaves identity undefined for tools missing from the agent snapshot", () => {
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, assistantCalls(toolCallPart("x1", "unknown-tool"))),
|
||||
result("x1", "unknown-tool", "runtime", "No such tool", true),
|
||||
]);
|
||||
|
|
@ -480,7 +480,7 @@ describe("tool execution", () => {
|
|||
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
||||
const state = reduceTurn([
|
||||
created({ config: { autoPermission: true, humanAvailable: true, maxModelCalls: 20 } }),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
permClassified("tc1", "allow"),
|
||||
|
|
@ -497,7 +497,7 @@ describe("tool execution", () => {
|
|||
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
permClassificationFailed(["tc1"]),
|
||||
|
|
@ -512,7 +512,7 @@ describe("tool execution", () => {
|
|||
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1"),
|
||||
progress("tc1"),
|
||||
|
|
@ -526,7 +526,7 @@ describe("tool execution", () => {
|
|||
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
permResolved("tc1", "deny"),
|
||||
|
|
@ -541,7 +541,7 @@ describe("context references", () => {
|
|||
it("accepts a referenced context with matching contextRef on requests", () => {
|
||||
const state = reduceTurn([
|
||||
created({ context: { previousTurnId: PREV_TURN_ID } }),
|
||||
requested(0, [{ kind: "input" }], {
|
||||
requested(0, ["input"], {
|
||||
contextRef: { previousTurnId: PREV_TURN_ID },
|
||||
}),
|
||||
completed(0, assistantText("done")),
|
||||
|
|
@ -554,7 +554,7 @@ describe("context references", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created({ context: { previousTurnId: PREV_TURN_ID } }),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
],
|
||||
/contextRef inconsistent/,
|
||||
);
|
||||
|
|
@ -564,7 +564,7 @@ describe("context references", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created({ context: { previousTurnId: PREV_TURN_ID } }),
|
||||
requested(0, [{ kind: "input" }], {
|
||||
requested(0, ["input"], {
|
||||
contextRef: { previousTurnId: "some-other-turn" },
|
||||
}),
|
||||
],
|
||||
|
|
@ -576,7 +576,7 @@ describe("context references", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }], {
|
||||
requested(0, ["input"], {
|
||||
contextRef: { previousTurnId: PREV_TURN_ID },
|
||||
}),
|
||||
],
|
||||
|
|
@ -592,13 +592,13 @@ describe("context references", () => {
|
|||
];
|
||||
const state = reduceTurn([
|
||||
created({ context }),
|
||||
requested(0, [{ kind: "context" }, { kind: "input" }]),
|
||||
requested(0, ["context", "input"]),
|
||||
completed(0, assistantText("done")),
|
||||
turnCompletedEv(),
|
||||
]);
|
||||
expect(deriveTurnStatus(state)).toBe("completed");
|
||||
expectCorruption(
|
||||
[created({ context }), requested(0, [{ kind: "input" }])],
|
||||
[created({ context }), requested(0, ["input"])],
|
||||
/references do not match/,
|
||||
);
|
||||
});
|
||||
|
|
@ -607,7 +607,7 @@ describe("context references", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created({ context: { previousTurnId: PREV_TURN_ID } }),
|
||||
requested(0, [{ kind: "input" }], {
|
||||
requested(0, ["input"], {
|
||||
contextRef: { previousTurnId: PREV_TURN_ID },
|
||||
}),
|
||||
completed(0, assistantCalls(toolCallPart("tc1", "echo"))),
|
||||
|
|
@ -616,8 +616,8 @@ describe("context references", () => {
|
|||
requested(
|
||||
1,
|
||||
[
|
||||
{ kind: "assistant", modelCallIndex: 0 },
|
||||
{ kind: "toolResult", toolCallId: "tc1" },
|
||||
"assistant:0",
|
||||
"toolResult:tc1",
|
||||
],
|
||||
{ contextRef: { previousTurnId: PREV_TURN_ID } },
|
||||
),
|
||||
|
|
@ -635,7 +635,7 @@ describe("suspension", () => {
|
|||
);
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
invocation("a1", fetchTool),
|
||||
|
|
@ -653,7 +653,7 @@ describe("suspension", () => {
|
|||
);
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
invocation("a1", fetchTool),
|
||||
|
|
@ -673,7 +673,7 @@ describe("suspension", () => {
|
|||
);
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("a1", fetchTool),
|
||||
invocation("a2", fetchTool),
|
||||
|
|
@ -682,9 +682,9 @@ describe("suspension", () => {
|
|||
suspendedEv([], [{ id: "a1", tool: fetchTool }]),
|
||||
result("a1", "fetch", "async", "first"),
|
||||
requested(1, [
|
||||
{ kind: "assistant", modelCallIndex: 0 },
|
||||
{ kind: "toolResult", toolCallId: "a1" },
|
||||
{ kind: "toolResult", toolCallId: "a2" },
|
||||
"assistant:0",
|
||||
"toolResult:a1",
|
||||
"toolResult:a2",
|
||||
]),
|
||||
completed(1, assistantText("done")),
|
||||
turnCompletedEv(),
|
||||
|
|
@ -697,7 +697,7 @@ describe("suspension", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
permResolved("tc1", "allow"),
|
||||
|
|
@ -717,7 +717,7 @@ describe("suspension", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
invocation("a1", fetchTool),
|
||||
|
|
@ -731,7 +731,7 @@ describe("suspension", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
suspendedEv([{ id: "tc1", name: "echo" }], []),
|
||||
],
|
||||
/suspension while a model call is unsettled/,
|
||||
|
|
@ -743,7 +743,7 @@ describe("recovery-shaped histories", () => {
|
|||
it("accepts an interrupted model call closed and re-issued (§23 fix)", () => {
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
callFailed(0, "interrupted by process restart"),
|
||||
requested(1, []),
|
||||
completed(1, assistantText("done")),
|
||||
|
|
@ -760,7 +760,7 @@ describe("recovery-shaped histories", () => {
|
|||
created({
|
||||
config: { autoPermission: false, humanAvailable: true, maxModelCalls: 1 },
|
||||
}),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
callFailed(0, "interrupted"),
|
||||
requested(1, []),
|
||||
],
|
||||
|
|
@ -772,7 +772,7 @@ describe("recovery-shaped histories", () => {
|
|||
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1"),
|
||||
result(
|
||||
|
|
@ -782,7 +782,7 @@ describe("recovery-shaped histories", () => {
|
|||
"Tool execution was interrupted; its outcome is unknown and it was not retried.",
|
||||
true,
|
||||
),
|
||||
requested(1, [{ kind: "assistant", modelCallIndex: 0 }, { kind: "toolResult", toolCallId: "tc1" }]),
|
||||
requested(1, ["assistant:0", "toolResult:tc1"]),
|
||||
completed(1, assistantText("done")),
|
||||
turnCompletedEv(),
|
||||
]);
|
||||
|
|
@ -794,7 +794,7 @@ describe("recovery-shaped histories", () => {
|
|||
const call0 = assistantCalls(toolCallPart("a1", "fetch"));
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("a1", fetchTool),
|
||||
suspendedEv([], [{ id: "a1", tool: fetchTool }]),
|
||||
|
|
@ -808,7 +808,7 @@ describe("recovery-shaped histories", () => {
|
|||
it("accepts a live model failure closing the turn", () => {
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
callFailed(0),
|
||||
turnFailedEv("provider exploded"),
|
||||
]);
|
||||
|
|
@ -821,7 +821,7 @@ describe("recovery-shaped histories", () => {
|
|||
created({
|
||||
config: { autoPermission: false, humanAvailable: true, maxModelCalls: 1 },
|
||||
}),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1"),
|
||||
result("tc1", "echo"),
|
||||
|
|
@ -840,7 +840,7 @@ describe("invariants", () => {
|
|||
});
|
||||
|
||||
it("rejects a log not starting with turn_created", () => {
|
||||
expectCorruption([requested(0, [{ kind: "input" }])], /first event must be turn_created/);
|
||||
expectCorruption([requested(0, ["input"])], /first event must be turn_created/);
|
||||
});
|
||||
|
||||
it("rejects duplicate turn_created", () => {
|
||||
|
|
@ -849,7 +849,7 @@ describe("invariants", () => {
|
|||
|
||||
it("rejects mismatched turn ids", () => {
|
||||
expectCorruption(
|
||||
[created(), { ...requested(0, [{ kind: "input" }]), turnId: "other" }],
|
||||
[created(), { ...requested(0, ["input"]), turnId: "other" }],
|
||||
/does not match turn/,
|
||||
);
|
||||
});
|
||||
|
|
@ -882,9 +882,9 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, assistantText("a")),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
],
|
||||
/out of order/,
|
||||
);
|
||||
|
|
@ -892,7 +892,7 @@ describe("invariants", () => {
|
|||
|
||||
it("rejects concurrent unresolved model requests", () => {
|
||||
expectCorruption(
|
||||
[created(), requested(0, [{ kind: "input" }]), requested(1, [])],
|
||||
[created(), requested(0, ["input"]), requested(1, [])],
|
||||
/concurrent unresolved model call requests/,
|
||||
);
|
||||
});
|
||||
|
|
@ -912,7 +912,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, assistantText("a")),
|
||||
completed(0, assistantText("b")),
|
||||
],
|
||||
|
|
@ -924,7 +924,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
callFailed(0),
|
||||
completed(0, assistantText("a")),
|
||||
],
|
||||
|
|
@ -937,9 +937,9 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
requested(1, [{ kind: "assistant", modelCallIndex: 0 }]),
|
||||
requested(1, ["assistant:0"]),
|
||||
],
|
||||
/while tool calls are unresolved/,
|
||||
);
|
||||
|
|
@ -952,11 +952,11 @@ describe("invariants", () => {
|
|||
created({
|
||||
config: { autoPermission: false, humanAvailable: true, maxModelCalls: 1 },
|
||||
}),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1"),
|
||||
result("tc1", "echo"),
|
||||
requested(1, [{ kind: "assistant", modelCallIndex: 0 }, { kind: "toolResult", toolCallId: "tc1" }]),
|
||||
requested(1, ["assistant:0", "toolResult:tc1"]),
|
||||
],
|
||||
/exceeds maxModelCalls/,
|
||||
);
|
||||
|
|
@ -966,7 +966,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(
|
||||
0,
|
||||
assistantCalls(toolCallPart("tc1", "echo"), toolCallPart("tc1", "echo")),
|
||||
|
|
@ -981,11 +981,11 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1"),
|
||||
result("tc1", "echo"),
|
||||
requested(1, [{ kind: "assistant", modelCallIndex: 0 }, { kind: "toolResult", toolCallId: "tc1" }]),
|
||||
requested(1, ["assistant:0", "toolResult:tc1"]),
|
||||
completed(1, assistantCalls(toolCallPart("tc1", "echo"))),
|
||||
],
|
||||
/duplicate tool call id/,
|
||||
|
|
@ -1000,16 +1000,16 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1"),
|
||||
result("tc1", "echo"),
|
||||
invocation("tc2"),
|
||||
result("tc2", "echo"),
|
||||
requested(1, [
|
||||
{ kind: "assistant", modelCallIndex: 0 },
|
||||
{ kind: "toolResult", toolCallId: "tc2" },
|
||||
{ kind: "toolResult", toolCallId: "tc1" },
|
||||
"assistant:0",
|
||||
"toolResult:tc2",
|
||||
"toolResult:tc1",
|
||||
]),
|
||||
],
|
||||
/references do not match/,
|
||||
|
|
@ -1018,7 +1018,7 @@ describe("invariants", () => {
|
|||
|
||||
it("rejects permission records targeting unknown tool calls", () => {
|
||||
expectCorruption(
|
||||
[created(), requested(0, [{ kind: "input" }]), completed(0, assistantText("a")), permRequired("ghost", "echo")],
|
||||
[created(), requested(0, ["input"]), completed(0, assistantText("a")), permRequired("ghost", "echo")],
|
||||
/unknown tool call/,
|
||||
);
|
||||
});
|
||||
|
|
@ -1028,7 +1028,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
permRequired("tc1", "echo"),
|
||||
|
|
@ -1042,7 +1042,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permClassified("tc1", "allow"),
|
||||
],
|
||||
|
|
@ -1055,7 +1055,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permResolved("tc1", "allow"),
|
||||
],
|
||||
|
|
@ -1068,7 +1068,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
permResolved("tc1", "allow"),
|
||||
|
|
@ -1083,7 +1083,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
invocation("tc1"),
|
||||
|
|
@ -1097,7 +1097,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
permResolved("tc1", "deny"),
|
||||
|
|
@ -1112,7 +1112,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1"),
|
||||
invocation("tc1"),
|
||||
|
|
@ -1126,7 +1126,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1", echoTool, { execution: "async" }),
|
||||
],
|
||||
|
|
@ -1137,7 +1137,7 @@ describe("invariants", () => {
|
|||
it("rejects progress without invocation", () => {
|
||||
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
||||
expectCorruption(
|
||||
[created(), requested(0, [{ kind: "input" }]), completed(0, call0), progress("tc1")],
|
||||
[created(), requested(0, ["input"]), completed(0, call0), progress("tc1")],
|
||||
/progress without invocation/,
|
||||
);
|
||||
});
|
||||
|
|
@ -1147,7 +1147,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1"),
|
||||
result("tc1", "echo"),
|
||||
|
|
@ -1162,7 +1162,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1"),
|
||||
result("tc1", "echo"),
|
||||
|
|
@ -1175,7 +1175,7 @@ describe("invariants", () => {
|
|||
it("rejects sync-sourced results without invocation", () => {
|
||||
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
||||
expectCorruption(
|
||||
[created(), requested(0, [{ kind: "input" }]), completed(0, call0), result("tc1", "echo", "sync")],
|
||||
[created(), requested(0, ["input"]), completed(0, call0), result("tc1", "echo", "sync")],
|
||||
/sync tool result without invocation/,
|
||||
);
|
||||
});
|
||||
|
|
@ -1185,7 +1185,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1"),
|
||||
result("tc1", "echo", "async"),
|
||||
|
|
@ -1199,7 +1199,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, assistantText("a")),
|
||||
stepEvent(0),
|
||||
],
|
||||
|
|
@ -1210,7 +1210,7 @@ describe("invariants", () => {
|
|||
it("rejects completion while tool calls remain unresolved", () => {
|
||||
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
||||
expectCorruption(
|
||||
[created(), requested(0, [{ kind: "input" }]), completed(0, call0), turnCompletedEv()],
|
||||
[created(), requested(0, ["input"]), completed(0, call0), turnCompletedEv()],
|
||||
/completion while tool calls lack terminal results/,
|
||||
);
|
||||
});
|
||||
|
|
@ -1220,7 +1220,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1"),
|
||||
result("tc1", "echo"),
|
||||
|
|
@ -1237,14 +1237,14 @@ describe("invariants", () => {
|
|||
it("rejects terminal failure while tool calls remain unresolved", () => {
|
||||
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
||||
expectCorruption(
|
||||
[created(), requested(0, [{ kind: "input" }]), completed(0, call0), turnFailedEv()],
|
||||
[created(), requested(0, ["input"]), completed(0, call0), turnFailedEv()],
|
||||
/failure while tool calls lack terminal results/,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects terminal events while a model call is unsettled", () => {
|
||||
expectCorruption(
|
||||
[created(), requested(0, [{ kind: "input" }]), turnCancelledEv()],
|
||||
[created(), requested(0, ["input"]), turnCancelledEv()],
|
||||
/cancellation while a model call is unsettled/,
|
||||
);
|
||||
});
|
||||
|
|
@ -1252,7 +1252,7 @@ describe("invariants", () => {
|
|||
it("rejects any event after a terminal event", () => {
|
||||
const base = [
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, assistantText("a")),
|
||||
];
|
||||
for (const terminal of [turnCompletedEv(), turnFailedEv(), turnCancelledEv()]) {
|
||||
|
|
@ -1267,7 +1267,7 @@ describe("invariants", () => {
|
|||
expectCorruption(
|
||||
[
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, assistantText("a")),
|
||||
turnCompletedEv(),
|
||||
turnFailedEv(),
|
||||
|
|
@ -1282,14 +1282,14 @@ describe("derivations", () => {
|
|||
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
||||
const pending = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
permRequired("tc1", "echo"),
|
||||
suspendedEv([{ id: "tc1", name: "echo" }], []),
|
||||
]);
|
||||
expect(deriveTurnStatus(pending)).toBe("suspended");
|
||||
|
||||
const idle = reduceTurn([created(), requested(0, [{ kind: "input" }]), completed(0, call0)]);
|
||||
const idle = reduceTurn([created(), requested(0, ["input"]), completed(0, call0)]);
|
||||
expect(deriveTurnStatus(idle)).toBe("idle");
|
||||
});
|
||||
|
||||
|
|
@ -1300,16 +1300,16 @@ describe("derivations", () => {
|
|||
);
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
invocation("tc1"),
|
||||
invocation("tc2"),
|
||||
result("tc2", "echo", "sync", { n: 2 }),
|
||||
result("tc1", "echo", "sync", "plain text"),
|
||||
requested(1, [
|
||||
{ kind: "assistant", modelCallIndex: 0 },
|
||||
{ kind: "toolResult", toolCallId: "tc1" },
|
||||
{ kind: "toolResult", toolCallId: "tc2" },
|
||||
"assistant:0",
|
||||
"toolResult:tc1",
|
||||
"toolResult:tc2",
|
||||
]),
|
||||
completed(1, assistantText("done")),
|
||||
turnCompletedEv(),
|
||||
|
|
@ -1326,7 +1326,7 @@ describe("derivations", () => {
|
|||
it("omits failed model calls from the transcript", () => {
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
callFailed(0, "interrupted"),
|
||||
requested(1, []),
|
||||
completed(1, assistantText("done")),
|
||||
|
|
@ -1338,7 +1338,7 @@ describe("derivations", () => {
|
|||
it("transcript excludes the context prefix", () => {
|
||||
const state = reduceTurn([
|
||||
created({ context: { previousTurnId: PREV_TURN_ID } }),
|
||||
requested(0, [{ kind: "input" }], {
|
||||
requested(0, ["input"], {
|
||||
contextRef: { previousTurnId: PREV_TURN_ID },
|
||||
}),
|
||||
completed(0, assistantText("done")),
|
||||
|
|
@ -1352,7 +1352,7 @@ describe("derivations", () => {
|
|||
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
||||
const state = reduceTurn([
|
||||
created(),
|
||||
requested(0, [{ kind: "input" }]),
|
||||
requested(0, ["input"]),
|
||||
completed(0, call0),
|
||||
]);
|
||||
expect(() => turnTranscript(state)).toThrowError(/unresolved/);
|
||||
|
|
|
|||
|
|
@ -130,15 +130,39 @@ export const TurnCreated = z.object({
|
|||
// identical to turn_created.agent.resolved by construction. The exact
|
||||
// provider payload is rebuilt deterministically by the request composer
|
||||
// (core), which is the same code path the loop sends through.
|
||||
export const ModelRequestMessageRef = z.discriminatedUnion("kind", [
|
||||
z.object({ kind: z.literal("context") }),
|
||||
z.object({ kind: z.literal("input") }),
|
||||
z.object({
|
||||
kind: z.literal("assistant"),
|
||||
modelCallIndex: z.number().int().nonnegative(),
|
||||
}),
|
||||
z.object({ kind: z.literal("toolResult"), toolCallId: z.string() }),
|
||||
]);
|
||||
// Compact string refs so raw JSONL reads naturally:
|
||||
// "context" | "input" | "assistant:<modelCallIndex>" | "toolResult:<toolCallId>"
|
||||
export const ModelRequestMessageRef = z
|
||||
.string()
|
||||
.regex(/^(context|input|assistant:\d+|toolResult:.+)$/);
|
||||
|
||||
export type ParsedRequestRef =
|
||||
| { kind: "context" }
|
||||
| { kind: "input" }
|
||||
| { kind: "assistant"; modelCallIndex: number }
|
||||
| { kind: "toolResult"; toolCallId: string };
|
||||
|
||||
export const assistantRef = (modelCallIndex: number): string =>
|
||||
`assistant:${modelCallIndex}`;
|
||||
export const toolResultRef = (toolCallId: string): string =>
|
||||
`toolResult:${toolCallId}`;
|
||||
|
||||
export function parseRequestRef(ref: string): ParsedRequestRef {
|
||||
if (ref === "context" || ref === "input") {
|
||||
return { kind: ref };
|
||||
}
|
||||
if (ref.startsWith("assistant:")) {
|
||||
const modelCallIndex = Number(ref.slice("assistant:".length));
|
||||
if (!Number.isInteger(modelCallIndex) || modelCallIndex < 0) {
|
||||
throw new Error(`malformed request ref: ${ref}`);
|
||||
}
|
||||
return { kind: "assistant", modelCallIndex };
|
||||
}
|
||||
if (ref.startsWith("toolResult:")) {
|
||||
return { kind: "toolResult", toolCallId: ref.slice("toolResult:".length) };
|
||||
}
|
||||
throw new Error(`malformed request ref: ${ref}`);
|
||||
}
|
||||
|
||||
export const ModelRequest = z.object({
|
||||
contextRef: TurnContextRef.optional(),
|
||||
|
|
@ -505,21 +529,18 @@ function applyModelCallRequested(
|
|||
}
|
||||
|
||||
const context = state.definition.context;
|
||||
let expectedRefs: Array<z.infer<typeof ModelRequestMessageRef>>;
|
||||
let expectedRefs: string[];
|
||||
if (event.modelCallIndex === 0) {
|
||||
if (isContextRef(context)) {
|
||||
if (event.request.contextRef?.previousTurnId !== context.previousTurnId) {
|
||||
fail("model request contextRef inconsistent with turn context");
|
||||
}
|
||||
expectedRefs = [{ kind: "input" }];
|
||||
expectedRefs = ["input"];
|
||||
} else {
|
||||
if (event.request.contextRef !== undefined) {
|
||||
fail("model request has contextRef but turn context is inline");
|
||||
}
|
||||
expectedRefs =
|
||||
context.length > 0
|
||||
? [{ kind: "context" }, { kind: "input" }]
|
||||
: [{ kind: "input" }];
|
||||
expectedRefs = context.length > 0 ? ["context", "input"] : ["input"];
|
||||
}
|
||||
} else {
|
||||
if (event.request.contextRef !== undefined) {
|
||||
|
|
@ -529,11 +550,10 @@ function applyModelCallRequested(
|
|||
expectedRefs =
|
||||
previous.response !== undefined
|
||||
? [
|
||||
{ kind: "assistant", modelCallIndex: previous.index },
|
||||
...batchToolCalls(state, previous.index).map((tc) => ({
|
||||
kind: "toolResult" as const,
|
||||
toolCallId: tc.toolCallId,
|
||||
})),
|
||||
assistantRef(previous.index),
|
||||
...batchToolCalls(state, previous.index).map((tc) =>
|
||||
toolResultRef(tc.toolCallId),
|
||||
),
|
||||
]
|
||||
: []; // re-issue after an interrupted call adds nothing new
|
||||
}
|
||||
|
|
@ -988,7 +1008,8 @@ export function requestMessagesFor(
|
|||
if (!call) {
|
||||
throw new Error(`no model call at index ${modelCallIndex}`);
|
||||
}
|
||||
return call.request.messages.flatMap((ref) => {
|
||||
return call.request.messages.flatMap((raw) => {
|
||||
const ref = parseRequestRef(raw);
|
||||
switch (ref.kind) {
|
||||
case "context": {
|
||||
const context = state.definition.context;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue