import { spawn } from "node:child_process"; import { Type, type Static } from "typebox"; import { defineTool, type AgentToolResult, type ExtensionContext, type ToolDefinition, } from "@earendil-works/pi-coding-agent"; import { helperPath } from "./parent-role.js"; // The `subagent` tool — one LLM-callable tool that wraps the full helper // surface. On pi this replaces bash invocations of `herdr-helper`: the model // calls this structured tool instead of constructing shell commands. The tool // builds the helper argv, spawns the binary by absolute path (helperPath), // and returns its JSON output. `watch` is excluded — the extension already // runs `helper watch` via parent-role.ts. const COMMANDS = [ "spawn", "prompt", "wait", "collect", "list", "close", "read", "unblock", ] as const; export type SubagentCommand = (typeof COMMANDS)[number]; /** Flat options shared across all commands; only the relevant subset is used per command. */ export interface SubagentOptions { kind?: string; agent?: string; label?: string; model?: string; worktree?: boolean; branch?: string; base?: string; pane_id?: string; tab_id?: string; body?: string; body_file?: string; timeout?: number; keys?: string; lines?: number; } const subagentSchema = Type.Object({ command: Type.Union([ Type.Literal("spawn"), Type.Literal("prompt"), Type.Literal("wait"), Type.Literal("collect"), Type.Literal("list"), Type.Literal("close"), Type.Literal("read"), Type.Literal("unblock"), ]), options: Type.Object({ kind: Type.Optional(Type.Union([Type.Literal("pi"), Type.Literal("claude")])), agent: Type.Optional( Type.String({ description: "Agent name — the definition's system prompt is applied automatically (never a path)", }), ), label: Type.Optional(Type.String({ description: "Tab label" })), model: Type.Optional( Type.String({ description: "Model the child's harness runs; omitted, the harness default (spawn only)", }), ), body: Type.Optional( Type.String({ description: "Prompt body wrapped in — the initial prompt on spawn, the message on prompt", }), ), body_file: Type.Optional( Type.String({ description: "Path to a file whose content is the prompt body — alternative to body (absolute, or relative to the cwd)", }), ), worktree: Type.Optional( Type.Boolean({ description: "Give the child its own git worktree (spawn only)" }), ), branch: Type.Optional( Type.String({ description: "Worktree branch; an existing one is joined (spawn only)" }), ), base: Type.Optional( Type.String({ description: "Ref a new worktree branch forks from (spawn only)" }), ), pane_id: Type.Optional(Type.String({ description: "Pane id" })), tab_id: Type.Optional(Type.String({ description: "Tab id" })), timeout: Type.Optional(Type.Number({ description: "Timeout in ms (wait only)" })), keys: Type.Optional( Type.String({ description: 'Space-separated key names, e.g. "1 enter" (unblock only)' }), ), lines: Type.Optional(Type.Number({ description: "Screen lines to read (read only)" })), }), }); export type SubagentParams = Static; export interface SubagentToolDetails { command: SubagentCommand; exitCode: number; } /** * Map `{ command, options }` to the helper CLI argv slice (post-subcommand * token). Pure — no I/O — so it is unit-tested directly. Mirrors the helper's * own flag/positional conventions (cli.ts): * * | command | argv | * | -------- | ------------------------------------------------------- | * | spawn | `spawn --kind --label