/** * Task tool — background sub-agent delegation (audit D4-1, Phase B). * * Mirrors the Claude Agent SDK's Task tool contract and feel: * - the model calls Task({description, prompt, subagent_type}) * - the tool returns IMMEDIATELY with an acknowledgement, so the parent turn * ends in seconds and the chat stays fully conversational * - the child runs as an in-process `createPiSession` (Bloby owns the agent * loop, so no subprocess is needed — unlike upstream pi's subagent * extension, which must spawn the pi CLI) * - completion is injected back into the parent's input queue as a synthetic * message that drives the user-facing "Done!" continuation turn * * Also registered under the alias 'Agent' (registry.ts): the pi system prompt * sells "the Agent tool" — with this tool live, those sections are true as * written, closing the audit's D4-2 finding without a prompt edit. * * Agent definitions come from `supervisor/agents/index.ts:buildAgents()` — * the same roster the Claude harness uses, so both harnesses stay in sync. */ import type { PiTool } from './types.js'; import type { PiToolDef } from '../providers/types.js'; import { buildAgents } from '../../../agents/index.js'; /** * Tool definition with a FRESH subagent enum — built per session start (not at * module load) so prompt-file/roster edits apply without a supervisor restart, * and so the workspace is guaranteed to exist when prompts are read. */ export function taskToolDef(): PiToolDef { const agents = buildAgents(); const names = Object.keys(agents); return { name: 'Task', description: 'Delegate heavy work to a background sub-agent and keep chatting while it runs. ' + 'Returns immediately — you will automatically receive the result when the agent finishes, ' + 'so acknowledge the user briefly and end your turn. ' + 'Available agents: ' + (names.map((n) => `${n} (${agents[n].description})`).join('; ') || 'none configured'), inputSchema: { type: 'object', properties: { description: { type: 'string', description: 'Short (3-5 word) description of the task, shown to the user as a progress card.', }, prompt: { type: 'string', description: 'Complete, self-contained instructions for the sub-agent. It cannot ask follow-up questions — include every detail it needs.', }, subagent_type: { type: 'string', ...(names.length > 0 ? { enum: names } : {}), description: 'Which sub-agent to delegate to.', }, }, required: ['description', 'prompt', 'subagent_type'], }, }; } export const taskTool: PiTool = { name: 'Task', description: 'Delegate heavy work to a background sub-agent.', // Static placeholder (no file I/O at module load) — providers receive the // dynamic enum schema from taskToolDef() via registry.toolDefsForProvider. inputSchema: { type: 'object', properties: { description: { type: 'string' }, prompt: { type: 'string' }, subagent_type: { type: 'string' }, }, required: ['description', 'prompt', 'subagent_type'], }, async run(input, ctx) { if (!ctx.tasks) { return { output: 'The Task tool is not available in this context — do the work yourself with your other tools.', isError: true, }; } const description = typeof input?.description === 'string' ? input.description.trim() : ''; const prompt = typeof input?.prompt === 'string' ? input.prompt.trim() : ''; const subagentType = typeof input?.subagent_type === 'string' ? input.subagent_type.trim() : ''; if (!prompt) { return { output: 'Task requires `prompt` — complete instructions for the sub-agent.', isError: true }; } const res = ctx.tasks.spawn({ description: description || prompt.slice(0, 60), prompt, subagentType: subagentType || 'coder', }); if (!res.ok) return { output: res.error, isError: true }; return { output: `Background task started (id: ${res.taskId}). It is now running while you keep chatting. ` + `Tell the user in ONE short sentence that you're on it (your usual voice — never mention ` + `agents, tasks, or ids), then end your turn. You will automatically receive the result ` + `when it finishes.`, }; }, };