From 3da6b8675bab9b765add061abbe2d51b42865e36 Mon Sep 17 00:00:00 2001 From: Pawan Osman Date: Wed, 15 Jul 2026 14:05:32 +0300 Subject: [PATCH] Enhance background subagent handling in runAgent function - Introduced a new function `awaitPendingBg` to manage background subagents more effectively, ensuring that the main agent waits for all unreported subagents to settle before proceeding. - Updated the logic to handle in-flight background tasks, preventing premature model calls while background work is still ongoing. - Improved status notifications to inform users about the waiting state for background subagents. - Refactored existing code to streamline the handling of background tasks and their results. --- src/agent/loop.ts | 66 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/src/agent/loop.ts b/src/agent/loop.ts index 4d658e5..78cf039 100644 --- a/src/agent/loop.ts +++ b/src/agent/loop.ts @@ -228,6 +228,26 @@ export async function runAgent(opts: RunAgentOptions): Promise { return done.length; }; + const bgPending = () => bgSubagents.length > bgReported; + + /** Block until all unreported background subagents settle, then flush into history. */ + const awaitPendingBg = async (): Promise => { + if (!bgPending()) return false; + flushSettledBg(); + if (!bgPending()) return true; + const pending = bgSubagents.slice(bgReported); + const n = pending.length; + emit({ type: "run-status", status: "running" }); + emit({ + type: "shell-notify", + message: `Waiting for ${n} background subagent${n > 1 ? "s" : ""} to finish — will resume when done…`, + }); + await Promise.allSettled(pending); + if (signal.aborted) return true; + flushSettledBg(); + return true; + }; + // Summarize older steps with the same model (non-streaming aggregate) so // compaction keeps task intent, decisions, file paths and unfinished work. const summarizeSteps = async (steps: Step[]): Promise => { @@ -385,6 +405,17 @@ export async function runAgent(opts: RunAgentOptions): Promise { }); continue; } + // In-flight background Task subagents: wait + feed results before any + // "continue" nudge. Otherwise the model gets another turn while workers + // are still running and often spawns a second wave of subagents. + if (bgPending()) { + await awaitPendingBg(); + if (signal.aborted) { + emit({ type: "run-status", status: "cancelled" }); + return; + } + continue; + } // Truncated response (hit max output tokens): the model didn't choose to // stop — never treat this as a final answer. Ask it to continue. if (isAgentic() && /length|max_tokens|max_output_tokens/i.test(finishReason)) { @@ -425,24 +456,6 @@ export async function runAgent(opts: RunAgentOptions): Promise { continue; } } - // Before truly finishing, if background subagents are still in flight (or - // completed but not yet reported), wait for them and feed their summaries - // back so the model continues its own loop and synthesizes the results. - if (bgSubagents.length > bgReported) { - // Report whatever already finished without blocking; only wait for the rest. - if (flushSettledBg() > 0) continue; - const pending = bgSubagents.slice(bgReported); - const n = pending.length; - emit({ type: "run-status", status: "running" }); - emit({ type: "shell-notify", message: `Waiting for ${n} background subagent${n > 1 ? "s" : ""} to finish — will resume when done…` }); - await Promise.allSettled(pending); - if (signal.aborted) { - emit({ type: "run-status", status: "cancelled" }); - return; - } - flushSettledBg(); - continue; - } finalText = assistantText; break; } @@ -587,6 +600,23 @@ export async function runAgent(opts: RunAgentOptions): Promise { }); history.push({ kind: "tool-result", callId: call.id, name: call.name, output: r.output, status: r.status, image: r.image }); } + // After launching background Task(s), wait for that wave before calling the + // model again. Otherwise the next turn (or empty-turn / todo nudge) races + // ahead and the coordinator spawns more subagents while workers still run. + if (bgPending()) { + const launchedBg = parsed.some((p, i) => { + if (p.call.name !== "Task") return false; + const out = results[i]?.output || ""; + return /Launched .+ in the background/i.test(out); + }); + if (launchedBg) { + await awaitPendingBg(); + if (signal.aborted) { + emit({ type: "run-status", status: "cancelled" }); + return; + } + } + } } // Paused at the step limit with work still in flight → surface a Continue