Add opt-in ACP commentary relay

This commit is contained in:
Eva
2026-06-02 22:06:50 +07:00
committed by Ayaan Zaidi
parent d07cce7bd1
commit 0c272958cf
9 changed files with 94 additions and 1 deletions

View File

@@ -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.

View File

@@ -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
(`<sessionId>.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.
</ParamField>
ACP `sessions_spawn` runs use `agents.defaults.subagents.runTimeoutSeconds` for

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -220,6 +220,8 @@ export const FIELD_HELP: Record<string, string> = {
"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":

View File

@@ -560,6 +560,7 @@ export const FIELD_LABELS: Record<string, string> = {
"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",

View File

@@ -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.

View File

@@ -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()