diff --git a/docs/architecture/28-model-driven-tool-discovery.md b/docs/architecture/28-model-driven-tool-discovery.md index 7ded305206..95833ab27b 100644 --- a/docs/architecture/28-model-driven-tool-discovery.md +++ b/docs/architecture/28-model-driven-tool-discovery.md @@ -97,7 +97,7 @@ mechanism. ## Durability and resume Exposure state is persisted as a private durable checkpoint event, -`AGENT_RUN_TOOL_EXPOSURE_CHECKPOINT`, carrying a versioned +`AGENT_RUN_TOOL_EXPOSURE_CHECKPOINTED`, carrying a versioned `ToolExposureCheckpoint`. `restoreToolExposureState` rehydrates it, so a resumed run keeps the schemas it had already loaded instead of starting from an empty set. Restoration returns exposure, not authorization: execution still re-checks diff --git a/src/agent/ag-ui/chunk-encoder.test.ts b/src/agent/ag-ui/chunk-encoder.test.ts index d13f379868..ce054727bd 100644 --- a/src/agent/ag-ui/chunk-encoder.test.ts +++ b/src/agent/ag-ui/chunk-encoder.test.ts @@ -121,7 +121,7 @@ describe("agent/ag-ui-chunk-encoder", () => { payload: { toolCallId: "tool-1", input: { query: "ag-ui" }, - result: { ok: true }, + content: { ok: true }, }, }], ); diff --git a/src/agent/ag-ui/encoder.test.ts b/src/agent/ag-ui/encoder.test.ts index 672cbc78ad..b7240409a5 100644 --- a/src/agent/ag-ui/encoder.test.ts +++ b/src/agent/ag-ui/encoder.test.ts @@ -112,7 +112,7 @@ describe("agent/ag-ui-encoder", () => { }), [{ event: "ToolCallResult", - payload: { toolCallId: "tool-1", result: { ok: true } }, + payload: { toolCallId: "tool-1", content: { ok: true } }, }], ); assertEquals( @@ -313,7 +313,7 @@ describe("agent/ag-ui-encoder", () => { }, { event: "ToolCallResult", - payload: { toolCallId: "tool-2", result: { error: "invalid url" }, isError: true }, + payload: { toolCallId: "tool-2", content: { error: "invalid url" }, isError: true }, }, ], ); @@ -324,7 +324,7 @@ describe("agent/ag-ui-encoder", () => { }), [{ event: "ToolCallResult", - payload: { toolCallId: "tool-3", result: { error: "Tool output denied" }, isError: true }, + payload: { toolCallId: "tool-3", content: { error: "Tool output denied" }, isError: true }, }], ); }); @@ -499,7 +499,7 @@ describe("agent/ag-ui-encoder", () => { event: "ToolCallResult", payload: { toolCallId: "tool-provider", - result: { type: "web_search_result", answer: "resident" }, + content: { type: "web_search_result", answer: "resident" }, }, }], ); diff --git a/src/agent/ag-ui/encoder.ts b/src/agent/ag-ui/encoder.ts index 3e543352a7..2aed69e74b 100644 --- a/src/agent/ag-ui/encoder.ts +++ b/src/agent/ag-ui/encoder.ts @@ -603,16 +603,20 @@ function completeToolInput( return events; } +/** + * Tool results carry the canonical `content` field. The value is passed through + * unchanged, which is exactly what the API stores for a legacy `result` field. + */ function createToolResultEvent( toolCallId: unknown, - result: Record | unknown, + content: Record | unknown, isError = false, ): AgUiEncodedEvent { return { event: "ToolCallResult", payload: { toolCallId, - result, + content, ...(isError ? { isError: true } : {}), }, }; @@ -958,7 +962,7 @@ function mapRuntimeStreamEventToAgUiEventsUnstamped( event: "ToolCallResult", payload: { toolCallId: event.toolCallId, - result: { + content: { error: typeof event.errorText === "string" ? event.errorText : "Tool input failed", }, isError: true, diff --git a/src/agent/ag-ui/lifecycle-adapter.test.ts b/src/agent/ag-ui/lifecycle-adapter.test.ts index 43f3fdfeab..f7bd24fec1 100644 --- a/src/agent/ag-ui/lifecycle-adapter.test.ts +++ b/src/agent/ag-ui/lifecycle-adapter.test.ts @@ -231,7 +231,7 @@ describe("lifecycle AG-UI adapter", () => { events.at(-1)?.payload, { toolCallId: "provider-1", - result: { content: "final" }, + content: { content: "final" }, }, "the final provider result must reach the client even when it carries preliminary: false", ); diff --git a/src/agent/ag-ui/lifecycle-adapter.ts b/src/agent/ag-ui/lifecycle-adapter.ts index 28445353d9..bd4cf90dd8 100644 --- a/src/agent/ag-ui/lifecycle-adapter.ts +++ b/src/agent/ag-ui/lifecycle-adapter.ts @@ -268,7 +268,7 @@ export function createLifecycleAgUiAdapter(input: { event: "ToolCallResult", payload: { toolCallId: event.toolCallId, - result: { error: "Tool input was rejected before handoff" }, + content: { error: "Tool input was rejected before handoff" }, isError: true, }, }, @@ -282,7 +282,7 @@ export function createLifecycleAgUiAdapter(input: { event: "ToolCallResult", payload: { toolCallId: event.toolCallId, - result: safeJson(event.output), + content: safeJson(event.output), ...(event.isError ? { isError: true } : {}), }, }]; @@ -292,7 +292,7 @@ export function createLifecycleAgUiAdapter(input: { event: "ToolCallResult", payload: { toolCallId: event.toolCallId, - result: { error: "Tool output denied" }, + content: { error: "Tool output denied" }, isError: true, }, }]; @@ -302,7 +302,7 @@ export function createLifecycleAgUiAdapter(input: { event: "ToolCallResult", payload: { toolCallId: event.toolCallId, - result: { error: "Provider tool execution was cancelled" }, + content: { error: "Provider tool execution was cancelled" }, isError: true, }, }]; diff --git a/src/agent/ag-ui/runtime-event-encoder.test.ts b/src/agent/ag-ui/runtime-event-encoder.test.ts index 264d182dd3..7b6d046075 100644 --- a/src/agent/ag-ui/runtime-event-encoder.test.ts +++ b/src/agent/ag-ui/runtime-event-encoder.test.ts @@ -57,7 +57,7 @@ describe("agent/ag-ui-runtime-event-encoder", () => { payload: { toolCallId: "tool-1", input: { query: "ag-ui" }, - result: { ok: true }, + content: { ok: true }, }, }], ); diff --git a/src/agent/ag-ui/sse-parser.test.ts b/src/agent/ag-ui/sse-parser.test.ts index bf415d3dc4..5e393cb73e 100644 --- a/src/agent/ag-ui/sse-parser.test.ts +++ b/src/agent/ag-ui/sse-parser.test.ts @@ -70,6 +70,30 @@ describe("agent/ag-ui-sse-parser", () => { ); }); + it("serializes a raw ToolCallResult content value the same way it serialized result", async () => { + const response = createSseResponse([ + 'event: ToolCallResult\ndata: {"toolCallId":"tool-1","content":{"ok":true}}\n\n', + 'event: ToolCallResult\ndata: {"toolCallId":"tool-2","result":{"ok":false}}\n\n', + ]); + + const run = await parseAgUiSseResponse(response); + + assertEquals(run.events[0]?.content, '{"ok":true}'); + assertEquals(run.events[1]?.content, '{"ok":false}'); + }); + + it("keeps a null ToolCallResult content value as the serialized canonical field", async () => { + const response = createSseResponse([ + 'event: ToolCallResult\ndata: {"toolCallId":"tool-1","content":null}\n\n', + 'event: ToolCallResult\ndata: {"toolCallId":"tool-2","content":null,"result":{"ok":true}}\n\n', + ]); + + const run = await parseAgUiSseResponse(response); + + assertEquals(run.events[0]?.content, "null"); + assertEquals(run.events[1]?.content, "null"); + }); + it("keeps parsing legacy raw AG-UI payloads", async () => { const response = createSseResponse([ 'id: 1\nevent: RunStarted\ndata: {"type":"RUN_STARTED"}\n\n', diff --git a/src/agent/ag-ui/sse-parser.ts b/src/agent/ag-ui/sse-parser.ts index 954a537634..0d1fdfdc3b 100644 --- a/src/agent/ag-ui/sse-parser.ts +++ b/src/agent/ag-ui/sse-parser.ts @@ -180,12 +180,16 @@ function coerceWireEvent( return { type: agUiSseEventTypes.toolCallArgs, ...payload }; case "ToolCallEnd": return { type: agUiSseEventTypes.toolCallEnd, ...payload }; - case "ToolCallResult": + case "ToolCallResult": { + // The canonical `content` field wins whenever it is present, even when it + // is null; only a frame without it falls back to the legacy `result`. + const toolResult = Object.hasOwn(payload, "content") ? payload.content : payload.result; return { type: agUiSseEventTypes.toolCallResult, ...payload, - ...(payload.result !== undefined ? { content: serializeToolResult(payload.result) } : {}), + ...(toolResult !== undefined ? { content: serializeToolResult(toolResult) } : {}), }; + } case "Custom": return { type: agUiSseEventTypes.custom, ...payload }; case "RunError": diff --git a/src/agent/conversation/durable.test.ts b/src/agent/conversation/durable.test.ts index ade4b1db90..a7f8abe963 100644 --- a/src/agent/conversation/durable.test.ts +++ b/src/agent/conversation/durable.test.ts @@ -40,7 +40,7 @@ const BRANCH_ID = "44444444-4444-4444-8444-444444444444"; function modelCallContextEvent(content: string) { return { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "system", content }], }; } @@ -663,7 +663,7 @@ describe("agent/durable", () => { let accessorReads = 0; let fetchCalls = 0; const event = { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], extra: "invalid", }; @@ -702,7 +702,7 @@ describe("agent/durable", () => { let accessorReads = 0; let fetchCalls = 0; const event = { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }; Object.defineProperty(event, "tools", { diff --git a/src/agent/conversation/legacy-run-read-adapter.test.ts b/src/agent/conversation/legacy-run-read-adapter.test.ts index 575fe66a0a..3df7b84368 100644 --- a/src/agent/conversation/legacy-run-read-adapter.test.ts +++ b/src/agent/conversation/legacy-run-read-adapter.test.ts @@ -141,7 +141,7 @@ describe("conversation run lifecycle read adapter", () => { event: "ToolCallResult", payload: { toolCallId: "legacy-fetch", - result: { error: "Stored tool call ended without a result" }, + content: { error: "Stored tool call ended without a result" }, isError: true, }, }], @@ -1151,7 +1151,7 @@ describe("conversation run lifecycle read adapter", () => { aguiEvents.filter((event) => event.event === "ToolCallResult"), [{ event: "ToolCallResult", - payload: { toolCallId: "provider-1", result: { forecast: "sunny" } }, + payload: { toolCallId: "provider-1", content: { forecast: "sunny" } }, }], ); }); @@ -1363,7 +1363,7 @@ describe("conversation run lifecycle read adapter", () => { event: "ToolCallResult", payload: { toolCallId: "provider-err", - result: diagnostic, + content: diagnostic, isError: true, }, }], diff --git a/src/agent/conversation/private-run-event.test.ts b/src/agent/conversation/private-run-event.test.ts index a80528aa04..fe29053681 100644 --- a/src/agent/conversation/private-run-event.test.ts +++ b/src/agent/conversation/private-run-event.test.ts @@ -9,7 +9,7 @@ import { describe("agent/conversation/private-run-event", () => { it("recognizes only well-shaped private provider replay checkpoint events", () => { const checkpoint = { - type: "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT", + type: "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINTED", version: 1, messageId: "assistant-message-1", provider: "anthropic", @@ -38,17 +38,43 @@ describe("agent/conversation/private-run-event", () => { ); }); + it("still classifies the pre-rename spellings as private, so they can never reach a public append", () => { + // Read compatibility: an older producer may still hand this boundary the + // spelling used before the past-tense rename. Treating it as public would + // leak model context into the public event sequence. + assertEquals( + isPrivateConversationRunEvent({ type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [] }), + true, + ); + assertEquals( + isPrivateConversationRunEvent({ + type: "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT", + version: 1, + messageId: "assistant-message-1", + provider: "anthropic", + providerBlocks: [{ + type: "provider-block", + provider: "anthropic", + block: { type: "thinking", thinking: "", signature: "test-signature" }, + }], + providerBlockPositions: [0], + totalPartCount: 1, + }), + true, + ); + }); + it("recognizes only well-shaped private model-call context events", () => { assertEquals( isPrivateConversationRunEvent({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }), true, ); assertEquals( isPrivateConversationRunEvent({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", model: { id: "veryfront-cloud/anthropic/claude-sonnet-4-6", modelProvider: "anthropic" }, request: { maxOutputTokens: 4096, reasoning: { enabled: true, budgetTokens: 2048 } }, messages: [], @@ -60,7 +86,7 @@ describe("agent/conversation/private-run-event", () => { ); assertEquals( isPrivateConversationRunEvent({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "system", content: "Cache safely.", @@ -75,22 +101,22 @@ describe("agent/conversation/private-run-event", () => { for ( const value of [ [], - { type: "AGENT_RUN_MODEL_CALL_CONTEXT" }, - { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: {} }, - { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [], tools: {} }, - { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [], model: { id: 1 } }, + { type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED" }, + { type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: {} }, + { type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], tools: {} }, + { type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], model: { id: 1 } }, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], model: { id: "x", provider: "anthropic" }, }, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], request: { reasoning: { arbitrary: true } }, }, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "system", content: "Do not persist provider secrets.", @@ -103,7 +129,7 @@ describe("agent/conversation/private-run-event", () => { }], }, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "system", content: "Reject unsupported cache policy.", @@ -113,7 +139,7 @@ describe("agent/conversation/private-run-event", () => { }], }, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "assistant", content: [{ @@ -125,7 +151,7 @@ describe("agent/conversation/private-run-event", () => { }], }, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "tool", content: [{ @@ -137,7 +163,7 @@ describe("agent/conversation/private-run-event", () => { }], }, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], tools: [{ type: "function", @@ -145,8 +171,8 @@ describe("agent/conversation/private-run-event", () => { inputSchema: undefined, }], }, - { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [], emittedAt: 1.5 }, - { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [], contextId: "legacy" }, + { type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], emittedAt: 1.5 }, + { type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], contextId: "legacy" }, { type: "TEXT_MESSAGE_CONTENT", messages: [] }, ] ) { @@ -157,7 +183,7 @@ describe("agent/conversation/private-run-event", () => { it("does not invoke accessors while checking private event shape", () => { let reads = 0; const event = { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }; Object.defineProperty(event, "tools", { @@ -183,7 +209,7 @@ describe("agent/conversation/private-run-event", () => { assertEquals( isPrivateConversationRunEvent({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], request: { reasoning: { effort } }, }), diff --git a/src/agent/conversation/private-run-event.ts b/src/agent/conversation/private-run-event.ts index de5a06972b..0938e24453 100644 --- a/src/agent/conversation/private-run-event.ts +++ b/src/agent/conversation/private-run-event.ts @@ -1,6 +1,7 @@ import { DURABLE_RUN_EVENT_PERSISTENCE_FAILED, VeryfrontError } from "../../errors/index.ts"; import { AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT_EVENT_TYPE, + isProviderReplayCheckpointEventType, parseProviderReplayCheckpointEvent, } from "#veryfront/agent/runtime/provider-replay.ts"; @@ -159,18 +160,40 @@ function isTool(value: unknown): boolean { isRecord(ownDataValue(value, "args")); } +const AGENT_RUN_MODEL_CALL_CONTEXT_EVENT_TYPE = "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED"; +/** Pre-rename spelling, accepted on read so an older producer's event stays private. */ +const LEGACY_AGENT_RUN_MODEL_CALL_CONTEXT_EVENT_TYPE = "AGENT_RUN_MODEL_CALL_CONTEXT"; + +/** + * The canonical private type an event declares, or `undefined` for a public + * event. Either spelling of a renamed private type maps to its past-tense name. + */ +export function getCanonicalPrivateConversationRunEventType(value: unknown): string | undefined { + if (!value || typeof value !== "object" || Array.isArray(value)) return undefined; + const type = ownDataValue(value, "type"); + if ( + type === AGENT_RUN_MODEL_CALL_CONTEXT_EVENT_TYPE || + type === LEGACY_AGENT_RUN_MODEL_CALL_CONTEXT_EVENT_TYPE + ) { + return AGENT_RUN_MODEL_CALL_CONTEXT_EVENT_TYPE; + } + return isProviderReplayCheckpointEventType(type) + ? AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT_EVENT_TYPE + : undefined; +} + /** Return whether an event declares the private durable run-event discriminator. */ export function hasPrivateConversationRunEventType(value: unknown): value is object { - if (!value || typeof value !== "object" || Array.isArray(value)) return false; - const type = ownDataValue(value, "type"); - return type === "AGENT_RUN_MODEL_CALL_CONTEXT" || - type === AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT_EVENT_TYPE; + return getCanonicalPrivateConversationRunEventType(value) !== undefined; } /** Return whether an event belongs to the private durable run-event sequence. */ export function isPrivateConversationRunEvent(value: unknown): boolean { if (!hasPrivateConversationRunEventType(value)) return false; - if (ownDataValue(value, "type") === AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT_EVENT_TYPE) { + if ( + getCanonicalPrivateConversationRunEventType(value) === + AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT_EVENT_TYPE + ) { try { parseProviderReplayCheckpointEvent(value); return true; diff --git a/src/agent/conversation/run-chunk-mirror.test.ts b/src/agent/conversation/run-chunk-mirror.test.ts index 45a1168945..bb1770b35a 100644 --- a/src/agent/conversation/run-chunk-mirror.test.ts +++ b/src/agent/conversation/run-chunk-mirror.test.ts @@ -140,7 +140,7 @@ describe("agent/conversation-run-chunk-mirror", () => { await mirror.handleChunk({ type: "text-delta", id: "m1", delta: "hello" }); await createDurableRunEventSink({ mirror })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }); diff --git a/src/agent/conversation/run-event-normalization.test.ts b/src/agent/conversation/run-event-normalization.test.ts index 4491ff0160..9d4b9a7d09 100644 --- a/src/agent/conversation/run-event-normalization.test.ts +++ b/src/agent/conversation/run-event-normalization.test.ts @@ -28,11 +28,39 @@ describe("agent/conversation-run-event-normalization", () => { assertEquals(normalizeConversationRunEvent(event), [event]); }); + it("rewrites a pre-rename private event spelling to the canonical past-tense type", () => { + // Read compatibility for an older producer: the event is still private, + // and what leaves this boundary carries only the canonical name. + assertEquals( + normalizeConversationRunEvent({ type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [] }), + [{ type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [] }], + ); + const checkpoint = { + version: 1, + messageId: "assistant-message-1", + provider: "anthropic", + providerBlocks: [{ + type: "provider-block", + provider: "anthropic", + block: { type: "thinking", thinking: "", signature: "test-signature" }, + }], + providerBlockPositions: [0], + totalPartCount: 1, + }; + assertEquals( + normalizeConversationRunEvent({ + type: "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT", + ...checkpoint, + }), + [{ type: "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINTED", ...checkpoint }], + ); + }); + it("fails closed for a malformed private event discriminator", () => { assertThrows( () => normalizeConversationRunEvent({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], contextId: "legacy", }), @@ -45,7 +73,7 @@ describe("agent/conversation-run-event-normalization", () => { assertThrows( () => normalizeConversationRunEvent({ - type: "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT", + type: "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINTED", version: 1, messageId: "assistant-message-1", provider: "anthropic", @@ -287,7 +315,7 @@ describe("agent/conversation-run-event-normalization", () => { it("preserves one direct private event above 2 MiB byte-for-byte", () => { const event = { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "system", content: "x".repeat(2 * 1024 * 1024 + 1) }], }; diff --git a/src/agent/conversation/run-event-normalization.ts b/src/agent/conversation/run-event-normalization.ts index 9d3ce60307..5a16720637 100644 --- a/src/agent/conversation/run-event-normalization.ts +++ b/src/agent/conversation/run-event-normalization.ts @@ -1,6 +1,7 @@ import { MAX_CONVERSATION_RUN_EVENT_PAYLOAD_BYTES } from "./run-event-limits.ts"; import { DurableRunEventPersistenceError, + getCanonicalPrivateConversationRunEventType, hasPrivateConversationRunEventType, isPrivateConversationRunEvent, } from "./private-run-event.ts"; @@ -60,6 +61,12 @@ export function normalizeConversationRunEvent( if (!isPrivateConversationRunEvent(event)) { throw new DurableRunEventPersistenceError("Invalid private run event shape"); } + // An older producer may still use the pre-rename spelling; only the + // canonical past-tense name leaves this boundary. + const canonicalType = getCanonicalPrivateConversationRunEventType(event); + if (canonicalType !== undefined && canonicalType !== event.type) { + event = { ...event, type: canonicalType }; + } if ( event.type === AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT_EVENT_TYPE && getConversationRunEventJsonByteLength(event) > MAX_CONVERSATION_RUN_EVENT_PAYLOAD_BYTES diff --git a/src/agent/hosted/chat-execution-runtime.test.ts b/src/agent/hosted/chat-execution-runtime.test.ts index 8087c95dd5..ccf6a4ddde 100644 --- a/src/agent/hosted/chat-execution-runtime.test.ts +++ b/src/agent/hosted/chat-execution-runtime.test.ts @@ -789,12 +789,12 @@ describe("agent/hosted-chat-execution-runtime", () => { assertEquals(providerDispatches, 1); assertEquals( persisted.filter((event) => - (event as { type?: string }).type === "AGENT_RUN_MODEL_CALL_CONTEXT" + (event as { type?: string }).type === "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED" ).length, 1, ); const persistedContext = persisted.find((event) => - (event as { type?: string }).type === "AGENT_RUN_MODEL_CALL_CONTEXT" + (event as { type?: string }).type === "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED" ) as Record | undefined; assertEquals( typeof persistedContext?.elapsedMs === "number" && @@ -913,7 +913,7 @@ describe("agent/hosted-chat-execution-runtime", () => { assertEquals(dispatches, 2); assertEquals( persisted.filter((event) => - (event as { type?: string }).type === "AGENT_RUN_MODEL_CALL_CONTEXT" + (event as { type?: string }).type === "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED" ).length, 2, ); diff --git a/src/agent/hosted/chat-preparation.test.ts b/src/agent/hosted/chat-preparation.test.ts index 1973ae6591..9667a24bd2 100644 --- a/src/agent/hosted/chat-preparation.test.ts +++ b/src/agent/hosted/chat-preparation.test.ts @@ -265,7 +265,7 @@ describe("provider replay checkpoint emission", () => { assertEquals(options.requireProviderReplayCheckpointPersistence, true); await options.persistProviderReplayCheckpoint?.(replayCheckpoint()); assertEquals(operations, [ - "append:AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT", + "append:AGENT_RUN_PROVIDER_REPLAY_CHECKPOINTED", "flush", ]); }); @@ -537,7 +537,7 @@ Deno.test("prepareHostedChatRuntimeCreationOptions builds runtime options from r loadedToolNames: ["get_release"], }); assertEquals(checkpointPersistenceOperations, [ - "append:AGENT_RUN_TOOL_EXPOSURE_CHECKPOINT", + "append:AGENT_RUN_TOOL_EXPOSURE_CHECKPOINTED", "flush", ]); assertEquals(publicCheckpointAppends, 0); @@ -553,7 +553,7 @@ Deno.test("prepareHostedChatRuntimeCreationOptions builds runtime options from r "not durably persisted", ); assertEquals(checkpointPersistenceOperations.slice(-3), [ - "append:AGENT_RUN_TOOL_EXPOSURE_CHECKPOINT", + "append:AGENT_RUN_TOOL_EXPOSURE_CHECKPOINTED", "flush", "dispose", ]); @@ -808,14 +808,14 @@ it("resolves private checkpoint persistence only after the durable flush complet await Promise.resolve(); assertEquals(operations, [ - "append:AGENT_RUN_TOOL_EXPOSURE_CHECKPOINT", + "append:AGENT_RUN_TOOL_EXPOSURE_CHECKPOINTED", "flush:start", ]); assertEquals(resolved, false); resolveFlush?.(); await persistence; assertEquals(operations, [ - "append:AGENT_RUN_TOOL_EXPOSURE_CHECKPOINT", + "append:AGENT_RUN_TOOL_EXPOSURE_CHECKPOINTED", "flush:start", "flush:end", ]); diff --git a/src/agent/hosted/chat-request.test.ts b/src/agent/hosted/chat-request.test.ts index 62693dc55b..79e1725967 100644 --- a/src/agent/hosted/chat-request.test.ts +++ b/src/agent/hosted/chat-request.test.ts @@ -2385,7 +2385,7 @@ describe("agent/hosted-chat-request", () => { id: "m1", role: "assistant", parts: [{ - type: "AGENT_RUN_TOOL_EXPOSURE_CHECKPOINT", + type: "AGENT_RUN_TOOL_EXPOSURE_CHECKPOINTED", version: 1, loadedToolNames: ["get_release"], }], diff --git a/src/agent/hosted/child-fork-execution-runner.test.ts b/src/agent/hosted/child-fork-execution-runner.test.ts index effba61abc..bc03b58138 100644 --- a/src/agent/hosted/child-fork-execution-runner.test.ts +++ b/src/agent/hosted/child-fork-execution-runner.test.ts @@ -505,7 +505,7 @@ Deno.test("executeHostedChildForkWithPreparedTools preserves the mandatory sink assertEquals(activeDuringIteration, true); assertEquals(order.slice(0, 4), ["append", "flush", "observe", "dispatch"]); assertEquals(persisted.map(withoutEventTiming), [{ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [ { role: "system", content: "Hosted child instructions" }, { role: "user", content: [{ type: "text", text: "Run child" }] }, @@ -646,7 +646,7 @@ Deno.test("executeHostedChildForkWithPreparedTools drains queued child events be assertEquals(dispatches, 2); assertEquals( persisted.filter((event) => - (event as { type?: string }).type === "AGENT_RUN_MODEL_CALL_CONTEXT" + (event as { type?: string }).type === "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED" ).length, 2, ); diff --git a/src/agent/hosted/durable-run-event-sink.test.ts b/src/agent/hosted/durable-run-event-sink.test.ts index f62a58e011..e2d7289036 100644 --- a/src/agent/hosted/durable-run-event-sink.test.ts +++ b/src/agent/hosted/durable-run-event-sink.test.ts @@ -81,7 +81,7 @@ function createModelCallContextEventWithText( textLength: number, ): AgentRunModelCallContextEvent { return { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "user", content: [{ type: "text", text: "x".repeat(textLength) }], @@ -102,7 +102,7 @@ describe("agent/hosted/durable-run-event-sink", () => { publicEncoder.encode({ type: "start", messageId: "message-1" }); const publicEvent = publicEncoder.encode({ type: "text-start", id: "text:0" })[0]; await createDurableRunEventSink({ mirror: target.result, timing })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }); const privateEvent = firstAppendedEvent(target.appended); @@ -128,7 +128,7 @@ describe("agent/hosted/durable-run-event-sink", () => { }, }); const event: AgentRunModelCallContextEvent = { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "system", content: "Use available skills." }], tools: [{ type: "function", name: "search", inputSchema: { type: "object" } }], }; @@ -180,7 +180,7 @@ describe("agent/hosted/durable-run-event-sink", () => { }); await sink({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], elapsedMs: 42, emittedAt: 1_786_866_357_364, @@ -358,7 +358,7 @@ describe("agent/hosted/durable-run-event-sink", () => { it("guarantees a fit when message count alone exceeds the budget", async () => { const target = mirror(); const event = { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: Array.from({ length: 120_000 }, () => ({ role: "user", content: [{ type: "text", text: "y".repeat(80) }], @@ -418,12 +418,12 @@ describe("agent/hosted/durable-run-event-sink", () => { dispose: () => {}, }; const first = createDurableRunEventSink({ mirror: target })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "system", content: "first" }], }); await firstAppendStarted.promise; const second = createDurableRunEventSink({ mirror: target })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "system", content: "second" }], }); @@ -449,7 +449,7 @@ describe("agent/hosted/durable-run-event-sink", () => { getSnapshot: () => snapshot({ disabled: true, disableReason }), }, })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }), DurableRunEventPersistenceError, @@ -483,7 +483,7 @@ describe("agent/hosted/durable-run-event-sink", () => { getSnapshot: () => snapshot({ disabled: true, disableReason: "run_terminal" }), }, })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }), DOMException, @@ -517,7 +517,7 @@ describe("agent/hosted/durable-run-event-sink", () => { const error = await assertRejects( async () => await createDurableRunEventSink({ mirror: target.result })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }), DOMException, @@ -561,12 +561,12 @@ describe("agent/hosted/durable-run-event-sink", () => { dispose: () => {}, }; const first = createDurableRunEventSink({ mirror: target })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "system", content: "first" }], }); await firstAppendStarted.promise; const second = createDurableRunEventSink({ mirror: target })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "system", content: "second" }], }); @@ -585,7 +585,7 @@ describe("agent/hosted/durable-run-event-sink", () => { await assertRejects( async () => await createDurableRunEventSink({ mirror: appendFailure.result })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }), Error, @@ -640,7 +640,7 @@ describe("agent/hosted/durable-run-event-sink", () => { mirror: target.result, abortSignal: controller.signal, })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }), DOMException, @@ -657,7 +657,7 @@ describe("agent/hosted/durable-run-event-sink", () => { await assertRejects( async () => await createDurableRunEventSink({ mirror: target.result })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }), DurableRunEventPersistenceError, @@ -678,7 +678,7 @@ describe("agent/hosted/durable-run-event-sink", () => { await assertRejects( async () => await createDurableRunEventSink({ mirror: target.result })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }), DurableRunEventPersistenceError, @@ -712,7 +712,7 @@ describe("agent/hosted/durable-run-event-sink", () => { }, }, })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }), DurableRunEventPersistenceError, @@ -739,7 +739,7 @@ describe("agent/hosted/durable-run-event-sink", () => { timeoutMs: 1, abortSignal: guard.signal, })({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], }), DurableRunEventPersistenceError, diff --git a/src/agent/hosted/executor-model-dispatch.test.ts b/src/agent/hosted/executor-model-dispatch.test.ts index e60fa8450d..0207a4d397 100644 --- a/src/agent/hosted/executor-model-dispatch.test.ts +++ b/src/agent/hosted/executor-model-dispatch.test.ts @@ -161,7 +161,7 @@ describe("hosted executor model dispatch", () => { assertEquals(order, ["persist"]); assert(isPrivateConversationRunEvent(events[0])); assertEquals(events[0], { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", model: { id: "synthetic-model", modelProvider: "openai" }, messages: [{ role: "system", @@ -723,7 +723,7 @@ describe("hosted executor model dispatch", () => { channels.caller.request("model.generate", { modelId, options: { prompt: [] }, - event: { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [] }, + event: { type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [] }, runId: "other-run", }) ); diff --git a/src/agent/hosted/executor-model-dispatch.ts b/src/agent/hosted/executor-model-dispatch.ts index ce025f715e..1fbdbba1b5 100644 --- a/src/agent/hosted/executor-model-dispatch.ts +++ b/src/agent/hosted/executor-model-dispatch.ts @@ -200,7 +200,7 @@ function createContextEvent(call: ExecutorModelDispatch): AgentRunModelCallConte const modelProvider = resolveModelCallProvider(call.model); const request = buildModelCallContextRequest(call.model, options); const event: AgentRunModelCallContextEvent = { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", ...(call.model.modelId ? { model: { id: call.model.modelId, ...(modelProvider ? { modelProvider } : {}) }, diff --git a/src/agent/hosted/executor-persistence-bridge.test.ts b/src/agent/hosted/executor-persistence-bridge.test.ts index 72a30184b0..2fdb2a7d9f 100644 --- a/src/agent/hosted/executor-persistence-bridge.test.ts +++ b/src/agent/hosted/executor-persistence-bridge.test.ts @@ -40,12 +40,12 @@ function parentProgressEvent( } const privateCheckpointEvents = [ executorPersistenceJson({ - type: "AGENT_RUN_TOOL_EXPOSURE_CHECKPOINT", + type: "AGENT_RUN_TOOL_EXPOSURE_CHECKPOINTED", version: 2, loadedToolNames: ["search"], }), executorPersistenceJson({ - type: "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT", + type: "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINTED", version: 1, messageId: "message-test", provider: "anthropic", diff --git a/src/agent/runtime/provider-replay.ts b/src/agent/runtime/provider-replay.ts index a252e738c5..7097611eff 100644 --- a/src/agent/runtime/provider-replay.ts +++ b/src/agent/runtime/provider-replay.ts @@ -135,7 +135,20 @@ export type ProviderReplayCheckpoint = { /** Private durable event discriminator for provider-native replay state. */ export const AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT_EVENT_TYPE = - "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT" as const; + "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINTED" as const; + +/** + * The spelling used before the past-tense rename. Read-only: nothing here + * writes it, but a reader still accepts it and hands back the canonical name. + */ +const LEGACY_AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT_EVENT_TYPE = + "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT"; + +/** Return whether a value is either spelling of the provider replay checkpoint type. */ +export function isProviderReplayCheckpointEventType(type: unknown): boolean { + return type === AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT_EVENT_TYPE || + type === LEGACY_AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT_EVENT_TYPE; +} /** Private durable event carrying provider-native replay state. */ export type ProviderReplayCheckpointEvent = ProviderReplayCheckpoint & { @@ -343,7 +356,7 @@ export function parseProviderReplayCheckpointEvent( } catch { invalidCheckpoint("provider replay checkpoint event exceeds raw metadata bounds"); } - if (!isRecord(snapshot) || snapshot.type !== AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT_EVENT_TYPE) { + if (!isRecord(snapshot) || !isProviderReplayCheckpointEventType(snapshot.type)) { invalidCheckpoint("provider replay checkpoint event type is invalid"); } const checkpointValue: Record = {}; diff --git a/src/agent/runtime/provider-transport.test.ts b/src/agent/runtime/provider-transport.test.ts index 20a83745c0..faa1f353c7 100644 --- a/src/agent/runtime/provider-transport.test.ts +++ b/src/agent/runtime/provider-transport.test.ts @@ -153,7 +153,7 @@ describe("agent provider transport hooks", () => { normalizeRunRuntimeContext(localContext), ); assertEquals(normalizeRunRuntimeContext(cloudContext), { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [ { role: "system", diff --git a/src/agent/runtime/tool-exposure.ts b/src/agent/runtime/tool-exposure.ts index 2d58df215c..ea7f45fdd1 100644 --- a/src/agent/runtime/tool-exposure.ts +++ b/src/agent/runtime/tool-exposure.ts @@ -101,7 +101,7 @@ export type ToolExposureCheckpoint = { }; export const AGENT_RUN_TOOL_EXPOSURE_CHECKPOINT_EVENT_TYPE = - "AGENT_RUN_TOOL_EXPOSURE_CHECKPOINT" as const; + "AGENT_RUN_TOOL_EXPOSURE_CHECKPOINTED" as const; /** Private durable event carrying trusted tool exposure state. */ export type ToolExposureCheckpointEvent = ToolExposureCheckpoint & { diff --git a/src/chat/ag-ui.test.ts b/src/chat/ag-ui.test.ts index e3f59c748c..8571b3b86a 100644 --- a/src/chat/ag-ui.test.ts +++ b/src/chat/ag-ui.test.ts @@ -456,6 +456,46 @@ describe("chat/ag-ui", () => { ]); }); + it("keeps a null tool output carried in the canonical content field", () => { + const state = createAgUiChatEventDecoderState(); + const result = decodeAgUiSseChunk( + state, + [ + "event: ToolCallStart", + 'data: {"toolCallId":"tool-null","toolCallName":"lookup"}', + "", + "event: ToolCallResult", + 'data: {"toolCallId":"tool-null","content":null}', + "", + "event: ToolCallStart", + 'data: {"toolCallId":"tool-both","toolCallName":"lookup"}', + "", + "event: ToolCallResult", + 'data: {"toolCallId":"tool-both","content":null,"result":{"stale":true}}', + "", + "", + ].join("\n"), + ); + + const outputs = result.events.flatMap((entry) => entry.chatEvents).filter((event) => + event.type === "tool-output-available" + ); + assertEquals(outputs, [ + { + type: "tool-output-available", + toolCallId: "tool-null", + output: null, + providerExecuted: true, + }, + { + type: "tool-output-available", + toolCallId: "tool-both", + output: null, + providerExecuted: true, + }, + ]); + }); + it("emits tool output errors when AG-UI result payloads are marked as failures", () => { const state = createAgUiChatEventDecoderState(); const result = decodeAgUiSseChunk( diff --git a/src/chat/ag-ui.ts b/src/chat/ag-ui.ts index ad1280cb02..e0ad68346b 100644 --- a/src/chat/ag-ui.ts +++ b/src/chat/ag-ui.ts @@ -952,8 +952,12 @@ function mapWireEventToChatEvents( case "ToolCallResult": { const toolCall = state.toolCalls.get(wireEvent.payload.toolCallId); state.toolCalls.delete(wireEvent.payload.toolCallId); + // The canonical `content` field wins whenever it is present, even when a + // tool returned null; only a frame without it falls back to `result`. const parsedResult = parseSerializedToolResult( - wireEvent.payload.content ?? wireEvent.payload.result, + Object.hasOwn(wireEvent.payload, "content") + ? wireEvent.payload.content + : wireEvent.payload.result, ); if (wireEvent.payload.isError) { diff --git a/src/eval/agent-service.test.ts b/src/eval/agent-service.test.ts index faf4f02c90..d6190f06c9 100644 --- a/src/eval/agent-service.test.ts +++ b/src/eval/agent-service.test.ts @@ -667,6 +667,45 @@ describe("eval/agent-service", () => { assertEquals(record.metrics?.[0]?.evidence, { failedTools: ["search"] }); }); + it("reads canonical ToolCallResult content before a legacy result", async () => { + const adapter = createAgentServiceEvalAdapter({ + endpoint: "http://127.0.0.1:4311/api/ag-ui", + authToken: "token", + fetch: async () => + createSseResponse([ + { event: "RunStarted", data: { runId: "run_123" } }, + { event: "ToolCallStart", data: { toolCallId: "tool_err", toolCallName: "search" } }, + { + event: "ToolCallResult", + data: { toolCallId: "tool_err", content: { error: "boom" }, isError: true }, + }, + { event: "ToolCallStart", data: { toolCallId: "tool_ok", toolCallName: "lookup" } }, + { + event: "ToolCallResult", + data: { toolCallId: "tool_ok", content: { fresh: true }, result: { stale: true } }, + }, + { event: "RunFinished", data: {} }, + ]), + }); + const definition = evalAgent({ + id: "eval:canonical-content", + target: "agent:veryfront", + dataset: datasets.inline([{ id: "smoke", input: "Search docs" }]), + metrics: [metrics.agent.noFailedTools()], + }); + + const report = await runEval(definition, { adapters: { agent: adapter } }); + + const toolCalls = report.records[0]!.trace.toolCalls ?? []; + assertEquals( + toolCalls.map((call) => [call.id, call.status, call.error, call.output]), + [ + ["tool_err", "error", "boom", undefined], + ["tool_ok", "ok", undefined, { fresh: true }], + ], + ); + }); + it("classifies denied AG-UI tool results apart from tool errors", async () => { const adapter = createAgentServiceEvalAdapter({ endpoint: "http://127.0.0.1:4311/api/ag-ui", diff --git a/src/eval/agent-service.ts b/src/eval/agent-service.ts index 1f51d54995..b80c35b580 100644 --- a/src/eval/agent-service.ts +++ b/src/eval/agent-service.ts @@ -319,7 +319,7 @@ function stringifyError(value: unknown): string | undefined { } function getToolResultError(event: Record): string | undefined { - return stringifyError(event.result) ?? stringifyError(event.content) ?? "Tool call failed"; + return stringifyError(readToolResultValue(event)) ?? "Tool call failed"; } function parseJsonString(value: string): unknown { @@ -334,15 +334,23 @@ function readToolInputDelta(event: Record): string | undefined return readString(event.delta) ?? readString(event.inputTextDelta) ?? readString(event.argsDelta); } -function readToolOutput(event: Record): unknown { - if (Object.hasOwn(event, "result")) return event.result; - if (Object.hasOwn(event, "output")) return event.output; +/** + * The value a tool result carries. The canonical `content` field wins whenever it + * is present; the legacy `result` and `output` fields are read only without it. + */ +function readToolResultValue(event: Record): unknown { if (Object.hasOwn(event, "content")) { return typeof event.content === "string" ? parseJsonString(event.content) : event.content; } + if (Object.hasOwn(event, "result")) return event.result; + if (Object.hasOwn(event, "output")) return event.output; return undefined; } +function readToolOutput(event: Record): unknown { + return readToolResultValue(event); +} + function isDeniedToolResult(event: Record, error: string | undefined): boolean { const status = getAgUiSseStringField(event, "status"); return status === "denied" || error?.toLowerCase().includes("denied") === true; diff --git a/src/internal-agents/ag-ui-sse.test.ts b/src/internal-agents/ag-ui-sse.test.ts index a0f7ff0fd3..fbb993ea28 100644 --- a/src/internal-agents/ag-ui-sse.test.ts +++ b/src/internal-agents/ag-ui-sse.test.ts @@ -99,7 +99,7 @@ describe("internal-agents/ag-ui-sse", () => { { event: "ToolCallEnd", payload: { toolCallId: "tool-1" } }, { event: "ToolCallResult", - payload: { toolCallId: "tool-1", result: { error: "boom" }, isError: true }, + payload: { toolCallId: "tool-1", content: { error: "boom" }, isError: true }, }, ], ); @@ -156,7 +156,7 @@ describe("internal-agents/ag-ui-sse", () => { }), [{ event: "ToolCallResult", - payload: { toolCallId: "tool-2", result: { ok: true } }, + payload: { toolCallId: "tool-2", content: { ok: true } }, }], ); assertEquals( @@ -247,7 +247,7 @@ describe("internal-agents/ag-ui-sse", () => { event: "ToolCallResult", payload: { toolCallId: "tool-4", - result: { error: "invalid url" }, + content: { error: "invalid url" }, isError: true, }, }, @@ -263,7 +263,7 @@ describe("internal-agents/ag-ui-sse", () => { event: "ToolCallResult", payload: { toolCallId: "tool-5", - result: { error: "Tool output denied" }, + content: { error: "Tool output denied" }, isError: true, }, }], @@ -459,6 +459,25 @@ describe("internal-agents/ag-ui-sse", () => { ); }); + it("carries a tool result's canonical content field through the payload allow-list", () => { + // `content` is the canonical tool result field. The allow-list returns only + // declared keys, so an undeclared `content` would be dropped before the wire. + const payload = new TextDecoder().decode( + formatAgUiEvent("ToolCallResult", { + toolCallId: "tool-1", + content: '{"ok":true}', + isError: true, + emittedAt: 8, + }), + ); + + assertEquals( + payload, + 'event: ToolCallResult\ndata: {"toolCallId":"tool-1","content":"{\\"ok\\":true}",' + + '"isError":true,"emittedAt":8}\n\n', + ); + }); + it("carries elapsedMs through to the wire without widening the allow-list", () => { // These payload schemas are an allow-list and `parse` returns only what // they declare, so a stamped field missing from a schema is dropped @@ -691,7 +710,7 @@ describe("internal-agents/ag-ui-sse", () => { event: "ToolCallResult", payload: { toolCallId: CANONICAL_TOOL_CALL_ID, - result: CANONICAL_TOOL_RESULT, + content: CANONICAL_TOOL_RESULT, }, }, { diff --git a/src/internal-agents/ag-ui-sse.ts b/src/internal-agents/ag-ui-sse.ts index abfa33a60e..e83a384d72 100644 --- a/src/internal-agents/ag-ui-sse.ts +++ b/src/internal-agents/ag-ui-sse.ts @@ -93,7 +93,8 @@ function buildAgUiEventPayloadSchemas(): Record { const body = JSON.parse(String(capturedInit?.body)) as { events: Array>; }; - assertEquals(body.events[0]?.type, "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT"); + assertEquals(body.events[0]?.type, "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINTED"); assertEquals(body.events[0]?.messageId, MESSAGE_ID); acknowledge?.(Response.json({ appended_count: 1 })); @@ -181,7 +181,7 @@ describe("run-scoped provider replay checkpoint persistence", () => { const body = JSON.parse(String(bodies[1])) as { events: Array>; }; - assertEquals(body.events[0]?.type, "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT"); + assertEquals(body.events[0]?.type, "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINTED"); assertEquals(body.events[0]?.messageId, MESSAGE_ID); }); diff --git a/src/internal-agents/run-stream.test.ts b/src/internal-agents/run-stream.test.ts index cd32d53bc7..d5cd0e2c02 100644 --- a/src/internal-agents/run-stream.test.ts +++ b/src/internal-agents/run-stream.test.ts @@ -3851,7 +3851,7 @@ describe("internal-agents/run-stream", () => { }); describe("model call context", () => { const modelCallContextEvent = { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [{ role: "system", content: "test system prompt" }], tools: [{ type: "function", name: "granted_tool", inputSchema: {} }], }; diff --git a/src/internal-agents/run-stream.ts b/src/internal-agents/run-stream.ts index 1bd7ee99d9..624fa97536 100644 --- a/src/internal-agents/run-stream.ts +++ b/src/internal-agents/run-stream.ts @@ -90,9 +90,14 @@ const INTERNAL_AGENT_RUNTIME_HEARTBEAT_FRAME = new TextEncoder().encode( ": internal-agent-runtime-heartbeat\n\n", ); /** - * SSE frame name carrying AGENT_RUN_MODEL_CALL_CONTEXT to veryfront-api. Not an + * SSE frame name carrying AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED to veryfront-api. Not an * AG-UI event: veryfront-api persists it under its own event type rather than * folding it into the run's public event sequence. + * + * These two frame names are a private channel the API matches exactly, so they + * stay as they are even though the payload `type` each frame carries uses the + * canonical past-tense name. Renaming a frame needs the API to accept the new + * name first. */ export const MODEL_CALL_CONTEXT_SSE_EVENT_NAME = "AgentRunModelCallContext"; export const PROVIDER_REPLAY_TURN_COMPLETE_SSE_EVENT_NAME = "AgentRunProviderReplayTurnComplete"; @@ -842,7 +847,7 @@ function createProviderReplayCheckpointRelay(): { buffered.push({ event: PROVIDER_REPLAY_TURN_COMPLETE_SSE_EVENT_NAME, payload: { - type: "AGENT_RUN_PROVIDER_REPLAY_TURN_COMPLETE", + type: "AGENT_RUN_PROVIDER_REPLAY_TURN_FINISHED", messageId, }, }); diff --git a/src/run-events/vocabulary.test.ts b/src/run-events/vocabulary.test.ts index 41864e189c..04d2b1d51f 100644 --- a/src/run-events/vocabulary.test.ts +++ b/src/run-events/vocabulary.test.ts @@ -26,8 +26,14 @@ import { * the copy under test, and a name changed there fails the digest. */ const API_RUN_EVENT_TYPE_COUNT = 53; +// Recomputed on 2026-09-14 after the eight control plane types took the +// past-tense names the API catalog declares (`RUNTIME_INVOKE_RETRIED`, +// `TOOL_EXPOSURE_CHECKPOINTED`, `PROVIDER_REPLAY_CHECKPOINTED`, +// `PROVIDER_REPLAY_TURN_FINISHED`, `MODEL_CALL_CONTEXT_RECORDED`, +// `CONTROL_PLANE_DISPATCH_ACCEPTED`, `INVOKE_AGENT_BILLING_MODE_RETAINED`, +// `BILLING_USAGE_RETAINED`). Only those names changed. const API_RUN_EVENT_TYPES_SHA256 = - "9aeb493b3010fc05f63ca401ddb92e546c3f0dca4b51000ed0cdb3cb680c5803"; + "f5ed5e0225a6ace61c670d43ab7a18f60186e2e63912202d063ad1091090a1e5"; /** * Read from the veryfront-api checkout on 2026-09-10 by importing @@ -38,8 +44,9 @@ const API_RUN_EVENT_TYPES_SHA256 = * digest is the ground truth, `RUN_EVENT_CLASS_BY_TYPE` in `vocabulary.ts` is * the copy under test, and a class changed there fails the digest. */ +// Recomputed on 2026-09-14 for the same eight renames; every class is unchanged. const API_RUN_EVENT_CLASS_BY_TYPE_SHA256 = - "75a339e05665a7f4e90600bc94f1dfc7115b7385a03b78d50a5d62211ec602af"; + "5bdf63c9744b56f639db8ecdd16a80f636f335a4725b2e95708f640a5eb3da8c"; async function sha256Hex(text: string): Promise { const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); diff --git a/src/run-events/vocabulary.ts b/src/run-events/vocabulary.ts index e62053ddd9..38e0cf5700 100644 --- a/src/run-events/vocabulary.ts +++ b/src/run-events/vocabulary.ts @@ -61,16 +61,16 @@ export const RUN_EVENT_TYPES = [ "AGENT_RUN_TOOL_RESULT_SUBMITTED", "AGENT_RUN_TOOL_RESULT_DELIVERY_FAILED", "AGENT_RUN_RUNTIME_OWNER_BOUND", - "AGENT_RUN_RUNTIME_INVOKE_RETRY", + "AGENT_RUN_RUNTIME_INVOKE_RETRIED", "AGENT_RUN_CONTEXT_COMPACTED", - "AGENT_RUN_TOOL_EXPOSURE_CHECKPOINT", - "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT", + "AGENT_RUN_TOOL_EXPOSURE_CHECKPOINTED", + "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINTED", "AGENT_RUN_PROVIDER_REPLAY_TURN_STARTED", - "AGENT_RUN_PROVIDER_REPLAY_TURN_COMPLETE", - "AGENT_RUN_MODEL_CALL_CONTEXT", - "AGENT_RUN_CONTROL_PLANE_DISPATCH_RECEIPT", - "AGENT_RUN_INVOKE_AGENT_BILLING_MODE_CHECKPOINT", - "AGENT_RUN_RETAINED_BILLING_USAGE", + "AGENT_RUN_PROVIDER_REPLAY_TURN_FINISHED", + "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", + "AGENT_RUN_CONTROL_PLANE_DISPATCH_ACCEPTED", + "AGENT_RUN_INVOKE_AGENT_BILLING_MODE_RETAINED", + "AGENT_RUN_BILLING_USAGE_RETAINED", // Extension types: the typed replacements for the registered CUSTOM names. "TOOL_CALL_STATUS_CHANGED", "INPUT_REQUEST_CREATED", @@ -192,16 +192,16 @@ const RUN_EVENT_WIRE_NAMES = { AGENT_RUN_TOOL_RESULT_SUBMITTED: "AgentRunToolResultSubmitted", AGENT_RUN_TOOL_RESULT_DELIVERY_FAILED: "AgentRunToolResultDeliveryFailed", AGENT_RUN_RUNTIME_OWNER_BOUND: "AgentRunRuntimeOwnerBound", - AGENT_RUN_RUNTIME_INVOKE_RETRY: "AgentRunRuntimeInvokeRetry", + AGENT_RUN_RUNTIME_INVOKE_RETRIED: "AgentRunRuntimeInvokeRetried", AGENT_RUN_CONTEXT_COMPACTED: "AgentRunContextCompacted", - AGENT_RUN_TOOL_EXPOSURE_CHECKPOINT: "AgentRunToolExposureCheckpoint", - AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT: "AgentRunProviderReplayCheckpoint", + AGENT_RUN_TOOL_EXPOSURE_CHECKPOINTED: "AgentRunToolExposureCheckpointed", + AGENT_RUN_PROVIDER_REPLAY_CHECKPOINTED: "AgentRunProviderReplayCheckpointed", AGENT_RUN_PROVIDER_REPLAY_TURN_STARTED: "AgentRunProviderReplayTurnStarted", - AGENT_RUN_PROVIDER_REPLAY_TURN_COMPLETE: "AgentRunProviderReplayTurnComplete", - AGENT_RUN_MODEL_CALL_CONTEXT: "AgentRunModelCallContext", - AGENT_RUN_CONTROL_PLANE_DISPATCH_RECEIPT: "AgentRunControlPlaneDispatchReceipt", - AGENT_RUN_INVOKE_AGENT_BILLING_MODE_CHECKPOINT: "AgentRunInvokeAgentBillingModeCheckpoint", - AGENT_RUN_RETAINED_BILLING_USAGE: "AgentRunRetainedBillingUsage", + AGENT_RUN_PROVIDER_REPLAY_TURN_FINISHED: "AgentRunProviderReplayTurnFinished", + AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED: "AgentRunModelCallContextRecorded", + AGENT_RUN_CONTROL_PLANE_DISPATCH_ACCEPTED: "AgentRunControlPlaneDispatchAccepted", + AGENT_RUN_INVOKE_AGENT_BILLING_MODE_RETAINED: "AgentRunInvokeAgentBillingModeRetained", + AGENT_RUN_BILLING_USAGE_RETAINED: "AgentRunBillingUsageRetained", TOOL_CALL_STATUS_CHANGED: "ToolCallStatusChanged", INPUT_REQUEST_CREATED: "InputRequestCreated", INPUT_REQUEST_UPDATED: "InputRequestUpdated", diff --git a/src/runtime/model-call-context.test.ts b/src/runtime/model-call-context.test.ts index 671d48e517..30cc62d8b3 100644 --- a/src/runtime/model-call-context.test.ts +++ b/src/runtime/model-call-context.test.ts @@ -25,14 +25,14 @@ describe("model-call-context", () => { args: { maxUses: 3 }, }]; const event: AgentRunModelCallContextEvent = { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", model: { id: "anthropic/claude-sonnet-4-6", modelProvider: "anthropic" }, request: { maxOutputTokens: 4096, reasoning: { enabled: true, budgetTokens: 2048 } }, messages, tools, }; assertEquals(event, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", model: { id: "anthropic/claude-sonnet-4-6", modelProvider: "anthropic" }, request: { maxOutputTokens: 4096, reasoning: { enabled: true, budgetTokens: 2048 } }, messages, @@ -40,12 +40,12 @@ describe("model-call-context", () => { }); const eventWithExtraField: AgentRunModelCallContextEvent = { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages, // @ts-expect-error model-call context events do not accept chunk metadata contextId: "context-1", }; - assertEquals(eventWithExtraField.type, "AGENT_RUN_MODEL_CALL_CONTEXT"); + assertEquals(eventWithExtraField.type, "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED"); const providerPrivateReasoning: ModelCallMessage = { role: "assistant", @@ -67,9 +67,9 @@ describe("model-call-context", () => { { nowMs: () => now, epochMs: () => 1_786_866_357_364.4, startedMs: 100 }, ); now = 142.6; - sink({ type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [] }); + sink({ type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [] }); sink({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], elapsedMs: 7, emittedAt: 8, @@ -90,7 +90,7 @@ describe("model-call-context", () => { assertThrows( () => sink({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], elapsedMs: Number.NaN, }), @@ -100,7 +100,7 @@ describe("model-call-context", () => { assertThrows( () => sink({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], emittedAt: 1.5, }), @@ -116,7 +116,7 @@ describe("model-call-context", () => { sinkCalls += 1; }); const event = Object.defineProperty( - { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [] }, + { type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [] }, "elapsedMs", { enumerable: true, @@ -143,7 +143,7 @@ describe("model-call-context", () => { startedMs: 0, }); assertThrows( - () => sink({ type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [] }), + () => sink({ type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [] }), TypeError, "elapsedMs must be a finite non-negative number", ); @@ -154,7 +154,7 @@ describe("model-call-context", () => { startedMs: 0, }); assertThrows( - () => wallClockSink({ type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [] }), + () => wallClockSink({ type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [] }), TypeError, "emittedAt must be a non-negative integer", ); diff --git a/src/runtime/model-call-context.ts b/src/runtime/model-call-context.ts index 4e6e29ed95..2c63587955 100644 --- a/src/runtime/model-call-context.ts +++ b/src/runtime/model-call-context.ts @@ -75,7 +75,7 @@ export interface ModelCallRequest { * provider-specific values are excluded because run events are durable. */ export type AgentRunModelCallContextEvent = { - type: "AGENT_RUN_MODEL_CALL_CONTEXT"; + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED"; model?: ModelCallModel; request?: ModelCallRequest; messages: ModelCallMessage[]; diff --git a/src/runtime/runtime-bridge.test.ts b/src/runtime/runtime-bridge.test.ts index fed71e6145..01170733ed 100644 --- a/src/runtime/runtime-bridge.test.ts +++ b/src/runtime/runtime-bridge.test.ts @@ -128,7 +128,7 @@ describe("runtime-bridge", () => { { role: "user", content: [{ type: "text", text: "Hello" }] }, ]); assertEquals(recorded, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", model: { id: "test/sanitized-context", modelProvider: "test" }, messages: [ { @@ -345,7 +345,7 @@ describe("runtime-bridge", () => { assertEquals(event.messages, options.prompt); assertEquals(event.tools, options.tools); assertEquals(recorded, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", model: { id: "test/model-call-context", modelProvider: "test" }, request: { temperature: 0.7 }, messages: options.prompt, @@ -409,7 +409,7 @@ describe("runtime-bridge", () => { assertEquals(order, ["persist", "dispatch"]); assertEquals(recorded, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", model: { id: "test/model-call-context", modelProvider: "test" }, request: { temperature: 0.7 }, messages: [ @@ -486,7 +486,7 @@ describe("runtime-bridge", () => { ); assertEquals(recorded, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", model: { id: "test/model-call-provider-redaction", modelProvider: "test" }, messages: [ { @@ -577,7 +577,7 @@ describe("runtime-bridge", () => { }]; assertEquals(contexts, [ { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", model: { id: "test/evolving-skill-context", modelProvider: "test" }, messages: [ { role: "system", content: system }, @@ -586,7 +586,7 @@ describe("runtime-bridge", () => { tools: resolvedTools, }, { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", model: { id: "test/evolving-skill-context", modelProvider: "test" }, messages: [ { role: "system", content: system }, diff --git a/src/runtime/runtime-bridge.ts b/src/runtime/runtime-bridge.ts index fa6406aba7..190ce72b31 100644 --- a/src/runtime/runtime-bridge.ts +++ b/src/runtime/runtime-bridge.ts @@ -740,7 +740,7 @@ async function emitModelCallContextEvent( const request = buildModelCallContextRequest(options.model, directOptions); const event: AgentRunModelCallContextEvent = { - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", ...(options.model.modelId ? { model: { diff --git a/tests/integration/agent/managed-broker-persistence.test.ts b/tests/integration/agent/managed-broker-persistence.test.ts index 6e43bfa464..ce3b3f7d13 100644 --- a/tests/integration/agent/managed-broker-persistence.test.ts +++ b/tests/integration/agent/managed-broker-persistence.test.ts @@ -103,7 +103,7 @@ for (const authority of ["token", "capability"] as const) { await assertRejects(async () => await persistence.modelRunEventSink({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], tools: [], }) @@ -166,7 +166,7 @@ for (const authority of ["token", "capability"] as const) { })); await persistence.output.write({ type: "text-delta", id: "message", delta: "hello" }); await persistence.modelRunEventSink({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], tools: [], }); @@ -204,14 +204,17 @@ for (const authority of ["token", "capability"] as const) { ); const events = calls.flatMap((call) => Array.isArray(call.events) ? call.events : []); assertEquals(events.some((event) => event.type === "TEXT_MESSAGE_CONTENT"), true); - assertEquals(events.some((event) => event.type === "AGENT_RUN_MODEL_CALL_CONTEXT"), true); + assertEquals( + events.some((event) => event.type === "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED"), + true, + ); assertEquals(events.some((event) => event.type === "STEP_STARTED"), true); assertEquals( - events.some((event) => event.type === "AGENT_RUN_TOOL_EXPOSURE_CHECKPOINT"), + events.some((event) => event.type === "AGENT_RUN_TOOL_EXPOSURE_CHECKPOINTED"), true, ); assertEquals( - events.some((event) => event.type === "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINT"), + events.some((event) => event.type === "AGENT_RUN_PROVIDER_REPLAY_CHECKPOINTED"), true, ); assertEquals(calls.at(-1)?.status, "completed"); @@ -346,7 +349,7 @@ for (const authority of ["token", "capability"] as const) { return result; }); const audit = persistence.modelRunEventSink({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", + type: "AGENT_RUN_MODEL_CALL_CONTEXT_RECORDED", messages: [], tools: [], });