feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
|
import { z } from "zod";
|
|
|
|
|
import {
|
|
|
|
|
type JsonValue,
|
|
|
|
|
ModelCallCompleted,
|
|
|
|
|
ModelCallFailed,
|
|
|
|
|
ModelCallRequested,
|
|
|
|
|
ModelStepEvent,
|
|
|
|
|
MODEL_CALL_LIMIT_ERROR_CODE,
|
|
|
|
|
ToolDescriptor,
|
|
|
|
|
ToolInvocationRequested,
|
|
|
|
|
ToolPermissionClassificationFailed,
|
|
|
|
|
ToolPermissionClassified,
|
|
|
|
|
ToolPermissionRequired,
|
|
|
|
|
ToolPermissionResolved,
|
|
|
|
|
ToolProgress,
|
|
|
|
|
ToolResult,
|
|
|
|
|
TurnCancelled,
|
|
|
|
|
TurnCompleted,
|
|
|
|
|
TurnCorruptionError,
|
|
|
|
|
TurnCreated,
|
|
|
|
|
TurnEvent,
|
|
|
|
|
TurnFailed,
|
|
|
|
|
TurnSuspended,
|
|
|
|
|
deriveTurnStatus,
|
|
|
|
|
outstandingAsyncTools,
|
|
|
|
|
outstandingPermissions,
|
|
|
|
|
reduceTurn,
|
|
|
|
|
turnTranscript,
|
|
|
|
|
} from "./turns.js";
|
|
|
|
|
|
|
|
|
|
type TEvent = z.infer<typeof TurnEvent>;
|
|
|
|
|
|
|
|
|
|
const TURN_ID = "2026-07-02T10-00-00Z-0000001-000";
|
|
|
|
|
const PREV_TURN_ID = "2026-07-02T09-00-00Z-0000001-000";
|
|
|
|
|
const TS = "2026-07-02T10:00:00Z";
|
|
|
|
|
|
|
|
|
|
const echoTool: z.infer<typeof ToolDescriptor> = {
|
|
|
|
|
toolId: "tool.echo",
|
|
|
|
|
name: "echo",
|
|
|
|
|
description: "Echo tool",
|
|
|
|
|
inputSchema: {},
|
|
|
|
|
execution: "sync",
|
|
|
|
|
requiresHuman: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fetchTool: z.infer<typeof ToolDescriptor> = {
|
|
|
|
|
toolId: "tool.fetch",
|
|
|
|
|
name: "fetch",
|
|
|
|
|
description: "Async fetch tool",
|
|
|
|
|
inputSchema: {},
|
|
|
|
|
execution: "async",
|
|
|
|
|
requiresHuman: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function user(text: string) {
|
|
|
|
|
return { role: "user" as const, content: text };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assistantText(text: string) {
|
|
|
|
|
return { role: "assistant" as const, content: text };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toolCallPart(id: string, name: string, args: JsonValue = {}) {
|
|
|
|
|
return {
|
|
|
|
|
type: "tool-call" as const,
|
|
|
|
|
toolCallId: id,
|
|
|
|
|
toolName: name,
|
|
|
|
|
arguments: args,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assistantCalls(...parts: Array<ReturnType<typeof toolCallPart>>) {
|
|
|
|
|
return { role: "assistant" as const, content: parts };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toolMsg(id: string, name: string, content = "ok") {
|
|
|
|
|
return { role: "tool" as const, content, toolCallId: id, toolName: name };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function created(
|
|
|
|
|
overrides: Partial<z.infer<typeof TurnCreated>> = {},
|
|
|
|
|
): z.infer<typeof TurnCreated> {
|
|
|
|
|
return {
|
|
|
|
|
type: "turn_created",
|
|
|
|
|
schemaVersion: 1,
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
sessionId: null,
|
|
|
|
|
agent: {
|
|
|
|
|
requested: { agentId: "copilot" },
|
|
|
|
|
resolved: {
|
|
|
|
|
agentId: "copilot",
|
|
|
|
|
systemPrompt: "SYS",
|
|
|
|
|
model: { provider: "openai", model: "gpt-test" },
|
|
|
|
|
tools: [echoTool, fetchTool],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
context: [],
|
|
|
|
|
input: user("hello"),
|
|
|
|
|
config: {
|
|
|
|
|
autoPermission: false,
|
|
|
|
|
humanAvailable: true,
|
|
|
|
|
maxModelCalls: 20,
|
|
|
|
|
},
|
|
|
|
|
...overrides,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function requested(
|
|
|
|
|
index: number,
|
|
|
|
|
messages: z.infer<typeof ModelCallRequested>["request"]["messages"],
|
|
|
|
|
requestOverrides: Partial<z.infer<typeof ModelCallRequested>["request"]> = {},
|
|
|
|
|
): z.infer<typeof ModelCallRequested> {
|
|
|
|
|
return {
|
|
|
|
|
type: "model_call_requested",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
modelCallIndex: index,
|
|
|
|
|
request: {
|
|
|
|
|
messages,
|
|
|
|
|
parameters: {},
|
|
|
|
|
...requestOverrides,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function completed(
|
|
|
|
|
index: number,
|
|
|
|
|
message: z.infer<typeof ModelCallCompleted>["message"],
|
|
|
|
|
usage: z.infer<typeof ModelCallCompleted>["usage"] = {
|
|
|
|
|
inputTokens: 10,
|
|
|
|
|
outputTokens: 5,
|
|
|
|
|
totalTokens: 15,
|
|
|
|
|
},
|
|
|
|
|
): z.infer<typeof ModelCallCompleted> {
|
|
|
|
|
return {
|
|
|
|
|
type: "model_call_completed",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
modelCallIndex: index,
|
|
|
|
|
message,
|
|
|
|
|
finishReason: "stop",
|
|
|
|
|
usage,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function callFailed(
|
|
|
|
|
index: number,
|
|
|
|
|
error = "provider exploded",
|
|
|
|
|
): z.infer<typeof ModelCallFailed> {
|
|
|
|
|
return {
|
|
|
|
|
type: "model_call_failed",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
modelCallIndex: index,
|
|
|
|
|
error,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stepEvent(
|
|
|
|
|
index: number,
|
|
|
|
|
event: z.infer<typeof ModelStepEvent>["event"] = {
|
|
|
|
|
type: "text_end",
|
|
|
|
|
text: "chunk",
|
|
|
|
|
},
|
|
|
|
|
): z.infer<typeof ModelStepEvent> {
|
|
|
|
|
return {
|
|
|
|
|
type: "model_step_event",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
modelCallIndex: index,
|
|
|
|
|
event,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function permRequired(
|
|
|
|
|
id: string,
|
|
|
|
|
name: string,
|
|
|
|
|
checkerError?: string,
|
|
|
|
|
): z.infer<typeof ToolPermissionRequired> {
|
|
|
|
|
return {
|
|
|
|
|
type: "tool_permission_required",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
toolCallId: id,
|
|
|
|
|
toolName: name,
|
|
|
|
|
request: { tool: name },
|
|
|
|
|
...(checkerError === undefined ? {} : { checkerError }),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function permClassified(
|
|
|
|
|
id: string,
|
|
|
|
|
decision: z.infer<typeof ToolPermissionClassified>["decision"],
|
|
|
|
|
): z.infer<typeof ToolPermissionClassified> {
|
|
|
|
|
return {
|
|
|
|
|
type: "tool_permission_classified",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
toolCallId: id,
|
|
|
|
|
decision,
|
|
|
|
|
reason: "because",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function permClassificationFailed(
|
|
|
|
|
ids: string[],
|
|
|
|
|
): z.infer<typeof ToolPermissionClassificationFailed> {
|
|
|
|
|
return {
|
|
|
|
|
type: "tool_permission_classification_failed",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
toolCallIds: ids,
|
|
|
|
|
error: "classifier timed out",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function permResolved(
|
|
|
|
|
id: string,
|
|
|
|
|
decision: z.infer<typeof ToolPermissionResolved>["decision"],
|
|
|
|
|
source: z.infer<typeof ToolPermissionResolved>["source"] = "human",
|
|
|
|
|
): z.infer<typeof ToolPermissionResolved> {
|
|
|
|
|
return {
|
|
|
|
|
type: "tool_permission_resolved",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
toolCallId: id,
|
|
|
|
|
decision,
|
|
|
|
|
source,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function invocation(
|
|
|
|
|
id: string,
|
|
|
|
|
tool: z.infer<typeof ToolDescriptor> = echoTool,
|
|
|
|
|
overrides: Partial<z.infer<typeof ToolInvocationRequested>> = {},
|
|
|
|
|
): z.infer<typeof ToolInvocationRequested> {
|
|
|
|
|
return {
|
|
|
|
|
type: "tool_invocation_requested",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
toolCallId: id,
|
|
|
|
|
toolId: tool.toolId,
|
|
|
|
|
toolName: tool.name,
|
|
|
|
|
execution: tool.execution,
|
|
|
|
|
input: {},
|
|
|
|
|
...overrides,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function progress(
|
|
|
|
|
id: string,
|
|
|
|
|
source: z.infer<typeof ToolProgress>["source"] = "sync",
|
|
|
|
|
): z.infer<typeof ToolProgress> {
|
|
|
|
|
return {
|
|
|
|
|
type: "tool_progress",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
toolCallId: id,
|
|
|
|
|
source,
|
|
|
|
|
progress: { pct: 50 },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function result(
|
|
|
|
|
id: string,
|
|
|
|
|
name: string,
|
|
|
|
|
source: z.infer<typeof ToolResult>["source"] = "sync",
|
|
|
|
|
output: JsonValue = "ok",
|
|
|
|
|
isError = false,
|
|
|
|
|
): z.infer<typeof ToolResult> {
|
|
|
|
|
return {
|
|
|
|
|
type: "tool_result",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
toolCallId: id,
|
|
|
|
|
toolName: name,
|
|
|
|
|
source,
|
|
|
|
|
result: { output, isError },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function suspendedEv(
|
|
|
|
|
perms: Array<{ id: string; name: string }>,
|
|
|
|
|
asyncs: Array<{ id: string; tool: z.infer<typeof ToolDescriptor> }>,
|
|
|
|
|
): z.infer<typeof TurnSuspended> {
|
|
|
|
|
return {
|
|
|
|
|
type: "turn_suspended",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
pendingPermissions: perms.map((p) => ({
|
|
|
|
|
toolCallId: p.id,
|
|
|
|
|
toolName: p.name,
|
|
|
|
|
request: { tool: p.name },
|
|
|
|
|
})),
|
|
|
|
|
pendingAsyncTools: asyncs.map((a) => ({
|
|
|
|
|
toolCallId: a.id,
|
|
|
|
|
toolId: a.tool.toolId,
|
|
|
|
|
toolName: a.tool.name,
|
|
|
|
|
input: {},
|
|
|
|
|
})),
|
|
|
|
|
usage: {},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function turnCompletedEv(
|
|
|
|
|
output: z.infer<typeof TurnCompleted>["output"] = assistantText("done"),
|
|
|
|
|
): z.infer<typeof TurnCompleted> {
|
|
|
|
|
return {
|
|
|
|
|
type: "turn_completed",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
output,
|
|
|
|
|
finishReason: "stop",
|
|
|
|
|
usage: {},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function turnFailedEv(
|
|
|
|
|
error = "it broke",
|
|
|
|
|
code?: string,
|
|
|
|
|
): z.infer<typeof TurnFailed> {
|
|
|
|
|
return {
|
|
|
|
|
type: "turn_failed",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
error,
|
|
|
|
|
...(code === undefined ? {} : { code }),
|
|
|
|
|
usage: {},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function turnCancelledEv(reason?: string): z.infer<typeof TurnCancelled> {
|
|
|
|
|
return {
|
|
|
|
|
type: "turn_cancelled",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
...(reason === undefined ? {} : { reason }),
|
|
|
|
|
usage: {},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A complete happy-path sequence with one sync tool round trip.
|
|
|
|
|
function syncToolSequence(): TEvent[] {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
return [
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
permResolved("tc1", "allow"),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(1, ["assistant:0", "toolResult:tc1"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(1, assistantText("done")),
|
|
|
|
|
turnCompletedEv(),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function expectCorruption(events: TEvent[], match: string | RegExp): void {
|
|
|
|
|
expect(() => reduceTurn(events)).toThrowError(TurnCorruptionError);
|
|
|
|
|
expect(() => reduceTurn(events)).toThrowError(match);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("schemas", () => {
|
|
|
|
|
it("every builder output round-trips through the TurnEvent schema", () => {
|
|
|
|
|
for (const event of syncToolSequence()) {
|
|
|
|
|
expect(TurnEvent.parse(event)).toEqual(event);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects system-role messages in inline context", () => {
|
|
|
|
|
const bad = created({
|
|
|
|
|
context: [
|
|
|
|
|
{ role: "system", content: "sneaky" },
|
|
|
|
|
] as unknown as z.infer<typeof TurnCreated>["context"],
|
|
|
|
|
});
|
|
|
|
|
expect(() => TurnCreated.parse(bad)).toThrowError();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("plain completion", () => {
|
|
|
|
|
it("reduces a created-only log to an idle empty state", () => {
|
|
|
|
|
const state = reduceTurn([created()]);
|
|
|
|
|
expect(state.definition.turnId).toBe(TURN_ID);
|
|
|
|
|
expect(state.modelCalls).toEqual([]);
|
|
|
|
|
expect(state.toolCalls).toEqual([]);
|
|
|
|
|
expect(state.terminal).toBeUndefined();
|
|
|
|
|
expect(deriveTurnStatus(state)).toBe("idle");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("reduces a plain model response to a completed turn", () => {
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
stepEvent(0),
|
|
|
|
|
completed(0, assistantText("done")),
|
|
|
|
|
turnCompletedEv(),
|
|
|
|
|
]);
|
|
|
|
|
expect(state.modelCalls).toHaveLength(1);
|
|
|
|
|
expect(state.modelCalls[0].response).toEqual(assistantText("done"));
|
|
|
|
|
expect(state.modelCalls[0].finishReason).toBe("stop");
|
|
|
|
|
expect(state.modelCalls[0].stepEvents).toHaveLength(1);
|
|
|
|
|
expect(state.terminal?.type).toBe("turn_completed");
|
|
|
|
|
expect(deriveTurnStatus(state)).toBe("completed");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("aggregates usage across model calls", () => {
|
|
|
|
|
const state = reduceTurn(syncToolSequence());
|
|
|
|
|
expect(state.usage).toEqual({
|
|
|
|
|
inputTokens: 20,
|
|
|
|
|
outputTokens: 10,
|
|
|
|
|
totalTokens: 30,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("leaves usage fields undefined when never reported", () => {
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, assistantText("a"), { inputTokens: 5 }),
|
|
|
|
|
]);
|
|
|
|
|
expect(state.usage).toEqual({ inputTokens: 5 });
|
|
|
|
|
expect(state.usage.cachedInputTokens).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("tool execution", () => {
|
|
|
|
|
it("extracts tool calls with order, batch index, and descriptor identity", () => {
|
|
|
|
|
const call0 = assistantCalls(
|
|
|
|
|
toolCallPart("tc1", "echo", { text: "a" }),
|
|
|
|
|
toolCallPart("a1", "fetch", { url: "u" }),
|
|
|
|
|
);
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
]);
|
|
|
|
|
expect(state.toolCalls).toHaveLength(2);
|
|
|
|
|
expect(state.toolCalls[0]).toMatchObject({
|
|
|
|
|
toolCallId: "tc1",
|
|
|
|
|
toolName: "echo",
|
|
|
|
|
toolId: "tool.echo",
|
|
|
|
|
execution: "sync",
|
|
|
|
|
modelCallIndex: 0,
|
|
|
|
|
order: 0,
|
|
|
|
|
input: { text: "a" },
|
|
|
|
|
});
|
|
|
|
|
expect(state.toolCalls[1]).toMatchObject({
|
|
|
|
|
toolCallId: "a1",
|
|
|
|
|
execution: "async",
|
|
|
|
|
order: 1,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("leaves identity undefined for tools missing from the agent snapshot", () => {
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, assistantCalls(toolCallPart("x1", "unknown-tool"))),
|
|
|
|
|
result("x1", "unknown-tool", "runtime", "No such tool", true),
|
|
|
|
|
]);
|
|
|
|
|
expect(state.toolCalls[0].toolId).toBeUndefined();
|
|
|
|
|
expect(state.toolCalls[0].execution).toBeUndefined();
|
|
|
|
|
expect(state.toolCalls[0].result?.result.isError).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("reduces a full sync tool round trip", () => {
|
|
|
|
|
const state = reduceTurn(syncToolSequence());
|
|
|
|
|
const tc = state.toolCalls[0];
|
|
|
|
|
expect(tc.permission?.required).toBeDefined();
|
|
|
|
|
expect(tc.permission?.resolved?.decision).toBe("allow");
|
|
|
|
|
expect(tc.invocation).toBeDefined();
|
|
|
|
|
expect(tc.result?.source).toBe("sync");
|
|
|
|
|
expect(state.terminal?.type).toBe("turn_completed");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("records classifier provenance separately from the effective decision", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created({ config: { autoPermission: true, humanAvailable: true, maxModelCalls: 20 } }),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
permClassified("tc1", "allow"),
|
|
|
|
|
permResolved("tc1", "allow", "classifier"),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
|
|
|
|
]);
|
|
|
|
|
const permission = state.toolCalls[0].permission;
|
|
|
|
|
expect(permission?.classification?.decision).toBe("allow");
|
|
|
|
|
expect(permission?.resolved?.source).toBe("classifier");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("accepts classification failure as audit and continues to human resolution", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
permClassificationFailed(["tc1"]),
|
|
|
|
|
permResolved("tc1", "deny", "human"),
|
|
|
|
|
result("tc1", "echo", "runtime", "Permission denied", true),
|
|
|
|
|
]);
|
feat(x/core): turn runtime with event-sourced execution loop (stage 2)
The TurnRuntime per turn-runtime-design.md: constructor-injected,
container-ignorant, no per-turn state — every advanceTurn reconstructs
from the JSONL log, so normal execution and crash recovery share one
path.
- runtime.ts: createTurn/advanceTurn/getTurn; durable-barrier appends
before side effects; sequential sync tools with progress; async tool
exposure + durable suspension; permission pipeline (checker →
optional classifier batch → human/deny fallback, fail-closed on
checker errors); cancellation with synthetic results; recovery that
re-issues interrupted model calls and continues after interrupted
sync tools; model-call-limit exhaustion with machine-readable code.
- fs-repo.ts: date-partitioned append-only JSONL, strict line
validation, path-traversal rejection, per-turn in-process locking.
- stream.ts: hot execution stream — buffers before the consumer
attaches, outcome independent of consumption, close-drops-events.
- context-resolver.ts: chain-walking materialization of context
references with cycle detection.
- Seam interfaces for stage-4 bridges: model/tool registries,
permission checker/classifier, agent resolver, lifecycle bus, clock.
- shared reducer: track classification failures durably so failed
classifications are never re-run.
67 new tests: all nine §26 end-to-end scenarios (mocked deps,
recovery boundaries seeded directly) plus repo/stream/resolver suites.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 09:32:27 +05:30
|
|
|
expect(state.toolCalls[0].permission?.classificationFailed).toBe(true);
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
expect(state.toolCalls[0].result?.result.isError).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("accumulates tool progress", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
progress("tc1"),
|
|
|
|
|
progress("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
|
|
|
|
]);
|
|
|
|
|
expect(state.toolCalls[0].progress).toHaveLength(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("accepts denial results without invocation (runtime source)", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
permResolved("tc1", "deny"),
|
|
|
|
|
result("tc1", "echo", "runtime", "Permission denied", true),
|
|
|
|
|
]);
|
|
|
|
|
expect(state.toolCalls[0].invocation).toBeUndefined();
|
|
|
|
|
expect(state.toolCalls[0].result?.source).toBe("runtime");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("context references", () => {
|
|
|
|
|
it("accepts a referenced context with matching contextRef on requests", () => {
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created({ context: { previousTurnId: PREV_TURN_ID } }),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"], {
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
contextRef: { previousTurnId: PREV_TURN_ID },
|
|
|
|
|
}),
|
|
|
|
|
completed(0, assistantText("done")),
|
|
|
|
|
turnCompletedEv(),
|
|
|
|
|
]);
|
|
|
|
|
expect(deriveTurnStatus(state)).toBe("completed");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects a request missing the contextRef when context is a reference", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created({ context: { previousTurnId: PREV_TURN_ID } }),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
],
|
|
|
|
|
/contextRef inconsistent/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects a request whose contextRef targets the wrong turn", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created({ context: { previousTurnId: PREV_TURN_ID } }),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"], {
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
contextRef: { previousTurnId: "some-other-turn" },
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
/contextRef inconsistent/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects a contextRef when the turn context is inline", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"], {
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
contextRef: { previousTurnId: PREV_TURN_ID },
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
/contextRef but turn context is inline/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
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>
2026-07-02 12:46:23 +05:30
|
|
|
it("requires a {context} ref before {input} when inline context is nonempty", () => {
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
const context = [
|
|
|
|
|
user("earlier"),
|
|
|
|
|
assistantCalls(toolCallPart("old1", "echo")),
|
|
|
|
|
toolMsg("old1", "echo"),
|
|
|
|
|
];
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created({ context }),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["context", "input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, assistantText("done")),
|
|
|
|
|
turnCompletedEv(),
|
|
|
|
|
]);
|
|
|
|
|
expect(deriveTurnStatus(state)).toBe("completed");
|
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>
2026-07-02 12:46:23 +05:30
|
|
|
expectCorruption(
|
2026-07-02 13:00:33 +05:30
|
|
|
[created({ context }), requested(0, ["input"])],
|
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>
2026-07-02 12:46:23 +05:30
|
|
|
/references do not match/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects a contextRef on a non-initial model call", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created({ context: { previousTurnId: PREV_TURN_ID } }),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"], {
|
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>
2026-07-02 12:46:23 +05:30
|
|
|
contextRef: { previousTurnId: PREV_TURN_ID },
|
|
|
|
|
}),
|
|
|
|
|
completed(0, assistantCalls(toolCallPart("tc1", "echo"))),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
|
|
|
|
requested(
|
|
|
|
|
1,
|
|
|
|
|
[
|
2026-07-02 13:00:33 +05:30
|
|
|
"assistant:0",
|
|
|
|
|
"toolResult:tc1",
|
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>
2026-07-02 12:46:23 +05:30
|
|
|
],
|
|
|
|
|
{ contextRef: { previousTurnId: PREV_TURN_ID } },
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
/contextRef on a non-initial model call/,
|
|
|
|
|
);
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
feat(x): agent snapshot inheritance, session inspection, runtime docs
Third application of the reference mechanism: session turns whose
system prompt + tools are byte-identical to the context predecessor's
materialized snapshot persist { agentId, model, inheritedFrom } instead
of ~70KB per turn (measured: turn 2 of a session is now ~1.1KB total).
Inheritance is decided at createTurn by equality against the
materialized predecessor; the model stays concrete, and on
materialization the inherited record's own agentId/model win — a rule
the new test matrix caught as a real bug (the chain base's model was
overriding a mid-session model switch). Reducer invariants: inherited
snapshots must reference the context predecessor; tool identity arrives
via invocation events.
Test matrix per review: prompt-diff -> full snapshot, tools-diff ->
full snapshot, model-switch -> inherits with concrete model, multi-hop
chains materialize, standalone turns never inherit, unreadable
predecessor falls back to full, cyclic inheritance is corruption,
sessions denormalize the model from inherited snapshots.
The inspector now handles sessions too (auto-detected): overview with
per-turn status/size/input preview, --turns to cascade full turn
inspection. Documented in a new repo-root AGENTS.md (storage layout,
reference model, inspector usage, invariants) with a CLAUDE.md pointer.
Breaking for dev turn files: wipe ~/.rowboat/storage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 13:17:30 +05:30
|
|
|
describe("inherited agent snapshots", () => {
|
|
|
|
|
const inherited = {
|
|
|
|
|
agentId: "copilot",
|
|
|
|
|
model: { provider: "openai", model: "gpt-test" },
|
|
|
|
|
inheritedFrom: PREV_TURN_ID,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
it("accepts inheritance referencing the context predecessor; identity arrives via invocation", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created({
|
|
|
|
|
context: { previousTurnId: PREV_TURN_ID },
|
|
|
|
|
agent: { requested: { agentId: "copilot" }, resolved: inherited },
|
|
|
|
|
}),
|
|
|
|
|
requested(0, ["input"], { contextRef: { previousTurnId: PREV_TURN_ID } }),
|
|
|
|
|
completed(0, call0),
|
|
|
|
|
]);
|
|
|
|
|
// No descriptor lookup without the concrete snapshot…
|
|
|
|
|
expect(state.toolCalls[0].toolId).toBeUndefined();
|
|
|
|
|
expect(state.toolCalls[0].execution).toBeUndefined();
|
|
|
|
|
|
|
|
|
|
// …until tool_invocation_requested supplies identity.
|
|
|
|
|
const withInvocation = reduceTurn([
|
|
|
|
|
created({
|
|
|
|
|
context: { previousTurnId: PREV_TURN_ID },
|
|
|
|
|
agent: { requested: { agentId: "copilot" }, resolved: inherited },
|
|
|
|
|
}),
|
|
|
|
|
requested(0, ["input"], { contextRef: { previousTurnId: PREV_TURN_ID } }),
|
|
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
|
|
|
|
]);
|
|
|
|
|
expect(withInvocation.toolCalls[0].toolId).toBe("tool.echo");
|
|
|
|
|
expect(withInvocation.toolCalls[0].execution).toBe("sync");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects an inherited snapshot on an inline-context turn", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created({
|
|
|
|
|
context: [],
|
|
|
|
|
agent: { requested: { agentId: "copilot" }, resolved: inherited },
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
/inherited agent snapshot must reference the turn's context predecessor/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects an inherited snapshot pointing at a different turn than the context", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created({
|
|
|
|
|
context: { previousTurnId: "some-other-turn" },
|
|
|
|
|
agent: { requested: { agentId: "copilot" }, resolved: inherited },
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
/inherited agent snapshot must reference the turn's context predecessor/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
describe("suspension", () => {
|
|
|
|
|
it("accepts a snapshot matching pending permissions and async tools", () => {
|
|
|
|
|
const call0 = assistantCalls(
|
|
|
|
|
toolCallPart("tc1", "echo"),
|
|
|
|
|
toolCallPart("a1", "fetch"),
|
|
|
|
|
);
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
invocation("a1", fetchTool),
|
|
|
|
|
suspendedEv([{ id: "tc1", name: "echo" }], [{ id: "a1", tool: fetchTool }]),
|
|
|
|
|
]);
|
|
|
|
|
expect(state.suspension?.pendingPermissions).toHaveLength(1);
|
|
|
|
|
expect(state.suspension?.pendingAsyncTools).toHaveLength(1);
|
|
|
|
|
expect(deriveTurnStatus(state)).toBe("suspended");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("replaces the snapshot as inputs arrive, one at a time", () => {
|
|
|
|
|
const call0 = assistantCalls(
|
|
|
|
|
toolCallPart("tc1", "echo"),
|
|
|
|
|
toolCallPart("a1", "fetch"),
|
|
|
|
|
);
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
invocation("a1", fetchTool),
|
|
|
|
|
suspendedEv([{ id: "tc1", name: "echo" }], [{ id: "a1", tool: fetchTool }]),
|
|
|
|
|
result("a1", "fetch", "async", { data: 1 }),
|
|
|
|
|
suspendedEv([{ id: "tc1", name: "echo" }], []),
|
|
|
|
|
]);
|
|
|
|
|
expect(state.suspension?.pendingAsyncTools).toHaveLength(0);
|
|
|
|
|
expect(outstandingPermissions(state)).toHaveLength(1);
|
|
|
|
|
expect(outstandingAsyncTools(state)).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("supports async results arriving in any order before the next model call", () => {
|
|
|
|
|
const call0 = assistantCalls(
|
|
|
|
|
toolCallPart("a1", "fetch"),
|
|
|
|
|
toolCallPart("a2", "fetch"),
|
|
|
|
|
);
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("a1", fetchTool),
|
|
|
|
|
invocation("a2", fetchTool),
|
|
|
|
|
suspendedEv([], [{ id: "a1", tool: fetchTool }, { id: "a2", tool: fetchTool }]),
|
|
|
|
|
result("a2", "fetch", "async", "second"),
|
|
|
|
|
suspendedEv([], [{ id: "a1", tool: fetchTool }]),
|
|
|
|
|
result("a1", "fetch", "async", "first"),
|
|
|
|
|
requested(1, [
|
2026-07-02 13:00:33 +05:30
|
|
|
"assistant:0",
|
|
|
|
|
"toolResult:a1",
|
|
|
|
|
"toolResult:a2",
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
]),
|
|
|
|
|
completed(1, assistantText("done")),
|
|
|
|
|
turnCompletedEv(),
|
|
|
|
|
]);
|
|
|
|
|
expect(deriveTurnStatus(state)).toBe("completed");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects a snapshot claiming already-resolved work", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
permResolved("tc1", "allow"),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
|
|
|
|
suspendedEv([{ id: "tc1", name: "echo" }], []),
|
|
|
|
|
],
|
|
|
|
|
/suspension without pending external work/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects a snapshot omitting pending work", () => {
|
|
|
|
|
const call0 = assistantCalls(
|
|
|
|
|
toolCallPart("tc1", "echo"),
|
|
|
|
|
toolCallPart("a1", "fetch"),
|
|
|
|
|
);
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
invocation("a1", fetchTool),
|
|
|
|
|
suspendedEv([{ id: "tc1", name: "echo" }], []),
|
|
|
|
|
],
|
|
|
|
|
/suspension snapshot inconsistent/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects suspension while a model call is unsettled", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
suspendedEv([{ id: "tc1", name: "echo" }], []),
|
|
|
|
|
],
|
|
|
|
|
/suspension while a model call is unsettled/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("recovery-shaped histories", () => {
|
|
|
|
|
it("accepts an interrupted model call closed and re-issued (§23 fix)", () => {
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
callFailed(0, "interrupted by process restart"),
|
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>
2026-07-02 12:46:23 +05:30
|
|
|
requested(1, []),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(1, assistantText("done")),
|
|
|
|
|
turnCompletedEv(),
|
|
|
|
|
]);
|
|
|
|
|
expect(state.modelCalls[0].error).toMatch(/interrupted/);
|
|
|
|
|
expect(state.modelCalls[1].response).toEqual(assistantText("done"));
|
|
|
|
|
expect(deriveTurnStatus(state)).toBe("completed");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("re-issued model calls count against maxModelCalls", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created({
|
|
|
|
|
config: { autoPermission: false, humanAvailable: true, maxModelCalls: 1 },
|
|
|
|
|
}),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
callFailed(0, "interrupted"),
|
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>
2026-07-02 12:46:23 +05:30
|
|
|
requested(1, []),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
],
|
|
|
|
|
/exceeds maxModelCalls/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("accepts an interrupted sync tool closed with an indeterminate runtime result, turn continuing (§23 fix)", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result(
|
|
|
|
|
"tc1",
|
|
|
|
|
"echo",
|
|
|
|
|
"runtime",
|
|
|
|
|
"Tool execution was interrupted; its outcome is unknown and it was not retried.",
|
|
|
|
|
true,
|
|
|
|
|
),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(1, ["assistant:0", "toolResult:tc1"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(1, assistantText("done")),
|
|
|
|
|
turnCompletedEv(),
|
|
|
|
|
]);
|
|
|
|
|
expect(state.toolCalls[0].result?.source).toBe("runtime");
|
|
|
|
|
expect(state.terminal?.type).toBe("turn_completed");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("accepts cancellation with synthetic runtime results for unresolved calls", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("a1", "fetch"));
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("a1", fetchTool),
|
|
|
|
|
suspendedEv([], [{ id: "a1", tool: fetchTool }]),
|
|
|
|
|
result("a1", "fetch", "runtime", "Cancelled", true),
|
|
|
|
|
turnCancelledEv("user stop"),
|
|
|
|
|
]);
|
|
|
|
|
expect(state.terminal?.type).toBe("turn_cancelled");
|
|
|
|
|
expect(deriveTurnStatus(state)).toBe("cancelled");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("accepts a live model failure closing the turn", () => {
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
callFailed(0),
|
|
|
|
|
turnFailedEv("provider exploded"),
|
|
|
|
|
]);
|
|
|
|
|
expect(deriveTurnStatus(state)).toBe("failed");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("accepts model-call-limit exhaustion with a machine-readable code", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created({
|
|
|
|
|
config: { autoPermission: false, humanAvailable: true, maxModelCalls: 1 },
|
|
|
|
|
}),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
|
|
|
|
turnFailedEv("model call limit reached", MODEL_CALL_LIMIT_ERROR_CODE),
|
|
|
|
|
]);
|
|
|
|
|
expect(state.terminal?.type).toBe("turn_failed");
|
|
|
|
|
expect(
|
|
|
|
|
state.terminal?.type === "turn_failed" ? state.terminal.code : undefined,
|
|
|
|
|
).toBe(MODEL_CALL_LIMIT_ERROR_CODE);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("invariants", () => {
|
|
|
|
|
it("rejects an empty log", () => {
|
|
|
|
|
expectCorruption([], /empty/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects a log not starting with turn_created", () => {
|
2026-07-02 13:00:33 +05:30
|
|
|
expectCorruption([requested(0, ["input"])], /first event must be turn_created/);
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects duplicate turn_created", () => {
|
|
|
|
|
expectCorruption([created(), created()], /duplicate turn_created/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects mismatched turn ids", () => {
|
|
|
|
|
expectCorruption(
|
2026-07-02 13:00:33 +05:30
|
|
|
[created(), { ...requested(0, ["input"]), turnId: "other" }],
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
/does not match turn/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects unsupported schema versions", () => {
|
|
|
|
|
const bad = {
|
|
|
|
|
...created(),
|
|
|
|
|
schemaVersion: 2,
|
|
|
|
|
} as unknown as TEvent;
|
|
|
|
|
expectCorruption([bad], /unsupported turn schema version/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects unknown event types", () => {
|
|
|
|
|
const bad = {
|
|
|
|
|
type: "wat",
|
|
|
|
|
turnId: TURN_ID,
|
|
|
|
|
ts: TS,
|
|
|
|
|
} as unknown as TEvent;
|
|
|
|
|
expectCorruption([created(), bad], /unknown turn event type/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects out-of-order model call indices", () => {
|
|
|
|
|
expectCorruption(
|
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>
2026-07-02 12:46:23 +05:30
|
|
|
[created(), requested(1, [])],
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
/out of order/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects a reused model call index", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, assistantText("a")),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
],
|
|
|
|
|
/out of order/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects concurrent unresolved model requests", () => {
|
|
|
|
|
expectCorruption(
|
2026-07-02 13:00:33 +05:30
|
|
|
[created(), requested(0, ["input"]), requested(1, [])],
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
/concurrent unresolved model call requests/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects completion without a matching request", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[created(), completed(0, assistantText("a"))],
|
|
|
|
|
/without matching request/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects failure without a matching request", () => {
|
|
|
|
|
expectCorruption([created(), callFailed(0)], /without matching request/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects duplicate model call completion", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, assistantText("a")),
|
|
|
|
|
completed(0, assistantText("b")),
|
|
|
|
|
],
|
|
|
|
|
/duplicate settlement/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects completion after failure of the same call", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
callFailed(0),
|
|
|
|
|
completed(0, assistantText("a")),
|
|
|
|
|
],
|
|
|
|
|
/duplicate settlement/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects the next model call while tool calls are unresolved", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(1, ["assistant:0"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
],
|
|
|
|
|
/while tool calls are unresolved/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects a model call past the budget", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created({
|
|
|
|
|
config: { autoPermission: false, humanAvailable: true, maxModelCalls: 1 },
|
|
|
|
|
}),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(1, ["assistant:0", "toolResult:tc1"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
],
|
|
|
|
|
/exceeds maxModelCalls/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects duplicate tool call ids within one response", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(
|
|
|
|
|
0,
|
|
|
|
|
assistantCalls(toolCallPart("tc1", "echo"), toolCallPart("tc1", "echo")),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
/duplicate tool call id/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects duplicate tool call ids across responses", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(1, ["assistant:0", "toolResult:tc1"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(1, assistantCalls(toolCallPart("tc1", "echo"))),
|
|
|
|
|
],
|
|
|
|
|
/duplicate tool call id/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
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>
2026-07-02 12:46:23 +05:30
|
|
|
it("rejects request references whose tool-result order differs from the batch", () => {
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
const call0 = assistantCalls(
|
|
|
|
|
toolCallPart("tc1", "echo"),
|
|
|
|
|
toolCallPart("tc2", "echo"),
|
|
|
|
|
);
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
|
|
|
|
invocation("tc2"),
|
|
|
|
|
result("tc2", "echo"),
|
|
|
|
|
requested(1, [
|
2026-07-02 13:00:33 +05:30
|
|
|
"assistant:0",
|
|
|
|
|
"toolResult:tc2",
|
|
|
|
|
"toolResult:tc1",
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
]),
|
|
|
|
|
],
|
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>
2026-07-02 12:46:23 +05:30
|
|
|
/references do not match/,
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects permission records targeting unknown tool calls", () => {
|
|
|
|
|
expectCorruption(
|
2026-07-02 13:00:33 +05:30
|
|
|
[created(), requested(0, ["input"]), completed(0, assistantText("a")), permRequired("ghost", "echo")],
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
/unknown tool call/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects duplicate permission requirements", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
],
|
|
|
|
|
/duplicate permission requirement/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects classification without a requirement", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permClassified("tc1", "allow"),
|
|
|
|
|
],
|
|
|
|
|
/classification without permission requirement/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects resolution without a requirement", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permResolved("tc1", "allow"),
|
|
|
|
|
],
|
|
|
|
|
/resolution without requirement/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects conflicting effective permission decisions", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
permResolved("tc1", "allow"),
|
|
|
|
|
permResolved("tc1", "deny"),
|
|
|
|
|
],
|
|
|
|
|
/conflicting permission decisions/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects invocation while permission is pending", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
],
|
|
|
|
|
/invocation without permission allowance/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects invocation of a denied tool call", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
permResolved("tc1", "deny"),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
],
|
|
|
|
|
/invocation without permission allowance/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects duplicate invocations", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
],
|
|
|
|
|
/duplicate tool invocation/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects invocations whose execution mode contradicts the snapshot", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1", echoTool, { execution: "async" }),
|
|
|
|
|
],
|
|
|
|
|
/execution mismatch/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects progress without invocation", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
2026-07-02 13:00:33 +05:30
|
|
|
[created(), requested(0, ["input"]), completed(0, call0), progress("tc1")],
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
/progress without invocation/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects progress after a terminal tool result", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
|
|
|
|
progress("tc1"),
|
|
|
|
|
],
|
|
|
|
|
/progress after terminal result/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects duplicate tool results", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
|
|
|
|
result("tc1", "echo"),
|
|
|
|
|
],
|
|
|
|
|
/duplicate tool result/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects sync-sourced results without invocation", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
2026-07-02 13:00:33 +05:30
|
|
|
[created(), requested(0, ["input"]), completed(0, call0), result("tc1", "echo", "sync")],
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
/sync tool result without invocation/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects async results for sync tools", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo", "async"),
|
|
|
|
|
],
|
|
|
|
|
/result source mismatch/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects step events without a matching open call", () => {
|
|
|
|
|
expectCorruption([created(), stepEvent(0)], /without matching model call request/);
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, assistantText("a")),
|
|
|
|
|
stepEvent(0),
|
|
|
|
|
],
|
|
|
|
|
/after model call 0 settled/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects completion while tool calls remain unresolved", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
2026-07-02 13:00:33 +05:30
|
|
|
[created(), requested(0, ["input"]), completed(0, call0), turnCompletedEv()],
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
/completion while tool calls lack terminal results/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects completion when the final response has tool calls", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
result("tc1", "echo"),
|
|
|
|
|
turnCompletedEv(),
|
|
|
|
|
],
|
|
|
|
|
/final response has tool calls/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects completion without any completed model response", () => {
|
|
|
|
|
expectCorruption([created(), turnCompletedEv()], /without a completed model response/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects terminal failure while tool calls remain unresolved", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
expectCorruption(
|
2026-07-02 13:00:33 +05:30
|
|
|
[created(), requested(0, ["input"]), completed(0, call0), turnFailedEv()],
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
/failure while tool calls lack terminal results/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects terminal events while a model call is unsettled", () => {
|
|
|
|
|
expectCorruption(
|
2026-07-02 13:00:33 +05:30
|
|
|
[created(), requested(0, ["input"]), turnCancelledEv()],
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
/cancellation while a model call is unsettled/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects any event after a terminal event", () => {
|
|
|
|
|
const base = [
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, assistantText("a")),
|
|
|
|
|
];
|
|
|
|
|
for (const terminal of [turnCompletedEv(), turnFailedEv(), turnCancelledEv()]) {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[...base, terminal, stepEvent(0)],
|
|
|
|
|
/event after terminal turn event/,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects multiple terminal events", () => {
|
|
|
|
|
expectCorruption(
|
|
|
|
|
[
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, assistantText("a")),
|
|
|
|
|
turnCompletedEv(),
|
|
|
|
|
turnFailedEv(),
|
|
|
|
|
],
|
|
|
|
|
/event after terminal turn event/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("derivations", () => {
|
|
|
|
|
it("derives suspended for pending permissions and idle otherwise", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
const pending = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
permRequired("tc1", "echo"),
|
|
|
|
|
suspendedEv([{ id: "tc1", name: "echo" }], []),
|
|
|
|
|
]);
|
|
|
|
|
expect(deriveTurnStatus(pending)).toBe("suspended");
|
|
|
|
|
|
2026-07-02 13:00:33 +05:30
|
|
|
const idle = reduceTurn([created(), requested(0, ["input"]), completed(0, call0)]);
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
expect(deriveTurnStatus(idle)).toBe("idle");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("builds the turn transcript in source order with serialized results", () => {
|
|
|
|
|
const call0 = assistantCalls(
|
|
|
|
|
toolCallPart("tc1", "echo"),
|
|
|
|
|
toolCallPart("tc2", "echo"),
|
|
|
|
|
);
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
invocation("tc1"),
|
|
|
|
|
invocation("tc2"),
|
|
|
|
|
result("tc2", "echo", "sync", { n: 2 }),
|
|
|
|
|
result("tc1", "echo", "sync", "plain text"),
|
|
|
|
|
requested(1, [
|
2026-07-02 13:00:33 +05:30
|
|
|
"assistant:0",
|
|
|
|
|
"toolResult:tc1",
|
|
|
|
|
"toolResult:tc2",
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
]),
|
|
|
|
|
completed(1, assistantText("done")),
|
|
|
|
|
turnCompletedEv(),
|
|
|
|
|
]);
|
|
|
|
|
expect(turnTranscript(state)).toEqual([
|
|
|
|
|
user("hello"),
|
|
|
|
|
call0,
|
|
|
|
|
{ role: "tool", content: "plain text", toolCallId: "tc1", toolName: "echo" },
|
|
|
|
|
{ role: "tool", content: '{"n":2}', toolCallId: "tc2", toolName: "echo" },
|
|
|
|
|
assistantText("done"),
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("omits failed model calls from the transcript", () => {
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
callFailed(0, "interrupted"),
|
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>
2026-07-02 12:46:23 +05:30
|
|
|
requested(1, []),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(1, assistantText("done")),
|
|
|
|
|
turnCompletedEv(),
|
|
|
|
|
]);
|
|
|
|
|
expect(turnTranscript(state)).toEqual([user("hello"), assistantText("done")]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("transcript excludes the context prefix", () => {
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created({ context: { previousTurnId: PREV_TURN_ID } }),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"], {
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
contextRef: { previousTurnId: PREV_TURN_ID },
|
|
|
|
|
}),
|
|
|
|
|
completed(0, assistantText("done")),
|
|
|
|
|
turnCompletedEv(),
|
|
|
|
|
]);
|
|
|
|
|
expect(turnTranscript(state)[0]).toEqual(user("hello"));
|
|
|
|
|
expect(turnTranscript(state)).toHaveLength(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("transcript throws on unresolved tool calls", () => {
|
|
|
|
|
const call0 = assistantCalls(toolCallPart("tc1", "echo"));
|
|
|
|
|
const state = reduceTurn([
|
|
|
|
|
created(),
|
2026-07-02 13:00:33 +05:30
|
|
|
requested(0, ["input"]),
|
feat(x/shared): turn + session event schemas and pure reducers (stage 1)
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 07:46:07 +05:30
|
|
|
completed(0, call0),
|
|
|
|
|
]);
|
|
|
|
|
expect(() => turnTranscript(state)).toThrowError(/unresolved/);
|
|
|
|
|
});
|
|
|
|
|
});
|