Files
pi-subagents/index.ts

221 lines
6.6 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 fs from "node:fs";
import * as path from "node:path";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "typebox";
import { FINALIZE_TOOL_NAME, MAX_FINALIZE_RETRIES } from "./src/constants.ts";
import { formatPromptList, formatSubagentContent } from "./src/format.ts";
import { discoverPrompts } from "./src/prompts.ts";
import { renderSubagentCall, renderSubagentResult } from "./src/render.ts";
import { runAgent } from "./src/runner.ts";
import { getSubagentSessionPath } from "./src/session.ts";
import { FinalizeStatus, type SubagentResult } from "./src/types.ts";
import { resolveTools, SubagentParams } from "./src/tools.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) {
const prompts = discoverPrompts();
const agent = prompts.find((prompt) => prompt.name === params.agent);
if (!agent) {
return {
content: [
{
type: "text",
text: `Unknown subagent: ${params.agent}. Available: ${formatPromptList(prompts)}`,
},
],
details: { available: prompts.map((prompt) => prompt.name) },
isError: true,
};
}
if (agent.approvedTools.length > 0 && agent.deniedTools.length > 0) {
return {
content: [
{
type: "text",
text:
`Invalid subagent config for ${agent.name}: define either approved_tools/allowed_tools ` +
"or denied_tools, not both.",
},
],
details: { agent: agent.name, filePath: agent.filePath },
isError: true,
};
}
const activeTools = pi.getActiveTools();
const tools = resolveTools(agent, activeTools);
if (tools.length === 0) {
return {
content: [
{
type: "text",
text: `Subagent ${agent.name} has no approved tools after applying denied_tools.`,
},
],
details: {
agent: agent.name,
approvedTools: agent.approvedTools,
deniedTools: agent.deniedTools,
},
isError: true,
};
}
const cwd = path.resolve(ctx.cwd, params.cwd ?? ".");
const sessionId = params.sessionId ?? randomUUID();
const sessionPath = getSubagentSessionPath(cwd, agent.name, sessionId);
await fs.promises.mkdir(path.dirname(sessionPath), { recursive: true });
let result: SubagentResult | null = null;
for (
let retryCount = 0;
retryCount <= MAX_FINALIZE_RETRIES;
retryCount += 1
) {
const task =
retryCount === 0
? `Task: ${params.task}`
: [
"Your previous response did not finalize correctly.",
`If you are finished, call ${FINALIZE_TOOL_NAME}.`,
"If you are not finished, continue the original task using available tools as needed.",
`Original task: ${params.task}`,
].join("\n\n");
result = await runAgent(
cwd,
agent,
task,
tools,
sessionId,
sessionPath,
signal,
_onUpdate,
);
if (result.finalized) break;
if (result.exitCode !== 0 || result.error) break;
}
if (!result) {
return {
content: [
{
type: "text",
text: formatSubagentContent(
FinalizeStatus.ERROR,
sessionId,
undefined,
"Subagent did not run.",
),
},
],
details: { sessionId },
isError: true,
};
}
if (!result.finalized) {
const fallback =
result.error ||
result.stderr ||
`Subagent did not call ${FINALIZE_TOOL_NAME}.`;
return {
content: [
{
type: "text",
text: formatSubagentContent(
FinalizeStatus.ERROR,
sessionId,
undefined,
fallback,
),
},
],
details: { sessionId },
isError: true,
};
}
if (result.finalized.status === FinalizeStatus.ERROR) {
return {
content: [
{
type: "text",
text: formatSubagentContent(
FinalizeStatus.ERROR,
sessionId,
result.finalized.result,
result.finalized.error ?? "Subagent failed.",
),
},
],
details: { sessionId, finalized: result.finalized },
isError: true,
};
}
return {
content: [
{
type: "text",
text: formatSubagentContent(
FinalizeStatus.SUCCESS,
sessionId,
result.finalized.result,
),
},
],
details: { sessionId, finalized: result.finalized },
isError: false,
};
},
renderCall(args, theme) {
return renderSubagentCall(args, theme);
},
renderResult(result, options, theme) {
return renderSubagentResult(result, options, theme);
},
});
}