feat: add background task notification system (#827)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-02-04 22:45:16 -08:00
committed by GitHub
parent 84e9a6d744
commit 48ccd8f220
44 changed files with 2244 additions and 234 deletions

View File

@@ -11,12 +11,22 @@ import {
getAllSubagentConfigs,
} from "../../agent/subagents";
import { spawnSubagent } from "../../agent/subagents/manager";
import { addToMessageQueue } from "../../cli/helpers/messageQueueBridge.js";
import {
completeSubagent,
generateSubagentId,
getSnapshot as getSubagentSnapshot,
registerSubagent,
} from "../../cli/helpers/subagentState.js";
import { formatTaskNotification } from "../../cli/helpers/taskNotifications.js";
import { runSubagentStopHooks } from "../../hooks";
import {
appendToOutputFile,
type BackgroundTask,
backgroundTasks,
createBackgroundOutputFile,
getNextTaskId,
} from "./process_manager.js";
import { LIMITS, truncateByChars } from "./truncation.js";
import { validateRequiredParams } from "./validation";
@@ -28,6 +38,8 @@ interface TaskArgs {
model?: string;
agent_id?: string; // Deploy an existing agent instead of creating new
conversation_id?: string; // Resume from an existing conversation
run_in_background?: boolean; // Run the task in background
max_turns?: number; // Maximum number of agentic turns
toolCallId?: string; // Injected by executeTool for linking subagent to parent tool call
signal?: AbortSignal; // Injected by executeTool for interruption handling
}
@@ -108,7 +120,184 @@ export async function task(args: TaskArgs): Promise<string> {
// Register subagent with state store for UI display
const subagentId = generateSubagentId();
registerSubagent(subagentId, subagent_type, description, toolCallId);
const isBackground = args.run_in_background ?? false;
registerSubagent(
subagentId,
subagent_type,
description,
toolCallId,
isBackground,
);
// Handle background execution
if (isBackground) {
const taskId = getNextTaskId();
const outputFile = createBackgroundOutputFile(taskId);
// Create abort controller for potential cancellation
const abortController = new AbortController();
// Register background task
const bgTask: BackgroundTask = {
description,
subagentType: subagent_type,
subagentId,
status: "running",
output: [],
startTime: new Date(),
outputFile,
abortController,
};
backgroundTasks.set(taskId, bgTask);
// Write initial status to output file
appendToOutputFile(
outputFile,
`[Task started: ${description}]\n[subagent_type: ${subagent_type}]\n\n`,
);
// Fire-and-forget: run subagent without awaiting
spawnSubagent(
subagent_type,
prompt,
model,
subagentId,
abortController.signal,
args.agent_id,
args.conversation_id,
args.max_turns,
)
.then((result) => {
// Update background task state
bgTask.status = result.success ? "completed" : "failed";
if (result.error) {
bgTask.error = result.error;
}
// Build output header
const header = [
`subagent_type=${subagent_type}`,
result.agentId ? `agent_id=${result.agentId}` : undefined,
result.conversationId
? `conversation_id=${result.conversationId}`
: undefined,
]
.filter(Boolean)
.join(" ");
// Write result to output file
if (result.success) {
appendToOutputFile(outputFile, `${header}\n\n${result.report}\n`);
bgTask.output.push(result.report || "");
} else {
appendToOutputFile(
outputFile,
`[error] ${result.error || "Subagent execution failed"}\n`,
);
}
appendToOutputFile(
outputFile,
`\n[Task ${result.success ? "completed" : "failed"}]\n`,
);
// Mark subagent as completed in state store
completeSubagent(subagentId, {
success: result.success,
error: result.error,
totalTokens: result.totalTokens,
});
const subagentSnapshot = getSubagentSnapshot();
const toolUses = subagentSnapshot.agents.find(
(agent) => agent.id === subagentId,
)?.toolCalls.length;
const durationMs = Math.max(0, Date.now() - bgTask.startTime.getTime());
// Build and truncate the result (same as foreground path)
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" },
);
// Format and queue notification for auto-firing when idle
const notificationXml = formatTaskNotification({
taskId,
status: result.success ? "completed" : "failed",
summary: `Agent "${description}" ${result.success ? "completed" : "failed"}`,
result: truncatedResult,
outputFile,
usage: {
totalTokens: result.totalTokens,
toolUses,
durationMs,
},
});
addToMessageQueue({ kind: "task_notification", text: notificationXml });
// Run SubagentStop hooks (fire-and-forget)
runSubagentStopHooks(
subagent_type,
subagentId,
result.success,
result.error,
result.agentId,
result.conversationId,
).catch(() => {
// Silently ignore hook errors
});
})
.catch((error) => {
const errorMessage =
error instanceof Error ? error.message : String(error);
bgTask.status = "failed";
bgTask.error = errorMessage;
appendToOutputFile(outputFile, `[error] ${errorMessage}\n`);
// Mark subagent as completed with error
completeSubagent(subagentId, { success: false, error: errorMessage });
const subagentSnapshot = getSubagentSnapshot();
const toolUses = subagentSnapshot.agents.find(
(agent) => agent.id === subagentId,
)?.toolCalls.length;
const durationMs = Math.max(0, Date.now() - bgTask.startTime.getTime());
// Format and queue notification for auto-firing when idle
const notificationXml = formatTaskNotification({
taskId,
status: "failed",
summary: `Agent "${description}" failed`,
result: errorMessage,
outputFile,
usage: {
toolUses,
durationMs,
},
});
addToMessageQueue({ kind: "task_notification", text: notificationXml });
// Run SubagentStop hooks for error case
runSubagentStopHooks(
subagent_type,
subagentId,
false,
errorMessage,
args.agent_id,
args.conversation_id,
).catch(() => {
// Silently ignore hook errors
});
});
// Return immediately with task ID and output file
return `Task running in background with ID: ${taskId}\nOutput file: ${outputFile}`;
}
try {
const result = await spawnSubagent(
@@ -119,6 +308,7 @@ export async function task(args: TaskArgs): Promise<string> {
signal,
args.agent_id,
args.conversation_id,
args.max_turns,
);
// Mark subagent as completed in state store