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
|
|
@ -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