From 0c1f2182a0eee36c2a044c9c2d7e5c7ee9269387 Mon Sep 17 00:00:00 2001 From: Devansh Jain <31609257+devanshrj@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:14:05 -0800 Subject: [PATCH] feat: add silentCompletion + onComplete to spawnBackgroundSubagentTask (#1217) --- .../tools/task-background-helper.test.ts | 39 ++++++ src/tools/impl/Task.ts | 112 +++++++++++------- 2 files changed, 106 insertions(+), 45 deletions(-) diff --git a/src/tests/tools/task-background-helper.test.ts b/src/tests/tools/task-background-helper.test.ts index 7e7c877..ab013e5 100644 --- a/src/tests/tools/task-background-helper.test.ts +++ b/src/tests/tools/task-background-helper.test.ts @@ -142,6 +142,45 @@ describe("spawnBackgroundSubagentTask", () => { expect(outputContent).toContain("[Task completed]"); }); + test("silentCompletion skips message queue notification", async () => { + const spawnSubagentImpl = mock(async () => ({ + agentId: "agent-silent", + conversationId: "default", + report: "init done", + success: true, + totalTokens: 30, + })); + + const launched = spawnBackgroundSubagentTask({ + subagentType: "init", + prompt: "Init memory", + description: "Initializing memory", + silentCompletion: true, + deps: { + spawnSubagentImpl, + addToMessageQueueImpl, + formatTaskNotificationImpl, + runSubagentStopHooksImpl, + generateSubagentIdImpl, + registerSubagentImpl, + completeSubagentImpl, + getSubagentSnapshotImpl, + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + const task = backgroundTasks.get(launched.taskId); + expect(task?.status).toBe("completed"); + expect(task?.output[0]).toContain("init done"); + expect(completeSubagentImpl).toHaveBeenCalledTimes(1); + // No notification queued + expect(queueMessages.length).toBe(0); + expect(formatTaskNotificationImpl).not.toHaveBeenCalled(); + // Hooks still run + expect(runSubagentStopHooksImpl).toHaveBeenCalledTimes(1); + }); + test("marks background task failed and emits notification on error", async () => { const spawnSubagentImpl = mock(async () => { throw new Error("subagent exploded"); diff --git a/src/tools/impl/Task.ts b/src/tools/impl/Task.ts index ab79596..8b239be 100644 --- a/src/tools/impl/Task.ts +++ b/src/tools/impl/Task.ts @@ -69,8 +69,15 @@ export interface SpawnBackgroundSubagentTaskArgs { /** * When true, skip injecting the completion notification into the primary * agent's message queue and hide from SubagentGroupDisplay. + * Use `onComplete` to show a user-facing notification without leaking + * into the agent's context. */ silentCompletion?: boolean; + /** + * Called after the subagent finishes (success or failure). + * Runs regardless of `silentCompletion`. + */ + onComplete?: (result: { success: boolean; error?: string }) => void; /** * Optional dependency overrides for tests. * Production callers should not provide this. @@ -197,6 +204,7 @@ export function spawnBackgroundSubagentTask( existingConversationId, maxTurns, silentCompletion, + onComplete, deps, } = args; @@ -268,36 +276,43 @@ export function spawnBackgroundSubagentTask( totalTokens: result.totalTokens, }); - const subagentSnapshot = getSubagentSnapshotFn(); - const toolUses = subagentSnapshot.agents.find( - (agent) => agent.id === subagentId, - )?.toolCalls.length; - const durationMs = Math.max(0, Date.now() - bgTask.startTime.getTime()); + onComplete?.({ success: result.success, error: result.error }); - const fullResult = result.success - ? `${header}\n\n${result.report || ""}` - : result.error || "Subagent execution failed"; - const userCwd = process.env.USER_CWD || process.cwd(); - const { content: truncatedResult } = truncateByChars( - fullResult, - LIMITS.TASK_OUTPUT_CHARS, - "Task", - { workingDirectory: userCwd, toolName: "Task" }, - ); + if (!silentCompletion) { + const subagentSnapshot = getSubagentSnapshotFn(); + const toolUses = subagentSnapshot.agents.find( + (agent) => agent.id === subagentId, + )?.toolCalls.length; + const durationMs = Math.max(0, Date.now() - bgTask.startTime.getTime()); - const notificationXml = formatTaskNotificationFn({ - taskId, - status: result.success ? "completed" : "failed", - summary: `Agent "${description}" ${result.success ? "completed" : "failed"}`, - result: truncatedResult, - outputFile, - usage: { - totalTokens: result.totalTokens, - toolUses, - durationMs, - }, - }); - addToMessageQueueFn({ kind: "task_notification", text: notificationXml }); + const fullResult = result.success + ? `${header}\n\n${result.report || ""}` + : result.error || "Subagent execution failed"; + const userCwd = process.env.USER_CWD || process.cwd(); + const { content: truncatedResult } = truncateByChars( + fullResult, + LIMITS.TASK_OUTPUT_CHARS, + "Task", + { workingDirectory: userCwd, toolName: "Task" }, + ); + + const notificationXml = formatTaskNotificationFn({ + taskId, + status: result.success ? "completed" : "failed", + summary: `Agent "${description}" ${result.success ? "completed" : "failed"}`, + result: truncatedResult, + outputFile, + usage: { + totalTokens: result.totalTokens, + toolUses, + durationMs, + }, + }); + addToMessageQueueFn({ + kind: "task_notification", + text: notificationXml, + }); + } runSubagentStopHooksFn( subagentType, @@ -318,23 +333,30 @@ export function spawnBackgroundSubagentTask( appendToOutputFile(outputFile, `[error] ${errorMessage}\n`); completeSubagentFn(subagentId, { success: false, error: errorMessage }); - const subagentSnapshot = getSubagentSnapshotFn(); - const toolUses = subagentSnapshot.agents.find( - (agent) => agent.id === subagentId, - )?.toolCalls.length; - const durationMs = Math.max(0, Date.now() - bgTask.startTime.getTime()); - const notificationXml = formatTaskNotificationFn({ - taskId, - status: "failed", - summary: `Agent "${description}" failed`, - result: errorMessage, - outputFile, - usage: { - toolUses, - durationMs, - }, - }); - addToMessageQueueFn({ kind: "task_notification", text: notificationXml }); + onComplete?.({ success: false, error: errorMessage }); + + if (!silentCompletion) { + const subagentSnapshot = getSubagentSnapshotFn(); + const toolUses = subagentSnapshot.agents.find( + (agent) => agent.id === subagentId, + )?.toolCalls.length; + const durationMs = Math.max(0, Date.now() - bgTask.startTime.getTime()); + const notificationXml = formatTaskNotificationFn({ + taskId, + status: "failed", + summary: `Agent "${description}" failed`, + result: errorMessage, + outputFile, + usage: { + toolUses, + durationMs, + }, + }); + addToMessageQueueFn({ + kind: "task_notification", + text: notificationXml, + }); + } runSubagentStopHooksFn( subagentType,