diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index cb3c14404a4c..d8c11fda9086 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -1178,6 +1178,7 @@ Notes: hiddenBoundarySeparator: "paragraph", // none | space | newline | paragraph maxOutputChars: 50000, maxSessionUpdateChars: 500, + assistantCommentary: false, }, runtime: { @@ -1201,6 +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.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 a11f449bec1e..5feae8f5f231 100644 --- a/docs/tools/acp-agents.md +++ b/docs/tools/acp-agents.md @@ -548,6 +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. 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 16d31032a7f4..d2201fd4db0a 100644 --- a/src/agents/acp-spawn-parent-stream.test.ts +++ b/src/agents/acp-spawn-parent-stream.test.ts @@ -485,6 +485,83 @@ describe("startAcpSpawnParentStreamRelay", () => { relay.dispose(); }); + it("relays commentary-phase assistant text when enabled", () => { + const relay = startAcpSpawnParentStreamRelay({ + runId: "run-commentary-enabled", + parentSessionKey: "agent:main:main", + childSessionKey: "agent:codex:acp:child-commentary-enabled", + agentId: "codex", + streamFlushMs: 10, + noOutputNoticeMs: 120_000, + assistantCommentary: true, + }); + + emitAgentEvent({ + runId: "run-commentary-enabled", + stream: "assistant", + data: { + delta: "checking thread context; then post a tight progress reply here.", + phase: "commentary", + }, + }); + vi.advanceTimersByTime(15); + + const texts = collectedTexts(); + expectTextWithFragment( + texts, + "codex: checking thread context; then post a tight progress reply here.", + ); + relay.dispose(); + }); + + it("classifies opted-in commentary as visible output for stall notices", () => { + const relay = startAcpSpawnParentStreamRelay({ + runId: "run-commentary-visible-stall", + parentSessionKey: "agent:main:main", + childSessionKey: "agent:codex:acp:child-commentary-visible-stall", + agentId: "codex", + streamFlushMs: 1, + noOutputNoticeMs: 1_000, + noOutputPollMs: 250, + assistantCommentary: true, + }); + + emitAgentEvent({ + runId: "run-commentary-visible-stall", + stream: "acp", + data: { + phase: "prompt_submitted", + at: Date.now(), + proxyEnvKeys: [], + }, + }); + emitAgentEvent({ + runId: "run-commentary-visible-stall", + stream: "acp", + data: { + phase: "runtime_event", + eventType: "status", + text: "connecting to upstream", + }, + }); + emitAgentEvent({ + runId: "run-commentary-visible-stall", + stream: "assistant", + data: { + delta: "checking active files before patching.", + phase: "commentary", + }, + }); + vi.advanceTimersByTime(5); + vi.advanceTimersByTime(1_500); + + const texts = collectedTexts(); + expectTextWithFragment(texts, "codex: checking active files before patching."); + expectNoTextWithFragment(texts, "has ACP runtime activity but no visible assistant output"); + expectTextWithFragment(texts, "has produced no visible output for 1s"); + relay.dispose(); + }); + it("still relays final_answer assistant text after suppressed commentary", () => { const relay = startAcpSpawnParentStreamRelay({ runId: "run-final", diff --git a/src/agents/acp-spawn-parent-stream.ts b/src/agents/acp-spawn-parent-stream.ts index 8a7d736818c8..dcb785845384 100644 --- a/src/agents/acp-spawn-parent-stream.ts +++ b/src/agents/acp-spawn-parent-stream.ts @@ -117,6 +117,7 @@ export function startAcpSpawnParentStreamRelay(params: { noOutputPollMs?: number; maxRelayLifetimeMs?: number; emitStartNotice?: boolean; + assistantCommentary?: boolean; }): AcpSpawnParentRelayHandle { const runId = normalizeOptionalString(params.runId) ?? ""; const parentSessionKey = normalizeOptionalString(params.parentSessionKey) ?? ""; @@ -209,6 +210,7 @@ export function startAcpSpawnParentStreamRelay(params: { }); }; const shouldSurfaceUpdates = params.surfaceUpdates !== false; + const shouldRelayAssistantCommentary = params.assistantCommentary === true; const eventRouting = params.eventRouting ?? { mainKey: params.mainKey, sessionScope: params.sessionScope, @@ -398,7 +400,7 @@ export function startAcpSpawnParentStreamRelay(params: { ...(assistantPhase ? { phase: assistantPhase } : {}), }); - if (assistantPhase === "commentary") { + if (assistantPhase === "commentary" && !shouldRelayAssistantCommentary) { lastProgressAt = Date.now(); return; } diff --git a/src/agents/acp-spawn.ts b/src/agents/acp-spawn.ts index 0da5c69940b3..da767cead725 100644 --- a/src/agents/acp-spawn.ts +++ b/src/agents/acp-spawn.ts @@ -1551,6 +1551,7 @@ export async function spawnAcpDirect( const parentEventRouting = parentSessionKey ? resolveEventSessionRoutingPolicy({ cfg, sessionKey: parentSessionKey }) : undefined; + const assistantCommentary = cfg.acp?.stream?.assistantCommentary === true; if (effectiveStreamToParent && parentSessionKey) { // Register relay before dispatch so fast lifecycle failures are not missed. parentRelay = startAcpSpawnParentStreamRelay({ @@ -1564,6 +1565,7 @@ export async function spawnAcpDirect( logPath: streamLogPath, deliveryContext: parentDeliveryCtx, emitStartNotice: false, + assistantCommentary, }); } const gatewayAttachments = toGatewayImageAttachments(params.attachments); @@ -1623,6 +1625,7 @@ export async function spawnAcpDirect( logPath: streamLogPath, deliveryContext: parentDeliveryCtx, emitStartNotice: false, + assistantCommentary, }); } parentRelay?.notifyStarted(); diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index d25cdec3a44a..890fbbb1e328 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -220,6 +220,8 @@ export const FIELD_HELP: Record = { "Maximum assistant output characters projected per ACP turn before truncation notice is emitted.", "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.", "acp.stream.tagVisibility": "Per-sessionUpdate visibility overrides for ACP projection (for example usage_update, available_commands_update).", "acp.runtime.ttlMinutes": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 03947211776e..4a0daf305411 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -560,6 +560,7 @@ export const FIELD_LABELS: Record = { "acp.stream.hiddenBoundarySeparator": "ACP Stream Hidden Boundary Separator", "acp.stream.maxOutputChars": "ACP Stream Max Output Chars", "acp.stream.maxSessionUpdateChars": "ACP Stream Max Session Update Chars", + "acp.stream.assistantCommentary": "ACP Stream Assistant Commentary", "acp.stream.tagVisibility": "ACP Stream Tag Visibility", "acp.runtime.ttlMinutes": "ACP Runtime TTL (minutes)", "acp.runtime.installCommand": "ACP Runtime Install Command", diff --git a/src/config/types.acp.ts b/src/config/types.acp.ts index 37263efd2df0..c60f7235f64a 100644 --- a/src/config/types.acp.ts +++ b/src/config/types.acp.ts @@ -21,6 +21,8 @@ export type AcpStreamConfig = { maxOutputChars?: number; /** Maximum visible characters for projected session/update lines. */ maxSessionUpdateChars?: number; + /** Relay assistant commentary/progress text into ACP parent stream updates. */ + assistantCommentary?: boolean; /** * Per-sessionUpdate visibility overrides. * Keys not listed here fall back to OpenClaw defaults. diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 2415d8620bb5..a173bc1e96f9 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -778,6 +778,7 @@ export const OpenClawSchema = z .optional(), maxOutputChars: z.number().int().positive().optional(), maxSessionUpdateChars: z.number().int().positive().optional(), + assistantCommentary: z.boolean().optional(), tagVisibility: z.record(z.string(), z.boolean()).optional(), }) .strict()