Task 2: Preset-driven subagent tool
- Replace chain mode with preset-driven single and parallel modes - Schema: require per-task preset; remove top-level required model - Tool: resolve presets via discoverSubagentPresets(), normalize models, resolve effective model per run, pass preset meta - Update tests and remove chain test
This commit is contained in:
@@ -8,7 +8,7 @@ test("the extension entrypoint registers the subagent tool with the currently av
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const registeredTools: any[] = [];
|
const registeredTools: any[] = [];
|
||||||
const handlers: Record<string, (event: any, ctx: any) => Promise<void> | void> = {};
|
const handlers: any = {};
|
||||||
|
|
||||||
subagentsExtension({
|
subagentsExtension({
|
||||||
on(event: string, handler: (event: any, ctx: any) => Promise<void> | void) {
|
on(event: string, handler: (event: any, ctx: any) => Promise<void> | void) {
|
||||||
@@ -36,17 +36,16 @@ test("the extension entrypoint registers the subagent tool with the currently av
|
|||||||
|
|
||||||
assert.equal(registeredTools.length, 1);
|
assert.equal(registeredTools.length, 1);
|
||||||
assert.equal(registeredTools[0]?.name, "subagent");
|
assert.equal(registeredTools[0]?.name, "subagent");
|
||||||
assert.deepEqual(registeredTools[0]?.parameters.required, ["model"]);
|
// no single top-level model required now (handled at runtime)
|
||||||
assert.equal("agent" in registeredTools[0]?.parameters.properties, false);
|
assert.equal("agent" in registeredTools[0]?.parameters.properties, false);
|
||||||
assert.equal("agentScope" in registeredTools[0]?.parameters.properties, false);
|
assert.equal("agentScope" in registeredTools[0]?.parameters.properties, false);
|
||||||
assert.equal("confirmProjectAgents" in registeredTools[0]?.parameters.properties, false);
|
assert.equal("confirmProjectAgents" in registeredTools[0]?.parameters.properties, false);
|
||||||
assert.equal("task" in registeredTools[0]?.parameters.properties, true);
|
assert.equal("task" in registeredTools[0]?.parameters.properties, true);
|
||||||
assert.deepEqual(registeredTools[0]?.parameters.properties.model.enum, [
|
assert.equal("preset" in registeredTools[0]?.parameters.properties, true);
|
||||||
"anthropic/claude-sonnet-4-5",
|
assert.equal(registeredTools[0]?.parameters.properties.model, undefined);
|
||||||
"openai/gpt-5",
|
|
||||||
]);
|
|
||||||
assert.equal("agent" in registeredTools[0]?.parameters.properties.tasks.items.properties, false);
|
assert.equal("agent" in registeredTools[0]?.parameters.properties.tasks.items.properties, false);
|
||||||
assert.equal("task" in registeredTools[0]?.parameters.properties.tasks.items.properties, true);
|
assert.equal("task" in registeredTools[0]?.parameters.properties.tasks.items.properties, true);
|
||||||
|
assert.equal("preset" in registeredTools[0]?.parameters.properties.tasks.items.properties, true);
|
||||||
assert.deepEqual(registeredTools[0]?.parameters.properties.tasks.items.properties.model.enum, [
|
assert.deepEqual(registeredTools[0]?.parameters.properties.tasks.items.properties.model.enum, [
|
||||||
"anthropic/claude-sonnet-4-5",
|
"anthropic/claude-sonnet-4-5",
|
||||||
"openai/gpt-5",
|
"openai/gpt-5",
|
||||||
@@ -63,7 +62,7 @@ test("before_agent_start re-applies subagent registration when available models
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const registeredTools: any[] = [];
|
const registeredTools: any[] = [];
|
||||||
const handlers: Record<string, (event: any, ctx: any) => Promise<void> | void> = {};
|
const handlers: any = {};
|
||||||
|
|
||||||
subagentsExtension({
|
subagentsExtension({
|
||||||
on(event: string, handler: (event: any, ctx: any) => Promise<void> | void) {
|
on(event: string, handler: (event: any, ctx: any) => Promise<void> | void) {
|
||||||
@@ -92,7 +91,7 @@ test("before_agent_start re-applies subagent registration when available models
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(registeredTools.length, 1);
|
assert.equal(registeredTools.length, 1);
|
||||||
assert.deepEqual(registeredTools[0]?.parameters.properties.model.enum, [
|
assert.deepEqual(registeredTools[0]?.parameters.properties.tasks.items.properties.model.enum, [
|
||||||
"anthropic/claude-sonnet-4-5",
|
"anthropic/claude-sonnet-4-5",
|
||||||
"openai/gpt-5",
|
"openai/gpt-5",
|
||||||
]);
|
]);
|
||||||
@@ -110,7 +109,6 @@ test("before_agent_start re-applies subagent registration when available models
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(registeredTools.length, 2);
|
assert.equal(registeredTools.length, 2);
|
||||||
assert.deepEqual(registeredTools[1]?.parameters.properties.model.enum, ["openai/gpt-6"]);
|
|
||||||
assert.deepEqual(registeredTools[1]?.parameters.properties.tasks.items.properties.model.enum, ["openai/gpt-6"]);
|
assert.deepEqual(registeredTools[1]?.parameters.properties.tasks.items.properties.model.enum, ["openai/gpt-6"]);
|
||||||
} finally {
|
} finally {
|
||||||
if (original === undefined) delete process.env.PI_SUBAGENTS_CHILD;
|
if (original === undefined) delete process.env.PI_SUBAGENTS_CHILD;
|
||||||
@@ -124,7 +122,7 @@ test("child subagent sessions skip registering the subagent tool", async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const registeredTools: any[] = [];
|
const registeredTools: any[] = [];
|
||||||
const handlers: Record<string, (event: any, ctx: any) => Promise<void> | void> = {};
|
const handlers: any = {};
|
||||||
|
|
||||||
subagentsExtension({
|
subagentsExtension({
|
||||||
on(event: string, handler: (event: any, ctx: any) => Promise<void> | void) {
|
on(event: string, handler: (event: any, ctx: any) => Promise<void> | void) {
|
||||||
@@ -180,7 +178,7 @@ test("registers github-copilot provider override when PI_SUBAGENTS_GITHUB_COPILO
|
|||||||
test("combined child+copilot run registers provider but no tools or startup handlers", () => {
|
test("combined child+copilot run registers provider but no tools or startup handlers", () => {
|
||||||
const registeredProviders: Array<{ name: string; config: any }> = [];
|
const registeredProviders: Array<{ name: string; config: any }> = [];
|
||||||
const registeredTools: any[] = [];
|
const registeredTools: any[] = [];
|
||||||
const handlers: Record<string, (event: any, ctx: any) => Promise<void> | void> = {};
|
const handlers: any = {};
|
||||||
|
|
||||||
const originalInitiator = process.env.PI_SUBAGENTS_GITHUB_COPILOT_INITIATOR;
|
const originalInitiator = process.env.PI_SUBAGENTS_GITHUB_COPILOT_INITIATOR;
|
||||||
const originalChild = process.env.PI_SUBAGENTS_CHILD;
|
const originalChild = process.env.PI_SUBAGENTS_CHILD;
|
||||||
@@ -225,7 +223,7 @@ test("does not re-register the subagent tool when models list unchanged, but re-
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let registerToolCalls = 0;
|
let registerToolCalls = 0;
|
||||||
const handlers: Record<string, (event: any, ctx: any) => Promise<void> | void> = {};
|
const handlers: any = {};
|
||||||
|
|
||||||
subagentsExtension({
|
subagentsExtension({
|
||||||
on(event: string, handler: (event: any, ctx: any) => Promise<void> | void) {
|
on(event: string, handler: (event: any, ctx: any) => Promise<void> | void) {
|
||||||
@@ -298,7 +296,7 @@ test("same model set in different orders should NOT trigger re-registration", as
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let registerToolCalls = 0;
|
let registerToolCalls = 0;
|
||||||
const handlers: Record<string, (event: any, ctx: any) => Promise<void> | void> = {};
|
const handlers: any = {};
|
||||||
|
|
||||||
subagentsExtension({
|
subagentsExtension({
|
||||||
on(event: string, handler: (event: any, ctx: any) => Promise<void> | void) {
|
on(event: string, handler: (event: any, ctx: any) => Promise<void> | void) {
|
||||||
@@ -355,7 +353,7 @@ test("empty model list should NOT register the tool, but a later non-empty list
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let registerToolCalls = 0;
|
let registerToolCalls = 0;
|
||||||
const handlers: Record<string, (event: any, ctx: any) => Promise<void> | void> = {};
|
const handlers: any = {};
|
||||||
|
|
||||||
subagentsExtension({
|
subagentsExtension({
|
||||||
on(event: string, handler: (event: any, ctx: any) => Promise<void> | void) {
|
on(event: string, handler: (event: any, ctx: any) => Promise<void> | void) {
|
||||||
|
|||||||
@@ -11,39 +11,30 @@ function createTaskModelSchema(availableModels: readonly string[]) {
|
|||||||
|
|
||||||
export function createTaskItemSchema(availableModels: readonly string[]) {
|
export function createTaskItemSchema(availableModels: readonly string[]) {
|
||||||
return Type.Object({
|
return Type.Object({
|
||||||
|
preset: Type.String({ description: "Subagent preset name to use for this task" }),
|
||||||
task: Type.String({ description: "Task to delegate to the child agent" }),
|
task: Type.String({ description: "Task to delegate to the child agent" }),
|
||||||
model: createTaskModelSchema(availableModels),
|
model: createTaskModelSchema(availableModels),
|
||||||
cwd: Type.Optional(Type.String({ description: "Optional working directory override" })),
|
cwd: Type.Optional(Type.String({ description: "Optional working directory override" })),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createChainItemSchema(availableModels: readonly string[]) {
|
|
||||||
return Type.Object({
|
|
||||||
task: Type.String({ description: "Task with optional {previous} placeholder" }),
|
|
||||||
model: createTaskModelSchema(availableModels),
|
|
||||||
cwd: Type.Optional(Type.String({ description: "Optional working directory override" })),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TaskItemSchema = createTaskItemSchema([]);
|
export const TaskItemSchema = createTaskItemSchema([]);
|
||||||
export const ChainItemSchema = createChainItemSchema([]);
|
|
||||||
|
|
||||||
export function createSubagentParamsSchema(availableModels: readonly string[]) {
|
export function createSubagentParamsSchema(availableModels: readonly string[]) {
|
||||||
return Type.Object({
|
return Type.Object({
|
||||||
|
// Single mode: provide preset + task
|
||||||
|
preset: Type.Optional(Type.String({ description: "Subagent preset name to use in single mode" })),
|
||||||
task: Type.Optional(Type.String({ description: "Single-mode delegated task" })),
|
task: Type.Optional(Type.String({ description: "Single-mode delegated task" })),
|
||||||
model: StringEnum(availableModels, {
|
// Parallel mode: provide tasks array where each item names its own preset
|
||||||
description: "Required top-level child model. Must be one of the currently available models.",
|
|
||||||
}),
|
|
||||||
tasks: Type.Optional(Type.Array(createTaskItemSchema(availableModels), { description: "Parallel tasks" })),
|
tasks: Type.Optional(Type.Array(createTaskItemSchema(availableModels), { description: "Parallel tasks" })),
|
||||||
chain: Type.Optional(Type.Array(createChainItemSchema(availableModels), { description: "Sequential tasks" })),
|
|
||||||
cwd: Type.Optional(Type.String({ description: "Single-mode working directory override" })),
|
cwd: Type.Optional(Type.String({ description: "Single-mode working directory override" })),
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubagentParamsSchema = createSubagentParamsSchema([]);
|
export const SubagentParamsSchema = createSubagentParamsSchema([]);
|
||||||
|
|
||||||
export type TaskItem = Static<typeof TaskItemSchema>;
|
export type TaskItem = Static<typeof TaskItemSchema>;
|
||||||
export type ChainItem = Static<typeof ChainItemSchema>;
|
|
||||||
export type SubagentParams = Static<typeof SubagentParamsSchema>;
|
export type SubagentParams = Static<typeof SubagentParamsSchema>;
|
||||||
|
|
||||||
export interface SubagentRunResult {
|
export interface SubagentRunResult {
|
||||||
@@ -66,6 +57,6 @@ export interface SubagentRunResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SubagentToolDetails {
|
export interface SubagentToolDetails {
|
||||||
mode: "single" | "parallel" | "chain";
|
mode: "single" | "parallel";
|
||||||
results: SubagentRunResult[];
|
results: SubagentRunResult[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
import test from "node:test";
|
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { createSubagentTool } from "./tool.ts";
|
|
||||||
|
|
||||||
test("chain mode substitutes {previous} into the next task", async () => {
|
|
||||||
const seenTasks: string[] = [];
|
|
||||||
|
|
||||||
const tool = createSubagentTool({
|
|
||||||
runSingleTask: async ({ meta }: any) => {
|
|
||||||
seenTasks.push(meta.task);
|
|
||||||
return {
|
|
||||||
runId: `run-${seenTasks.length}`,
|
|
||||||
task: meta.task,
|
|
||||||
exitCode: 0,
|
|
||||||
finalText: seenTasks.length === 1 ? "Inspection output" : "Plan output",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const result = await tool.execute(
|
|
||||||
"tool-1",
|
|
||||||
{
|
|
||||||
model: "anthropic/claude-sonnet-4-5",
|
|
||||||
chain: [
|
|
||||||
{ task: "inspect auth" },
|
|
||||||
{ task: "use this context: {previous}" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
cwd: "/repo",
|
|
||||||
modelRegistry: {
|
|
||||||
getAvailable: () => [{ provider: "anthropic", id: "claude-sonnet-4-5" }],
|
|
||||||
},
|
|
||||||
hasUI: false,
|
|
||||||
} as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.deepEqual(seenTasks, ["inspect auth", "use this context: Inspection output"]);
|
|
||||||
assert.equal(result.content[0]?.type === "text" ? result.content[0].text : "", "Plan output");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("chain mode stops on the first failed step", async () => {
|
|
||||||
const tool = createSubagentTool({
|
|
||||||
runSingleTask: async ({ meta }: any) => {
|
|
||||||
if (meta.task.includes("Inspection output")) {
|
|
||||||
return {
|
|
||||||
runId: "run-2",
|
|
||||||
task: meta.task,
|
|
||||||
exitCode: 1,
|
|
||||||
finalText: "",
|
|
||||||
stopReason: "error",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
runId: "run-1",
|
|
||||||
task: meta.task,
|
|
||||||
exitCode: 0,
|
|
||||||
finalText: "Inspection output",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const result = await tool.execute(
|
|
||||||
"tool-1",
|
|
||||||
{
|
|
||||||
model: "anthropic/claude-sonnet-4-5",
|
|
||||||
chain: [
|
|
||||||
{ task: "inspect auth" },
|
|
||||||
{ task: "use this context: {previous}" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
cwd: "/repo",
|
|
||||||
modelRegistry: {
|
|
||||||
getAvailable: () => [{ provider: "anthropic", id: "claude-sonnet-4-5" }],
|
|
||||||
},
|
|
||||||
hasUI: false,
|
|
||||||
} as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(result.isError, true);
|
|
||||||
assert.match(result.content[0]?.type === "text" ? result.content[0].text : "", /Chain stopped at step 2/);
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user