feat: add silentCompletion + onComplete to spawnBackgroundSubagentTask (#1217)
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user