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:
parent
faca80caa9
commit
34aaef2219
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user