refactor: simplify subagent tool contract
This commit is contained in:
103
src/tool.ts
103
src/tool.ts
@@ -1,5 +1,4 @@
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
import { discoverAgents } from "./agents.ts";
|
||||
import {
|
||||
listAvailableModelReferences,
|
||||
normalizeAvailableModelReference,
|
||||
@@ -7,7 +6,6 @@ import {
|
||||
} from "./models.ts";
|
||||
import {
|
||||
SubagentParamsSchema,
|
||||
type AgentScope,
|
||||
type SubagentRunResult,
|
||||
type SubagentToolDetails,
|
||||
} from "./schema.ts";
|
||||
@@ -42,28 +40,20 @@ function isFailure(result: Pick<SubagentRunResult, "exitCode" | "stopReason">) {
|
||||
|
||||
function makeDetails(
|
||||
mode: "single" | "parallel" | "chain",
|
||||
agentScope: AgentScope,
|
||||
projectAgentsDir: string | null,
|
||||
results: SubagentRunResult[],
|
||||
): SubagentToolDetails {
|
||||
return { mode, agentScope, projectAgentsDir, results };
|
||||
return { mode, results };
|
||||
}
|
||||
|
||||
function makeErrorResult(
|
||||
text: string,
|
||||
mode: "single" | "parallel" | "chain",
|
||||
agentScope: AgentScope,
|
||||
projectAgentsDir: string | null,
|
||||
) {
|
||||
function makeErrorResult(text: string, mode: "single" | "parallel" | "chain") {
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
details: makeDetails(mode, agentScope, projectAgentsDir, []),
|
||||
details: makeDetails(mode, []),
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function createSubagentTool(deps: {
|
||||
discoverAgents?: typeof discoverAgents;
|
||||
listAvailableModelReferences?: typeof listAvailableModelReferences;
|
||||
normalizeAvailableModelReference?: typeof normalizeAvailableModelReference;
|
||||
parameters?: typeof SubagentParamsSchema;
|
||||
@@ -77,21 +67,19 @@ export function createSubagentTool(deps: {
|
||||
return {
|
||||
name: "subagent",
|
||||
label: "Subagent",
|
||||
description: "Delegate tasks to specialized agents running in separate child sessions.",
|
||||
description: "Delegate tasks to generic subagents running in separate child sessions.",
|
||||
parameters: deps.parameters ?? SubagentParamsSchema,
|
||||
async execute(_toolCallId: string, params: any, _signal: AbortSignal | undefined, onUpdate: any, ctx: any) {
|
||||
const hasSingle = Boolean(params.agent && params.task);
|
||||
const hasSingle = Boolean(params.task);
|
||||
const hasParallel = Boolean(params.tasks?.length);
|
||||
const hasChain = Boolean(params.chain?.length);
|
||||
const modeCount = Number(hasSingle) + Number(hasParallel) + Number(hasChain);
|
||||
const mode = hasParallel ? "parallel" : hasChain ? "chain" : "single";
|
||||
const agentScope = (params.agentScope ?? "user") as AgentScope;
|
||||
|
||||
if (modeCount !== 1) {
|
||||
return makeErrorResult("Provide exactly one mode: single, parallel, or chain.", "single", agentScope, null);
|
||||
return makeErrorResult("Provide exactly one mode: single, parallel, or chain.", "single");
|
||||
}
|
||||
|
||||
const discovery = (deps.discoverAgents ?? discoverAgents)(ctx.cwd, { scope: agentScope });
|
||||
const availableModelReferences = (deps.listAvailableModelReferences ?? listAvailableModelReferences)(ctx.modelRegistry);
|
||||
const availableModelsText = availableModelReferences.join(", ") || "(none)";
|
||||
const normalizeModelReference = (requestedModel?: string) =>
|
||||
@@ -101,8 +89,6 @@ export function createSubagentTool(deps: {
|
||||
return makeErrorResult(
|
||||
"No available models are configured. Configure at least one model before using subagent.",
|
||||
mode,
|
||||
agentScope,
|
||||
discovery.projectAgentsDir,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,7 +98,7 @@ export function createSubagentTool(deps: {
|
||||
typeof params.model !== "string" || params.model.trim().length === 0
|
||||
? `Subagent requires a top-level model chosen from the available models: ${availableModelsText}`
|
||||
: `Invalid top-level model "${params.model}". Choose one of the available models: ${availableModelsText}`;
|
||||
return makeErrorResult(message, mode, agentScope, discovery.projectAgentsDir);
|
||||
return makeErrorResult(message, mode);
|
||||
}
|
||||
params.model = topLevelModel;
|
||||
|
||||
@@ -122,10 +108,8 @@ export function createSubagentTool(deps: {
|
||||
const normalizedTaskModel = normalizeModelReference(task.model);
|
||||
if (!normalizedTaskModel) {
|
||||
return makeErrorResult(
|
||||
`Invalid model for parallel task ${index + 1} (${task.agent}): "${task.model}". Choose one of the available models: ${availableModelsText}`,
|
||||
`Invalid model for parallel task ${index + 1}: "${task.model}". Choose one of the available models: ${availableModelsText}`,
|
||||
mode,
|
||||
agentScope,
|
||||
discovery.projectAgentsDir,
|
||||
);
|
||||
}
|
||||
task.model = normalizedTaskModel;
|
||||
@@ -137,49 +121,14 @@ export function createSubagentTool(deps: {
|
||||
const normalizedStepModel = normalizeModelReference(step.model);
|
||||
if (!normalizedStepModel) {
|
||||
return makeErrorResult(
|
||||
`Invalid model for chain step ${index + 1} (${step.agent}): "${step.model}". Choose one of the available models: ${availableModelsText}`,
|
||||
`Invalid model for chain step ${index + 1}: "${step.model}". Choose one of the available models: ${availableModelsText}`,
|
||||
mode,
|
||||
agentScope,
|
||||
discovery.projectAgentsDir,
|
||||
);
|
||||
}
|
||||
step.model = normalizedStepModel;
|
||||
}
|
||||
|
||||
const requestedAgentNames = [
|
||||
...(hasSingle ? [params.agent] : []),
|
||||
...((params.tasks ?? []).map((task: any) => task.agent)),
|
||||
...((params.chain ?? []).map((step: any) => step.agent)),
|
||||
];
|
||||
const projectAgents = requestedAgentNames
|
||||
.map((name) => discovery.agents.find((candidate) => candidate.name === name))
|
||||
.filter((agent): agent is NonNullable<typeof agent> => Boolean(agent && agent.source === "project"));
|
||||
|
||||
if (projectAgents.length > 0 && (params.confirmProjectAgents ?? true) && ctx.hasUI) {
|
||||
const ok = await ctx.ui.confirm(
|
||||
"Run project-local agents?",
|
||||
`Agents: ${projectAgents.map((agent) => agent.name).join(", ")}\nSource: ${
|
||||
discovery.projectAgentsDir ?? "(unknown)"
|
||||
}`,
|
||||
);
|
||||
if (!ok) {
|
||||
return makeErrorResult(
|
||||
"Canceled: project-local agents not approved.",
|
||||
mode,
|
||||
agentScope,
|
||||
discovery.projectAgentsDir,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const resolveAgent = (name: string) => {
|
||||
const agent = discovery.agents.find((candidate) => candidate.name === name);
|
||||
if (!agent) throw new Error(`Unknown agent: ${name}`);
|
||||
return agent;
|
||||
};
|
||||
|
||||
const runTask = async (input: {
|
||||
agentName: string;
|
||||
task: string;
|
||||
cwd?: string;
|
||||
taskModel?: string;
|
||||
@@ -187,7 +136,6 @@ export function createSubagentTool(deps: {
|
||||
step?: number;
|
||||
mode: "single" | "parallel" | "chain";
|
||||
}) => {
|
||||
const agent = resolveAgent(input.agentName);
|
||||
const model = (deps.resolveChildModel ?? resolveChildModel)({
|
||||
taskModel: input.taskModel,
|
||||
topLevelModel: params.model,
|
||||
@@ -197,22 +145,18 @@ export function createSubagentTool(deps: {
|
||||
cwd: input.cwd ?? ctx.cwd,
|
||||
onEvent(event) {
|
||||
onUpdate?.({
|
||||
content: [{ type: "text", text: `Running ${input.agentName}: ${event.type}` }],
|
||||
details: makeDetails(input.mode, agentScope, discovery.projectAgentsDir, []),
|
||||
content: [{ type: "text", text: `Running subagent: ${event.type}` }],
|
||||
details: makeDetails(input.mode, []),
|
||||
});
|
||||
},
|
||||
meta: {
|
||||
mode: input.mode,
|
||||
taskIndex: input.taskIndex,
|
||||
step: input.step,
|
||||
agent: agent.name,
|
||||
agentSource: agent.source,
|
||||
task: input.task,
|
||||
cwd: input.cwd ?? ctx.cwd,
|
||||
requestedModel: model.requestedModel,
|
||||
resolvedModel: model.resolvedModel,
|
||||
systemPrompt: agent.systemPrompt,
|
||||
tools: agent.tools,
|
||||
},
|
||||
}) as Promise<SubagentRunResult>;
|
||||
};
|
||||
@@ -220,7 +164,6 @@ export function createSubagentTool(deps: {
|
||||
if (hasSingle) {
|
||||
try {
|
||||
const result = await runTask({
|
||||
agentName: params.agent,
|
||||
task: params.task,
|
||||
cwd: params.cwd,
|
||||
mode: "single",
|
||||
@@ -228,13 +171,13 @@ export function createSubagentTool(deps: {
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text: result.finalText }],
|
||||
details: makeDetails("single", agentScope, discovery.projectAgentsDir, [result]),
|
||||
details: makeDetails("single", [result]),
|
||||
isError: isFailure(result),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: (error as Error).message }],
|
||||
details: makeDetails("single", agentScope, discovery.projectAgentsDir, []),
|
||||
details: makeDetails("single", []),
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
@@ -249,7 +192,7 @@ export function createSubagentTool(deps: {
|
||||
text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
|
||||
},
|
||||
],
|
||||
details: makeDetails("parallel", agentScope, discovery.projectAgentsDir, []),
|
||||
details: makeDetails("parallel", []),
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
@@ -257,7 +200,6 @@ export function createSubagentTool(deps: {
|
||||
const liveResults: SubagentRunResult[] = [];
|
||||
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (task: any, index) => {
|
||||
const result = await runTask({
|
||||
agentName: task.agent,
|
||||
task: task.task,
|
||||
cwd: task.cwd,
|
||||
taskModel: task.model,
|
||||
@@ -267,19 +209,19 @@ export function createSubagentTool(deps: {
|
||||
liveResults[index] = result;
|
||||
onUpdate?.({
|
||||
content: [{ type: "text", text: `Parallel: ${liveResults.filter(Boolean).length}/${params.tasks.length} finished` }],
|
||||
details: makeDetails("parallel", agentScope, discovery.projectAgentsDir, liveResults.filter(Boolean)),
|
||||
details: makeDetails("parallel", liveResults.filter(Boolean)),
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
const successCount = results.filter((result) => !isFailure(result)).length;
|
||||
const summary = results
|
||||
.map((result) => `[${result.agent}] ${isFailure(result) ? "failed" : "completed"}: ${result.finalText || "(no output)"}`)
|
||||
.map((result, index) => `[task ${index + 1}] ${isFailure(result) ? "failed" : "completed"}: ${result.finalText || "(no output)"}`)
|
||||
.join("\n\n");
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summary}` }],
|
||||
details: makeDetails("parallel", agentScope, discovery.projectAgentsDir, results),
|
||||
details: makeDetails("parallel", results),
|
||||
isError: successCount !== results.length,
|
||||
};
|
||||
}
|
||||
@@ -290,7 +232,6 @@ export function createSubagentTool(deps: {
|
||||
const item = params.chain[index];
|
||||
const task = item.task.replaceAll("{previous}", previous);
|
||||
const result = await runTask({
|
||||
agentName: item.agent,
|
||||
task,
|
||||
cwd: item.cwd,
|
||||
taskModel: item.model,
|
||||
@@ -299,7 +240,7 @@ export function createSubagentTool(deps: {
|
||||
});
|
||||
onUpdate?.({
|
||||
content: [{ type: "text", text: `Chain: completed step ${index + 1}/${params.chain.length}` }],
|
||||
details: makeDetails("chain", agentScope, discovery.projectAgentsDir, [...results, result]),
|
||||
details: makeDetails("chain", [...results, result]),
|
||||
});
|
||||
results.push(result);
|
||||
if (isFailure(result)) {
|
||||
@@ -307,10 +248,10 @@ export function createSubagentTool(deps: {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Chain stopped at step ${index + 1} (${item.agent}): ${result.finalText || result.stopReason || "failed"}`,
|
||||
text: `Chain stopped at step ${index + 1}: ${result.finalText || result.stopReason || "failed"}`,
|
||||
},
|
||||
],
|
||||
details: makeDetails("chain", agentScope, discovery.projectAgentsDir, results),
|
||||
details: makeDetails("chain", results),
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
@@ -320,13 +261,13 @@ export function createSubagentTool(deps: {
|
||||
const finalResult = results[results.length - 1];
|
||||
return {
|
||||
content: [{ type: "text" as const, text: finalResult?.finalText ?? "" }],
|
||||
details: makeDetails("chain", agentScope, discovery.projectAgentsDir, results),
|
||||
details: makeDetails("chain", results),
|
||||
};
|
||||
},
|
||||
renderCall(args: any) {
|
||||
if (args.tasks?.length) return new Text(`subagent parallel (${args.tasks.length} tasks)`, 0, 0);
|
||||
if (args.chain?.length) return new Text(`subagent chain (${args.chain.length} steps)`, 0, 0);
|
||||
return new Text(`subagent ${args.agent ?? ""}`.trim(), 0, 0);
|
||||
return new Text("subagent", 0, 0);
|
||||
},
|
||||
renderResult(result: { content: Array<{ type: string; text?: string }> }) {
|
||||
const first = result.content[0];
|
||||
|
||||
Reference in New Issue
Block a user