diff --git a/src/backend/codex-stdio.ts b/src/backend/codex-stdio.ts index d5c08e9..ef28bbc 100644 --- a/src/backend/codex-stdio.ts +++ b/src/backend/codex-stdio.ts @@ -77,6 +77,14 @@ interface CompletedAssistantMessage { text: string; } +interface FinalAssistantMessage { + itemId: string | null; + text: string; + messageIndex: number | null; + messageCount: number | null; + source: string; +} + interface AssistantDeltaProgressItem { itemId: string | null; text: string; @@ -604,7 +612,9 @@ async function runCodexStdioTurnWithSession(options: CodexStdioTurnOptions, sess if (pendingInterrupt) await pendingInterrupt.catch(() => undefined); if (terminal.status !== "completed") emitEvents(await session.close()); emitEvents(flushAssistantDeltaProgress(assistantDeltaProgress)); - if (completedAssistantMessages.length === 0) emitEvents(assistantMessageEventsForTurn(assistantText, terminal.status === "completed")); + const finalAssistant = terminal.status === "completed" ? finalAssistantMessageForTurn(completedAssistantMessages, assistantText) : null; + if (finalAssistant) emitEvent(assistantFinalResponseEvent(finalAssistant)); + else if (completedAssistantMessages.length === 0) emitEvents(assistantMessageEventsForTurn(assistantText, false)); emitEvents(suppressedNotificationEvents(suppressedNotifications)); emitEvent({ type: "terminal_status", payload: { terminalStatus: terminal.status, failureKind: terminal.failureKind, message: terminal.message } }); await liveEventWrite; @@ -883,6 +893,42 @@ function assistantMessageEventsForTurn(assistantDeltaText: string, completed: bo }]; } +function finalAssistantMessageForTurn(completedMessages: CompletedAssistantMessage[], assistantDeltaText: string): FinalAssistantMessage | null { + const latestCompleted = completedMessages.at(-1) ?? null; + if (latestCompleted && latestCompleted.text.trim().length > 0) { + return { + itemId: latestCompleted.itemId, + text: latestCompleted.text, + messageIndex: completedMessages.length, + messageCount: completedMessages.length, + source: "completed-agent-message-final", + }; + } + if (assistantDeltaText.trim().length === 0) return null; + return { + itemId: null, + text: assistantDeltaText, + messageIndex: 1, + messageCount: 1, + source: "agent-message-delta-final", + }; +} + +function assistantFinalResponseEvent(message: FinalAssistantMessage): BackendEvent { + return { + type: "assistant_message", + payload: { + text: message.text, + itemId: message.itemId, + source: message.source, + messageIndex: message.messageIndex, + messageCount: message.messageCount, + replyAuthority: true, + final: true, + }, + }; +} + function createAssistantDeltaProgressState(): AssistantDeltaProgressState { return new Map(); } diff --git a/src/common/events.ts b/src/common/events.ts index 367a345..53833a7 100644 --- a/src/common/events.ts +++ b/src/common/events.ts @@ -1,7 +1,7 @@ import { AgentRunError } from "./errors.js"; import type { EventType, JsonRecord, RunEvent, TerminalStatus } from "./types.js"; import { boundedTextSummary, commandOutputPayload } from "./output.js"; -import { redactJson } from "./redaction.js"; +import { redactJson, redactText } from "./redaction.js"; export const eventTypes = ["backend_status", "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[]; @@ -70,10 +70,16 @@ function normalizeCommandOutputPayload(payload: JsonRecord): JsonRecord { } function normalizeTextPayload(payload: JsonRecord): JsonRecord { - const { text: _text, delta: _delta, content: _content, summary: _summary, ...rest } = payload; + 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.replyAuthority === true || payload.final === true) { + const text = redactText(value); + const outputBytes = Buffer.byteLength(text, "utf8"); + const summary = { text, textChars: text.length, textBytes: outputBytes, outputBytes, textTruncated: false, outputTruncated: false } satisfies JsonRecord; + return { ...rest, text, summary, textBytes: outputBytes, textTruncated: false, outputBytes, outputTruncated: false }; + } const summary = boundedTextSummary(value); - return { ...rest, text: summary.text, summary, textBytes: summary.textBytes, textTruncated: summary.textTruncated }; + return { ...rest, text: summary.text, summary, textBytes: summary.textBytes, textTruncated: summary.textTruncated, outputBytes: summary.outputBytes, outputTruncated: summary.outputTruncated }; } function normalizeToolCallPayload(payload: JsonRecord): JsonRecord {