From 78692a7f2a06d6b30e1b75385888fdb2f823a26a Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 25 Aug 2026 14:46:33 +0200 Subject: [PATCH] fix(agent-manager): allow explicit provider selection --- .changeset/explicit-agent-manager-provider.md | 6 +++ .../kilo-docs/pages/automate/agent-manager.md | 2 +- .../src/kilocode/tool/agent-manager-models.ts | 2 +- .../kilocode/tool/agent-manager-models.txt | 4 +- .../src/kilocode/tool/agent-manager.ts | 29 +++++++++-- .../src/kilocode/tool/agent-manager.txt | 2 +- .../test/kilocode/agent-manager-tool.test.ts | 51 +++++++++++++++++++ 7 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 .changeset/explicit-agent-manager-provider.md diff --git a/.changeset/explicit-agent-manager-provider.md b/.changeset/explicit-agent-manager-provider.md new file mode 100644 index 0000000000..31ed86c6ab --- /dev/null +++ b/.changeset/explicit-agent-manager-provider.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Allow Agent Manager task model overrides to specify an explicit provider when resolving model names. diff --git a/packages/kilo-docs/pages/automate/agent-manager.md b/packages/kilo-docs/pages/automate/agent-manager.md index 70a4dc10db..8c4fa2c233 100644 --- a/packages/kilo-docs/pages/automate/agent-manager.md +++ b/packages/kilo-docs/pages/automate/agent-manager.md @@ -214,7 +214,7 @@ The tool supports two modes: | `worktree` | Creates one Agent Manager git worktree and session per task | | `local` | Creates Agent Manager sessions in the current workspace without git worktree isolation | -Each request can include 1-20 tasks. Each task must include at least one of `prompt`, `name`, or `branchName`. Prompted tasks inherit the model and reasoning variant used by the chat turn that starts them. A task can override that selection with a `model` (by name, e.g. `Claude Opus 4.1`) when you explicitly request a different model, or with one of the current model's reasoning `variant` values when you request a different variant. Agent Manager resolves the provider for a model override, preferring the provider used by the current turn and falling back to the Kilo Gateway; a qualified `provider/model` ID is also accepted to force a specific provider. Prepared sessions without an initial prompt use the normal model defaults. Use `versions: true` only when the tasks are alternate versions of the same work to compare; otherwise, multiple tasks start as independent sessions. +Each request can include 1-20 tasks. Each task must include at least one of `prompt`, `name`, or `branchName`. Prompted tasks inherit the model and reasoning variant used by the chat turn that starts them. A task can override that selection with a `model` (by name, e.g. `Claude Opus 4.1`) when you explicitly request a different model, or with one of the current model's reasoning `variant` values when you request a different variant. Add `provider` beside `model` to force a model-name match to one of the listed provider IDs. Agent Manager resolves the provider for a model override when `provider` is omitted, preferring the provider used by the current turn and falling back to the Kilo Gateway; a qualified `provider/model` ID is also accepted. Prepared sessions without an initial prompt use the normal model defaults. Use `versions: true` only when the tasks are alternate versions of the same work to compare; otherwise, multiple tasks start as independent sessions. The companion `agent_manager_models` tool searches models and their supported reasoning variants on demand. Results are grouped by model name (with the offering providers listed for reference) and limited to 20 per call, so the full catalog is never added to the conversation context. diff --git a/packages/opencode/src/kilocode/tool/agent-manager-models.ts b/packages/opencode/src/kilocode/tool/agent-manager-models.ts index 613489a1c8..d70d697db1 100644 --- a/packages/opencode/src/kilocode/tool/agent-manager-models.ts +++ b/packages/opencode/src/kilocode/tool/agent-manager-models.ts @@ -89,7 +89,7 @@ export const AgentManagerModelsTool = Tool.define< offset, total: matches.length, nextOffset, - hint: "Pass a model name (or one of its providers/IDs) as the agent_manager task `model`. Agent Manager picks the provider, preferring the one used by the current turn.", + hint: "Pass a model name (or one of its providers/IDs) as the agent_manager task `model`. Add the task `provider` to force one of the listed providers; otherwise Agent Manager prefers the provider used by the current turn.", }), metadata: { count: models.length, total: matches.length }, } diff --git a/packages/opencode/src/kilocode/tool/agent-manager-models.txt b/packages/opencode/src/kilocode/tool/agent-manager-models.txt index 8c73797bde..8571d6045d 100644 --- a/packages/opencode/src/kilocode/tool/agent-manager-models.txt +++ b/packages/opencode/src/kilocode/tool/agent-manager-models.txt @@ -1,5 +1,5 @@ Search the models available to Agent Manager sessions and inspect their reasoning variants. -Use this tool before `agent_manager` when you need to pick a model or reasoning effort. Results are grouped by model, not by provider, because you select a model and Agent Manager chooses the provider for you. With no arguments it returns the top available models (capped at 20); pass `query` to search by model name or ID, and `offset` to page further. The query is matched leniently: it is case-insensitive, ignores spacing and punctuation, and is order-independent, so `opus claude`, `glm5.2`, and `gpt5` all work. You do not need the exact model name. +Use this tool before `agent_manager` when you need to pick a model or reasoning effort. Results are grouped by model, not by provider, and list every provider that offers each model so you can constrain the provider when needed. With no arguments it returns the top available models (capped at 20); pass `query` to search by model name or ID, and `offset` to page further. The query is matched leniently: it is case-insensitive, ignores spacing and punctuation, and is order-independent, so `opus claude`, `glm5.2`, and `gpt5` all work. You do not need the exact model name. -Each result includes the model name, its reasoning variant names, and the providers that offer it (informational only). Pass the model name back as the `agent_manager` task `model`. Agent Manager resolves the provider automatically, preferring the provider used by the current turn and falling back to the Kilo Gateway, so you do not need to choose a provider yourself. +Each result includes the model name, its reasoning variant names, and the providers that offer it. Pass the model name back as the `agent_manager` task `model`; pass one of the listed provider IDs as the task `provider` when the provider must be explicit. When `provider` is omitted, Agent Manager resolves it automatically, preferring the one used by the current turn and falling back to the Kilo Gateway. diff --git a/packages/opencode/src/kilocode/tool/agent-manager.ts b/packages/opencode/src/kilocode/tool/agent-manager.ts index 8166f8302e..a1b098d0ff 100644 --- a/packages/opencode/src/kilocode/tool/agent-manager.ts +++ b/packages/opencode/src/kilocode/tool/agent-manager.ts @@ -28,6 +28,10 @@ const Task = Schema.Struct({ description: "Optional model override from agent_manager_models (e.g. 'Claude Opus 4.1'). Omit unless the user requests a different model. Agent Manager otherwise inherits the current turn's model. A qualified provider/model ID is also accepted to force a specific provider.", }), + provider: Schema.optional(Schema.NullOr(Schema.String)).annotate({ + description: + "Optional provider ID to constrain model resolution (e.g. 'anthropic'). Use with model to select a model from a specific provider; omit to use the current-turn provider preference.", + }), variant: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "Optional reasoning variant override from agent_manager_models. Specify it without model to override the inherited model's variant. Omit both to inherit the current turn's selection.", @@ -41,6 +45,9 @@ const Task = Schema.Struct({ Schema.makeFilter((task) => task.model?.trim() && !task.prompt?.trim() ? "A task model requires an initial prompt" : undefined, ), + Schema.makeFilter((task) => + task.provider?.trim() && !task.model?.trim() ? "A task provider requires a model" : undefined, + ), Schema.makeFilter((task) => task.variant?.trim() && !task.prompt?.trim() ? "A task variant requires an initial prompt" : undefined, ), @@ -245,7 +252,9 @@ function select( ...(task.branchName != null ? { branchName: task.branchName } : {}), } const value = task.model?.trim() + const provider = task.provider?.trim() const variant = task.variant?.trim() + if (provider && !value) return { error: `Task ${index + 1} provider requires a model.` } if (!value) { if (!variant) { if (!task.prompt?.trim() || !source) return { task: base } @@ -271,12 +280,21 @@ function select( return { task: { ...base, model: source.model, variant } } } - const { pool, names } = lookup(all, value) + const scope = provider ? all.filter((item) => item.providerID === provider) : all + if (provider && scope.length === 0) { + return { + error: `Task ${index + 1} provider is not available for model selection: ${provider}. Requested model: ${value}.`, + } + } + + const { pool, names } = lookup(scope, value) if (pool.length === 0) { - const close = suggest(all, value) + const close = suggest(scope, value) const hint = close.length ? ` Closest matches: ${close.join(", ")}.` : "" return { - error: `Task ${index + 1} model is not available: ${value}.${hint} Use agent_manager_models to search models.`, + error: provider + ? `Task ${index + 1} model is not available from provider "${provider}": ${value}.${hint} Use agent_manager_models to search models.` + : `Task ${index + 1} model is not available: ${value}.${hint} Use agent_manager_models to search models.`, } } if (names.length > 1) { @@ -479,8 +497,9 @@ export const AgentManagerTool = Tool.define< ...(msg.model.variant ? { variant: msg.model.variant } : {}), } : undefined - const need = params.tasks.some((task) => task.model?.trim() || task.variant?.trim()) - const all = need ? candidates(yield* provider.list()) : [] + const need = params.tasks.some((task) => task.model?.trim() || task.provider?.trim() || task.variant?.trim()) + const providers = need ? yield* provider.list() : undefined + const all = providers ? candidates(providers) : [] const preferred = need ? (source?.model.providerID ?? (yield* provider.defaultModel().pipe( diff --git a/packages/opencode/src/kilocode/tool/agent-manager.txt b/packages/opencode/src/kilocode/tool/agent-manager.txt index 1d081b91df..4dbb410b84 100644 --- a/packages/opencode/src/kilocode/tool/agent-manager.txt +++ b/packages/opencode/src/kilocode/tool/agent-manager.txt @@ -14,7 +14,7 @@ Modes: - `worktree`: creates a new Agent Manager git worktree for each task, like the New Worktree dialog. - `local`: creates Agent Manager sessions in the current workspace directory without git worktree isolation. -Each task may provide a prompt, a short display name, a branch name, a `model`, and a model-specific reasoning `variant`. By default, omit `model` and `variant`: prompted tasks inherit the exact model and reasoning variant used by the current turn. Only specify `model` when the user explicitly asks to use or compare a different model, and only specify `variant` when the user explicitly asks for a different reasoning variant. A variant can be specified without a model to override the inherited model's variant. Never choose a different model merely because work is being fanned out. Specify an override `model` by name (e.g. "Claude Opus 4.1"); the name is matched leniently (case-insensitive, punctuation/spacing-insensitive, order-independent), so an approximate name like "opus 4.1" works and you do not need the exact name. Agent Manager picks the provider for you, preferring the provider used by the current turn and falling back to the Kilo Gateway. A qualified `provider/model` ID is also accepted to force a specific provider. If the name is ambiguous and matches several different models, the tool returns the candidates so you can choose. A model or variant selection requires an initial prompt so the session can persist that selection. Keep display names short because Agent Manager cards are narrow. Branch names are sanitized before worktree creation. Use `agent_manager_models` to search available models and variants on demand instead of guessing or loading the full model catalog. Prepared sessions without an initial prompt use the normal defaults. The agent and base branch settings always use the normal defaults. +Each task may provide a prompt, a short display name, a branch name, a `model`, an optional `provider`, and a model-specific reasoning `variant`. By default, omit `model`, `provider`, and `variant`: prompted tasks inherit the exact model and reasoning variant used by the current turn. Only specify `model` when the user explicitly asks to use or compare a different model, and only specify `variant` when the user explicitly asks for a different reasoning variant. A variant can be specified without a model to override the inherited model's variant. Specify `provider` with `model` to force a model-name match to one provider ID. Never choose a different model merely because work is being fanned out. Specify an override `model` by name (e.g. "Claude Opus 4.1"); the name is matched leniently (case-insensitive, punctuation/spacing-insensitive, order-independent), so an approximate name like "opus 4.1" works and you do not need the exact name. Agent Manager picks the provider for you, preferring the provider used by the current turn and falling back to the Kilo Gateway. A qualified `provider/model` ID is also accepted to force a specific provider. If the name is ambiguous and matches several different models, the tool returns the candidates so you can choose. A model or variant selection requires an initial prompt so the session can persist that selection. Keep display names short because Agent Manager cards are narrow. Branch names are sanitized before worktree creation. Use `agent_manager_models` to search available models and variants on demand instead of guessing or loading the full model catalog. Prepared sessions without an initial prompt use the normal defaults. The agent and base branch settings always use the normal defaults. By default, multiple tasks are started as independent Agent Manager sessions. Set `versions` to true only when all tasks are alternate versions of the same work that should be compared together. Versioned worktrees are grouped in Agent Manager and branch names may receive version suffixes. diff --git a/packages/opencode/test/kilocode/agent-manager-tool.test.ts b/packages/opencode/test/kilocode/agent-manager-tool.test.ts index 69b33d4238..411d2d173f 100644 --- a/packages/opencode/test/kilocode/agent-manager-tool.test.ts +++ b/packages/opencode/test/kilocode/agent-manager-tool.test.ts @@ -229,6 +229,14 @@ describe("agent_manager tool", () => { expect(Schema.is(Params)({ action: "stop", sessionID: "invalid" })).toBe(false) }) + test("validates provider selectors at the task level", () => { + expect(Schema.is(Params)({ mode: "local", tasks: [{ prompt: "Fix", model: "Shared", provider: "kilo" }] })).toBe( + true, + ) + expect(Schema.is(Params)({ mode: "local", tasks: [{ prompt: "Fix", provider: "kilo" }] })).toBe(false) + expect(Schema.is(Params)({ mode: "local", tasks: [{ prompt: "Fix", model: "Shared", provider: 42 }] })).toBe(false) + }) + // Regression for #13029: the OpenAI Responses API forces a value for every // advertised property. With action nullable the model can decline it and the // start request survives; with a populated action the action wins instead. @@ -673,6 +681,12 @@ describe("agent_manager tool", () => { expect(task?.variant).toBe("low") }) + test("uses an explicitly selected provider for a shared model name", async () => { + const task = await publish(runtime, { prompt: "Fix", model: " Shared ", provider: " kilo " }) + expect(String(task?.model?.providerID)).toBe("kilo") + expect(String(task?.model?.modelID)).toBe("kilo/shared") + }) + test("uses the provider of a different default model when that is the user's choice", async () => { const rt = makeRuntime("kilo") const task = await publish(rt, { prompt: "Fix", model: "Shared", variant: "low" }) @@ -717,6 +731,43 @@ describe("agent_manager tool", () => { expect(result.metadata.count).toBe(0) }) + test("reports a model unavailable from an explicit provider", async () => { + const tool = await init() + const calls: unknown[] = [] + + const result = await runtime.runPromise( + provideTmpdirInstance(() => + tool.execute( + { mode: "local", tasks: [{ prompt: "Fix", model: "Reasoning Model", provider: "kilo" }] }, + { ...ctx, ask: (input: unknown) => Effect.sync(() => calls.push(input)) }, + ), + ).pipe(Effect.scoped), + ) + + expect(calls).toEqual([]) + expect(result.output).toContain('model is not available from provider "kilo": Reasoning Model') + expect(result.metadata.count).toBe(0) + }) + + test("rejects an unknown provider without touching inherited object properties", async () => { + const tool = await init() + const calls: unknown[] = [] + + const result = await runtime.runPromise( + provideTmpdirInstance(() => + tool.execute( + { mode: "local", tasks: [{ prompt: "Fix", model: "Shared", provider: "__proto__" }] }, + { ...ctx, ask: (input: unknown) => Effect.sync(() => calls.push(input)) }, + ), + ).pipe(Effect.scoped), + ) + + expect(calls).toEqual([]) + expect(result.output).toContain("provider is not available for model selection: __proto__") + expect(result.output).toContain("Requested model: Shared") + expect(result.metadata.count).toBe(0) + }) + test("echoes how each named model resolved", async () => { const tool = await init() const result = await runtime.runPromise(