221 lines
6.6 KiB
TypeScript
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);
|
|
},
|
|
});
|
|
}
|