diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 0331e7c8eb03..cb3c14404a4c 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -1178,7 +1178,6 @@ Notes: hiddenBoundarySeparator: "paragraph", // none | space | newline | paragraph maxOutputChars: 50000, maxSessionUpdateChars: 500, - assistantCommentary: false, }, runtime: { @@ -1202,7 +1201,6 @@ 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 and ACP status progress allowed by `stream.tagVisibility` 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 f8822fb81124..4a46390557bd 100644 --- a/docs/tools/acp-agents.md +++ b/docs/tools/acp-agents.md @@ -548,10 +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 and ACP status progress text are hidden by default; set - `acp.stream.assistantCommentary: true` to include commentary in parent stream - updates while keeping final-answer delivery unchanged. Status progress still - honors `acp.stream.tagVisibility`, so tags such as `plan` remain hidden + Assistant commentary and ACP status progress text are shown for progress-mode + parent channels unless `streaming.progress.commentary=false`. Status progress + still honors `acp.stream.tagVisibility`, so tags such as `plan` remain hidden unless explicitly enabled. diff --git a/src/agents/acp-spawn-parent-stream.test.ts b/src/agents/acp-spawn-parent-stream.test.ts index c330dc7dad70..e130ffff8390 100644 --- a/src/agents/acp-spawn-parent-stream.test.ts +++ b/src/agents/acp-spawn-parent-stream.test.ts @@ -1,5 +1,6 @@ /** Tests ACP child-to-parent stream relay notices, routing, and log path resolution. */ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; import { mergeMockedModule } from "../test-utils/vitest-module-mocks.js"; const enqueueSystemEventMock = vi.fn(); @@ -48,10 +49,29 @@ vi.mock("../config/sessions/paths.js", async () => { }); let emitAgentEvent: typeof import("../infra/agent-events.js").emitAgentEvent; -let resolveAcpProjectionSettings: typeof import("../auto-reply/reply/acp-stream-settings.js").resolveAcpProjectionSettings; let resolveAcpSpawnStreamLogPath: typeof import("./acp-spawn-parent-stream.js").resolveAcpSpawnStreamLogPath; let startAcpSpawnParentStreamRelay: typeof import("./acp-spawn-parent-stream.js").startAcpSpawnParentStreamRelay; +const progressCommentaryDeliveryContext = { + channel: "forum", + to: "-1001234567890", + accountId: "default", + threadId: 1122, +}; + +function progressModeConfig(acp?: OpenClawConfig["acp"]): OpenClawConfig { + return { + ...(acp ? { acp } : {}), + channels: { + forum: { + streaming: { + mode: "progress", + }, + }, + }, + }; +} + function collectedTexts() { return enqueueSystemEventMock.mock.calls.map((call) => typeof call[0] === "string" ? call[0] : (JSON.stringify(call[0]) ?? ""), @@ -80,7 +100,6 @@ function firstMockCall( describe("startAcpSpawnParentStreamRelay", () => { beforeAll(async () => { ({ emitAgentEvent } = await import("../infra/agent-events.js")); - ({ resolveAcpProjectionSettings } = await import("../auto-reply/reply/acp-stream-settings.js")); ({ resolveAcpSpawnStreamLogPath, startAcpSpawnParentStreamRelay } = await import("./acp-spawn-parent-stream.js")); }); @@ -487,19 +506,20 @@ describe("startAcpSpawnParentStreamRelay", () => { relay.dispose(); }); - it("relays commentary-phase assistant text when enabled", () => { + it("relays commentary-phase assistant text in parent progress mode by default", () => { const relay = startAcpSpawnParentStreamRelay({ - runId: "run-commentary-enabled", + runId: "run-commentary-default", parentSessionKey: "agent:main:main", - childSessionKey: "agent:codex:acp:child-commentary-enabled", + childSessionKey: "agent:codex:acp:child-commentary-default", agentId: "codex", + cfg: progressModeConfig(), + deliveryContext: progressCommentaryDeliveryContext, streamFlushMs: 10, noOutputNoticeMs: 120_000, - assistantCommentary: true, }); emitAgentEvent({ - runId: "run-commentary-enabled", + runId: "run-commentary-default", stream: "assistant", data: { delta: "checking thread context; then post a tight progress reply here.", @@ -516,24 +536,149 @@ describe("startAcpSpawnParentStreamRelay", () => { relay.dispose(); }); - it("relays ACP status progress when assistant commentary and tag visibility are enabled", () => { + it("suppresses commentary-phase assistant text when parent progress commentary is disabled", () => { + const relay = startAcpSpawnParentStreamRelay({ + runId: "run-commentary-disabled", + parentSessionKey: "agent:main:main", + childSessionKey: "agent:codex:acp:child-commentary-disabled", + agentId: "codex", + cfg: { + channels: { + forum: { + streaming: { + mode: "progress", + progress: { + commentary: false, + }, + }, + }, + }, + }, + deliveryContext: progressCommentaryDeliveryContext, + streamFlushMs: 10, + noOutputNoticeMs: 120_000, + }); + + emitAgentEvent({ + runId: "run-commentary-disabled", + stream: "assistant", + data: { + delta: "checking thread context; then post a tight progress reply here.", + phase: "commentary", + }, + }); + vi.advanceTimersByTime(15); + + expectNoTextWithFragment(collectedTexts(), "checking thread context"); + relay.dispose(); + }); + + it("inherits parent channel progress mode for account commentary overrides", () => { + const relay = startAcpSpawnParentStreamRelay({ + runId: "run-account-commentary-enabled", + parentSessionKey: "agent:main:main", + childSessionKey: "agent:codex:acp:child-account-commentary-enabled", + agentId: "codex", + cfg: { + channels: { + forum: { + streaming: { + mode: "progress", + }, + accounts: { + work: { + streaming: { + progress: { + commentary: true, + }, + }, + }, + }, + }, + }, + }, + deliveryContext: { + ...progressCommentaryDeliveryContext, + accountId: "work", + }, + streamFlushMs: 10, + noOutputNoticeMs: 120_000, + }); + + emitAgentEvent({ + runId: "run-account-commentary-enabled", + stream: "assistant", + data: { + delta: "checking account-scoped progress config.", + phase: "commentary", + }, + }); + vi.advanceTimersByTime(15); + + expectTextWithFragment(collectedTexts(), "codex: checking account-scoped progress config."); + relay.dispose(); + }); + + it("inherits legacy parent channel progress mode for account commentary overrides", () => { + const relay = startAcpSpawnParentStreamRelay({ + runId: "run-account-legacy-commentary-enabled", + parentSessionKey: "agent:main:main", + childSessionKey: "agent:codex:acp:child-account-legacy-commentary-enabled", + agentId: "codex", + cfg: { + channels: { + forum: { + streaming: "progress", + accounts: { + work: { + streaming: { + progress: { + commentary: true, + }, + }, + }, + }, + }, + }, + }, + deliveryContext: { + ...progressCommentaryDeliveryContext, + accountId: "work", + }, + streamFlushMs: 10, + noOutputNoticeMs: 120_000, + }); + + emitAgentEvent({ + runId: "run-account-legacy-commentary-enabled", + stream: "assistant", + data: { + delta: "checking legacy progress config.", + phase: "commentary", + }, + }); + vi.advanceTimersByTime(15); + + expectTextWithFragment(collectedTexts(), "codex: checking legacy progress config."); + relay.dispose(); + }); + + it("relays ACP status progress when progress commentary and tag visibility are 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, - acpProjectionSettings: resolveAcpProjectionSettings({ - acp: { - stream: { - tagVisibility: { - plan: true, - }, + cfg: progressModeConfig({ + stream: { + tagVisibility: { + plan: true, }, }, }), + deliveryContext: progressCommentaryDeliveryContext, + streamFlushMs: 10, + noOutputNoticeMs: 120_000, }); emitAgentEvent({ @@ -552,15 +697,16 @@ describe("startAcpSpawnParentStreamRelay", () => { relay.dispose(); }); - it("does not relay hidden ACP status tags when assistant commentary is enabled", () => { + it("does not relay hidden ACP status tags when progress 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", + cfg: progressModeConfig(), + deliveryContext: progressCommentaryDeliveryContext, streamFlushMs: 10, noOutputNoticeMs: 120_000, - assistantCommentary: true, }); emitAgentEvent({ @@ -591,15 +737,16 @@ describe("startAcpSpawnParentStreamRelay", () => { relay.dispose(); }); - it("does not relay ACP status tags hidden by default when assistant commentary is enabled", () => { + it("does not relay ACP status tags hidden by default when progress commentary is enabled", () => { const relay = startAcpSpawnParentStreamRelay({ runId: "run-status-commentary-default-hidden", parentSessionKey: "agent:main:main", childSessionKey: "agent:codex:acp:child-status-commentary-default-hidden", agentId: "codex", + cfg: progressModeConfig(), + deliveryContext: progressCommentaryDeliveryContext, streamFlushMs: 10, noOutputNoticeMs: 120_000, - assistantCommentary: true, }); emitAgentEvent({ @@ -624,10 +771,11 @@ describe("startAcpSpawnParentStreamRelay", () => { parentSessionKey: "agent:main:main", childSessionKey: "agent:codex:acp:child-commentary-visible-stall", agentId: "codex", + cfg: progressModeConfig(), + deliveryContext: progressCommentaryDeliveryContext, streamFlushMs: 1, noOutputNoticeMs: 1_000, noOutputPollMs: 250, - assistantCommentary: true, }); emitAgentEvent({ diff --git a/src/agents/acp-spawn-parent-stream.ts b/src/agents/acp-spawn-parent-stream.ts index b53cbfa8cf46..318a04774bbc 100644 --- a/src/agents/acp-spawn-parent-stream.ts +++ b/src/agents/acp-spawn-parent-stream.ts @@ -9,7 +9,12 @@ import { resolveAcpProjectionSettings, type AcpProjectionSettings, } from "../auto-reply/reply/acp-stream-settings.js"; +import { + resolveChannelStreamingProgressCommentary, + type StreamingCompatEntry, +} from "../channels/streaming.js"; import { resolveSessionFilePath, resolveSessionFilePathOptions } from "../config/sessions/paths.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; import { onAgentEvent } from "../infra/agent-events.js"; import { type EventSessionRoutingPolicy, @@ -19,6 +24,8 @@ import { import { requestHeartbeat } from "../infra/heartbeat-wake.js"; import { appendRegularFile } from "../infra/regular-file.js"; import { enqueueSystemEvent } from "../infra/system-events.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; +import { normalizeAccountId } from "../routing/session-key.js"; import { normalizeAssistantPhase } from "../shared/chat-message-content.js"; import { recordTaskRunProgressByRunId } from "../tasks/detached-task-runtime.js"; import type { DeliveryContext } from "../utils/delivery-context.types.js"; @@ -30,6 +37,10 @@ const DEFAULT_MAX_RELAY_LIFETIME_MS = 6 * 60 * 60 * 1000; const STREAM_BUFFER_MAX_CHARS = 4_000; const STREAM_SNIPPET_MAX_CHARS = 220; +type AcpParentProgressStreamingConfig = StreamingCompatEntry & { + accounts?: Record; +}; + function compactWhitespace(value: string): string { return value.replace(/\s+/g, " ").trim(); } @@ -58,6 +69,75 @@ function formatProxyEnvSummary(keys: string[]): string { return `proxy env: ${keys.join(", ")}`; } +function asObjectRecord(value: unknown): Record | undefined { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +function mergeStreamingConfig(base: unknown, override: unknown): unknown { + const baseRecord = + asObjectRecord(base) ?? (typeof base === "string" ? { mode: base } : undefined); + const overrideRecord = asObjectRecord(override); + if (!baseRecord || !overrideRecord) { + return override ?? base; + } + return { + ...baseRecord, + ...overrideRecord, + progress: { + ...(asObjectRecord(baseRecord.progress) ?? {}), + ...(asObjectRecord(overrideRecord.progress) ?? {}), + }, + }; +} + +function mergeStreamingEntry( + base: AcpParentProgressStreamingConfig, + override: StreamingCompatEntry | undefined, +): StreamingCompatEntry { + if (!override) { + return base; + } + return { + ...base, + ...override, + streaming: mergeStreamingConfig(base.streaming, override.streaming), + }; +} + +function resolveParentProgressStreamingEntry(params: { + cfg: OpenClawConfig | undefined; + deliveryContext: DeliveryContext | undefined; +}): StreamingCompatEntry | undefined { + const channelId = normalizeOptionalString(params.deliveryContext?.channel); + if (!params.cfg || !channelId) { + return undefined; + } + const channels = params.cfg.channels as + | Record + | undefined; + const channelCfg = channels?.[channelId]; + if (!channelCfg) { + return undefined; + } + const accountCfg = resolveAccountEntry( + channelCfg.accounts, + normalizeAccountId(params.deliveryContext?.accountId), + ); + return mergeStreamingEntry(channelCfg, accountCfg); +} + +function resolveParentProgressCommentary(params: { + cfg: OpenClawConfig | undefined; + deliveryContext: DeliveryContext | undefined; +}): boolean { + return resolveChannelStreamingProgressCommentary( + resolveParentProgressStreamingEntry(params), + true, + ); +} + function shouldRelayAcpStatusProgress(params: { eventType: string | undefined; tag: string | undefined; @@ -134,8 +214,7 @@ export function startAcpSpawnParentStreamRelay(params: { noOutputPollMs?: number; maxRelayLifetimeMs?: number; emitStartNotice?: boolean; - assistantCommentary?: boolean; - acpProjectionSettings?: AcpProjectionSettings; + cfg?: OpenClawConfig; }): AcpSpawnParentRelayHandle { const runId = normalizeOptionalString(params.runId) ?? ""; const parentSessionKey = normalizeOptionalString(params.parentSessionKey) ?? ""; @@ -228,8 +307,11 @@ export function startAcpSpawnParentStreamRelay(params: { }); }; const shouldSurfaceUpdates = params.surfaceUpdates !== false; - const shouldRelayAssistantCommentary = params.assistantCommentary === true; - const acpProjectionSettings = params.acpProjectionSettings ?? resolveAcpProjectionSettings({}); + const shouldRelayProgressCommentary = resolveParentProgressCommentary({ + cfg: params.cfg, + deliveryContext: params.deliveryContext, + }); + const acpProjectionSettings = resolveAcpProjectionSettings(params.cfg ?? {}); const eventRouting = params.eventRouting ?? { mainKey: params.mainKey, sessionScope: params.sessionScope, @@ -445,7 +527,7 @@ export function startAcpSpawnParentStreamRelay(params: { ...(assistantPhase ? { phase: assistantPhase } : {}), }); - if (assistantPhase === "commentary" && !shouldRelayAssistantCommentary) { + if (assistantPhase === "commentary" && !shouldRelayProgressCommentary) { lastProgressAt = Date.now(); return; } @@ -481,7 +563,7 @@ export function startAcpSpawnParentStreamRelay(params: { firstRuntimeEventAt ??= Date.now(); lastRuntimeEventType = eventType; if ( - shouldRelayAssistantCommentary && + shouldRelayProgressCommentary && shouldRelayAcpStatusProgress({ eventType, tag, diff --git a/src/agents/acp-spawn.ts b/src/agents/acp-spawn.ts index ffb3ddb87d96..f8bcfdf4f076 100644 --- a/src/agents/acp-spawn.ts +++ b/src/agents/acp-spawn.ts @@ -19,7 +19,6 @@ import { import { isAcpEnabledByPolicy, resolveAcpAgentPolicyError } from "../acp/policy.js"; import { readAcpSessionMeta } from "../acp/runtime/session-meta.js"; import { DEFAULT_HEARTBEAT_EVERY } from "../auto-reply/heartbeat.js"; -import { resolveAcpProjectionSettings } from "../auto-reply/reply/acp-stream-settings.js"; import { formatThinkingLevels } from "../auto-reply/thinking.js"; import { resolveChannelDefaultBindingPlacement, @@ -1552,8 +1551,6 @@ export async function spawnAcpDirect( const parentEventRouting = parentSessionKey ? resolveEventSessionRoutingPolicy({ cfg, sessionKey: parentSessionKey }) : undefined; - const assistantCommentary = cfg.acp?.stream?.assistantCommentary === true; - const acpProjectionSettings = resolveAcpProjectionSettings(cfg); if (effectiveStreamToParent && parentSessionKey) { // Register relay before dispatch so fast lifecycle failures are not missed. parentRelay = startAcpSpawnParentStreamRelay({ @@ -1567,8 +1564,7 @@ export async function spawnAcpDirect( logPath: streamLogPath, deliveryContext: parentDeliveryCtx, emitStartNotice: false, - assistantCommentary, - acpProjectionSettings, + cfg, }); } const gatewayAttachments = toGatewayImageAttachments(params.attachments); @@ -1628,8 +1624,7 @@ export async function spawnAcpDirect( logPath: streamLogPath, deliveryContext: parentDeliveryCtx, emitStartNotice: false, - assistantCommentary, - acpProjectionSettings, + cfg, }); } parentRelay?.notifyStarted(); diff --git a/src/auto-reply/reply/acp-stream-settings.ts b/src/auto-reply/reply/acp-stream-settings.ts index 6baa3c9e4af0..2baaef597516 100644 --- a/src/auto-reply/reply/acp-stream-settings.ts +++ b/src/auto-reply/reply/acp-stream-settings.ts @@ -153,7 +153,7 @@ export function isAcpTagVisible(settings: AcpProjectionSettings, tag: string | u return override; } if (Object.hasOwn(ACP_TAG_VISIBILITY_DEFAULTS, tag)) { - return ACP_TAG_VISIBILITY_DEFAULTS[tag]; + return ACP_TAG_VISIBILITY_DEFAULTS[tag as AcpSessionUpdateTag]; } return true; } diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 96247a80b832..d25cdec3a44a 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -220,8 +220,6 @@ 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 and ACP status progress allowed by acp.stream.tagVisibility 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 4a0daf305411..03947211776e 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -560,7 +560,6 @@ 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 c60f7235f64a..37263efd2df0 100644 --- a/src/config/types.acp.ts +++ b/src/config/types.acp.ts @@ -21,8 +21,6 @@ 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 a173bc1e96f9..2415d8620bb5 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -778,7 +778,6 @@ 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()