refactor: unify subagent output selection

This commit is contained in:
Ayaan Zaidi 2026-03-21 08:26:47 +05:30
parent 277a0e5c13
commit ea0f2cd5fb
No known key found for this signature in database

View File

@ -45,7 +45,6 @@ import {
import { type AnnounceQueueItem, enqueueAnnounce } from "./subagent-announce-queue.js"; import { type AnnounceQueueItem, enqueueAnnounce } from "./subagent-announce-queue.js";
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js"; import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
import type { SpawnSubagentMode } from "./subagent-spawn.js"; import type { SpawnSubagentMode } from "./subagent-spawn.js";
import { readLatestAssistantReply } from "./tools/agent-step.js";
import { sanitizeTextContent, extractAssistantText } from "./tools/sessions-helpers.js"; import { sanitizeTextContent, extractAssistantText } from "./tools/sessions-helpers.js";
import { isAnnounceSkip } from "./tools/sessions-send-helpers.js"; import { isAnnounceSkip } from "./tools/sessions-send-helpers.js";
@ -71,6 +70,14 @@ type ToolResultMessage = {
content?: unknown; content?: unknown;
}; };
type SubagentOutputSnapshot = {
latestAssistantText?: string;
latestSilentText?: string;
latestRawText?: string;
assistantFragments: string[];
toolCallCount: number;
};
function resolveSubagentAnnounceTimeoutMs(cfg: ReturnType<typeof loadConfig>): number { function resolveSubagentAnnounceTimeoutMs(cfg: ReturnType<typeof loadConfig>): number {
const configured = cfg.agents?.defaults?.subagents?.announceTimeoutMs; const configured = cfg.agents?.defaults?.subagents?.announceTimeoutMs;
if (typeof configured !== "number" || !Number.isFinite(configured)) { if (typeof configured !== "number" || !Number.isFinite(configured)) {
@ -275,31 +282,114 @@ function extractSubagentOutputText(message: unknown): string {
return ""; return "";
} }
async function readLatestSubagentOutput(sessionKey: string): Promise<string | undefined> { function countAssistantToolCalls(content: unknown): number {
try { if (!Array.isArray(content)) {
const latestAssistant = await readLatestAssistantReply({ return 0;
sessionKey,
limit: 50,
});
if (latestAssistant?.trim()) {
return latestAssistant;
}
} catch {
// Best-effort: fall back to richer history parsing below.
} }
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<unknown>): 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<string | undefined> {
const history = await callGateway<{ messages?: Array<unknown> }>({ const history = await callGateway<{ messages?: Array<unknown> }>({
method: "chat.history", method: "chat.history",
params: { sessionKey, limit: 50 }, params: { sessionKey, limit: 100 },
}); });
const messages = Array.isArray(history?.messages) ? history.messages : []; const messages = Array.isArray(history?.messages) ? history.messages : [];
for (let i = messages.length - 1; i >= 0; i -= 1) { return selectSubagentOutputText(summarizeSubagentOutputHistory(messages), outcome);
const msg = messages[i];
const text = extractSubagentOutputText(msg);
if (text) {
return text;
}
}
return undefined;
} }
/** /**
@ -402,12 +492,13 @@ async function readSubagentPartialProgress(sessionKey: string): Promise<string |
async function readLatestSubagentOutputWithRetry(params: { async function readLatestSubagentOutputWithRetry(params: {
sessionKey: string; sessionKey: string;
maxWaitMs: number; maxWaitMs: number;
outcome?: SubagentRunOutcome;
}): Promise<string | undefined> { }): Promise<string | undefined> {
const RETRY_INTERVAL_MS = FAST_TEST_MODE ? FAST_TEST_RETRY_INTERVAL_MS : 100; 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)); const deadline = Date.now() + Math.max(0, Math.min(params.maxWaitMs, 15_000));
let result: string | undefined; let result: string | undefined;
while (Date.now() < deadline) { while (Date.now() < deadline) {
result = await readLatestSubagentOutput(params.sessionKey); result = await readSubagentOutput(params.sessionKey, params.outcome);
if (result?.trim()) { if (result?.trim()) {
return result; return result;
} }
@ -419,7 +510,7 @@ async function readLatestSubagentOutputWithRetry(params: {
export async function captureSubagentCompletionReply( export async function captureSubagentCompletionReply(
sessionKey: string, sessionKey: string,
): Promise<string | undefined> { ): Promise<string | undefined> {
const immediate = await readLatestSubagentOutput(sessionKey); const immediate = await readSubagentOutput(sessionKey);
if (immediate?.trim()) { if (immediate?.trim()) {
return immediate; return immediate;
} }
@ -1395,13 +1486,14 @@ export async function runSubagentAnnounceFlow(params: {
(isAnnounceSkip(fallbackReply) || isSilentReplyText(fallbackReply, SILENT_REPLY_TOKEN)); (isAnnounceSkip(fallbackReply) || isSilentReplyText(fallbackReply, SILENT_REPLY_TOKEN));
if (!reply) { if (!reply) {
reply = await readLatestSubagentOutput(params.childSessionKey); reply = await readSubagentOutput(params.childSessionKey, outcome);
} }
if (!reply?.trim()) { if (!reply?.trim()) {
reply = await readLatestSubagentOutputWithRetry({ reply = await readLatestSubagentOutputWithRetry({
sessionKey: params.childSessionKey, sessionKey: params.childSessionKey,
maxWaitMs: params.timeoutMs, maxWaitMs: params.timeoutMs,
outcome,
}); });
} }