fix: 区分助手实时进度与最终正文

This commit is contained in:
root
2026-07-12 10:10:07 +02:00
parent 352f359749
commit f90da964f5
11 changed files with 110 additions and 27 deletions
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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,
+19 -2
View File
@@ -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<string>(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") {
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+7 -6
View File
@@ -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 {
+1 -1
View File
@@ -1675,5 +1675,5 @@ function workQueueStatus<T extends JsonRecord>(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";
}
+16 -7
View File
@@ -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);
+3 -3
View File
@@ -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<JsonRecord>;
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");
+1 -1
View File
@@ -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");
+58 -2
View File
@@ -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<void> {
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);