feat: add background task notification system (#827)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user