diff --git a/src/extension.test.ts b/src/extension.test.ts index 2db97d8..fbfb784 100644 --- a/src/extension.test.ts +++ b/src/extension.test.ts @@ -8,7 +8,7 @@ test("the extension entrypoint registers the subagent tool with the currently av try { const registeredTools: any[] = []; - const handlers: Record Promise | void> = {}; + const handlers: any = {}; subagentsExtension({ on(event: string, handler: (event: any, ctx: any) => Promise | 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[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("agentScope" 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.deepEqual(registeredTools[0]?.parameters.properties.model.enum, [ - "anthropic/claude-sonnet-4-5", - "openai/gpt-5", - ]); + assert.equal("preset" in registeredTools[0]?.parameters.properties, true); + assert.equal(registeredTools[0]?.parameters.properties.model, undefined); 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("preset" in registeredTools[0]?.parameters.properties.tasks.items.properties, true); assert.deepEqual(registeredTools[0]?.parameters.properties.tasks.items.properties.model.enum, [ "anthropic/claude-sonnet-4-5", "openai/gpt-5", @@ -63,7 +62,7 @@ test("before_agent_start re-applies subagent registration when available models try { const registeredTools: any[] = []; - const handlers: Record Promise | void> = {}; + const handlers: any = {}; subagentsExtension({ on(event: string, handler: (event: any, ctx: any) => Promise | void) { @@ -92,7 +91,7 @@ test("before_agent_start re-applies subagent registration when available models ); 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", "openai/gpt-5", ]); @@ -110,7 +109,6 @@ test("before_agent_start re-applies subagent registration when available models ); 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"]); } finally { if (original === undefined) delete process.env.PI_SUBAGENTS_CHILD; @@ -124,7 +122,7 @@ test("child subagent sessions skip registering the subagent tool", async () => { try { const registeredTools: any[] = []; - const handlers: Record Promise | void> = {}; + const handlers: any = {}; subagentsExtension({ on(event: string, handler: (event: any, ctx: any) => Promise | 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", () => { const registeredProviders: Array<{ name: string; config: any }> = []; const registeredTools: any[] = []; - const handlers: Record Promise | void> = {}; + const handlers: any = {}; const originalInitiator = process.env.PI_SUBAGENTS_GITHUB_COPILOT_INITIATOR; 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 { let registerToolCalls = 0; - const handlers: Record Promise | void> = {}; + const handlers: any = {}; subagentsExtension({ on(event: string, handler: (event: any, ctx: any) => Promise | void) { @@ -298,7 +296,7 @@ test("same model set in different orders should NOT trigger re-registration", as try { let registerToolCalls = 0; - const handlers: Record Promise | void> = {}; + const handlers: any = {}; subagentsExtension({ on(event: string, handler: (event: any, ctx: any) => Promise | void) { @@ -355,7 +353,7 @@ test("empty model list should NOT register the tool, but a later non-empty list try { let registerToolCalls = 0; - const handlers: Record Promise | void> = {}; + const handlers: any = {}; subagentsExtension({ on(event: string, handler: (event: any, ctx: any) => Promise | void) { diff --git a/src/schema.ts b/src/schema.ts index a148f49..09c8ab7 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -11,39 +11,30 @@ function createTaskModelSchema(availableModels: readonly string[]) { export function createTaskItemSchema(availableModels: readonly string[]) { 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" }), model: createTaskModelSchema(availableModels), 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 ChainItemSchema = createChainItemSchema([]); export function createSubagentParamsSchema(availableModels: readonly string[]) { 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" })), - model: StringEnum(availableModels, { - description: "Required top-level child model. Must be one of the currently available models.", - }), + // Parallel mode: provide tasks array where each item names its own preset 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" })), + }); } export const SubagentParamsSchema = createSubagentParamsSchema([]); export type TaskItem = Static; -export type ChainItem = Static; export type SubagentParams = Static; export interface SubagentRunResult { @@ -66,6 +57,6 @@ export interface SubagentRunResult { } export interface SubagentToolDetails { - mode: "single" | "parallel" | "chain"; + mode: "single" | "parallel"; results: SubagentRunResult[]; } diff --git a/src/tool-chain.test.ts b/src/tool-chain.test.ts deleted file mode 100644 index 47005cd..0000000 --- a/src/tool-chain.test.ts +++ /dev/null @@ -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/); -});