fix(delegate-task): pass registered agent model explicitly for subagent_type (#1225)

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
This commit is contained in:
YeonGyu-Kim 2026-01-29 11:27:07 +09:00 committed by GitHub
parent faca80caa9
commit 34aaef2219
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 195 additions and 1 deletions

View File

@ -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

View File

@ -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
}