Merge pull request #314 from pikasTech/fix/219-assistant-final-seal
Pipelines as Code CI / agentrun-nc01-v02-ci-f95f83789273ee6e6c869dc08357ab53770c51a7 Success
Pipelines as Code CI / agentrun-nc01-v02-ci-f95f83789273ee6e6c869dc08357ab53770c51a7 Success
修复 assistant final 重复与历史通知重放
This commit is contained in:
+31
-10
@@ -130,7 +130,7 @@ interface FinalAssistantMessage {
|
||||
text: string;
|
||||
messageIndex: number | null;
|
||||
messageCount: number | null;
|
||||
source: string;
|
||||
targetSource: string;
|
||||
}
|
||||
|
||||
interface AssistantDeltaProgressItem {
|
||||
@@ -157,6 +157,7 @@ export class CodexStdioEventReducer {
|
||||
private assistantText = "";
|
||||
private readonly assistantDeltaProgress = createAssistantDeltaProgressState();
|
||||
private readonly completedAssistantMessages: CompletedAssistantMessage[] = [];
|
||||
private readonly completedAssistantItemIds = new Set<string>();
|
||||
private readonly deferredTerminalEvents: BackendEvent[] = [];
|
||||
private readonly suppressedNotifications = createSuppressedNotificationSummary();
|
||||
|
||||
@@ -169,8 +170,14 @@ export class CodexStdioEventReducer {
|
||||
if (progress) events.push(progress);
|
||||
}
|
||||
if (normalized.completedAssistantMessage) {
|
||||
this.completedAssistantMessages.push(normalized.completedAssistantMessage);
|
||||
events.push(assistantMessageEventForCompleted(normalized.completedAssistantMessage, this.completedAssistantMessages.length));
|
||||
const itemId = normalized.completedAssistantMessage.itemId;
|
||||
if (itemId && this.completedAssistantItemIds.has(itemId)) {
|
||||
recordSuppressedNotification(this.suppressedNotifications, "item/completed:duplicate-agentMessage", "agentMessage");
|
||||
} else {
|
||||
if (itemId) this.completedAssistantItemIds.add(itemId);
|
||||
this.completedAssistantMessages.push(normalized.completedAssistantMessage);
|
||||
events.push(assistantMessageEventForCompleted(normalized.completedAssistantMessage, this.completedAssistantMessages.length));
|
||||
}
|
||||
}
|
||||
if (normalized.terminalEvents) this.deferredTerminalEvents.push(...normalized.terminalEvents);
|
||||
return {
|
||||
@@ -183,9 +190,9 @@ export class CodexStdioEventReducer {
|
||||
|
||||
finalize(terminal: { status: TerminalStatus; failureKind: FailureKind | null; message: string | null } | null): BackendEvent[] {
|
||||
const events = flushAssistantDeltaProgress(this.assistantDeltaProgress);
|
||||
if (this.completedAssistantMessages.length === 0) events.push(...assistantMessageEventsForTurn(this.assistantText, false));
|
||||
const finalAssistant = terminal?.status === "completed" ? finalAssistantMessageForTurn(this.completedAssistantMessages, this.assistantText) : null;
|
||||
if (finalAssistant) events.push(assistantFinalResponseEvent(finalAssistant));
|
||||
else if (this.completedAssistantMessages.length === 0) events.push(...assistantMessageEventsForTurn(this.assistantText, false));
|
||||
if (finalAssistant) events.push(assistantFinalSealEvent(finalAssistant));
|
||||
events.push(...suppressedNotificationEvents(this.suppressedNotifications));
|
||||
events.push(...this.deferredTerminalEvents);
|
||||
if (terminal) events.push({ type: "terminal_status", payload: { terminalStatus: terminal.status, failureKind: terminal.failureKind, message: terminal.message } });
|
||||
@@ -1297,6 +1304,7 @@ function assistantMessageEventForCompleted(message: CompletedAssistantMessage, m
|
||||
messageCount: null,
|
||||
replyAuthority: false,
|
||||
final: false,
|
||||
durableText: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1313,6 +1321,7 @@ function assistantMessageEventsForTurn(assistantDeltaText: string, completed: bo
|
||||
messageCount: 1,
|
||||
replyAuthority: completed,
|
||||
final: completed,
|
||||
durableText: true,
|
||||
},
|
||||
}];
|
||||
}
|
||||
@@ -1325,7 +1334,7 @@ function finalAssistantMessageForTurn(completedMessages: CompletedAssistantMessa
|
||||
text: latestCompleted.text,
|
||||
messageIndex: completedMessages.length,
|
||||
messageCount: completedMessages.length,
|
||||
source: "completed-agent-message-final",
|
||||
targetSource: "completed-agent-message",
|
||||
};
|
||||
}
|
||||
if (assistantDeltaText.trim().length === 0) return null;
|
||||
@@ -1334,25 +1343,37 @@ function finalAssistantMessageForTurn(completedMessages: CompletedAssistantMessa
|
||||
text: assistantDeltaText,
|
||||
messageIndex: 1,
|
||||
messageCount: 1,
|
||||
source: "agent-message-delta-final",
|
||||
targetSource: "agent-message-delta-fallback",
|
||||
};
|
||||
}
|
||||
|
||||
function assistantFinalResponseEvent(message: FinalAssistantMessage): BackendEvent {
|
||||
function assistantFinalSealEvent(message: FinalAssistantMessage): BackendEvent {
|
||||
return {
|
||||
type: "assistant_message",
|
||||
payload: {
|
||||
text: message.text,
|
||||
itemId: message.itemId,
|
||||
source: message.source,
|
||||
source: "assistant-message-final-seal",
|
||||
messageIndex: message.messageIndex,
|
||||
messageCount: message.messageCount,
|
||||
replyAuthority: true,
|
||||
final: true,
|
||||
finalSeal: true,
|
||||
replyRef: {
|
||||
eventType: "assistant_message",
|
||||
itemId: message.itemId,
|
||||
messageIndex: message.messageIndex,
|
||||
messageCount: message.messageCount,
|
||||
source: message.targetSource,
|
||||
textSha256: sha256Text(redactText(message.text)),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function sha256Text(value: string): string {
|
||||
return createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
function createAssistantDeltaProgressState(): AssistantDeltaProgressState {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
@@ -123,6 +123,8 @@ function nonEmptyText(value: unknown): string | null {
|
||||
function normalizeTextPayload(payload: JsonRecord): JsonRecord {
|
||||
const { text: _text, delta: _delta, content: _content, summary: _summary, textBytes: _textBytes, textTruncated: _textTruncated, outputBytes: _outputBytes, outputTruncated: _outputTruncated, ...rest } = payload;
|
||||
const value = typeof payload.text === "string" ? payload.text : typeof payload.delta === "string" ? payload.delta : typeof payload.content === "string" ? payload.content : "";
|
||||
if (payload.finalSeal === true) return normalizeAssistantFinalSeal(rest, value);
|
||||
if (payload.durableText === true) return normalizeDurableAssistantText(rest, value);
|
||||
if (payload.replyAuthority === true || payload.final === true) {
|
||||
const text = redactText(value);
|
||||
const outputBytes = Buffer.byteLength(text, "utf8");
|
||||
@@ -133,6 +135,34 @@ function normalizeTextPayload(payload: JsonRecord): JsonRecord {
|
||||
return { ...rest, text: summary.text, summary, textBytes: summary.textBytes, textTruncated: summary.textTruncated, outputBytes: summary.outputBytes, outputTruncated: summary.outputTruncated };
|
||||
}
|
||||
|
||||
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") {
|
||||
throw new AgentRunError("schema-invalid", "durable assistant text requires a completed-agent-message or agent-message-delta-fallback source", { httpStatus: 400 });
|
||||
}
|
||||
const text = redactText(body);
|
||||
const textBytes = Buffer.byteLength(text, "utf8");
|
||||
return {
|
||||
...payload,
|
||||
text,
|
||||
summary: { textChars: text.length, textBytes, outputBytes: textBytes, textTruncated: false, outputTruncated: false },
|
||||
textBytes,
|
||||
textTruncated: false,
|
||||
outputBytes: textBytes,
|
||||
outputTruncated: false,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAssistantFinalSeal(payload: JsonRecord, body: string): JsonRecord {
|
||||
const replyRef = typeof payload.replyRef === "object" && payload.replyRef !== null && !Array.isArray(payload.replyRef) ? payload.replyRef as JsonRecord : null;
|
||||
const textSha256 = replyRef && typeof replyRef.textSha256 === "string" ? replyRef.textSha256 : "";
|
||||
if (body.length > 0) throw new AgentRunError("schema-invalid", "assistant final seal must not carry text, content, or delta", { httpStatus: 400 });
|
||||
if (payload.replyAuthority !== true || payload.final !== true || replyRef?.eventType !== "assistant_message" || typeof replyRef.source !== "string" || !/^[0-9a-f]{64}$/u.test(textSha256)) {
|
||||
throw new AgentRunError("schema-invalid", "assistant final seal requires final replyAuthority and an assistant_message replyRef with source and textSha256", { httpStatus: 400 });
|
||||
}
|
||||
return { ...payload, replyRef: redactJson(replyRef), textBytes: 0, textTruncated: false, outputBytes: 0, outputTruncated: false };
|
||||
}
|
||||
|
||||
function normalizeToolCallPayload(payload: JsonRecord): JsonRecord {
|
||||
const redacted = redactJson(payload);
|
||||
if (isCommandExecutionToolCall(redacted)) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { AgentRunStore } from "./store.js";
|
||||
import type { CommandRecord, FailureKind, JsonRecord, JsonValue, RunEvent, RunRecord, RunnerJobRecord, TerminalStatus } from "../common/types.js";
|
||||
import { boundedTextSummary, outputBytesFromPayload, outputTruncatedFromPayload } from "../common/output.js";
|
||||
@@ -794,6 +795,8 @@ function resultFailureDetails(events: RunEvent[], terminal: TerminalStatus | nul
|
||||
|
||||
function assistantReply(events: RunEvent[]): AssistantReplySummary {
|
||||
const assistantEvents = events.filter((event) => event.type === "assistant_message");
|
||||
const sealedReply = assistantReplyFromFinalSeal(assistantEvents);
|
||||
if (sealedReply) return sealedReply;
|
||||
const latestAuthoritative = [...assistantEvents].reverse().find((event) => (event.payload.replyAuthority === true || event.payload.final === true) && textPayload(event.payload).length > 0);
|
||||
if (latestAuthoritative) return assistantReplySummary(latestAuthoritative);
|
||||
const latestAssistant = [...assistantEvents].reverse().find((event) => textPayload(event.payload).length > 0);
|
||||
@@ -801,6 +804,34 @@ function assistantReply(events: RunEvent[]): AssistantReplySummary {
|
||||
return { text: "", seq: null, source: null, final: false, replyAuthority: false, textTruncated: false, outputTruncated: false };
|
||||
}
|
||||
|
||||
function assistantReplyFromFinalSeal(events: RunEvent[]): AssistantReplySummary | null {
|
||||
const seals = [...events].reverse().filter((event) => event.payload.finalSeal === true && event.payload.replyAuthority === true);
|
||||
for (const seal of seals) {
|
||||
const replyRef = jsonRecordValue(seal.payload.replyRef);
|
||||
if (!replyRef || replyRef.eventType !== "assistant_message") continue;
|
||||
const target = [...events].reverse().find((event) => event.seq < seal.seq && assistantReplyRefMatches(event, replyRef));
|
||||
if (!target) continue;
|
||||
const targetSummary = assistantReplySummary(target);
|
||||
return {
|
||||
...targetSummary,
|
||||
seq: seal.seq,
|
||||
source: typeof seal.payload.source === "string" ? seal.payload.source : "assistant-message-final-seal",
|
||||
final: true,
|
||||
replyAuthority: true,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function assistantReplyRefMatches(event: RunEvent, replyRef: JsonRecord): boolean {
|
||||
if (event.payload.finalSeal === true || textPayload(event.payload).length === 0) return false;
|
||||
if (stringJsonValue(event.payload.source) !== stringJsonValue(replyRef.source)) return false;
|
||||
if ((stringJsonValue(event.payload.itemId) ?? null) !== (stringJsonValue(replyRef.itemId) ?? null)) return false;
|
||||
if ((numberJsonValue(event.payload.messageIndex) ?? null) !== (numberJsonValue(replyRef.messageIndex) ?? null)) return false;
|
||||
const textSha256 = stringJsonValue(replyRef.textSha256);
|
||||
return textSha256 !== null && textSha256 === sha256Text(textPayload(event.payload));
|
||||
}
|
||||
|
||||
function assistantReplySummary(event: RunEvent): AssistantReplySummary {
|
||||
return {
|
||||
text: textPayload(event.payload),
|
||||
@@ -821,6 +852,10 @@ function textPayload(payload: JsonRecord): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
function sha256Text(value: string): string {
|
||||
return createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
function artifactSummary(events: RunEvent[]): JsonRecord {
|
||||
let commandOutputEvents = 0;
|
||||
let diffEvents = 0;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
closeAgentRunKafkaProducer,
|
||||
publishAgentRunKafkaMessage,
|
||||
setAgentRunKafkaProducerFactoryForSelfTest,
|
||||
sha256Hex,
|
||||
type AgentRunKafkaConfig,
|
||||
} from "../../common/kafka-events.js";
|
||||
import type { JsonRecord } from "../../common/types.js";
|
||||
@@ -53,12 +54,23 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
assert.equal((record.value.reconstruction as JsonRecord).originalOutboxSeq, null);
|
||||
assert.equal(((record.value.reconstruction as JsonRecord).identityDerivation as JsonRecord).reversible, true);
|
||||
}
|
||||
const finalAssistant = reconstructed.records.find((record) => {
|
||||
const finalSeal = reconstructed.records.find((record) => {
|
||||
const event = record.value.event as JsonRecord;
|
||||
const payload = event.payload as JsonRecord;
|
||||
return event.type === "assistant_message" && payload.replyAuthority === true;
|
||||
});
|
||||
assert.equal(((finalAssistant?.value.event as JsonRecord).payload as JsonRecord).text, "stdio reconstruction fixture answer");
|
||||
const finalSealPayload = (finalSeal?.value.event as JsonRecord).payload as JsonRecord;
|
||||
assert.equal(finalSealPayload.finalSeal, true);
|
||||
assert.equal(Object.hasOwn(finalSealPayload, "text"), false);
|
||||
const replyRef = finalSealPayload.replyRef as JsonRecord;
|
||||
const finalAssistant = reconstructed.records.find((record) => {
|
||||
const event = record.value.event as JsonRecord;
|
||||
const payload = event.payload as JsonRecord;
|
||||
return event.type === "assistant_message" && payload.itemId === replyRef.itemId && payload.source === replyRef.source;
|
||||
});
|
||||
const finalAssistantText = String(((finalAssistant?.value.event as JsonRecord).payload as JsonRecord).text ?? "");
|
||||
assert.equal(finalAssistantText, "stdio reconstruction fixture answer");
|
||||
assert.equal(replyRef.textSha256, sha256Hex(finalAssistantText));
|
||||
assert.equal((reconstructed.records.at(-1)?.value.event as JsonRecord).type, "terminal_status");
|
||||
|
||||
let readGroupId = "";
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import assert from "node:assert/strict";
|
||||
import type { JsonRecord } from "../../common/types.js";
|
||||
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 { createRunWithCommand, type SelfTestCase, type SelfTestContext } from "../harness.js";
|
||||
|
||||
interface EventRecord {
|
||||
seq: number;
|
||||
type: string;
|
||||
payload: JsonRecord;
|
||||
}
|
||||
|
||||
const selfTest: SelfTestCase = async (context) => {
|
||||
const server = await startManagerServer({
|
||||
port: 0,
|
||||
host: "127.0.0.1",
|
||||
sourceCommit: "self-test",
|
||||
store: new MemoryAgentRunStore(),
|
||||
runnerJobDefaults: {
|
||||
namespace: "agentrun-v02",
|
||||
managerUrl: "http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080",
|
||||
runnerApiKeySecretRef: { name: "agentrun-v02-api-key", key: "HWLAB_API_KEY" },
|
||||
image: "registry.invalid/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
||||
serviceAccountName: "agentrun-v02-runner",
|
||||
jobNamePrefix: "agentrun-v02-runner",
|
||||
lane: "v0.2",
|
||||
},
|
||||
});
|
||||
try {
|
||||
const client = new ManagerClient(server.baseUrl);
|
||||
await assertHistoryReplaySealedOnce(client, context, server.baseUrl);
|
||||
await assertDeltaFallbackUsesBodylessSeal(client, context, server.baseUrl);
|
||||
return {
|
||||
name: "assistant-final-seal",
|
||||
tests: [
|
||||
"assistant-final-seal-history-replay",
|
||||
"assistant-final-seal-distinct-identical-text",
|
||||
"assistant-final-seal-late-final",
|
||||
"assistant-final-seal-manager-result-pointer",
|
||||
"assistant-final-seal-delta-fallback",
|
||||
],
|
||||
};
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
};
|
||||
|
||||
async function assertHistoryReplaySealedOnce(client: ManagerClient, context: SelfTestContext, managerUrl: string): Promise<void> {
|
||||
const lateFinalText = `Late unique final answer:${"x".repeat(5_000)} Authorization: Bearer REDACTED`;
|
||||
const rawLateFinalSecret = "final-secret-token-value";
|
||||
const command = await createRunWithCommand(client, context, "history replay final seal", "selftest-assistant-history-replay-final-seal", 15_000);
|
||||
const run = await runOnce({
|
||||
managerUrl,
|
||||
runId: command.runId,
|
||||
codexCommand: context.fakeCodexCommand,
|
||||
codexArgs: context.fakeCodexArgs,
|
||||
codexHome: context.codexHome,
|
||||
env: {
|
||||
CODEX_HOME: context.codexHome,
|
||||
AGENTRUN_FAKE_CODEX_MODE: "assistant-history-replay-final-seal",
|
||||
AGENTRUN_CODEX_TERMINAL_NOTIFICATION_DRAIN_MS: "200",
|
||||
},
|
||||
oneShot: true,
|
||||
}) as JsonRecord;
|
||||
assert.equal(run.terminalStatus, "completed");
|
||||
|
||||
const events = await listEvents(client, command.runId);
|
||||
const completedMessages = events.filter((event) => event.type === "assistant_message" && event.payload.source === "completed-agent-message");
|
||||
assert.deepEqual(completedMessages.map((event) => event.payload.itemId), [
|
||||
"msg_replay_progress",
|
||||
"msg_same_text_a",
|
||||
"msg_same_text_b",
|
||||
"msg_late_final_seal",
|
||||
]);
|
||||
assert.equal(completedMessages.filter((event) => event.payload.text === "Legitimate identical assistant text.").length, 2, "different itemId values with identical text must both remain durable");
|
||||
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 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);
|
||||
assert.equal(JSON.stringify(events).includes(rawLateFinalSecret), false, "secret-like final text must be redacted before becoming durable");
|
||||
|
||||
const seals = events.filter((event) => event.type === "assistant_message" && event.payload.finalSeal === true);
|
||||
assert.equal(seals.length, 1, "one completed turn must have exactly one final seal");
|
||||
const seal = seals[0]!;
|
||||
assert.equal(Object.hasOwn(seal.payload, "text"), false, "final seal must not duplicate the assistant body");
|
||||
assert.equal(Object.hasOwn(seal.payload, "content"), false, "final seal must not duplicate the assistant body through content");
|
||||
assert.equal(Object.hasOwn(seal.payload, "delta"), false, "final seal must not duplicate the assistant body through delta");
|
||||
assert.equal(seal.payload.source, "assistant-message-final-seal");
|
||||
assert.equal(seal.payload.replyAuthority, true);
|
||||
assert.equal(seal.payload.final, true);
|
||||
const replyRef = asRecord(seal.payload.replyRef);
|
||||
assert.equal(replyRef.itemId, "msg_late_final_seal");
|
||||
assert.equal(replyRef.source, "completed-agent-message");
|
||||
assert.match(String(replyRef.textSha256), /^[0-9a-f]{64}$/u);
|
||||
|
||||
const suppression = events.find((event) => event.type === "backend_status" && event.payload.phase === "codex-app-server-notifications-suppressed");
|
||||
assert.ok(suppression, "history replay suppression must remain visible without persisting duplicate bodies");
|
||||
const replayMethod = asRecords(suppression?.payload.methods).find((item) => item.method === "item/completed:duplicate-agentMessage");
|
||||
assert.equal(replayMethod?.count, 5);
|
||||
|
||||
const toolEvents = events.filter((event) => event.type === "tool_call" && event.payload.itemId === "tool_before_history_replay");
|
||||
assert.equal(toolEvents.length, 2, "assistant replay suppression must neither replay nor drop the tool start/completion pair");
|
||||
assert.deepEqual(toolEvents.map((event) => event.payload.method), ["item/started", "item/completed"]);
|
||||
|
||||
const turnCompleted = events.find((event) => event.type === "backend_status" && event.payload.phase === "turn/completed");
|
||||
assert.ok(turnCompleted && seal.seq < turnCompleted.seq, "bodyless final seal must precede the deferred turn terminal event");
|
||||
|
||||
const result = await client.get(`/api/v1/runs/${command.runId}/commands/${command.commandId}/result`) as JsonRecord;
|
||||
assert.equal(result.reply, lateFinalText, "Manager result must dereference the untruncated final seal target instead of reading duplicate final text");
|
||||
assert.equal(JSON.stringify(result).includes(rawLateFinalSecret), false, "Manager result must not re-expose the redacted secret-like source text");
|
||||
assert.equal(result.finalResponseAuthority, "authoritative");
|
||||
assert.equal(result.finalAssistantSeq, seal.seq);
|
||||
const finalResponse = asRecord(result.finalResponse);
|
||||
assert.equal(finalResponse.source, "assistant-message-final-seal");
|
||||
assert.equal(finalResponse.replyAuthority, true);
|
||||
assert.equal(finalResponse.final, true);
|
||||
}
|
||||
|
||||
async function assertDeltaFallbackUsesBodylessSeal(client: ManagerClient, context: SelfTestContext, managerUrl: string): Promise<void> {
|
||||
const command = await createRunWithCommand(client, context, "delta fallback final seal", "selftest-assistant-delta-fallback-final-seal", 15_000);
|
||||
const run = await runOnce({
|
||||
managerUrl,
|
||||
runId: command.runId,
|
||||
codexCommand: context.fakeCodexCommand,
|
||||
codexArgs: context.fakeCodexArgs,
|
||||
codexHome: context.codexHome,
|
||||
env: { CODEX_HOME: context.codexHome },
|
||||
oneShot: true,
|
||||
}) as JsonRecord;
|
||||
assert.equal(run.terminalStatus, "completed");
|
||||
|
||||
const events = await listEvents(client, command.runId);
|
||||
const fallback = events.filter((event) => event.type === "assistant_message" && event.payload.source === "agent-message-delta-fallback");
|
||||
assert.equal(fallback.length, 1, "delta-only turns must persist one full fallback body before sealing it");
|
||||
assert.equal(fallback[0]?.payload.text, "fake codex stdio reply");
|
||||
const seal = events.find((event) => event.type === "assistant_message" && event.payload.finalSeal === true);
|
||||
assert.ok(seal);
|
||||
assert.equal(Object.hasOwn(seal.payload, "text"), false);
|
||||
assert.equal(asRecord(seal.payload.replyRef).source, "agent-message-delta-fallback");
|
||||
|
||||
const result = await client.get(`/api/v1/runs/${command.runId}/commands/${command.commandId}/result`) as JsonRecord;
|
||||
assert.equal(result.reply, "fake codex stdio reply");
|
||||
assert.equal(result.finalResponseAuthority, "authoritative");
|
||||
assert.equal(result.finalAssistantSeq, seal.seq);
|
||||
}
|
||||
|
||||
async function listEvents(client: ManagerClient, runId: string): Promise<EventRecord[]> {
|
||||
const response = await client.get(`/api/v1/runs/${runId}/events?afterSeq=0&limit=200`) as { items?: EventRecord[] };
|
||||
return response.items ?? [];
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): JsonRecord {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as JsonRecord : {};
|
||||
}
|
||||
|
||||
function asRecords(value: unknown): JsonRecord[] {
|
||||
return Array.isArray(value) ? value.map(asRecord) : [];
|
||||
}
|
||||
|
||||
export default selfTest;
|
||||
@@ -239,6 +239,32 @@ for await (const line of rl) {
|
||||
}, 450).unref?.();
|
||||
continue;
|
||||
}
|
||||
if (mode === "assistant-history-replay-final-seal") {
|
||||
turnCounter += 1;
|
||||
const turn = { id: `turn_selftest_${turnCounter}`, status: "completed" };
|
||||
const lateFinalText = `Late unique final answer:${"x".repeat(5_000)} Authorization: Bearer final-secret-token-value`;
|
||||
respond(message.id, { turn });
|
||||
notify("turn/started", { turn });
|
||||
notify("item/started", { item: { id: "tool_before_history_replay", type: "commandExecution", command: "echo durable-once", status: "running" } });
|
||||
notify("item/completed", { item: { id: "tool_before_history_replay", type: "commandExecution", command: "echo durable-once", status: "completed", exitCode: 0 } });
|
||||
notify("item/completed", { item: { id: "msg_replay_progress", type: "agentMessage", text: "Progress before history replay." } });
|
||||
notify("item/completed", { item: { id: "msg_same_text_a", type: "agentMessage", text: "Legitimate identical assistant text." } });
|
||||
notify("item/completed", { item: { id: "msg_same_text_b", type: "agentMessage", text: "Legitimate identical assistant text." } });
|
||||
notify("item/completed", { item: { id: "msg_replay_progress", type: "agentMessage", text: "Progress before history replay." } });
|
||||
notify("turn/completed", { turn });
|
||||
setTimeout(() => {
|
||||
notify("item/completed", { item: { id: "msg_replay_progress", type: "agentMessage", text: "Progress before history replay." } });
|
||||
notify("item/completed", { item: { id: "msg_same_text_a", type: "agentMessage", text: "Legitimate identical assistant text." } });
|
||||
notify("item/completed", { item: { id: "msg_same_text_b", type: "agentMessage", text: "Legitimate identical assistant text." } });
|
||||
}, 20).unref?.();
|
||||
setTimeout(() => {
|
||||
notify("item/completed", { item: { id: "msg_late_final_seal", type: "agentMessage", text: lateFinalText } });
|
||||
}, 40).unref?.();
|
||||
setTimeout(() => {
|
||||
notify("item/completed", { item: { id: "msg_late_final_seal", type: "agentMessage", text: lateFinalText } });
|
||||
}, 60).unref?.();
|
||||
continue;
|
||||
}
|
||||
if (mode === "web-search-progress") {
|
||||
turnCounter += 1;
|
||||
const turn = { id: `turn_selftest_${turnCounter}`, status: "completed" };
|
||||
|
||||
Reference in New Issue
Block a user