diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 11025c730..71d62f509 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -1668,7 +1668,7 @@ describe("BackgroundManager.resume model persistence", () => { // then - model should be passed in prompt body expect(promptCalls).toHaveLength(1) expect(promptCalls[0].body.model).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-20250514" }) - expect("agent" in promptCalls[0].body).toBe(false) + expect(promptCalls[0].body.agent).toBe("explore") }) test("should NOT pass model when task has no model (backward compatibility)", async () => { @@ -1832,7 +1832,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { expect(task2.status).toBe("pending") }) - test("should omit agent when launch has model and keep agent without model", async () => { + test("should keep agent when launch has model and keep agent without model", async () => { // given const promptBodies: Array> = [] let resolveFirstPromptStarted: (() => void) | undefined @@ -1894,7 +1894,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { expect(taskWithoutModel.status).toBe("pending") expect(promptBodies).toHaveLength(2) expect(promptBodies[0].model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" }) - expect("agent" in promptBodies[0]).toBe(false) + expect(promptBodies[0].agent).toBe("test-agent") expect(promptBodies[1].agent).toBe("test-agent") expect("model" in promptBodies[1]).toBe(false) }) @@ -4752,6 +4752,53 @@ describe("BackgroundManager - tool permission spread order", () => { manager.shutdown() }) + test("startTask keeps agent when explicit model is configured", async () => { + //#given + const promptCalls: Array<{ path: { id: string }; body: Record }> = [] + const client = { + session: { + get: async () => ({ data: { directory: "/test/dir" } }), + create: async () => ({ data: { id: "session-1" } }), + promptAsync: async (args: { path: { id: string }; body: Record }) => { + promptCalls.push(args) + return {} + }, + }, + } + const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) + const task: BackgroundTask = { + id: "task-explicit-model", + status: "pending", + queuedAt: new Date(), + description: "test task", + prompt: "test prompt", + agent: "sisyphus-junior", + parentSessionID: "parent-session", + parentMessageID: "parent-message", + model: { providerID: "openai", modelID: "gpt-5.4", variant: "medium" }, + } + const input: import("./types").LaunchInput = { + description: task.description, + prompt: task.prompt, + agent: task.agent, + parentSessionID: task.parentSessionID, + parentMessageID: task.parentMessageID, + model: task.model, + } + + //#when + await (manager as unknown as { startTask: (item: { task: BackgroundTask; input: import("./types").LaunchInput }) => Promise }) + .startTask({ task, input }) + + //#then + expect(promptCalls).toHaveLength(1) + expect(promptCalls[0].body.agent).toBe("sisyphus-junior") + expect(promptCalls[0].body.model).toEqual({ providerID: "openai", modelID: "gpt-5.4" }) + expect(promptCalls[0].body.variant).toBe("medium") + + manager.shutdown() + }) + test("resume respects explore agent restrictions", async () => { //#given let capturedTools: Record | undefined @@ -4796,4 +4843,48 @@ describe("BackgroundManager - tool permission spread order", () => { manager.shutdown() }) + + test("resume keeps agent when explicit model is configured", async () => { + //#given + let promptCall: { path: { id: string }; body: Record } | undefined + const client = { + session: { + promptAsync: async (args: { path: { id: string }; body: Record }) => { + promptCall = args + return {} + }, + abort: async () => ({}), + }, + } + const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) + const task: BackgroundTask = { + id: "task-explicit-model-resume", + sessionID: "session-3", + parentSessionID: "parent-session", + parentMessageID: "parent-message", + description: "resume task", + prompt: "resume prompt", + agent: "explore", + status: "completed", + startedAt: new Date(), + completedAt: new Date(), + model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" }, + } + getTaskMap(manager).set(task.id, task) + + //#when + await manager.resume({ + sessionId: "session-3", + prompt: "continue", + parentSessionID: "parent-session", + parentMessageID: "parent-message", + }) + + //#then + expect(promptCall).toBeDefined() + expect(promptCall?.body.agent).toBe("explore") + expect(promptCall?.body.model).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-20250514" }) + + manager.shutdown() + }) }) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index c858667a8..c4ea7528b 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -515,9 +515,7 @@ export class BackgroundManager { promptWithModelSuggestionRetry(this.client, { path: { id: sessionID }, body: { - // When a model is explicitly provided, omit the agent name so opencode's - // built-in agent fallback chain does not override the user-specified model. - ...(launchModel ? {} : { agent: input.agent }), + agent: input.agent, ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), system: input.skillContent, @@ -794,9 +792,7 @@ export class BackgroundManager { this.client.session.promptAsync({ path: { id: existingTask.sessionID }, body: { - // When a model is explicitly provided, omit the agent name so opencode's - // built-in agent fallback chain does not override the user-specified model. - ...(resumeModel ? {} : { agent: existingTask.agent }), + agent: existingTask.agent, ...(resumeModel ? { model: resumeModel } : {}), ...(resumeVariant ? { variant: resumeVariant } : {}), tools: (() => { diff --git a/src/features/background-agent/spawner.test.ts b/src/features/background-agent/spawner.test.ts index 27d26a519..0ff9b3f78 100644 --- a/src/features/background-agent/spawner.test.ts +++ b/src/features/background-agent/spawner.test.ts @@ -64,4 +64,63 @@ describe("background-agent spawner.startTask", () => { { permission: "question", action: "deny", pattern: "*" }, ]) }) + + test("keeps agent when explicit model is configured", async () => { + //#given + const promptCalls: any[] = [] + + const client = { + session: { + get: async () => ({ data: { directory: "/parent/dir" } }), + create: async () => ({ data: { id: "ses_child" } }), + promptAsync: async (args?: any) => { + promptCalls.push(args) + return {} + }, + }, + } + + const task = createTask({ + description: "Test task", + prompt: "Do work", + agent: "sisyphus-junior", + parentSessionID: "ses_parent", + parentMessageID: "msg_parent", + model: { providerID: "openai", modelID: "gpt-5.4", variant: "medium" }, + }) + + const item = { + task, + input: { + description: task.description, + prompt: task.prompt, + agent: task.agent, + parentSessionID: task.parentSessionID, + parentMessageID: task.parentMessageID, + parentModel: task.parentModel, + parentAgent: task.parentAgent, + model: task.model, + }, + } + + const ctx = { + client, + directory: "/fallback", + concurrencyManager: { release: () => {} }, + tmuxEnabled: false, + onTaskError: () => {}, + } + + //#when + await startTask(item as any, ctx as any) + + //#then + expect(promptCalls).toHaveLength(1) + expect(promptCalls[0]?.body?.agent).toBe("sisyphus-junior") + expect(promptCalls[0]?.body?.model).toEqual({ + providerID: "openai", + modelID: "gpt-5.4", + }) + expect(promptCalls[0]?.body?.variant).toBe("medium") + }) }) diff --git a/src/features/background-agent/spawner.ts b/src/features/background-agent/spawner.ts index a66720966..c4f435720 100644 --- a/src/features/background-agent/spawner.ts +++ b/src/features/background-agent/spawner.ts @@ -135,9 +135,7 @@ export async function startTask( promptWithModelSuggestionRetry(client, { path: { id: sessionID }, body: { - // When a model is explicitly provided, omit the agent name so opencode's - // built-in agent fallback chain does not override the user-specified model. - ...(launchModel ? {} : { agent: input.agent }), + agent: input.agent, ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), system: input.skillContent, @@ -222,9 +220,7 @@ export async function resumeTask( client.session.promptAsync({ path: { id: task.sessionID }, body: { - // When a model is explicitly provided, omit the agent name so opencode's - // built-in agent fallback chain does not override the user-specified model. - ...(resumeModel ? {} : { agent: task.agent }), + agent: task.agent, ...(resumeModel ? { model: resumeModel } : {}), ...(resumeVariant ? { variant: resumeVariant } : {}), tools: { diff --git a/src/tools/delegate-task/sync-prompt-sender.test.ts b/src/tools/delegate-task/sync-prompt-sender.test.ts index d7e0eb0e3..39bf6bd6f 100644 --- a/src/tools/delegate-task/sync-prompt-sender.test.ts +++ b/src/tools/delegate-task/sync-prompt-sender.test.ts @@ -165,6 +165,55 @@ bunDescribe("sendSyncPrompt", () => { bunExpect(promptArgs.body.tools.call_omo_agent).toBe(true) }) + bunTest("includes agent alongside explicit category model", async () => { + //#given + const { sendSyncPrompt } = require("./sync-prompt-sender") + + let promptArgs: any + const promptAsync = bunMock(async (input: any) => { + promptArgs = input + return { data: {} } + }) + + const mockClient = { + session: { + promptAsync, + }, + } + + const input = { + sessionID: "test-session", + agentToUse: "sisyphus-junior", + args: { + description: "test task", + prompt: "test prompt", + category: "quick", + run_in_background: false, + load_skills: [], + }, + systemContent: undefined, + categoryModel: { + providerID: "openai", + modelID: "gpt-5.4", + variant: "medium", + }, + toastManager: null, + taskId: undefined, + } + + //#when + await sendSyncPrompt(mockClient, input) + + //#then + bunExpect(promptAsync).toHaveBeenCalled() + bunExpect(promptArgs.body.agent).toBe("sisyphus-junior") + bunExpect(promptArgs.body.model).toEqual({ + providerID: "openai", + modelID: "gpt-5.4", + }) + bunExpect(promptArgs.body.variant).toBe("medium") + }) + bunTest("retries with promptSync for oracle when promptAsync fails with unexpected EOF", async () => { //#given const { sendSyncPrompt } = require("./sync-prompt-sender") diff --git a/src/tools/delegate-task/sync-prompt-sender.ts b/src/tools/delegate-task/sync-prompt-sender.ts index f34009744..fe4f8a693 100644 --- a/src/tools/delegate-task/sync-prompt-sender.ts +++ b/src/tools/delegate-task/sync-prompt-sender.ts @@ -56,9 +56,7 @@ export async function sendSyncPrompt( const promptArgs = { path: { id: input.sessionID }, body: { - // When a custom model is configured, omit the agent name so opencode's - // built-in agent fallback chain does not override the user-specified model. - ...(input.categoryModel ? {} : { agent: input.agentToUse }), + agent: input.agentToUse, system: input.systemContent, tools, parts: [createInternalAgentTextPart(effectivePrompt)],