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", () => {
|
describe("prometheus subagent delegate_task permission", () => {
|
||||||
test("prometheus subagent should have delegate_task permission enabled", async () => {
|
test("prometheus subagent should have delegate_task permission enabled", async () => {
|
||||||
// #given - sisyphus delegates to prometheus
|
// #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.
|
// Uses case-insensitive matching to allow "Oracle", "oracle", "ORACLE" etc.
|
||||||
try {
|
try {
|
||||||
const agentsResult = await client.app.agents()
|
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 agents = (agentsResult as { data?: AgentInfo[] }).data ?? agentsResult as unknown as AgentInfo[]
|
||||||
|
|
||||||
const callableAgents = agents.filter((a) => a.mode !== "primary")
|
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
|
// Use the canonical agent name from registration
|
||||||
agentToUse = matchedAgent.name
|
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 {
|
} catch {
|
||||||
// If we can't fetch agents, proceed anyway - the session.prompt will fail with a clearer error
|
// 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