import { spawn } from "node:child_process"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; const EXPLICIT_SUBAGENT = /\b(?:use|using|run|invoke|spawn|delegate\s+to)\s+(?:(?:a|the)\s+)?(?:sequential[- ]?)?subagent(?:\s+skill|\s+tool)?\b/i; const extensionDir = path.dirname(fileURLToPath(import.meta.url)); const runnerPath = path.resolve( extensionDir, "..", "skills", "subagent", "scripts", "run-subagent.mjs", ); const LIVE_OUTPUT_MAX = 20_000; export function shouldRouteSubagent(text: string): boolean { const trimmed = text.trimStart(); if (trimmed.startsWith("/skill:subagent")) return false; return EXPLICIT_SUBAGENT.test(text); } export function routeSubagent(text: string): string { return `/skill:subagent ${text}`; } function boundedLiveOutput(text: string): string { if (text.length <= LIVE_OUTPUT_MAX) return text; return `…[earlier subagent output omitted]\n${text.slice(-LIVE_OUTPUT_MAX)}`; } export const subagentTool = { name: "subagent", label: "Subagent", description: "Run one fresh Pi agent with isolated conversation context for a bounded task. " + "The child inherits normal global and project prompts, tools, extensions, packages, and skills, " + "shares the working tree, streams live progress, and returns its final answer. Runs sequentially.", promptSnippet: "Delegate a bounded investigation, implementation, review, or verification task to a fresh isolated Pi agent", promptGuidelines: [ "Use subagent when independent work would benefit from a fresh context or when the user asks for a subagent.", "Give subagent a self-contained task because it does not inherit the current conversation.", "Do not search for a subagent binary or substitute Superpowers' subagent-driven-development workflow.", ], executionMode: "sequential" as const, parameters: { type: "object", properties: { task: { type: "string", description: "Self-contained multiline task, expected deliverable, edit permission, and facts not discoverable from the project", minLength: 1, }, }, required: ["task"], additionalProperties: false, }, async execute( _toolCallId: string, params: { task: string }, signal: AbortSignal | undefined, onUpdate: ((result: { content: Array<{ type: "text"; text: string }> }) => void) | undefined, ctx: { cwd: string }, ) { const task = params.task.trim(); if (!task) { return { content: [{ type: "text" as const, text: "Subagent task must not be empty." }], isError: true, }; } const child = spawn(process.execPath, [runnerPath], { cwd: ctx.cwd, env: process.env, stdio: ["pipe", "pipe", "pipe"], }); let liveOutput = ""; let spawnError: Error | undefined; const publish = (chunk: Buffer | string) => { liveOutput = boundedLiveOutput(liveOutput + chunk.toString()); onUpdate?.({ content: [{ type: "text", text: liveOutput }] }); }; child.stdout.on("data", publish); child.stderr.on("data", publish); child.on("error", (error) => { spawnError = error; publish(`\nSubagent runner failed: ${error.message}\n`); }); const abort = () => child.kill("SIGTERM"); if (signal?.aborted) { abort(); } else { signal?.addEventListener("abort", abort, { once: true }); } child.stdin.on("error", () => {}); child.stdin.end( `Complete this task yourself. Do not call the subagent tool or delegate again.\n\n${task}\n`, ); const exitCode = await new Promise((resolve) => { child.on("close", (code, closedSignal) => resolve(code ?? (closedSignal ? 1 : 0)), ); }); signal?.removeEventListener("abort", abort); const finalPath = liveOutput.match(/^Final response: (.+)$/m)?.[1]; let finalText = ""; if (finalPath) { try { finalText = await readFile(finalPath, "utf8"); } catch { // Fall back to the bounded live transcript below. } } const resultText = finalText.trim() || liveOutput.trim() || "Subagent completed without emitting text."; return { content: [{ type: "text" as const, text: resultText }], details: { exitCode, finalPath }, ...(exitCode !== 0 || spawnError ? { isError: true } : {}), }; }, }; export default function (pi: ExtensionAPI) { pi.registerTool(subagentTool); pi.on("input", (event) => { if (event.source === "extension" || !shouldRouteSubagent(event.text)) { return { action: "continue" }; } return { action: "transform", text: routeSubagent(event.text), }; }); }