Adds task_decompose tool to the pi extension for heuristic domain-based decomposition of complex requests. Adds src/task-group.ts for tracking and synthesizing subtask groups. Adds parent_task_id column migration to controlplane_db. Updates controlplane-runner to map specialist agent IDs to .agent/identities/ files and pre-load each specialist's skills from library.yaml into their system prompt at session start. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- Build: pass | Tests: FAIL — Tests 45 failed | 761 passed (806)
106 lines
3.5 KiB
TypeScript
106 lines
3.5 KiB
TypeScript
/**
|
|
* src/task-group.ts — Task group management for coordinator delegation.
|
|
*
|
|
* When the coordinator decomposes a request into subtasks, each subtask
|
|
* is created with parent_task_id pointing to the originating task. These
|
|
* functions track and synthesize the group.
|
|
*/
|
|
import type { Pool } from 'pg';
|
|
|
|
import { getSubtasks, getTaskById, updateTaskOutput } from './controlplane-db.js';
|
|
import type { Task } from './controlplane-db.js';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface TaskGroup {
|
|
parentTaskId: string;
|
|
subtasks: Task[];
|
|
total: number;
|
|
completed: number;
|
|
failed: number;
|
|
pending: number;
|
|
inProgress: number;
|
|
allDone: boolean;
|
|
anyFailed: boolean;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Queries
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function getTaskGroup(pool: Pool, parentTaskId: string): Promise<TaskGroup> {
|
|
const subtasks = await getSubtasks(pool, parentTaskId);
|
|
|
|
const completed = subtasks.filter((t) => t.status === 'done' || t.status === 'completed').length;
|
|
const failed = subtasks.filter((t) => t.status === 'failed' || t.status === 'error').length;
|
|
const inProgress = subtasks.filter((t) => t.status === 'in_progress' || t.status === 'running').length;
|
|
const pending = subtasks.filter((t) => t.status === 'pending').length;
|
|
|
|
return {
|
|
parentTaskId,
|
|
subtasks,
|
|
total: subtasks.length,
|
|
completed,
|
|
failed,
|
|
pending,
|
|
inProgress,
|
|
allDone: subtasks.length > 0 && completed === subtasks.length,
|
|
anyFailed: failed > 0,
|
|
};
|
|
}
|
|
|
|
export async function isTaskGroupComplete(pool: Pool, parentTaskId: string): Promise<boolean> {
|
|
const group = await getTaskGroup(pool, parentTaskId);
|
|
return group.allDone;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Synthesis
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function synthesizeGroupResults(pool: Pool, parentTaskId: string): Promise<string> {
|
|
const parent = await getTaskById(pool, parentTaskId);
|
|
const group = await getTaskGroup(pool, parentTaskId);
|
|
|
|
if (group.subtasks.length === 0) {
|
|
return `Task ${parentTaskId} has no subtasks.`;
|
|
}
|
|
|
|
const lines: string[] = [
|
|
`Task: ${parent?.title ?? parentTaskId}`,
|
|
`Subtasks: ${group.total} total — ${group.completed} completed, ${group.failed} failed`,
|
|
'',
|
|
];
|
|
|
|
for (const t of group.subtasks) {
|
|
const statusEmoji = (t.status === 'done' || t.status === 'completed') ? '✓'
|
|
: (t.status === 'failed' || t.status === 'error') ? '✗'
|
|
: '…';
|
|
lines.push(`${statusEmoji} [${t.assigned_to ?? '?'}] ${t.title}`);
|
|
const output = (t.context as Record<string, unknown> | null)?.output;
|
|
if (typeof output === 'string' && output.trim()) {
|
|
lines.push(` → ${output.trim().slice(0, 200)}`);
|
|
}
|
|
}
|
|
|
|
if (group.failed > 0) {
|
|
lines.push('');
|
|
lines.push(`${group.failed} subtask(s) failed. Review above for details.`);
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Output recording (called by specialist agents when they finish)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function recordSubtaskOutput(
|
|
pool: Pool,
|
|
taskId: string,
|
|
output: string,
|
|
): Promise<void> {
|
|
await updateTaskOutput(pool, taskId, output);
|
|
}
|