diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index d8c11fda9086..c7e4d3729757 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -1202,7 +1202,7 @@ Notes: - `stream.hiddenBoundarySeparator`: separator before visible text after hidden tool events (default: `"paragraph"`). - `stream.maxOutputChars`: maximum assistant output characters projected per ACP turn. - `stream.maxSessionUpdateChars`: maximum characters for projected ACP status/update lines. -- `stream.assistantCommentary`: when `true`, relay assistant commentary/progress text into ACP parent stream updates. Defaults to `false`. +- `stream.assistantCommentary`: when `true`, relay assistant commentary and selected ACP status progress text into ACP parent stream updates. Defaults to `false`. - `stream.tagVisibility`: record of tag names to boolean visibility overrides for streamed events. - `runtime.ttlMinutes`: idle TTL in minutes for ACP session workers before eligible cleanup. - `runtime.installCommand`: optional install command to run when bootstrapping an ACP runtime environment. diff --git a/docs/tools/acp-agents.md b/docs/tools/acp-agents.md index 5feae8f5f231..8b214ad45346 100644 --- a/docs/tools/acp-agents.md +++ b/docs/tools/acp-agents.md @@ -548,9 +548,9 @@ Two ways to start an ACP session: requester session as system events. Accepted responses include `streamLogPath` pointing to a session-scoped JSONL log (`.acp-stream.jsonl`) you can tail for full relay history. - Assistant commentary/progress text is hidden by default; set - `acp.stream.assistantCommentary: true` to include it in parent stream - updates while keeping final-answer delivery unchanged. + Assistant commentary and selected ACP status progress text are hidden by + default; set `acp.stream.assistantCommentary: true` to include them in parent + stream updates while keeping final-answer delivery unchanged. ACP `sessions_spawn` runs use `agents.defaults.subagents.runTimeoutSeconds` for diff --git a/src/agents/acp-spawn-parent-stream.test.ts b/src/agents/acp-spawn-parent-stream.test.ts index d2201fd4db0a..aafeb071bdbe 100644 --- a/src/agents/acp-spawn-parent-stream.test.ts +++ b/src/agents/acp-spawn-parent-stream.test.ts @@ -514,6 +514,72 @@ describe("startAcpSpawnParentStreamRelay", () => { relay.dispose(); }); + it("relays ACP status progress when assistant commentary is enabled", () => { + const relay = startAcpSpawnParentStreamRelay({ + runId: "run-status-commentary-enabled", + parentSessionKey: "agent:main:main", + childSessionKey: "agent:codex:acp:child-status-commentary-enabled", + agentId: "codex", + streamFlushMs: 10, + noOutputNoticeMs: 120_000, + assistantCommentary: true, + }); + + emitAgentEvent({ + runId: "run-status-commentary-enabled", + stream: "acp", + data: { + phase: "runtime_event", + eventType: "status", + tag: "plan", + text: "plan: inspect the runtime handoff first", + }, + }); + vi.advanceTimersByTime(15); + + expectTextWithFragment(collectedTexts(), "codex: plan: inspect the runtime handoff first"); + relay.dispose(); + }); + + it("does not relay hidden ACP status tags when assistant commentary is enabled", () => { + const relay = startAcpSpawnParentStreamRelay({ + runId: "run-status-commentary-hidden", + parentSessionKey: "agent:main:main", + childSessionKey: "agent:codex:acp:child-status-commentary-hidden", + agentId: "codex", + streamFlushMs: 10, + noOutputNoticeMs: 120_000, + assistantCommentary: true, + }); + + emitAgentEvent({ + runId: "run-status-commentary-hidden", + stream: "acp", + data: { + phase: "runtime_event", + eventType: "status", + tag: "usage_update", + text: "usage updated: 10/100", + }, + }); + emitAgentEvent({ + runId: "run-status-commentary-hidden", + stream: "acp", + data: { + phase: "runtime_event", + eventType: "status", + tag: "available_commands_update", + text: "available commands updated (7)", + }, + }); + vi.advanceTimersByTime(15); + + const texts = collectedTexts(); + expectNoTextWithFragment(texts, "usage updated"); + expectNoTextWithFragment(texts, "available commands updated"); + relay.dispose(); + }); + it("classifies opted-in commentary as visible output for stall notices", () => { const relay = startAcpSpawnParentStreamRelay({ runId: "run-commentary-visible-stall", diff --git a/src/agents/acp-spawn-parent-stream.ts b/src/agents/acp-spawn-parent-stream.ts index dcb785845384..e7cb75a623e7 100644 --- a/src/agents/acp-spawn-parent-stream.ts +++ b/src/agents/acp-spawn-parent-stream.ts @@ -24,6 +24,16 @@ const DEFAULT_NO_OUTPUT_POLL_MS = 15_000; const DEFAULT_MAX_RELAY_LIFETIME_MS = 6 * 60 * 60 * 1000; const STREAM_BUFFER_MAX_CHARS = 4_000; const STREAM_SNIPPET_MAX_CHARS = 220; +const HIDDEN_ACP_STATUS_TAGS = new Set([ + "agent_thought_chunk", + "available_commands_update", + "config_option_update", + "current_mode_update", + "session_info_update", + "tool_call", + "tool_call_update", + "usage_update", +]); function compactWhitespace(value: string): string { return value.replace(/\s+/g, " ").trim(); @@ -53,6 +63,20 @@ function formatProxyEnvSummary(keys: string[]): string { return `proxy env: ${keys.join(", ")}`; } +function shouldRelayAcpStatusProgress(params: { + eventType: string | undefined; + tag: string | undefined; + text: string | undefined; +}): boolean { + if (params.eventType !== "status" || !params.text) { + return false; + } + if (!params.tag) { + return true; + } + return !HIDDEN_ACP_STATUS_TAGS.has(params.tag); +} + function resolveAcpStreamLogPathFromSessionFile(sessionFile: string, sessionId: string): string { const baseDir = path.dirname(path.resolve(sessionFile)); return path.join(baseDir, `${sessionId}.acp-stream.jsonl`); @@ -311,6 +335,32 @@ export function startAcpSpawnParentStreamRelay(params: { flushTimer.unref?.(); }; + const appendVisibleProgress = (delta: string) => { + if (stallNotified) { + stallNotified = false; + recordTaskRunProgressByRunId({ + runId, + runtime: "acp", + sessionKey: params.childSessionKey, + lastEventAt: Date.now(), + eventSummary: "Resumed output.", + }); + emit(`${relayLabel} resumed output.`, `${contextPrefix}:resumed`); + } + + lastProgressAt = Date.now(); + firstVisibleOutputAt ??= lastProgressAt; + pendingText += delta; + if (pendingText.length > STREAM_BUFFER_MAX_CHARS) { + pendingText = pendingText.slice(-STREAM_BUFFER_MAX_CHARS); + } + if (pendingText.length >= STREAM_SNIPPET_MAX_CHARS || delta.includes("\n\n")) { + flushPending(); + return; + } + scheduleFlush(); + }; + const buildNoOutputNotice = () => { const seconds = Math.round(noOutputNoticeMs / 1000); if (!promptSubmittedAt) { @@ -405,29 +455,7 @@ export function startAcpSpawnParentStreamRelay(params: { return; } - if (stallNotified) { - stallNotified = false; - recordTaskRunProgressByRunId({ - runId, - runtime: "acp", - sessionKey: params.childSessionKey, - lastEventAt: Date.now(), - eventSummary: "Resumed output.", - }); - emit(`${relayLabel} resumed output.`, `${contextPrefix}:resumed`); - } - - lastProgressAt = Date.now(); - firstVisibleOutputAt ??= lastProgressAt; - pendingText += delta; - if (pendingText.length > STREAM_BUFFER_MAX_CHARS) { - pendingText = pendingText.slice(-STREAM_BUFFER_MAX_CHARS); - } - if (pendingText.length >= STREAM_SNIPPET_MAX_CHARS || delta.includes("\n\n")) { - flushPending(); - return; - } - scheduleFlush(); + appendVisibleProgress(delta); return; } @@ -451,8 +479,21 @@ export function startAcpSpawnParentStreamRelay(params: { } if (phase === "runtime_event") { const eventType = normalizeOptionalString(data?.eventType); + const text = normalizeOptionalString(data?.text); + const tag = normalizeOptionalString(data?.tag); firstRuntimeEventAt ??= Date.now(); lastRuntimeEventType = eventType; + if ( + shouldRelayAssistantCommentary && + shouldRelayAcpStatusProgress({ + eventType, + tag, + text, + }) + ) { + appendVisibleProgress(`${text}\n\n`); + return; + } lastProgressAt = Date.now(); return; } diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 890fbbb1e328..e848ea08c834 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -221,7 +221,7 @@ export const FIELD_HELP: Record = { "acp.stream.maxSessionUpdateChars": "Maximum characters for projected ACP session/update lines (tool/status updates).", "acp.stream.assistantCommentary": - "When true, relay assistant commentary/progress text into ACP parent stream updates. Defaults off.", + "When true, relay assistant commentary and selected ACP status progress text into ACP parent stream updates. Defaults off.", "acp.stream.tagVisibility": "Per-sessionUpdate visibility overrides for ACP projection (for example usage_update, available_commands_update).", "acp.runtime.ttlMinutes":