From 34aaef2219a11a611835723bd35efa7ef6c26b30 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 29 Jan 2026 11:27:07 +0900 Subject: [PATCH] fix(delegate-task): pass registered agent model explicitly for subagent_type (#1225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When delegate_task uses subagent_type, extract the matched agent's model object and pass it explicitly to session.prompt/manager.launch. This ensures the model is always in the correct object format regardless of how OpenCode handles string→object conversion for plugin-registered agents. Closes #1225 --- src/tools/delegate-task/tools.test.ts | 186 ++++++++++++++++++++++++++ src/tools/delegate-task/tools.ts | 10 +- 2 files changed, 195 insertions(+), 1 deletion(-) diff --git a/src/tools/delegate-task/tools.test.ts b/src/tools/delegate-task/tools.test.ts index 13acb3c6..bd74303f 100644 --- a/src/tools/delegate-task/tools.test.ts +++ b/src/tools/delegate-task/tools.test.ts @@ -2035,6 +2035,192 @@ describe("sisyphus-task", () => { }) }) + describe("subagent_type model extraction (issue #1225)", () => { + test("background mode passes matched agent model to manager.launch", async () => { + // #given - agent with model registered, using subagent_type with run_in_background=true + const { createDelegateTask } = require("./tools") + let launchInput: any + + const mockManager = { + launch: async (input: any) => { + launchInput = input + return { + id: "task-explore", + sessionID: "ses_explore_model", + description: "Explore task", + agent: "explore", + status: "running", + } + }, + } + + const mockClient = { + app: { + agents: async () => ({ + data: [ + { name: "explore", mode: "subagent", model: { providerID: "anthropic", modelID: "claude-haiku-4-5" } }, + ], + }), + }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + create: async () => ({ data: { id: "ses_explore_model" } }), + prompt: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } + + // #when - delegating to explore agent via subagent_type + await tool.execute( + { + description: "Explore codebase", + prompt: "Find auth patterns", + subagent_type: "explore", + run_in_background: true, + load_skills: [], + }, + toolContext + ) + + // #then - matched agent's model should be passed to manager.launch + expect(launchInput.model).toEqual({ + providerID: "anthropic", + modelID: "claude-haiku-4-5", + }) + }) + + test("sync mode passes matched agent model to session.prompt", async () => { + // #given - agent with model registered, using subagent_type with run_in_background=false + const { createDelegateTask } = require("./tools") + let promptBody: any + + const mockManager = { launch: async () => ({}) } + + const mockClient = { + app: { + agents: async () => ({ + data: [ + { name: "oracle", mode: "subagent", model: { providerID: "anthropic", modelID: "claude-opus-4-5" } }, + ], + }), + }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_oracle_model" } }), + prompt: async (input: any) => { + promptBody = input.body + return { data: {} } + }, + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Consultation done" }] }], + }), + status: async () => ({ data: { "ses_oracle_model": { type: "idle" } } }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } + + // #when - delegating to oracle agent via subagent_type in sync mode + await tool.execute( + { + description: "Consult oracle", + prompt: "Review architecture", + subagent_type: "oracle", + run_in_background: false, + load_skills: [], + }, + toolContext + ) + + // #then - matched agent's model should be passed to session.prompt + expect(promptBody.model).toEqual({ + providerID: "anthropic", + modelID: "claude-opus-4-5", + }) + }, { timeout: 20000 }) + + test("agent without model does not override categoryModel", async () => { + // #given - agent registered without model field + const { createDelegateTask } = require("./tools") + let promptBody: any + + const mockManager = { launch: async () => ({}) } + + const mockClient = { + app: { + agents: async () => ({ + data: [ + { name: "explore", mode: "subagent" }, // no model field + ], + }), + }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_no_model_agent" } }), + prompt: async (input: any) => { + promptBody = input.body + return { data: {} } + }, + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }], + }), + status: async () => ({ data: { "ses_no_model_agent": { type: "idle" } } }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } + + // #when - delegating to agent without model + await tool.execute( + { + description: "Explore without model", + prompt: "Find something", + subagent_type: "explore", + run_in_background: false, + load_skills: [], + }, + toolContext + ) + + // #then - no model should be passed to session.prompt + expect(promptBody.model).toBeUndefined() + }, { timeout: 20000 }) + }) + describe("prometheus subagent delegate_task permission", () => { test("prometheus subagent should have delegate_task permission enabled", async () => { // #given - sisyphus delegates to prometheus diff --git a/src/tools/delegate-task/tools.ts b/src/tools/delegate-task/tools.ts index 8009b41a..5e96f605 100644 --- a/src/tools/delegate-task/tools.ts +++ b/src/tools/delegate-task/tools.ts @@ -784,7 +784,7 @@ Create the work plan directly - that's your job as the planning agent.` // Uses case-insensitive matching to allow "Oracle", "oracle", "ORACLE" etc. try { const agentsResult = await client.app.agents() - type AgentInfo = { name: string; mode?: "subagent" | "primary" | "all" } + type AgentInfo = { name: string; mode?: "subagent" | "primary" | "all"; model?: { providerID: string; modelID: string } } const agents = (agentsResult as { data?: AgentInfo[] }).data ?? agentsResult as unknown as AgentInfo[] const callableAgents = agents.filter((a) => a.mode !== "primary") @@ -807,6 +807,14 @@ Create the work plan directly - that's your job as the planning agent.` } // Use the canonical agent name from registration agentToUse = matchedAgent.name + + // Extract registered agent's model to pass explicitly to session.prompt. + // This ensures the model is always in the correct object format ({providerID, modelID}) + // regardless of how OpenCode handles string→object conversion for plugin-registered agents. + // See: https://github.com/code-yeongyu/oh-my-opencode/issues/1225 + if (matchedAgent.model) { + categoryModel = matchedAgent.model + } } catch { // If we can't fetch agents, proceed anyway - the session.prompt will fail with a clearer error }