Task 2: Implement preset-driven subagent tool, update schema and tests

This commit is contained in:
pi
2026-04-12 11:59:20 +01:00
parent 8e2c7e49e0
commit c3dd769df0
3 changed files with 239 additions and 133 deletions

View File

@@ -2,16 +2,23 @@ import test from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { createSubagentTool } from "./tool.ts"; import { createSubagentTool } from "./tool.ts";
test("parallel mode runs each task and uses the top-level model unless a task overrides it", async () => { test("parallel mode runs each task and resolves model from preset unless a task overrides it", async () => {
const requestedModels: Array<string | undefined> = []; const requestedModels: Array<string | undefined> = [];
const tool = createSubagentTool({ const tool = createSubagentTool({
discoverSubagentPresets: () => ({
presets: [
{ name: "openai-preset", description: "openai", model: "openai/gpt-5", systemPrompt: "", source: "project", filePath: "/repo/.pi/subagents/openai.md" },
{ name: "anthropic-preset", description: "anthropic", model: "anthropic/claude-opus-4-5", systemPrompt: "", source: "project", filePath: "/repo/.pi/subagents/anthropic.md" },
],
projectPresetsDir: "/repo/.pi/subagents",
}),
resolveChildModel: ({ taskModel, topLevelModel }: any) => ({ resolveChildModel: ({ taskModel, topLevelModel }: any) => ({
requestedModel: taskModel ?? topLevelModel, requestedModel: taskModel ?? topLevelModel,
resolvedModel: taskModel ?? topLevelModel, resolvedModel: taskModel ?? topLevelModel,
}), }),
runSingleTask: async ({ meta }: any) => { runSingleTask: async ({ meta }: any) => {
requestedModels.push(meta.requestedModel); requestedModels.push(meta.requestedModel as string | undefined);
return { return {
runId: `run-${requestedModels.length}`, runId: `run-${requestedModels.length}`,
task: meta.task, task: meta.task,
@@ -26,10 +33,9 @@ test("parallel mode runs each task and uses the top-level model unless a task ov
const result = await tool.execute( const result = await tool.execute(
"tool-1", "tool-1",
{ {
model: "openai/gpt-5",
tasks: [ tasks: [
{ task: "find auth code" }, { preset: "openai-preset", task: "find auth code" },
{ task: "review auth code", model: "anthropic/claude-opus-4-5" }, { preset: "anthropic-preset", task: "review auth code", model: "anthropic/claude-opus-4-5" },
], ],
}, },
undefined, undefined,
@@ -57,6 +63,12 @@ test("parallel mode rejects per-task model overrides that are not currently avai
let didRun = false; let didRun = false;
const tool = createSubagentTool({ const tool = createSubagentTool({
discoverSubagentPresets: () => ({
presets: [
{ name: "auth-preset", description: "auth", model: "anthropic/claude-sonnet-4-5", systemPrompt: "", source: "project", filePath: "/repo/.pi/subagents/auth.md" },
],
projectPresetsDir: "/repo/.pi/subagents",
}),
runSingleTask: async () => { runSingleTask: async () => {
didRun = true; didRun = true;
throw new Error("should not run"); throw new Error("should not run");
@@ -66,8 +78,7 @@ test("parallel mode rejects per-task model overrides that are not currently avai
const result = await tool.execute( const result = await tool.execute(
"tool-1", "tool-1",
{ {
model: "anthropic/claude-sonnet-4-5", tasks: [{ preset: "auth-preset", task: "find auth code", model: "openai/gpt-5" }],
tasks: [{ task: "find auth code", model: "openai/gpt-5" }],
}, },
undefined, undefined,
undefined, undefined,

View File

@@ -2,10 +2,24 @@ import test from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { createSubagentTool } from "./tool.ts"; import { createSubagentTool } from "./tool.ts";
test("single-mode subagent uses the required top-level model and emits humanized live progress", async () => { test("single-mode subagent uses preset default model and emits humanized live progress", async () => {
const updates: string[] = []; const updates: string[] = [];
const tool = createSubagentTool({ const tool = createSubagentTool({
discoverSubagentPresets: () => ({
presets: [
{
name: "auth-inspector",
description: "inspect auth",
model: "anthropic/claude-sonnet-4-5",
tools: ["read"],
systemPrompt: "",
source: "project",
filePath: "/repo/.pi/subagents/auth.md",
},
],
projectPresetsDir: "/repo/.pi/subagents",
}),
runSingleTask: async ({ onEvent, meta }: any) => { runSingleTask: async ({ onEvent, meta }: any) => {
onEvent?.({ type: "assistant_text", text: "Inspecting auth flow" }); onEvent?.({ type: "assistant_text", text: "Inspecting auth flow" });
onEvent?.({ type: "tool_call", toolName: "read", args: { path: "src/auth.ts" } }); onEvent?.({ type: "tool_call", toolName: "read", args: { path: "src/auth.ts" } });
@@ -27,8 +41,8 @@ test("single-mode subagent uses the required top-level model and emits humanized
const result = await tool.execute( const result = await tool.execute(
"tool-1", "tool-1",
{ {
preset: "auth-inspector",
task: "inspect auth", task: "inspect auth",
model: "anthropic/claude-sonnet-4-5",
}, },
undefined, undefined,
(partial: any) => { (partial: any) => {
@@ -50,7 +64,6 @@ test("single-mode subagent uses the required top-level model and emits humanized
assert.equal(result.details.results[0]?.task, "inspect auth"); assert.equal(result.details.results[0]?.task, "inspect auth");
assert.equal(result.details.results[0]?.paneId, "%3"); assert.equal(result.details.results[0]?.paneId, "%3");
assert.equal(result.details.results[0]?.requestedModel, "anthropic/claude-sonnet-4-5"); assert.equal(result.details.results[0]?.requestedModel, "anthropic/claude-sonnet-4-5");
assert.equal("agent" in (result.details.results[0] ?? {}), false);
assert.deepEqual(updates, ["Inspecting auth flow", "Reading src/auth.ts", "Finished reading src/auth.ts"]); assert.deepEqual(updates, ["Inspecting auth flow", "Reading src/auth.ts", "Finished reading src/auth.ts"]);
assert.doesNotMatch(updates.join("\n"), /tool_call|tool_result/); assert.doesNotMatch(updates.join("\n"), /tool_call|tool_result/);
}); });
@@ -59,6 +72,20 @@ test("single-mode subagent ignores blank assistant text and falls back to tool a
const updates: string[] = []; const updates: string[] = [];
const tool = createSubagentTool({ const tool = createSubagentTool({
discoverSubagentPresets: () => ({
presets: [
{
name: "auth-inspector",
description: "inspect auth",
model: "anthropic/claude-sonnet-4-5",
tools: ["grep"],
systemPrompt: "",
source: "project",
filePath: "/repo/.pi/subagents/auth.md",
},
],
projectPresetsDir: "/repo/.pi/subagents",
}),
runSingleTask: async ({ onEvent, meta }: any) => { runSingleTask: async ({ onEvent, meta }: any) => {
onEvent?.({ type: "assistant_text", text: " " }); onEvent?.({ type: "assistant_text", text: " " });
onEvent?.({ type: "tool_call", toolName: "grep", args: { pattern: "auth" } }); onEvent?.({ type: "tool_call", toolName: "grep", args: { pattern: "auth" } });
@@ -76,8 +103,8 @@ test("single-mode subagent ignores blank assistant text and falls back to tool a
await tool.execute( await tool.execute(
"tool-1", "tool-1",
{ {
preset: "auth-inspector",
task: "inspect auth", task: "inspect auth",
model: "anthropic/claude-sonnet-4-5",
}, },
undefined, undefined,
(partial: any) => { (partial: any) => {
@@ -96,10 +123,22 @@ test("single-mode subagent ignores blank assistant text and falls back to tool a
assert.deepEqual(updates, ["Searching code for auth"]); assert.deepEqual(updates, ["Searching code for auth"]);
}); });
test("single-mode subagent requires a top-level model even when execute is called directly", async () => { test("single-mode subagent requires a model (either explicit or preset default) when execute is called directly", async () => {
let didRun = false; let didRun = false;
const tool = createSubagentTool({ const tool = createSubagentTool({
discoverSubagentPresets: () => ({
presets: [
{
name: "bare-preset",
description: "no model",
systemPrompt: "",
source: "project",
filePath: "/repo/.pi/subagents/bare.md",
},
],
projectPresetsDir: "/repo/.pi/subagents",
}),
runSingleTask: async () => { runSingleTask: async () => {
didRun = true; didRun = true;
throw new Error("should not run"); throw new Error("should not run");
@@ -108,7 +147,7 @@ test("single-mode subagent requires a top-level model even when execute is calle
const result = await tool.execute( const result = await tool.execute(
"tool-1", "tool-1",
{ task: "inspect auth" }, { preset: "bare-preset", task: "inspect auth" },
undefined, undefined,
undefined, undefined,
{ {
@@ -122,13 +161,25 @@ test("single-mode subagent requires a top-level model even when execute is calle
assert.equal(didRun, false); assert.equal(didRun, false);
assert.equal(result.isError, true); assert.equal(result.isError, true);
assert.match(result.content[0]?.type === "text" ? result.content[0].text : "", /top-level model/i); assert.match(result.content[0]?.type === "text" ? result.content[0].text : "", /no model provided/i);
}); });
test("single-mode subagent rejects models that are not currently available", async () => { test("single-mode subagent rejects models that are not currently available", async () => {
let didRun = false; let didRun = false;
const tool = createSubagentTool({ const tool = createSubagentTool({
discoverSubagentPresets: () => ({
presets: [
{
name: "auth-inspector",
description: "inspect auth",
systemPrompt: "",
source: "project",
filePath: "/repo/.pi/subagents/auth.md",
},
],
projectPresetsDir: "/repo/.pi/subagents",
}),
runSingleTask: async () => { runSingleTask: async () => {
didRun = true; didRun = true;
throw new Error("should not run"); throw new Error("should not run");
@@ -138,6 +189,7 @@ test("single-mode subagent rejects models that are not currently available", asy
const result = await tool.execute( const result = await tool.execute(
"tool-1", "tool-1",
{ {
preset: "auth-inspector",
task: "inspect auth", task: "inspect auth",
model: "openai/gpt-5", model: "openai/gpt-5",
}, },
@@ -163,9 +215,9 @@ test("subagent rejects requests that combine single and parallel modes", async (
const result = await tool.execute( const result = await tool.execute(
"tool-1", "tool-1",
{ {
preset: "auth-inspector",
task: "inspect auth", task: "inspect auth",
model: "anthropic/claude-sonnet-4-5", tasks: [{ preset: "auth-inspector", task: "review auth" }],
tasks: [{ task: "review auth" }],
}, },
undefined, undefined,
undefined, undefined,

View File

@@ -4,6 +4,7 @@ import {
normalizeAvailableModelReference, normalizeAvailableModelReference,
resolveChildModel, resolveChildModel,
} from "./models.ts"; } from "./models.ts";
import { discoverSubagentPresets } from "./presets.ts";
import { import {
SubagentParamsSchema, SubagentParamsSchema,
type SubagentRunResult, type SubagentRunResult,
@@ -39,14 +40,11 @@ function isFailure(result: Pick<SubagentRunResult, "exitCode" | "stopReason">) {
return result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted"; return result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
} }
function makeDetails( function makeDetails(mode: "single" | "parallel", results: SubagentRunResult[]): SubagentToolDetails {
mode: "single" | "parallel" | "chain",
results: SubagentRunResult[],
): SubagentToolDetails {
return { mode, results }; return { mode, results };
} }
function makeErrorResult(text: string, mode: "single" | "parallel" | "chain") { function makeErrorResult(text: string, mode: "single" | "parallel") {
return { return {
content: [{ type: "text" as const, text }], content: [{ type: "text" as const, text }],
details: makeDetails(mode, []), details: makeDetails(mode, []),
@@ -69,6 +67,7 @@ export function createSubagentTool(deps: {
resolveChildModel?: resolveChildModel?:
| ((input: { callModel?: string; presetModel?: string; taskModel?: string; topLevelModel?: string }) => ModelSelection) | ((input: { callModel?: string; presetModel?: string; taskModel?: string; topLevelModel?: string }) => ModelSelection)
| typeof resolveChildModel; | typeof resolveChildModel;
discoverSubagentPresets?: typeof discoverSubagentPresets;
runSingleTask?: (input: { runSingleTask?: (input: {
cwd: string; cwd: string;
meta: Record<string, unknown>; meta: Record<string, unknown>;
@@ -83,12 +82,11 @@ export function createSubagentTool(deps: {
async execute(_toolCallId: string, params: any, _signal: AbortSignal | undefined, onUpdate: any, ctx: any) { async execute(_toolCallId: string, params: any, _signal: AbortSignal | undefined, onUpdate: any, ctx: any) {
const hasSingle = Boolean(params.task); const hasSingle = Boolean(params.task);
const hasParallel = Boolean(params.tasks?.length); const hasParallel = Boolean(params.tasks?.length);
const hasChain = Boolean(params.chain?.length); const modeCount = Number(hasSingle) + Number(hasParallel);
const modeCount = Number(hasSingle) + Number(hasParallel) + Number(hasChain); const mode = hasParallel ? "parallel" : "single";
const mode = hasParallel ? "parallel" : hasChain ? "chain" : "single";
if (modeCount !== 1) { if (modeCount !== 1) {
return makeErrorResult("Provide exactly one mode: single, parallel, or chain.", "single"); return makeErrorResult("Provide exactly one mode: single or parallel.", "single");
} }
const availableModelReferences = (deps.listAvailableModelReferences ?? listAvailableModelReferences)(ctx.modelRegistry); const availableModelReferences = (deps.listAvailableModelReferences ?? listAvailableModelReferences)(ctx.modelRegistry);
@@ -103,41 +101,8 @@ export function createSubagentTool(deps: {
); );
} }
const topLevelModel = normalizeModelReference(params.model); const discovery = (deps.discoverSubagentPresets ?? discoverSubagentPresets)(ctx.cwd);
if (!topLevelModel) { const presets = discovery.presets;
const message =
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);
}
params.model = topLevelModel;
for (const [index, task] of (params.tasks ?? []).entries()) {
if (task.model === undefined) continue;
const normalizedTaskModel = normalizeModelReference(task.model);
if (!normalizedTaskModel) {
return makeErrorResult(
`Invalid model for parallel task ${index + 1}: "${task.model}". Choose one of the available models: ${availableModelsText}`,
mode,
);
}
task.model = normalizedTaskModel;
}
for (const [index, step] of (params.chain ?? []).entries()) {
if (step.model === undefined) continue;
const normalizedStepModel = normalizeModelReference(step.model);
if (!normalizedStepModel) {
return makeErrorResult(
`Invalid model for chain step ${index + 1}: "${step.model}". Choose one of the available models: ${availableModelsText}`,
mode,
);
}
step.model = normalizedStepModel;
}
// Adapter: accept only the flattened shape { callModel?, presetModel? } // Adapter: accept only the flattened shape { callModel?, presetModel? }
// to keep the tool logic simple. If a resolver was injected with the // to keep the tool logic simple. If a resolver was injected with the
@@ -160,12 +125,35 @@ export function createSubagentTool(deps: {
const runTask = async (input: { const runTask = async (input: {
task: string; task: string;
cwd?: string; cwd?: string;
taskModel?: string; taskModel?: string | undefined;
taskIndex?: number; taskIndex?: number;
step?: number; preset: typeof presets[number];
mode: "single" | "parallel" | "chain"; mode: "single" | "parallel";
}) => { }) => {
const model = callResolveChildModel({ callModel: input.taskModel, presetModel: params.model }); // Normalize explicit/task model and preset default model against available models
const normalizedCallModel = normalizeModelReference(input.taskModel);
const normalizedPresetModel = normalizeModelReference(input.preset.model);
// Use the resolver to allow custom selection logic, but fall back to
// explicit then preset default. We pass normalized values to the
// resolver so it always sees canonical available-model strings.
const selection = callResolveChildModel({ callModel: normalizedCallModel, presetModel: normalizedPresetModel });
const requestedModel = selection.requestedModel ?? normalizedCallModel ?? normalizedPresetModel;
const resolvedModel = selection.resolvedModel ?? requestedModel;
if (!requestedModel) {
return Promise.resolve({
runId: "",
task: input.task,
requestedModel: undefined,
resolvedModel: undefined,
exitCode: 1,
finalText: "",
stopReason: "error",
errorMessage: `No model provided for preset "${input.preset.name}". Choose one of the available models: ${availableModelsText}`,
} as SubagentRunResult);
}
const progressFormatter = createProgressFormatter(); const progressFormatter = createProgressFormatter();
@@ -182,20 +170,72 @@ export function createSubagentTool(deps: {
meta: { meta: {
mode: input.mode, mode: input.mode,
taskIndex: input.taskIndex, taskIndex: input.taskIndex,
step: input.step,
task: input.task, task: input.task,
cwd: input.cwd ?? ctx.cwd, cwd: input.cwd ?? ctx.cwd,
requestedModel: model.requestedModel, preset: input.preset.name,
resolvedModel: model.resolvedModel, presetSource: input.preset.source,
systemPrompt: input.preset.systemPrompt,
tools: input.preset.tools,
requestedModel,
resolvedModel,
}, },
}) as Promise<SubagentRunResult>; }) as Promise<SubagentRunResult>;
}; };
if (hasSingle) { if (hasSingle) {
if (typeof params.preset !== "string" || params.preset.trim() === "") {
return makeErrorResult("Single mode requires a 'preset' string and a 'task'.", "single");
}
const preset = presets.find((p) => p.name === params.preset);
if (!preset) {
const names = presets.map((p) => p.name).join(", ") || "(none)";
return makeErrorResult(`Unknown preset "${params.preset}". Available presets: ${names}`, "single");
}
// Validate explicit model if provided
if (params.model !== undefined) {
const normalized = normalizeModelReference(params.model);
if (!normalized) {
return makeErrorResult(
typeof params.model !== "string" || params.model.trim().length === 0
? `Single-mode requires a model chosen from the available models: ${availableModelsText}`
: `Invalid model "${params.model}". Choose one of the available models: ${availableModelsText}`,
"single",
);
}
params.model = normalized;
}
// Validate preset default model if present
if (preset.model !== undefined) {
const normalizedPresetModel = normalizeModelReference(preset.model);
if (!normalizedPresetModel) {
return makeErrorResult(
`Preset "${preset.name}" specifies an invalid model "${preset.model}". Choose one of the available models: ${availableModelsText}`,
"single",
);
}
// Use canonical preset model
preset.model = normalizedPresetModel;
}
// Ensure an effective model exists for this run (explicit override wins)
const singleSelection = callResolveChildModel({ callModel: params.model, presetModel: preset.model });
const singleRequested = singleSelection.requestedModel ?? params.model ?? preset.model;
if (!singleRequested) {
return makeErrorResult(
`No model provided for preset "${preset.name}". Choose one of the available models: ${availableModelsText}`,
"single",
);
}
try { try {
const result = await runTask({ const result = await runTask({
task: params.task, task: params.task,
cwd: params.cwd, cwd: params.cwd,
preset,
taskModel: params.model,
mode: "single", mode: "single",
}); });
@@ -213,90 +253,93 @@ export function createSubagentTool(deps: {
} }
} }
if (hasParallel) { // Parallel
if (params.tasks.length > MAX_PARALLEL_TASKS) { // Validate tasks and presets first
return { for (const [index, t] of (params.tasks ?? []).entries()) {
content: [ if (typeof t.preset !== "string" || t.preset.trim() === "") {
{ return makeErrorResult(`Parallel task ${index + 1} missing required 'preset'`, "parallel");
type: "text" as const, }
text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`, const preset = presets.find((p) => p.name === t.preset);
}, if (!preset) {
], const names = presets.map((p) => p.name).join(", ") || "(none)";
details: makeDetails("parallel", []), return makeErrorResult(`Unknown preset "${t.preset}" for task ${index + 1}. Available presets: ${names}`, "parallel");
isError: true,
};
} }
const liveResults: SubagentRunResult[] = []; if (t.model !== undefined) {
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (task: any, index) => { const normalized = normalizeModelReference(t.model);
const result = await runTask({ if (!normalized) {
task: task.task, return makeErrorResult(`Invalid model for parallel task ${index + 1}: "${t.model}". Choose one of the available models: ${availableModelsText}`, "parallel");
cwd: task.cwd, }
taskModel: task.model, t.model = normalized;
taskIndex: index, }
mode: "parallel",
});
liveResults[index] = result;
onUpdate?.({
content: [{ type: "text", text: `Parallel: ${liveResults.filter(Boolean).length}/${params.tasks.length} finished` }],
details: makeDetails("parallel", liveResults.filter(Boolean)),
});
return result;
});
const successCount = results.filter((result) => !isFailure(result)).length; if (preset.model !== undefined) {
const summary = results const normalizedPresetModel = normalizeModelReference(preset.model);
.map((result, index) => `[task ${index + 1}] ${isFailure(result) ? "failed" : "completed"}: ${result.finalText || "(no output)"}`) if (!normalizedPresetModel) {
.join("\n\n"); return makeErrorResult(
`Preset "${preset.name}" specifies an invalid model "${preset.model}". Choose one of the available models: ${availableModelsText}`,
"parallel",
);
}
preset.model = normalizedPresetModel;
}
// Ensure an effective model exists for this task
const sel = callResolveChildModel({ callModel: t.model, presetModel: preset.model });
const requested = sel.requestedModel ?? t.model ?? preset.model;
if (!requested) {
return makeErrorResult(
`Parallel task ${index + 1} has no model. Provide an explicit 'model' or set a default model on preset "${preset.name}". Available models: ${availableModelsText}`,
"parallel",
);
}
}
if (params.tasks.length > MAX_PARALLEL_TASKS) {
return { return {
content: [{ type: "text" as const, text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summary}` }], content: [
details: makeDetails("parallel", results), {
isError: successCount !== results.length, type: "text" as const,
text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
},
],
details: makeDetails("parallel", []),
isError: true,
}; };
} }
const results: SubagentRunResult[] = []; const liveResults: SubagentRunResult[] = [];
let previous = ""; const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (task: any, index) => {
for (let index = 0; index < params.chain.length; index += 1) { const preset = presets.find((p) => p.name === task.preset)!;
const item = params.chain[index];
const task = item.task.replaceAll("{previous}", previous);
const result = await runTask({ const result = await runTask({
task, task: task.task,
cwd: item.cwd, cwd: task.cwd,
taskModel: item.model, taskModel: task.model,
step: index + 1, taskIndex: index,
mode: "chain", preset,
mode: "parallel",
}); });
liveResults[index] = result;
onUpdate?.({ onUpdate?.({
content: [{ type: "text", text: `Chain: completed step ${index + 1}/${params.chain.length}` }], content: [{ type: "text", text: `Parallel: ${liveResults.filter(Boolean).length}/${params.tasks.length} finished` }],
details: makeDetails("chain", [...results, result]), details: makeDetails("parallel", liveResults.filter(Boolean)),
}); });
results.push(result); return result;
if (isFailure(result)) { });
return {
content: [ const successCount = results.filter((result) => !isFailure(result)).length;
{ const summary = results
type: "text" as const, .map((result, index) => `[task ${index + 1}] ${isFailure(result) ? "failed" : "completed"}: ${result.finalText || "(no output)"}`)
text: `Chain stopped at step ${index + 1}: ${result.finalText || result.stopReason || "failed"}`, .join("\n\n");
},
],
details: makeDetails("chain", results),
isError: true,
};
}
previous = result.finalText;
}
const finalResult = results[results.length - 1];
return { return {
content: [{ type: "text" as const, text: finalResult?.finalText ?? "" }], content: [{ type: "text" as const, text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summary}` }],
details: makeDetails("chain", results), details: makeDetails("parallel", results),
isError: successCount !== results.length,
}; };
}, },
renderCall(args: any) { renderCall(args: any) {
if (args.tasks?.length) return new Text(`subagent parallel (${args.tasks.length} tasks)`, 0, 0); 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", 0, 0); return new Text("subagent", 0, 0);
}, },
renderResult(result: { content: Array<{ type: string; text?: string }> }) { renderResult(result: { content: Array<{ type: string; text?: string }> }) {