Task 2: Implement preset-driven subagent tool, update schema and tests
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
227
src/tool.ts
227
src/tool.ts
@@ -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,7 +253,48 @@ export function createSubagentTool(deps: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasParallel) {
|
// Parallel
|
||||||
|
// Validate tasks and presets first
|
||||||
|
for (const [index, t] of (params.tasks ?? []).entries()) {
|
||||||
|
if (typeof t.preset !== "string" || t.preset.trim() === "") {
|
||||||
|
return makeErrorResult(`Parallel task ${index + 1} missing required 'preset'`, "parallel");
|
||||||
|
}
|
||||||
|
const preset = presets.find((p) => p.name === t.preset);
|
||||||
|
if (!preset) {
|
||||||
|
const names = presets.map((p) => p.name).join(", ") || "(none)";
|
||||||
|
return makeErrorResult(`Unknown preset "${t.preset}" for task ${index + 1}. Available presets: ${names}`, "parallel");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.model !== undefined) {
|
||||||
|
const normalized = normalizeModelReference(t.model);
|
||||||
|
if (!normalized) {
|
||||||
|
return makeErrorResult(`Invalid model for parallel task ${index + 1}: "${t.model}". Choose one of the available models: ${availableModelsText}`, "parallel");
|
||||||
|
}
|
||||||
|
t.model = normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`,
|
||||||
|
"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) {
|
if (params.tasks.length > MAX_PARALLEL_TASKS) {
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -229,11 +310,13 @@ export function createSubagentTool(deps: {
|
|||||||
|
|
||||||
const liveResults: SubagentRunResult[] = [];
|
const liveResults: SubagentRunResult[] = [];
|
||||||
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (task: any, index) => {
|
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (task: any, index) => {
|
||||||
|
const preset = presets.find((p) => p.name === task.preset)!;
|
||||||
const result = await runTask({
|
const result = await runTask({
|
||||||
task: task.task,
|
task: task.task,
|
||||||
cwd: task.cwd,
|
cwd: task.cwd,
|
||||||
taskModel: task.model,
|
taskModel: task.model,
|
||||||
taskIndex: index,
|
taskIndex: index,
|
||||||
|
preset,
|
||||||
mode: "parallel",
|
mode: "parallel",
|
||||||
});
|
});
|
||||||
liveResults[index] = result;
|
liveResults[index] = result;
|
||||||
@@ -254,49 +337,9 @@ export function createSubagentTool(deps: {
|
|||||||
details: makeDetails("parallel", results),
|
details: makeDetails("parallel", results),
|
||||||
isError: successCount !== results.length,
|
isError: successCount !== results.length,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const results: SubagentRunResult[] = [];
|
|
||||||
let previous = "";
|
|
||||||
for (let index = 0; index < params.chain.length; index += 1) {
|
|
||||||
const item = params.chain[index];
|
|
||||||
const task = item.task.replaceAll("{previous}", previous);
|
|
||||||
const result = await runTask({
|
|
||||||
task,
|
|
||||||
cwd: item.cwd,
|
|
||||||
taskModel: item.model,
|
|
||||||
step: index + 1,
|
|
||||||
mode: "chain",
|
|
||||||
});
|
|
||||||
onUpdate?.({
|
|
||||||
content: [{ type: "text", text: `Chain: completed step ${index + 1}/${params.chain.length}` }],
|
|
||||||
details: makeDetails("chain", [...results, result]),
|
|
||||||
});
|
|
||||||
results.push(result);
|
|
||||||
if (isFailure(result)) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text" as const,
|
|
||||||
text: `Chain stopped at step ${index + 1}: ${result.finalText || result.stopReason || "failed"}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
details: makeDetails("chain", results),
|
|
||||||
isError: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
previous = result.finalText;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalResult = results[results.length - 1];
|
|
||||||
return {
|
|
||||||
content: [{ type: "text" as const, text: finalResult?.finalText ?? "" }],
|
|
||||||
details: makeDetails("chain", results),
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
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 }> }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user