Phase 7 of v2.0. BooCoder gains a terminal-driven UX and subagent isolation primitive. CLI (src/cli.ts): standalone entry point for terminal use. - boocode run "task" [--agent x] [--model y] — create + stream output - boocode ls [--state x] — formatted task table - boocode attach <id> — WS stream of running task - boocode send <id> "msg" — follow-up message to task session Connects to BOOCODER_URL (default http://100.114.205.53:9502). Human inbox (routes/inbox.ts): GET /api/inbox (failed/blocked tasks), POST /api/inbox/:id/retry (reset to pending for re-dispatch). Cost tracking: dispatcher aggregates tokens_used from all messages in the task's session after completion, stores in tasks.cost_tokens. GET /api/stats/costs?group_by=project|agent|day for aggregation. Boomerang subagent isolation (3 new tools): - new_task: creates child task with parent_task_id linkage, runs in fresh isolated session. Orchestrator sees only output_summary. - list_tasks: query child tasks of current parent - check_task_status: read task state + output_summary The orchestrator pattern: an agent with tools: [new_task, list_tasks, check_task_status] can ONLY dispatch — can't read files or MCP. This is the Roo Code Boomerang Tasks capability-restriction principle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
import { z } from 'zod';
|
|
import type { ToolDef, ToolContext } from './types.js';
|
|
import { getInferenceContext } from './inference_context.js';
|
|
|
|
const NewTaskInput = z.object({
|
|
input: z.string().min(1).describe('Task description for the child subtask'),
|
|
agent: z.string().optional().describe('Optional: dispatch to a specific agent'),
|
|
model: z.string().optional().describe('Optional: model override for the subtask'),
|
|
});
|
|
|
|
type NewTaskInputT = z.infer<typeof NewTaskInput>;
|
|
|
|
export const newTaskTool: ToolDef<NewTaskInputT> = {
|
|
name: 'new_task',
|
|
description:
|
|
'Spawn a subtask that runs in isolation. The subtask gets its own session and ' +
|
|
'worktree. Use check_task_status to monitor progress. Only the output_summary is ' +
|
|
'accessible to the parent — full isolation (Boomerang pattern).',
|
|
inputSchema: NewTaskInput,
|
|
jsonSchema: {
|
|
type: 'function',
|
|
function: {
|
|
name: 'new_task',
|
|
description:
|
|
'Spawn a subtask that runs in isolation. The subtask gets its own session and ' +
|
|
'worktree. Use check_task_status to monitor progress.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
input: { type: 'string', description: 'Task description for the child subtask' },
|
|
agent: { type: 'string', description: 'Optional: dispatch to a specific agent' },
|
|
model: { type: 'string', description: 'Optional: model override for the subtask' },
|
|
},
|
|
required: ['input'],
|
|
},
|
|
},
|
|
},
|
|
|
|
async execute(input: NewTaskInputT, _projectRoot: string, context: ToolContext): Promise<unknown> {
|
|
const { sql } = context;
|
|
// Get the current task's project_id from the inference context
|
|
const ctx = getInferenceContext();
|
|
const currentTaskId = ctx.taskId;
|
|
|
|
// Look up the project_id from the current session
|
|
const [session] = await sql<{ project_id: string }[]>`
|
|
SELECT project_id FROM sessions WHERE id = ${ctx.sessionId}
|
|
`;
|
|
if (!session) {
|
|
return { error: 'Cannot determine project_id from current session' };
|
|
}
|
|
|
|
const [task] = await sql<{ id: string; state: string }[]>`
|
|
INSERT INTO tasks (project_id, parent_task_id, input, agent, model)
|
|
VALUES (${session.project_id}, ${currentTaskId}, ${input.input}, ${input.agent ?? null}, ${input.model ?? null})
|
|
RETURNING id, state
|
|
`;
|
|
|
|
return {
|
|
message: `Subtask created (id: ${task!.id}). It will run in isolation. Use check_task_status to monitor.`,
|
|
task_id: task!.id,
|
|
state: task!.state,
|
|
};
|
|
},
|
|
};
|