/** * Subagents v2 — Extension entry point. * * Davis-architecture subagent management: * - Harness-based backends (pi, codex) * - Five tools: subagent_spawn, subagent_wait, subagent_cancel, * subagent_check, subagent_list * - Commands: /subagents (TUI overlay), /btw (background task watch) * - Max 4 concurrent, configurable via ~/.pi/agent/subagents.json * - Security: trust-store cwd, codex sandbox, danger-full-access opt-in * - Normalized events, transcript, follow-up exactly once via pi.sendMessage * - No backward compatibility with orchestrator */ import { fileURLToPath } from "node:url"; import * as path from "node:path"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { Container, Markdown, Spacer, Text, truncateToWidth } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import { AgentManager } from "./src/manager.ts"; import { loadConfig, resolveHarnessKind, resolveMaxConcurrent } from "./src/config.ts"; import type { SubagentRecord, HarnessKind, SubagentSpawnParams } from "./src/types.ts"; // ── Constants ───────────────────────────────────────────────────────────────── const EXT_DIR = path.dirname(fileURLToPath(import.meta.url)); // ── Helpers ─────────────────────────────────────────────────────────────────── function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`; } function formatTokens(n: number): string { return n < 1000 ? String(n) : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`; } function formatRecordOneLine(r: SubagentRecord, width: number): string { const icon = r.status === "running" ? "⟳" : r.status === "completed" ? "✓" : r.status === "failed" ? "✗" : r.status === "cancelled" ? "✕" : "○"; const dur = r.completedAt ? formatDuration(r.completedAt - r.startedAt) : formatDuration(Date.now() - r.startedAt); const modelStr = r.model ? ` (${r.model})` : ""; return truncateToWidth( `${icon} [${r.harness}] ${r.label}${modelStr} — ${r.status} · ${r.toolCount}t · ${dur}`, Math.max(1, width), ); } // ── Extension ───────────────────────────────────────────────────────────────── export default function (pi: ExtensionAPI) { const config = loadConfig(); let manager: AgentManager | undefined; let managerCtx: ExtensionContext | undefined; // ── Manager init (lazy, bound to session) ─────────────────────────────── function getManager(ctx: ExtensionContext): AgentManager { if (!manager) { manager = new AgentManager( ctx.sessionManager.getSessionId(), config.maxConcurrent ?? 4, ); manager.bindApi(pi); managerCtx = ctx; } return manager; } // ── Session lifecycle ────────────────────────────────────────────────── pi.on("session_start", () => { manager = undefined; managerCtx = undefined; }); pi.on("session_shutdown", async () => { if (manager) { await manager.dispose(); manager = undefined; managerCtx = undefined; } }); // Flush pending follow-ups when the parent agent settles pi.on("agent_settled", () => { manager?.flushPendingFollowUps(); }); // ══════════════════════════════════════════════════════════════════════════ // Tools // ══════════════════════════════════════════════════════════════════════════ // ── subagent_spawn ────────────────────────────────────────────────────── pi.registerTool({ name: "subagent_spawn", label: "Spawn Subagent", description: "Spawn a subagent to run a task in background. Returns an agent ID immediately. " + "Use a named profile from subagents.json or choose the harness/model directly. " + "Use subagent_wait to retrieve results, subagent_check for status, or subagent_cancel to abort. " + "Max 4 concurrent subagents; excess are queued automatically.", promptSnippet: "Spawn subagents for parallel background tasks", promptGuidelines: [ "For multiple independent subagent tasks, emit multiple subagent_spawn calls in the same turn — they run in parallel.", "Subagents have NO context from the current conversation — include ALL necessary context in the task description.", "Prefer a configured planner, coder, or reviewer profile when it matches the task.", "Use subagent_wait to collect results when you need them.", ], parameters: Type.Object({ task: Type.String({ description: "Task description with all necessary context", }), profile: Type.Optional( Type.String({ description: "Named profile from subagents.json (for example planner, coder, or reviewer)", }), ), harness: Type.Optional( Type.String({ description: 'Backend: "pi" (default) or "codex"' }), ), label: Type.Optional( Type.String({ description: "Short label for the dashboard" }), ), cwd: Type.Optional( Type.String({ description: "Working directory (must be allowed by trust store)", }), ), model: Type.Optional( Type.String({ description: "Model override (provider/id for pi, slug for codex)", }), ), thinking: Type.Optional( Type.String({ description: "Thinking/effort override" }), ), maxTurns: Type.Optional( Type.Number({ description: "Max turns before wrap-up" }), ), }), async execute(toolCallId, rawParams, signal, _onUpdate, ctx) { const mgr = getManager(ctx); mgr.setLatestCtx(ctx); const spawnParams: SubagentSpawnParams = { task: rawParams.task as string, profile: rawParams.profile as string | undefined, harness: rawParams.harness as HarnessKind | undefined, label: rawParams.label as string | undefined, cwd: rawParams.cwd as string | undefined, model: rawParams.model as string | undefined, thinking: rawParams.thinking as string | undefined, maxTurns: rawParams.maxTurns as number | undefined, }; try { const record = mgr.spawn(spawnParams, ctx); // Wire abort signal if (signal) { signal.addEventListener( "abort", () => { mgr.cancel(record.id); }, { once: true }, ); } // Live status ctx.ui.setStatus(`subagent:${record.id}`, `⟳ ${record.label}`); return { content: [ { type: "text" as const, text: `Subagent spawned.\n` + `ID: ${record.id}\n` + `Harness: ${record.harness}\n` + `Status: ${record.status}\n\n` + `Use subagent_wait("${record.id}") to retrieve results, or subagent_check("${record.id}") for status.`, }, ], details: { record }, }; } catch (err) { throw new Error( `Failed to spawn subagent: ${err instanceof Error ? err.message : String(err)}`, ); } }, renderCall(args, theme) { const profile = args.profile ? ` @${args.profile}` : ""; const harness = args.harness ? ` [${args.harness}]` : ""; const label = args.label ? ` ${args.label}` : ""; return new Text( theme.fg("toolTitle", theme.bold("subagent_spawn")) + profile + harness + label, 0, 0, ); }, renderResult(result, _options, theme) { const record = (result.details as any)?.record as | SubagentRecord | undefined; if (!record) { const text = result.content[0]?.type === "text" ? result.content[0].text : ""; return new Text(text, 0, 0); } return new Text( theme.fg( "success", `✓ Spawned ${record.id} [${record.harness}] — ${record.status}`, ), 0, 0, ); }, }); // ── subagent_wait ─────────────────────────────────────────────────────── pi.registerTool({ name: "subagent_wait", label: "Wait Subagent", description: "Wait for background subagents to complete and retrieve their results. " + "Blocks until the subagents finish (or fail). Follow-up is delivered exactly once. " + "Accepts a single agent ID string or an array of IDs.", promptSnippet: "Wait for and retrieve subagent results", parameters: Type.Object({ agent_id: Type.Optional( Type.String({ description: "Single subagent ID to wait for (alias for ids)", }), ), ids: Type.Optional( Type.Array(Type.String(), { description: "Array of subagent IDs to wait for", }), ), }), async execute(_toolCallId, rawParams, _signal, _onUpdate, ctx) { const mgr = getManager(ctx); mgr.setLatestCtx(ctx); // Resolve IDs: use ids array if present, otherwise single agent_id const ids: string[] = rawParams.ids ? (rawParams.ids as string[]) : rawParams.agent_id ? [rawParams.agent_id as string] : []; if (ids.length === 0) { throw new Error( 'subagent_wait requires "ids" (array) or "agent_id" (single ID)', ); } const records: SubagentRecord[] = []; for (const id of ids) { const record = await mgr.wait(id); if (!record) { throw new Error(`Subagent not found: "${id}"`); } records.push(record); // Clear status ctx.ui.setStatus(`subagent:${id}`, undefined); } // Build combined output const lines: string[] = []; for (const record of records) { const statusIcon = record.status === "completed" ? "✓" : record.status === "failed" ? "✗" : "?"; const dur = record.completedAt ? formatDuration(record.completedAt - record.startedAt) : formatDuration(Date.now() - record.startedAt); lines.push( `${statusIcon} Subagent ${record.id} [${record.harness}] ${record.status}`, ); lines.push( `Label: ${record.label} | Tools: ${record.toolCount} | Duration: ${dur}`, ); lines.push( `Tokens: ↑${formatTokens(record.usage.input)} ↓${formatTokens(record.usage.output)}`, ); if (record.error) lines.push(`Error: ${record.error}`); lines.push(""); lines.push(record.output || "(no output)"); if (records.length > 1) lines.push("---"); } return { content: [ { type: "text" as const, text: lines.join("\n"), }, ], details: { records }, }; }, renderCall(args, theme) { const ids = args.ids ? (args.ids as string[]).join(", ") : (args.agent_id as string) ?? ""; return new Text( theme.fg("toolTitle", theme.bold("subagent_wait")) + ` ${ids}`, 0, 0, ); }, renderResult(result, _options, theme) { const records = (result.details as any)?.records as | SubagentRecord[] | undefined; if (!records?.length) { const text = result.content[0]?.type === "text" ? result.content[0].text.slice(0, 200) : ""; return new Text(text, 0, 0); } const icon = records[0].status === "completed" ? "✓" : "✗"; return new Text( theme.fg( records[0].status === "completed" ? "success" : "error", `${icon} ${records[0].label} — ${records[0].status}` + (records.length > 1 ? ` (+${records.length - 1} more)` : ""), ), 0, 0, ); }, }); // ── subagent_cancel ───────────────────────────────────────────────────── pi.registerTool({ name: "subagent_cancel", label: "Cancel Subagent", description: "Cancel running or queued subagents. Accepts a single agent ID or an array of IDs. " + "The subagent process tree is terminated.", promptSnippet: "Cancel running subagents", parameters: Type.Object({ agent_id: Type.Optional( Type.String({ description: "Single subagent ID to cancel (alias for ids)", }), ), ids: Type.Optional( Type.Array(Type.String(), { description: "Array of subagent IDs to cancel", }), ), }), async execute(_toolCallId, rawParams, _signal, _onUpdate, ctx) { const mgr = getManager(ctx); mgr.setLatestCtx(ctx); const ids: string[] = rawParams.ids ? (rawParams.ids as string[]) : rawParams.agent_id ? [rawParams.agent_id as string] : []; if (ids.length === 0) { throw new Error( 'subagent_cancel requires "ids" (array) or "agent_id" (single ID)', ); } const cancelled = await mgr.cancel(ids); // Clear statuses for (const id of ids) { ctx.ui.setStatus(`subagent:${id}`, undefined); } if (!cancelled) { throw new Error( `Cannot cancel subagent(s): not found or not running`, ); } return { content: [ { type: "text" as const, text: ids.length === 1 ? `Subagent "${ids[0]}" cancelled.` : `${ids.length} subagents cancelled.`, }, ], details: { ids }, }; }, renderCall(args, theme) { const ids = args.ids ? (args.ids as string[]).join(", ") : (args.agent_id as string) ?? ""; return new Text( theme.fg("toolTitle", theme.bold("subagent_cancel")) + ` ${ids}`, 0, 0, ); }, renderResult(_result, _options, theme) { return new Text(theme.fg("warning", "✕ Cancelled"), 0, 0); }, }); // ── subagent_check ────────────────────────────────────────────────────── pi.registerTool({ name: "subagent_check", label: "Check Subagent", description: "Check the status and details of a subagent without waiting.", promptSnippet: "Check subagent status", parameters: Type.Object({ agent_id: Type.String({ description: "The subagent ID to check", }), }), async execute(_toolCallId, rawParams, _signal, _onUpdate, ctx) { const mgr = getManager(ctx); mgr.setLatestCtx(ctx); const record = mgr.check(rawParams.agent_id as string); if (!record) { throw new Error( `Subagent not found: "${rawParams.agent_id}"`, ); } const dur = record.completedAt ? formatDuration(record.completedAt - record.startedAt) : `${formatDuration(Date.now() - record.startedAt)} (running)`; return { content: [ { type: "text" as const, text: `Subagent: ${record.id}\n` + `Label: ${record.label}\n` + `Harness: ${record.harness}\n` + `Status: ${record.status}\n` + `Model: ${record.model ?? "default"}\n` + `Tools: ${record.toolCount} · Duration: ${dur}\n` + `Tokens: ↑${formatTokens(record.usage.input)} ↓${formatTokens(record.usage.output)}\n` + (record.error ? `Error: ${record.error}\n` : "") + (record.transcriptPath ? `Transcript: ${record.transcriptPath}\n` : ""), }, ], details: { record }, }; }, renderCall(args, theme) { return new Text( theme.fg("toolTitle", theme.bold("subagent_check")) + ` ${args.agent_id}`, 0, 0, ); }, renderResult(result, _options, theme) { const record = (result.details as any)?.record as | SubagentRecord | undefined; if (!record) return new Text("", 0, 0); const icon = record.status === "running" ? "⟳" : record.status === "completed" ? "✓" : "✗"; return new Text( theme.fg( "dim", `${icon} ${record.label} — ${record.status} · ${record.toolCount}t`, ), 0, 0, ); }, }); // ── subagent_list ─────────────────────────────────────────────────────── pi.registerTool({ name: "subagent_list", label: "List Subagents", description: "List all subagents (running, queued, and recently completed).", promptSnippet: "List all subagents", parameters: Type.Object({}), async execute(_toolCallId, _rawParams, _signal, _onUpdate, ctx) { const mgr = getManager(ctx); mgr.setLatestCtx(ctx); const records = mgr.list(); if (records.length === 0) { return { content: [ { type: "text" as const, text: "No subagents." }, ], details: { records: [] }, }; } const lines = records.map((r) => { const icon = r.status === "running" ? "⟳" : r.status === "completed" ? "✓" : r.status === "failed" ? "✗" : r.status === "cancelled" ? "✕" : "○"; const dur = r.completedAt ? formatDuration(r.completedAt - r.startedAt) : `${formatDuration(Date.now() - r.startedAt)} (running)`; return `${icon} ${r.id} [${r.harness}] ${r.label} — ${r.status} · ${r.toolCount}t · ${dur}`; }); return { content: [ { type: "text" as const, text: `${records.length} subagent(s):\n${lines.join("\n")}`, }, ], details: { records }, }; }, renderCall(_args, theme) { return new Text( theme.fg("toolTitle", theme.bold("subagent_list")), 0, 0, ); }, renderResult(result, _options, theme) { const records = (result.details as any)?.records as | SubagentRecord[] | undefined; if (!records?.length) return new Text(theme.fg("dim", "No subagents"), 0, 0); return new Text( theme.fg( "dim", `${records.length} subagent(s): ${records.filter((r) => r.status === "running").length} running`, ), 0, 0, ); }, }); // ══════════════════════════════════════════════════════════════════════════ // Commands // ══════════════════════════════════════════════════════════════════════════ // ── /subagents ────────────────────────────────────────────────────────── pi.registerCommand("subagents", { description: "Subagent dashboard and takeover", handler: async (args, ctx) => { const subCmd = (args ?? "").trim().toLowerCase(); // Dashboard: TUI overlay with live list → takeover if (subCmd === "dashboard" || subCmd === "" || subCmd === "dash") { const mgr = getManager(ctx); const records = mgr.list(); if (records.length === 0) { ctx.ui.notify("No subagents.", "info"); return; } const items = records.map((r) => ({ value: r.id, label: formatRecordOneLine(r, 80), })); // TUI overlay with select for takeover const choice = await ctx.ui.select( "Subagents Dashboard — select to view details, Esc to close", items.map((i) => i.label), ); if (!choice) return; const selected = items.find((i) => i.label === choice); if (!selected) return; const record = mgr.check(selected.value); if (!record) { ctx.ui.notify( `Subagent ${selected.value} no longer exists.`, "warning", ); return; } // Show details in a notification const details = [ `ID: ${record.id}`, `Harness: ${record.harness}`, `Status: ${record.status}`, `Model: ${record.model ?? "default"}`, `Tools: ${record.toolCount}`, `Duration: ${formatDuration(Date.now() - record.startedAt)}`, `Tokens: ↑${formatTokens(record.usage.input)} ↓${formatTokens(record.usage.output)}`, record.transcriptPath ? `Transcript: ${record.transcriptPath}` : "", "", "Recent tools:", ...record.recentTools .slice(-5) .map( (t) => ` ${t.status === "running" ? "▸" : " "} ${t.tool}: ${t.args.slice(0, 80)}`, ), ].filter(Boolean); ctx.ui.notify(details.join("\n"), "info"); return; } // Takeover alias if (subCmd === "takeover" || subCmd === "take") { const mgr = getManager(ctx); const records = mgr .list() .filter((r) => r.status === "running"); if (records.length === 0) { ctx.ui.notify( "No running subagents to take over.", "info", ); return; } const items = records.map((r) => ({ value: r.id, label: `${r.label} [${r.harness}] — ${formatDuration(Date.now() - r.startedAt)}`, })); const choice = await ctx.ui.select( "Take over subagent", items.map((i) => i.label), ); if (!choice) return; const selected = items.find((i) => i.label === choice); if (!selected) return; const record = mgr.check(selected.value); if (!record) { ctx.ui.notify( `Subagent ${selected.value} no longer exists.`, "warning", ); return; } // Show details via appendEntry (persistent, non-model) pi.appendEntry("subagent_takeover", { id: record.id, harness: record.harness, status: record.status, label: record.label, task: record.task, output: record.output.slice(0, 2000), usage: record.usage, recentTools: record.recentTools.slice(-10), }); ctx.ui.notify( `Taking over ${record.label} [${record.harness}] — output appended above.`, "info", ); return; } ctx.ui.notify( "Usage: /subagents [dashboard|takeover]", "warning", ); }, }); // ── /btw ──────────────────────────────────────────────────────────────── pi.registerCommand("btw", { description: "Background Task Watch — ask a question, spawn Pi origin btw, show takeover", handler: async (args, ctx) => { const mgr = getManager(ctx); // If no question, show current tasks const question = (args ?? "").trim(); if (!question) { const records = mgr.list(); if (records.length === 0) { ctx.ui.notify( "No background tasks. Use /btw to start one.", "info", ); return; } const running = records.filter( (r) => r.status === "running", ); const queued = records.filter( (r) => r.status === "queued", ); const done = records.filter( (r) => r.status !== "running" && r.status !== "queued", ); const lines = [ "Background Tasks", "═══════════════", "", ]; if (running.length) { lines.push(`Running (${running.length}):`); for (const r of running) { lines.push( ` ⟳ ${r.label} [${r.harness}] — ${formatDuration(Date.now() - r.startedAt)} · ${r.toolCount}t`, ); } lines.push(""); } if (queued.length) { lines.push(`Queued (${queued.length}):`); for (const r of queued) { lines.push(` ○ ${r.label} [${r.harness}]`); } lines.push(""); } if (done.length) { lines.push( `Completed (${done.length}):`, ); for (const r of done.slice(-5)) { const icon = r.status === "completed" ? "✓" : r.status === "failed" ? "✗" : "✕"; const dur = r.completedAt ? formatDuration( r.completedAt - r.startedAt, ) : "?"; lines.push( ` ${icon} ${r.label} [${r.harness}] — ${dur} · ${r.toolCount}t`, ); } } ctx.ui.notify(lines.join("\n"), "info"); return; } // Spawn a Pi origin btw subagent const spawnParams: SubagentSpawnParams = { task: question, harness: "pi", label: `btw: ${question.slice(0, 40)}`, }; const record = mgr.spawn(spawnParams, ctx); // Set live status ctx.ui.setStatus( `subagent:${record.id}`, `⟳ ${record.label}`, ); // Notify that it's running ctx.ui.notify( `Background task started: ${record.label}\n` + `ID: ${record.id} | Harness: pi\n` + `Use /subagents takeover to inspect, or subagent_wait("${record.id}") in a prompt.`, "info", ); // Wait for result in background and deliver via appendEntry + notify (async () => { try { const finalRecord = await mgr.wait(record.id); if (!finalRecord) return; ctx.ui.setStatus(`subagent:${record.id}`, undefined); // Deliver result via appendEntry (not model follow-up) pi.appendEntry("btw_result", { label: finalRecord.label, harness: finalRecord.harness, status: finalRecord.status, output: finalRecord.output.slice(0, 5000), usage: finalRecord.usage, duration: finalRecord.completedAt ? formatDuration( finalRecord.completedAt - finalRecord.startedAt, ) : "?", error: finalRecord.error, }); const statusIcon = finalRecord.status === "completed" ? "✓" : "✗"; ctx.ui.notify( `${statusIcon} BTW complete: ${finalRecord.label}\n` + `Output: ${finalRecord.output.slice(0, 500)}…`, finalRecord.status === "completed" ? "info" : "warning", ); } catch (err) { ctx.ui.setStatus(`subagent:${record.id}`, undefined); ctx.ui.notify( `BTW failed: ${err instanceof Error ? err.message : String(err)}`, "error", ); } })(); }, }); }