diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index b7dd51f4ada..a04f0da5e2e 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -45,7 +45,6 @@ import { import { type AnnounceQueueItem, enqueueAnnounce } from "./subagent-announce-queue.js"; import { getSubagentDepthFromSessionStore } from "./subagent-depth.js"; import type { SpawnSubagentMode } from "./subagent-spawn.js"; -import { readLatestAssistantReply } from "./tools/agent-step.js"; import { sanitizeTextContent, extractAssistantText } from "./tools/sessions-helpers.js"; import { isAnnounceSkip } from "./tools/sessions-send-helpers.js"; @@ -71,6 +70,14 @@ type ToolResultMessage = { content?: unknown; }; +type SubagentOutputSnapshot = { + latestAssistantText?: string; + latestSilentText?: string; + latestRawText?: string; + assistantFragments: string[]; + toolCallCount: number; +}; + function resolveSubagentAnnounceTimeoutMs(cfg: ReturnType): number { const configured = cfg.agents?.defaults?.subagents?.announceTimeoutMs; if (typeof configured !== "number" || !Number.isFinite(configured)) { @@ -275,31 +282,114 @@ function extractSubagentOutputText(message: unknown): string { return ""; } -async function readLatestSubagentOutput(sessionKey: string): Promise { - try { - const latestAssistant = await readLatestAssistantReply({ - sessionKey, - limit: 50, - }); - if (latestAssistant?.trim()) { - return latestAssistant; - } - } catch { - // Best-effort: fall back to richer history parsing below. +function countAssistantToolCalls(content: unknown): number { + if (!Array.isArray(content)) { + return 0; } + let count = 0; + for (const block of content) { + if (!block || typeof block !== "object") { + continue; + } + const type = (block as { type?: unknown }).type; + if ( + type === "toolCall" || + type === "tool_use" || + type === "toolUse" || + type === "functionCall" || + type === "function_call" + ) { + count += 1; + } + } + return count; +} + +function summarizeSubagentOutputHistory(messages: Array): SubagentOutputSnapshot { + const snapshot: SubagentOutputSnapshot = { + assistantFragments: [], + toolCallCount: 0, + }; + for (const message of messages) { + if (!message || typeof message !== "object") { + continue; + } + const role = (message as { role?: unknown }).role; + if (role === "assistant") { + snapshot.toolCallCount += countAssistantToolCalls((message as { content?: unknown }).content); + const text = extractSubagentOutputText(message).trim(); + if (!text) { + continue; + } + if (isAnnounceSkip(text) || isSilentReplyText(text, SILENT_REPLY_TOKEN)) { + snapshot.latestSilentText = text; + snapshot.latestAssistantText = undefined; + snapshot.assistantFragments = []; + continue; + } + snapshot.latestSilentText = undefined; + snapshot.latestAssistantText = text; + snapshot.assistantFragments.push(text); + continue; + } + const text = extractSubagentOutputText(message).trim(); + if (text) { + snapshot.latestRawText = text; + } + } + return snapshot; +} + +function formatSubagentPartialProgress( + snapshot: SubagentOutputSnapshot, + outcome?: SubagentRunOutcome, +): string | undefined { + if (snapshot.latestSilentText) { + return undefined; + } + const timedOut = outcome?.status === "timeout"; + if (snapshot.assistantFragments.length === 0 && (!timedOut || snapshot.toolCallCount === 0)) { + return undefined; + } + const parts: string[] = []; + if (timedOut && snapshot.toolCallCount > 0) { + parts.push( + `[Partial progress: ${snapshot.toolCallCount} tool call(s) executed before timeout]`, + ); + } + if (snapshot.assistantFragments.length > 0) { + parts.push(snapshot.assistantFragments.slice(-3).join("\n\n---\n\n")); + } + return parts.join("\n\n") || undefined; +} + +function selectSubagentOutputText( + snapshot: SubagentOutputSnapshot, + outcome?: SubagentRunOutcome, +): string | undefined { + if (snapshot.latestSilentText) { + return snapshot.latestSilentText; + } + if (snapshot.latestAssistantText) { + return snapshot.latestAssistantText; + } + const partialProgress = formatSubagentPartialProgress(snapshot, outcome); + if (partialProgress) { + return partialProgress; + } + return snapshot.latestRawText; +} + +async function readSubagentOutput( + sessionKey: string, + outcome?: SubagentRunOutcome, +): Promise { const history = await callGateway<{ messages?: Array }>({ method: "chat.history", - params: { sessionKey, limit: 50 }, + params: { sessionKey, limit: 100 }, }); const messages = Array.isArray(history?.messages) ? history.messages : []; - for (let i = messages.length - 1; i >= 0; i -= 1) { - const msg = messages[i]; - const text = extractSubagentOutputText(msg); - if (text) { - return text; - } - } - return undefined; + return selectSubagentOutputText(summarizeSubagentOutputHistory(messages), outcome); } /** @@ -402,12 +492,13 @@ async function readSubagentPartialProgress(sessionKey: string): Promise { const RETRY_INTERVAL_MS = FAST_TEST_MODE ? FAST_TEST_RETRY_INTERVAL_MS : 100; const deadline = Date.now() + Math.max(0, Math.min(params.maxWaitMs, 15_000)); let result: string | undefined; while (Date.now() < deadline) { - result = await readLatestSubagentOutput(params.sessionKey); + result = await readSubagentOutput(params.sessionKey, params.outcome); if (result?.trim()) { return result; } @@ -419,7 +510,7 @@ async function readLatestSubagentOutputWithRetry(params: { export async function captureSubagentCompletionReply( sessionKey: string, ): Promise { - const immediate = await readLatestSubagentOutput(sessionKey); + const immediate = await readSubagentOutput(sessionKey); if (immediate?.trim()) { return immediate; } @@ -1395,13 +1486,14 @@ export async function runSubagentAnnounceFlow(params: { (isAnnounceSkip(fallbackReply) || isSilentReplyText(fallbackReply, SILENT_REPLY_TOKEN)); if (!reply) { - reply = await readLatestSubagentOutput(params.childSessionKey); + reply = await readSubagentOutput(params.childSessionKey, outcome); } if (!reply?.trim()) { reply = await readLatestSubagentOutputWithRetry({ sessionKey: params.childSessionKey, maxWaitMs: params.timeoutMs, + outcome, }); }