diff --git a/scripts/src/cli.ts b/scripts/src/cli.ts index 713d61f..67fef58 100644 --- a/scripts/src/cli.ts +++ b/scripts/src/cli.ts @@ -633,7 +633,7 @@ export function summarizeQueueAttemptListResult(result: JsonValue, taskId: strin function summarizeRunEvent(event: JsonRecord, summaryChars: number): JsonRecord { const payload = jsonRecordValue(event.payload) ?? {}; const type = stringValue(event.type) ?? "unknown"; - const messageEvent = type === "user_message" || type === "assistant_message"; + const messageEvent = type === "user_message" || type === "assistant_progress" || type === "assistant_message"; const command = messageEvent ? null : boundedSummaryString(stringValue(payload.command), Math.min(summaryChars, 240)); const text = messageEvent ? boundedSummaryString(stringValue(payload.text), summaryChars) : null; const outputSummary = boundedSummaryString(stringValue(payload.outputSummary) ?? nestedSummaryText(payload), summaryChars); @@ -668,7 +668,7 @@ function summarizeRunEvent(event: JsonRecord, summaryChars: number): JsonRecord function isDefaultVisibleSessionEvent(item: JsonRecord): boolean { const type = stringValue(item.type); - return type === "user_message" || type === "assistant_message" || type === "tool_call" || type === "error"; + return type === "user_message" || type === "assistant_progress" || type === "assistant_message" || type === "tool_call" || type === "error"; } function summarizeSuppressedSessionEvents(all: JsonRecord[], visible: JsonRecord[]): JsonRecord { diff --git a/src/backend/codex-stdio.ts b/src/backend/codex-stdio.ts index 86b0d24..d364d1d 100644 --- a/src/backend/codex-stdio.ts +++ b/src/backend/codex-stdio.ts @@ -1424,7 +1424,7 @@ function flushAssistantDeltaProgress(state: AssistantDeltaProgressState): Backen function assistantDeltaProgressEvent(item: AssistantDeltaProgressItem, flush: boolean): BackendEvent { const summary = boundedTextSummary(item.text.trim(), { limitChars: assistantDeltaProgressLimitChars }); return { - type: "assistant_message", + type: "assistant_progress", payload: { text: summary.text, itemId: item.itemId, diff --git a/src/common/events.ts b/src/common/events.ts index c66c870..ad6716f 100644 --- a/src/common/events.ts +++ b/src/common/events.ts @@ -4,8 +4,8 @@ import { boundedTextSummary, commandOutputPayload } from "./output.js"; import { agentRunBusinessTraceId } from "./otel-trace.js"; import { redactJson, redactText } from "./redaction.js"; -export const eventTypes = ["backend_status", "user_message", "assistant_message", "tool_call", "command_output", "diff", "error", "terminal_status"] as const satisfies readonly EventType[]; -export const externallyAppendableEventTypes = ["backend_status", "assistant_message", "tool_call", "command_output", "diff", "error", "terminal_status"] as const satisfies readonly EventType[]; +export const eventTypes = ["backend_status", "user_message", "assistant_progress", "assistant_message", "tool_call", "command_output", "diff", "error", "terminal_status"] as const satisfies readonly EventType[]; +export const externallyAppendableEventTypes = ["backend_status", "assistant_progress", "assistant_message", "tool_call", "command_output", "diff", "error", "terminal_status"] as const satisfies readonly EventType[]; export const terminalStatuses = ["completed", "failed", "blocked", "cancelled"] as const satisfies readonly TerminalStatus[]; const eventTypeSet = new Set(eventTypes); @@ -41,6 +41,7 @@ export function normalizeRunEventPayload(type: EventType, payload: JsonRecord): if (type === "terminal_status") return normalizeTerminalStatusPayload(payload); if (type === "command_output") return normalizeCommandOutputPayload(payload); if (type === "user_message") return normalizeUserMessagePayload(payload); + if (type === "assistant_progress") return normalizeAssistantProgressPayload(payload); if (type === "assistant_message") return normalizeTextPayload(payload); if (type === "tool_call") return normalizeToolCallPayload(payload); return payload; @@ -140,6 +141,22 @@ function normalizeTextPayload(payload: JsonRecord): JsonRecord { return { ...rest, text: summary.text, summary, textBytes: summary.textBytes, textTruncated: summary.textTruncated, outputBytes: summary.outputBytes, outputTruncated: summary.outputTruncated }; } +function normalizeAssistantProgressPayload(payload: JsonRecord): JsonRecord { + const source = nonEmptyText(payload.source); + const text = typeof payload.text === "string" ? payload.text : typeof payload.delta === "string" ? payload.delta : typeof payload.content === "string" ? payload.content : ""; + if (source !== "agent-message-delta-progress" || payload.progress !== true || payload.replyAuthority !== false || payload.final !== false || payload.durableText === true || text.trim().length === 0) { + throw new AgentRunError("schema-invalid", "assistant_progress requires non-empty delta progress with replyAuthority=false, final=false, and non-durable text", { httpStatus: 400 }); + } + return normalizeTextPayload({ + ...payload, + source, + progress: true, + replyAuthority: false, + final: false, + valuesPrinted: false, + }); +} + function normalizeDurableAssistantText(payload: JsonRecord, body: string): JsonRecord { const source = typeof payload.source === "string" ? payload.source : ""; if (source !== "completed-agent-message" && source !== "agent-message-delta-fallback") { diff --git a/src/common/types.ts b/src/common/types.ts index 038d275..2425b9b 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -336,7 +336,7 @@ export interface CancelRequestRecord extends JsonRecord { updatedAt: string; } -export type EventType = "backend_status" | "user_message" | "assistant_message" | "tool_call" | "command_output" | "diff" | "error" | "terminal_status"; +export type EventType = "backend_status" | "user_message" | "assistant_progress" | "assistant_message" | "tool_call" | "command_output" | "diff" | "error" | "terminal_status"; export interface RunEvent extends JsonRecord { id: string; diff --git a/src/mgr/diagnosis.ts b/src/mgr/diagnosis.ts index acabea4..27204d5 100644 --- a/src/mgr/diagnosis.ts +++ b/src/mgr/diagnosis.ts @@ -295,7 +295,7 @@ function isRunnerJobActivity(job: RunnerJobRecord, event: RunEvent): boolean { if (payload?.phase === "runner-job-created") return false; if (payload?.runnerId === job.runnerId || payload?.attemptId === job.attemptId) return true; if (payload?.commandId === job.commandId) { - if (event.type === "tool_call" || event.type === "assistant_message" || event.type === "command_output" || event.type === "error") return true; + if (event.type === "tool_call" || event.type === "assistant_progress" || event.type === "assistant_message" || event.type === "command_output" || event.type === "error") return true; if (event.type === "backend_status" && typeof payload.phase === "string" && payload.phase !== "command-created") return true; } return false; diff --git a/src/mgr/result.ts b/src/mgr/result.ts index dc20229..d62e7a1 100644 --- a/src/mgr/result.ts +++ b/src/mgr/result.ts @@ -213,7 +213,7 @@ function livenessPhase(input: { active: boolean; command: CommandRecord | null; if (status === "completed") return "idle-after-tool"; } if (input.lastVisibleActivity?.type === "command_output") return "waiting-tool-output"; - if (input.lastVisibleActivity?.type === "assistant_message") return "waiting-model-output"; + if (input.lastVisibleActivity?.type === "assistant_progress" || input.lastVisibleActivity?.type === "assistant_message") return "waiting-model-output"; const remainingMs = numberJsonValue(input.timeoutBudget.remainingMs); const timeoutMs = numberJsonValue(input.timeoutBudget.timeoutMs); const activityAgeMs = numberJsonValue(input.lastActivity?.ageMs); @@ -402,15 +402,15 @@ function latestLivenessActivity(events: RunEvent[]): RunEvent | null { } function latestVisibleActivity(events: RunEvent[]): RunEvent | null { - return [...events].reverse().find((event) => event.type === "assistant_message" || event.type === "tool_call" || event.type === "command_output" || event.type === "diff" || event.type === "error" || event.type === "terminal_status") ?? null; + return [...events].reverse().find((event) => event.type === "assistant_progress" || event.type === "assistant_message" || event.type === "tool_call" || event.type === "command_output" || event.type === "diff" || event.type === "error" || event.type === "terminal_status") ?? null; } function latestBusinessActivity(events: RunEvent[]): RunEvent | null { - return [...events].reverse().find((event) => event.type === "assistant_message" || event.type === "tool_call" || event.type === "command_output" || event.type === "diff") ?? null; + return [...events].reverse().find((event) => event.type === "assistant_progress" || event.type === "assistant_message" || event.type === "tool_call" || event.type === "command_output" || event.type === "diff") ?? null; } function isLivenessActivityEvent(event: RunEvent): boolean { - if (event.type === "assistant_message" || event.type === "tool_call" || event.type === "command_output" || event.type === "diff" || event.type === "error" || event.type === "terminal_status") return true; + if (event.type === "assistant_progress" || event.type === "assistant_message" || event.type === "tool_call" || event.type === "command_output" || event.type === "diff" || event.type === "error" || event.type === "terminal_status") return true; if (event.type !== "backend_status") return false; const phase = typeof event.payload.phase === "string" ? event.payload.phase : ""; return ["backend-turn-started", "thread/start:completed", "thread/resume:completed", "turn/start:completed", "backend-turn-finished", "codex-app-server-closed"].includes(phase); @@ -458,7 +458,8 @@ function activityKind(event: RunEvent): string { return "tool"; } if (event.type === "command_output") return "tool-output"; - if (event.type === "assistant_message") return event.payload.progress === true ? "assistant-progress" : "assistant-output"; + if (event.type === "assistant_progress") return "assistant-progress"; + if (event.type === "assistant_message") return "assistant-output"; if (event.type === "diff") return "diff"; if (event.type === "error") return "error"; if (event.type === "terminal_status") return "terminal"; @@ -621,7 +622,7 @@ function targetFollowUpAfterSeq(events: RunEvent[], targetCommandId: string, aft } function isTargetFollowUpEvent(event: RunEvent): boolean { - return event.type === "assistant_message" || event.type === "tool_call" || event.type === "command_output" || event.type === "diff" || event.type === "error" || event.type === "terminal_status"; + return event.type === "assistant_progress" || event.type === "assistant_message" || event.type === "tool_call" || event.type === "command_output" || event.type === "diff" || event.type === "error" || event.type === "terminal_status"; } function runIsTerminal(run: RunRecord): boolean { diff --git a/src/mgr/store.ts b/src/mgr/store.ts index 5774cd2..915b572 100644 --- a/src/mgr/store.ts +++ b/src/mgr/store.ts @@ -1675,5 +1675,5 @@ function workQueueStatus(items: T[], component: string, co } export function isSessionOutputEvent(event: RunEvent): boolean { - return event.type === "assistant_message" || event.type === "command_output" || event.type === "diff" || event.type === "error" || event.type === "terminal_status"; + return event.type === "assistant_progress" || event.type === "assistant_message" || event.type === "command_output" || event.type === "diff" || event.type === "error" || event.type === "terminal_status"; } diff --git a/src/selftest/cases/15-cli-events-summary.ts b/src/selftest/cases/15-cli-events-summary.ts index b0edbbe..5f1aa9a 100644 --- a/src/selftest/cases/15-cli-events-summary.ts +++ b/src/selftest/cases/15-cli-events-summary.ts @@ -38,11 +38,20 @@ export default function selfTest(_context: SelfTestContext): SelfTestResult { assert.equal(userMessageItem?.summary, "replay this user input"); assert.equal(userMessageItem?.text, "replay this user input"); + const progressSummary = summarizeRunEventPage({ items: [ + { id: "evt_progress", seq: 15, type: "assistant_progress", payload: { commandId: "cmd_progress", itemId: "msg_progress", text: "live cumulative assistant progress", source: "agent-message-delta-progress", progress: true, replyAuthority: false, final: false } }, + ] }, { runId: "run_selftest", afterSeq: 14, limit: 1, tail: null, summaryChars: 80 }); + const progressItem = (progressSummary.items as JsonRecord[])[0]; + assert.equal(progressItem?.type, "assistant_progress"); + assert.equal(progressItem?.summary, "live cumulative assistant progress"); + assert.equal(progressItem?.text, "live cumulative assistant progress"); + const hugeRunnerTrace = "runner trace line with test-token-material\n".repeat(2_000); const sessionSummary = summarizeSessionEventPage({ sessionId: "sess_noise", runId: "run_noise", items: [ + { id: "evt_progress", seq: 264, type: "assistant_progress", payload: { itemId: "msg_noise", text: "still working", source: "agent-message-delta-progress", progress: true, replyAuthority: false, final: false } }, { id: "evt_tool", seq: 265, type: "tool_call", payload: { itemId: "tool_noise", method: "commandExecution", status: "completed", command: "printf hidden", outputSummary: "tool completed" } }, { id: "evt_output", seq: 266, type: "command_output", payload: { itemId: "tool_noise", text: hugeRunnerTrace, outputSummary: "large stdout", outputBytes: 100_000, outputTruncated: true } }, { @@ -58,21 +67,21 @@ export default function selfTest(_context: SelfTestContext): SelfTestResult { }, }, ], - count: 3, + count: 4, cursor: "266", }, { kind: "trace", sessionId: "sess_noise", afterSeq: 250, limit: 100, runId: null, tail: null, summaryChars: 80, includeOutput: false }); const sessionItems = sessionSummary.items as JsonRecord[]; assert.equal(sessionSummary.action, "session-trace-summary"); - assert.equal(sessionSummary.sourceCount, 3); - assert.equal(sessionSummary.displayedCount, 1); + assert.equal(sessionSummary.sourceCount, 4); + assert.equal(sessionSummary.displayedCount, 2); assert.equal(sessionSummary.nextAfterSeq, 267); assert.equal((sessionSummary.suppressedEvents as JsonRecord).count, 2); assert.equal(((sessionSummary.suppressedEvents as JsonRecord).byType as JsonRecord).command_output, 1); assert.equal(((sessionSummary.suppressedEvents as JsonRecord).byType as JsonRecord).backend_status, 1); - assert.deepEqual(sessionItems.map((item) => item.type), ["tool_call"]); - assert.equal(((sessionItems[0]?.detailCommands as JsonRecord).item as string).includes("--after-seq 264 --limit 1"), true); - assert.equal(((sessionItems[0]?.detailCommands as JsonRecord).item as string).includes("--item-id tool_noise"), true); - assert.equal(((sessionItems[0]?.detailCommands as JsonRecord).seq as string).includes("--seq 265"), true); + assert.deepEqual(sessionItems.map((item) => item.type), ["assistant_progress", "tool_call"]); + assert.equal(((sessionItems[1]?.detailCommands as JsonRecord).item as string).includes("--after-seq 264 --limit 1"), true); + assert.equal(((sessionItems[1]?.detailCommands as JsonRecord).item as string).includes("--item-id tool_noise"), true); + assert.equal(((sessionItems[1]?.detailCommands as JsonRecord).seq as string).includes("--seq 265"), true); assert.ok(Number((sessionSummary.suppressedEvents as JsonRecord).outputBytes) > 0); assert.equal(JSON.stringify(sessionSummary).includes("runner trace line"), false); assert.equal(String((sessionSummary.drillDownCommands as JsonRecord).full).includes("--full"), true); diff --git a/src/selftest/cases/30-codex-stdio.ts b/src/selftest/cases/30-codex-stdio.ts index 20d3c59..6cbcae5 100644 --- a/src/selftest/cases/30-codex-stdio.ts +++ b/src/selftest/cases/30-codex-stdio.ts @@ -130,7 +130,7 @@ const selfTest: SelfTestCase = async (context) => { const webSearch = await createRunWithCommand(client, context, "hello web search progress", "selftest-web-search-progress", 15_000); const webSearchPromise = runOnce({ managerUrl: server.baseUrl, runId: webSearch.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_FAKE_CODEX_MODE: "web-search-progress" }, oneShot: true }) as Promise; await waitForEvent(client, webSearch.runId, (event) => event.type === "tool_call" && eventPayload(event).type === "webSearch" && eventPayload(event).method === "item/started", "webSearch tool_call start event"); - await waitForEvent(client, webSearch.runId, (event) => event.type === "assistant_message" && eventPayload(event).source === "agent-message-delta-progress", "assistant delta progress event"); + await waitForEvent(client, webSearch.runId, (event) => event.type === "assistant_progress" && eventPayload(event).source === "agent-message-delta-progress", "assistant delta progress event"); const webSearchResult = await webSearchPromise; assert.equal(webSearchResult.terminalStatus, "completed", "web search progress turn should complete"); const webSearchEnvelope = await client.get(`/api/v1/runs/${webSearch.runId}/commands/${webSearch.commandId}/result`) as JsonRecord; @@ -138,9 +138,9 @@ const selfTest: SelfTestCase = async (context) => { const webSearchEvents = await client.get(`/api/v1/runs/${webSearch.runId}/events?afterSeq=0&limit=100`) as { items?: Array<{ type: string; payload: unknown }> }; const webSearchItems = webSearchEvents.items ?? []; assert.ok(webSearchItems.some((event) => event.type === "tool_call" && eventPayload(event).type === "webSearch" && eventPayload(event).method === "item/completed"), "webSearch completion must remain visible as a tool_call"); - assert.ok(webSearchItems.some((event) => event.type === "assistant_message" && eventPayload(event).source === "agent-message-delta-progress" && eventPayload(event).progress === true), "assistant delta progress must be visible before final reply"); + assert.ok(webSearchItems.some((event) => event.type === "assistant_progress" && eventPayload(event).source === "agent-message-delta-progress" && eventPayload(event).progress === true), "assistant delta progress must be visible before final reply"); const webSearchStartIndex = webSearchItems.findIndex((event) => event.type === "tool_call" && eventPayload(event).type === "webSearch" && eventPayload(event).method === "item/started"); - const webSearchProgressIndex = webSearchItems.findIndex((event) => event.type === "assistant_message" && eventPayload(event).source === "agent-message-delta-progress"); + const webSearchProgressIndex = webSearchItems.findIndex((event) => event.type === "assistant_progress" && eventPayload(event).source === "agent-message-delta-progress"); const webSearchCompletedIndex = webSearchItems.findIndex((event) => event.type === "tool_call" && eventPayload(event).type === "webSearch" && eventPayload(event).method === "item/completed"); const webSearchFinalIndex = webSearchItems.findIndex((event) => event.type === "assistant_message" && eventPayload(event).source === "completed-agent-message" && eventPayload(event).itemId === "msg_search"); assert.ok(webSearchStartIndex >= 0 && webSearchStartIndex < webSearchProgressIndex, "webSearch start should be visible before assistant progress"); diff --git a/src/selftest/cases/55-timeout-liveness.ts b/src/selftest/cases/55-timeout-liveness.ts index 88d3a33..cee6566 100644 --- a/src/selftest/cases/55-timeout-liveness.ts +++ b/src/selftest/cases/55-timeout-liveness.ts @@ -24,7 +24,7 @@ const selfTest: SelfTestCase = async (context: SelfTestContext) => { assert.ok(Array.isArray(toolLive.recoveryActions)); const assistant = await createActiveRun(client, context, "timeout-liveness-assistant", 120_000); - await client.post(`/api/v1/runs/${assistant.runId}/events`, { type: "assistant_message", payload: { commandId: assistant.commandId, itemId: "msg_progress", progress: true, text: "正在生成 apply-patch,先汇总待改文件。" } }); + await client.post(`/api/v1/runs/${assistant.runId}/events`, { type: "assistant_progress", payload: { commandId: assistant.commandId, itemId: "msg_progress", source: "agent-message-delta-progress", progress: true, replyAuthority: false, final: false, text: "正在生成 apply-patch,先汇总待改文件。" } }); const assistantLive = (await commandResult(client, assistant)).liveness as JsonRecord; assert.equal(assistantLive.phase, "waiting-model-output"); assert.equal(((assistantLive.lastActivity as JsonRecord).activityKind), "assistant-progress"); diff --git a/src/selftest/cases/80-assistant-final-seal.ts b/src/selftest/cases/80-assistant-final-seal.ts index 6daff94..2983c40 100644 --- a/src/selftest/cases/80-assistant-final-seal.ts +++ b/src/selftest/cases/80-assistant-final-seal.ts @@ -4,6 +4,8 @@ import { ManagerClient } from "../../mgr/client.js"; import { startManagerServer } from "../../mgr/server.js"; import { MemoryAgentRunStore } from "../../mgr/store.js"; import { runOnce } from "../../runner/run-once.js"; +import { CodexStdioEventReducer } from "../../backend/codex-stdio.js"; +import { normalizeRunEventPayload } from "../../common/events.js"; import { createRunWithCommand, type SelfTestCase, type SelfTestContext } from "../harness.js"; interface EventRecord { @@ -13,6 +15,7 @@ interface EventRecord { } const selfTest: SelfTestCase = async (context) => { + assertCumulativeProgressEventContract(); const server = await startManagerServer({ port: 0, host: "127.0.0.1", @@ -43,6 +46,7 @@ const selfTest: SelfTestCase = async (context) => { "assistant-final-seal-delta-fallback", "assistant-progress-completed-item-fence", "assistant-progress-anonymous-generation-fence", + "assistant-progress-distinct-event-contract", "assistant-final-seal-control-event", ], }; @@ -51,6 +55,57 @@ const selfTest: SelfTestCase = async (context) => { } }; +function assertCumulativeProgressEventContract(): void { + const reducer = new CodexStdioEventReducer(); + const first = "a".repeat(500); + const second = "b".repeat(503); + const third = "c".repeat(187); + const text = `${first}${second}${third}`; + const rawEvents = [ + ...reducer.reduce(notification("item/agentMessage/delta", { itemId: "msg_fixed_progress", delta: first })).events, + ...reducer.reduce(notification("item/agentMessage/delta", { itemId: "msg_fixed_progress", delta: second })).events, + ...reducer.reduce(notification("item/agentMessage/delta", { itemId: "msg_fixed_progress", delta: third })).events, + ...reducer.reduce(notification("item/completed", { item: { id: "msg_fixed_progress", type: "agentMessage", text } })).events, + ]; + const terminal = reducer.reduce(notification("turn/completed", { turn: { id: "turn_fixed_progress", status: "completed" } })); + const events = [...rawEvents, ...terminal.events, ...reducer.finalize(terminal.terminal ?? null)].map((event) => ({ + ...event, + payload: normalizeRunEventPayload(event.type, event.payload), + })); + + assert.deepEqual(events.map((event) => event.type), [ + "assistant_progress", + "assistant_progress", + "assistant_message", + "backend_status", + "backend_status", + "terminal_status", + ]); + const progress = events.filter((event) => event.type === "assistant_progress"); + assert.deepEqual(progress.map((event) => event.payload.textBytes), [500, 1_003]); + assert.ok(progress.every((event) => event.payload.source === "agent-message-delta-progress" && event.payload.progress === true && event.payload.replyAuthority === false && event.payload.final === false)); + const completed = events.find((event) => event.type === "assistant_message"); + assert.equal(completed?.payload.source, "completed-agent-message"); + assert.equal(completed?.payload.textBytes, 1_190); + assert.equal(events.some((event) => event.type === "assistant_message" && event.payload.source === "agent-message-delta-progress"), false); + const seal = events.find((event) => event.type === "backend_status" && event.payload.finalSeal === true); + assert.equal(seal?.payload.phase, "assistant-message-final-sealed"); + assert.equal(Object.hasOwn(seal?.payload ?? {}, "text"), false); + assert.ok(events.some((event) => event.type === "terminal_status" && event.payload.terminalStatus === "completed")); + + assert.throws(() => normalizeRunEventPayload("assistant_progress", { + text: "invalid authoritative progress", + source: "agent-message-delta-progress", + progress: true, + replyAuthority: true, + final: false, + }), /assistant_progress requires/u); +} + +function notification(method: string, params: JsonRecord): JsonRecord { + return { jsonrpc: "2.0", method, params }; +} + async function assertAnonymousGenerationFence(client: ManagerClient, context: SelfTestContext, managerUrl: string): Promise { const command = await createRunWithCommand(client, context, "anonymous assistant generation fence", "selftest-assistant-anonymous-generation-fence", 15_000); const run = await runOnce({ @@ -71,7 +126,7 @@ async function assertAnonymousGenerationFence(client: ManagerClient, context: Se const completedMessages = events.filter((event) => event.type === "assistant_message" && event.payload.source === "completed-agent-message"); assert.deepEqual(completedMessages.map((event) => event.payload.text), ["Anonymous first.", "Anonymous second final."], "new anonymous item/started must open a distinct legal generation"); assert.ok(completedMessages.every((event) => event.payload.itemId === null)); - const progressEvents = events.filter((event) => event.type === "assistant_message" && event.payload.source === "agent-message-delta-progress"); + const progressEvents = events.filter((event) => event.type === "assistant_progress" && event.payload.source === "agent-message-delta-progress"); assert.equal(progressEvents.length, 1, "only the reopened anonymous generation's live progress must remain visible"); assert.equal(progressEvents[0]?.payload.progressFlush, false); assert.ok(progressEvents[0]!.seq < completedMessages[1]!.seq); @@ -118,7 +173,7 @@ async function assertHistoryReplaySealedOnce(client: ManagerClient, context: Sel for (const itemId of ["msg_replay_progress", "msg_same_text_a", "msg_same_text_b", "msg_late_final_seal"]) { assert.equal(completedMessages.filter((event) => event.payload.itemId === itemId).length, 1, `item ${itemId} must be durable exactly once`); } - const progressEvents = events.filter((event) => event.type === "assistant_message" && event.payload.source === "agent-message-delta-progress"); + const progressEvents = events.filter((event) => event.type === "assistant_progress" && event.payload.source === "agent-message-delta-progress"); assert.ok(progressEvents.some((event) => event.payload.itemId === "msg_late_final_seal" && event.payload.progressFlush === false), "long assistant deltas must remain visible before completion"); assert.equal(progressEvents.some((event) => event.payload.progressFlush === true), false, "completed item delta buffers must not be replayed by finalize()"); for (const progress of progressEvents) { @@ -126,6 +181,7 @@ async function assertHistoryReplaySealedOnce(client: ManagerClient, context: Sel assert.ok(completed && progress.seq < completed.seq, `item ${String(progress.payload.itemId)} progress must precede its durable completion`); } assert.equal(events.some((event) => event.type === "assistant_message" && typeof event.payload.text !== "string"), false, "bodyless control events must not be encoded as assistant messages"); + assert.equal(events.some((event) => event.type === "assistant_message" && event.payload.source === "agent-message-delta-progress"), false, "live progress must not be encoded as an assistant message"); const lateFinal = completedMessages.find((event) => event.payload.itemId === "msg_late_final_seal"); assert.equal(lateFinal?.payload.text, lateFinalText, "the unique durable message body must remain untruncated for seal dereferencing"); assert.equal(lateFinal?.payload.outputTruncated, false);