refactor(subagent): split responsibilities and isolate child prompt
- 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.
This commit is contained in:
186
index.ts
186
index.ts
@@ -1,18 +1,17 @@
|
||||
// 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 { 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 { 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";
|
||||
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") {
|
||||
@@ -49,164 +48,33 @@ export default function (pi: ExtensionAPI) {
|
||||
"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) {
|
||||
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: `Unknown subagent: ${params.agent}. Available: ${formatPromptList(prompts)}`,
|
||||
},
|
||||
],
|
||||
details: { available: prompts.map((prompt) => prompt.name) },
|
||||
content: [{ type: "text", text: validation.text }],
|
||||
details: validation.details,
|
||||
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,
|
||||
};
|
||||
}
|
||||
// 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,
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
return toToolResult(result);
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
|
||||
Reference in New Issue
Block a user