// v2.x: Background subagent tools. Three tools that let the model spawn // non-blocking subagent tasks, poll their status, and retrieve results. // // spawn_subagent — Create a background session+chat, dispatch inference, // return immediately with a task_id. // subagent_status — Poll the status of a previously spawned task. // subagent_result — Retrieve the full output of a completed task. // // These tools reuse the existing sessions/chats/messages/tables and the // inference pipeline — no new tables or services needed. // // Registered in tools.ts ALL_TOOLS. Lives in its own file so tests can // import executors without dragging in the full tool registry. // // Follows the read_tab_by_number.ts pattern: a pure executor function plus // a ToolDef wrapper. Type-only import from tools.ts to dodge runtime cycles. import { z } from 'zod'; import type { Sql } from '../../db.js'; import type { ToolDef, ToolExecCtx } from '../tools.js'; import { spawnBackgroundTask, getBackgroundTaskStatus, getBackgroundTaskResult, } from '../background-task.js'; // --------------------------------------------------------------------------- // spawn_subagent // --------------------------------------------------------------------------- export const SpawnSubagentInput = z.object({ input: z.string().min(1).describe('The task to execute in the background'), model: z .string() .min(1) .optional() .describe('Model to use (defaults to session model)'), agent: z .string() .min(1) .optional() .describe('Agent to use (defaults to boocode)'), label: z .string() .max(100) .optional() .describe('Human-readable label for display'), }); export type SpawnSubagentInputT = z.infer; export async function executeSpawnSubagent( input: SpawnSubagentInputT, sql: Sql, sessionId: string, ): Promise> { // Resolve project_id + model from the current session. const sessRows = await sql< { project_id: string; model: string }[] >` SELECT project_id, model FROM sessions WHERE id = ${sessionId} `; if (sessRows.length === 0) { return { error: 'current session not found' }; } const projectId = sessRows[0]!.project_id; const model = input.model ?? sessRows[0]!.model; const task = await spawnBackgroundTask( sql, // We pass a minimal logger shim — the real logger is wired by the // inference pipeline. This keeps the tool's execute signature clean. { info: () => {}, warn: () => {}, error: () => {} } as unknown as import('fastify').FastifyBaseLogger, projectId, input.input, model, input.agent, input.label, ); // Elapsed time since creation is negligible (task was just spawned). return { task_id: task.id, status: task.status, session_id: task.session_id, chat_id: task.chat_id, created_at: task.created_at, }; } export const spawnSubagent: ToolDef = { name: 'spawn_subagent', description: 'Spawn a background subagent task. Creates a new session and chat, dispatches inference asynchronously, and returns immediately with a task_id. Use subagent_status to poll for completion and subagent_result to retrieve the full output. Non-blocking — the model continues while the subagent works in the background.', inputSchema: SpawnSubagentInput, jsonSchema: { type: 'function', function: { name: 'spawn_subagent', description: 'Spawn a background subagent task. Returns immediately with a task_id — poll with subagent_status.', parameters: { type: 'object', properties: { input: { type: 'string', description: 'The task to execute in the background', }, model: { type: 'string', description: 'Model to use (defaults to session model)', }, agent: { type: 'string', description: 'Agent to use (defaults to boocode)', }, label: { type: 'string', maxLength: 100, description: 'Human-readable label for display', }, }, required: ['input'], additionalProperties: false, }, }, }, async execute(input, _projectRoot, _extraRoots, toolCtx?: ToolExecCtx) { if (!toolCtx) { return { error: 'spawn_subagent unavailable: no session context' }; } try { return await executeSpawnSubagent(input, toolCtx.sql, toolCtx.sessionId); } catch (err) { return { error: `spawn_subagent failed: ${err instanceof Error ? err.message : String(err)}`, }; } }, }; // --------------------------------------------------------------------------- // subagent_status // --------------------------------------------------------------------------- export const SubagentStatusInput = z.object({ task_id: z.string().uuid().describe('Task ID from spawn_subagent'), }); export type SubagentStatusInputT = z.infer; export async function executeSubagentStatus( input: SubagentStatusInputT, sql: Sql, ): Promise> { const task = await getBackgroundTaskStatus(sql, input.task_id); if (!task) { return { error: 'task not found', task_id: input.task_id }; } // Compute elapsed time from created_at (ISO string). let elapsed_seconds: number | null = null; try { const created = new Date(task.created_at).getTime(); const finished = task.finished_at ? new Date(task.finished_at).getTime() : Date.now(); elapsed_seconds = Math.round((finished - created) / 1000); } catch { elapsed_seconds = null; } return { task_id: task.id, status: task.status, output_summary: task.output_summary, finished_at: task.finished_at, elapsed_seconds, }; } export const subagentStatus: ToolDef = { name: 'subagent_status', description: 'Poll the status of a background subagent task by task_id. Returns the current status (running/completed/failed/cancelled), an output summary if completed, and elapsed time. Useful after spawn_subagent to check if work is done.', inputSchema: SubagentStatusInput, jsonSchema: { type: 'function', function: { name: 'subagent_status', description: 'Poll the status of a background subagent task. Returns status, output summary, and elapsed time.', parameters: { type: 'object', properties: { task_id: { type: 'string', format: 'uuid', description: 'Task ID from spawn_subagent', }, }, required: ['task_id'], additionalProperties: false, }, }, }, async execute(input, _projectRoot, _extraRoots, toolCtx?: ToolExecCtx) { if (!toolCtx) { return { error: 'subagent_status unavailable: no session context' }; } try { return await executeSubagentStatus(input, toolCtx.sql); } catch (err) { return { error: `subagent_status failed: ${err instanceof Error ? err.message : String(err)}`, }; } }, }; // --------------------------------------------------------------------------- // subagent_result // --------------------------------------------------------------------------- export const SubagentResultInput = z.object({ task_id: z.string().uuid().describe('Task ID from spawn_subagent'), }); export type SubagentResultInputT = z.infer; export async function executeSubagentResult( input: SubagentResultInputT, sql: Sql, ): Promise> { const task = await getBackgroundTaskStatus(sql, input.task_id); if (!task) { return { error: 'task not found', task_id: input.task_id }; } if (task.status !== 'completed') { return { task_id: task.id, status: task.status, error: `task is not yet completed (status: ${task.status})`, }; } if (!task.chat_id) { return { error: 'task has no chat data', task_id: input.task_id }; } const result = await getBackgroundTaskResult(sql, input.task_id, task.chat_id); if (!result) { return { task_id: task.id, status: task.status, error: 'task completed but no output message found', }; } return { task_id: task.id, output: result.output, token_usage: result.token_usage, }; } export const subagentResult: ToolDef = { name: 'subagent_result', description: 'Retrieve the full output of a completed background subagent task by task_id. Returns the response text and token usage. The task must be in completed status — poll with subagent_status first.', inputSchema: SubagentResultInput, jsonSchema: { type: 'function', function: { name: 'subagent_result', description: 'Retrieve the full output of a completed background subagent task. Returns output text and token usage.', parameters: { type: 'object', properties: { task_id: { type: 'string', format: 'uuid', description: 'Task ID from spawn_subagent', }, }, required: ['task_id'], additionalProperties: false, }, }, }, async execute(input, _projectRoot, _extraRoots, toolCtx?: ToolExecCtx) { if (!toolCtx) { return { error: 'subagent_result unavailable: no session context' }; } try { return await executeSubagentResult(input, toolCtx.sql); } catch (err) { return { error: `subagent_result failed: ${err instanceof Error ? err.message : String(err)}`, }; } }, };