- Extract validateAgent, toToolResult, runAgentWithRetries so index.ts is wiring only; orchestration, validation, and result shaping each own their concern. - Separate runner internals: createRunState, handleEvent (pure event reducer), spawnPi (process lifecycle), runOnce (single attempt). - Track attempt/maxAttempts on SubagentStatus; surface "try x/y" in the UI without overwriting the user-facing task with the retry preamble. - Replace pi's default system prompt (--system-prompt) instead of appending, so the subagent .md body is authoritative. - Document prompt-replacement and retry/cache semantics in AGENTS.md.
89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
// Subagent Extension - Registers a tool for delegating work to prompt-defined
|
|
// subagents with constrained tool permissions.
|
|
import { randomUUID } from "node:crypto";
|
|
import * as path from "node:path";
|
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
import { Type } from "typebox";
|
|
import { FINALIZE_TOOL_NAME } from "./src/constants.ts";
|
|
import { formatPromptList, toToolResult } from "./src/format.ts";
|
|
import { discoverPrompts } from "./src/prompts.ts";
|
|
import { renderSubagentCall, renderSubagentResult } from "./src/render.ts";
|
|
import { runAgentWithRetries } from "./src/runner.ts";
|
|
import { SubagentParams } from "./src/tools.ts";
|
|
import { FinalizeStatus } from "./src/types.ts";
|
|
import { validateAgent } from "./src/validate.ts";
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
if (process.env.PI_SUBAGENT_CHILD === "1") {
|
|
pi.registerTool({
|
|
name: FINALIZE_TOOL_NAME,
|
|
label: "Subagent Finalize",
|
|
description:
|
|
"Internal subagent-only tool. Call this as your final action when delegated work is complete.",
|
|
promptSnippet:
|
|
"Call subagent_finalize as your final action when subagent work is complete.",
|
|
parameters: Type.Object({
|
|
status: Type.Enum(FinalizeStatus),
|
|
result: Type.Optional(Type.String()),
|
|
error: Type.Optional(Type.String()),
|
|
}),
|
|
|
|
async execute(_toolCallId, params) {
|
|
return {
|
|
content: [{ type: "text", text: "Subagent finalized." }],
|
|
details: params,
|
|
terminate: true,
|
|
};
|
|
},
|
|
});
|
|
}
|
|
|
|
pi.registerTool({
|
|
name: "subagent",
|
|
label: "Subagent",
|
|
description:
|
|
"Delegate a task to a prompt-defined subagent from this extension's prompts/ directory. " +
|
|
`Available at startup: ${formatPromptList(discoverPrompts())}`,
|
|
promptSnippet:
|
|
"Delegate tasks to subagents by name. Subagent prompts live in prompts/*.md and define approved_tools/denied_tools.",
|
|
parameters: SubagentParams,
|
|
|
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
// Validate Agent
|
|
const validation = validateAgent(
|
|
params.agent,
|
|
discoverPrompts(),
|
|
pi.getActiveTools(),
|
|
);
|
|
if (!validation.ok) {
|
|
return {
|
|
content: [{ type: "text", text: validation.text }],
|
|
details: validation.details,
|
|
isError: true,
|
|
};
|
|
}
|
|
|
|
// Run Subagent
|
|
const result = await runAgentWithRetries({
|
|
cwd: path.resolve(ctx.cwd, params.cwd ?? "."),
|
|
agent: validation.agent,
|
|
task: params.task,
|
|
tools: validation.tools,
|
|
sessionId: params.sessionId ?? randomUUID(),
|
|
signal,
|
|
onUpdate,
|
|
});
|
|
|
|
return toToolResult(result);
|
|
},
|
|
|
|
renderCall(args, theme) {
|
|
return renderSubagentCall(args, theme);
|
|
},
|
|
|
|
renderResult(result, options, theme) {
|
|
return renderSubagentResult(result, options, theme);
|
|
},
|
|
});
|
|
}
|