refactor: unify subagent output selection
This commit is contained in:
parent
277a0e5c13
commit
ea0f2cd5fb
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user