Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-subagent-selection-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Keep normal subagent model and reasoning defaults when selection fields are omitted or null, and only select overrides on explicit request.
20 changes: 10 additions & 10 deletions packages/opencode/src/kilocode/tool/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,22 @@ const ModelState = z

export namespace KiloTask {
export const ModelFields = {
model: Schema.optional(Schema.String).annotate({
model: Schema.optional(Schema.NullOr(Schema.String)).annotate({
description:
"Optional subagent model name or qualified provider/model ID from agent_manager_models. Choose a model suited to this task; omit to use the normal subagent model.",
"Optional subagent model name or qualified provider/model ID from agent_manager_models. Only set when the user explicitly requests a different model. Omit or send null to keep the normal subagent model.",
}),
provider: Schema.optional(Schema.String).annotate({
provider: Schema.optional(Schema.NullOr(Schema.String)).annotate({
description:
"Optional provider ID from agent_manager_models. Requires model; omit to prefer the current turn's provider, then Kilo Gateway.",
"Optional provider ID from agent_manager_models. Only set when the user explicitly requests a provider. Requires model; omit or send null to prefer the current turn's provider, then Kilo Gateway.",
}),
variant: Schema.optional(Schema.String).annotate({
variant: Schema.optional(Schema.NullOr(Schema.String)).annotate({
description:
"Optional reasoning effort variant from agent_manager_models. Can be used without model to change only the normal subagent model's reasoning effort.",
"Optional reasoning effort override from agent_manager_models. Only set when the user explicitly requests a different reasoning effort. Omit or send null to keep the normal reasoning effort. Can be used without model.",
}),
}

export const modelDescription =
"Experimental subagent model selection is enabled. You may choose model, provider, and variant (reasoning effort) for each task based on its complexity, cost, and latency needs. Use agent_manager_models to find available models, providers, and variants before selecting them; do not guess. This does not create Agent Manager sessions. Omit these fields to keep the normal subagent defaults. Resumed tasks keep their last model and variant unless overridden. A model override does not inherit the parent's reasoning effort; specify variant when a particular effort is needed."
"Experimental subagent model selection is enabled. Omit these fields, or send null, to keep the normal subagent model and reasoning defaults. Only override model, provider, or variant when the user explicitly requests it. Do not choose overrides on your own based on task complexity, cost, or latency. Use agent_manager_models only when an override is requested to find available models, providers, and variants; do not guess names or use model knowledge from training. This does not create Agent Manager sessions. Resumed tasks keep their last model and variant unless overridden. A variant-only override keeps the resolved model. A model override does not inherit the parent's reasoning effort."

/** Reject primary agents used as subagents */
export function validate(info: Agent.Info, name: string) {
Expand Down Expand Up @@ -232,18 +232,18 @@ export namespace KiloTask {
export const resolveModel = Effect.fn("KiloTask.resolveModel")(function* (
input: Parameters<typeof defaults>[0] & {
enabled?: boolean
selection?: { model?: string; provider?: string; variant?: string }
selection?: { model?: string | null; provider?: string | null; variant?: string | null }
resume?: Session.Info["model"]
},
) {
const selection = input.selection ?? {}
const requested = Object.values(selection).some((value) => value !== undefined)
const requested = Object.values(selection).some((value) => value != null)
if (requested && !input.enabled) {
return yield* Effect.fail(
new Error("Task model selection requires experimental.task_model_selection=true in Kilo config"),
)
}
if (requested && Object.values(selection).some((value) => value !== undefined && !value.trim())) {
if (requested && Object.values(selection).some((value) => value != null && !value.trim())) {
return yield* Effect.fail(new Error("Task model, provider, and variant must not be empty when specified"))
}
if (selection.provider && !selection.model) {
Expand Down
65 changes: 49 additions & 16 deletions packages/opencode/test/kilocode/tool-task-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ function run(input: {
variant?: string
config?: Pick<Config.Info, "subagent_model" | "subagent_variant" | "subagent_variant_overrides">
enabled?: boolean
selection?: { model?: string; provider?: string; variant?: string }
selection?: { model?: string | null; provider?: string | null; variant?: string | null }
resume?: Session.Info["model"]
}) {
return provideTmpdirInstance(
Expand Down Expand Up @@ -281,6 +281,9 @@ describe("tool.task model resolution", () => {
{ selection: { model: "SUB model", provider: "sub-provider" }, model: sub, variant: undefined },
{ selection: { variant: overrideVariant }, model: cfg, variant: overrideVariant },
{ selection: {}, model: cfg, variant: cfgVariant },
{ selection: { model: null, provider: null, variant: null }, model: cfg, variant: cfgVariant },
{ selection: { model: "sub-model", provider: null, variant: null }, model: sub, variant: undefined },
{ selection: { model: null, provider: null, variant: overrideVariant }, model: cfg, variant: overrideVariant },
]) {
it.live(`selects ${JSON.stringify(example.selection)} when enabled`, () =>
run({ agent: "pinned", enabled: true, selection: example.selection, variant: inherited }).pipe(
Expand Down Expand Up @@ -322,20 +325,40 @@ describe("tool.task model resolution", () => {
}

for (const enabled of [false, true]) {
it.live(`uses ${enabled ? "persisted" : "normal"} defaults on resume`, () =>
run({
agent: "pinned",
enabled,
resume: { id: sub.modelID, providerID: sub.providerID, variant: subVariant },
}).pipe(
Effect.tap((result) =>
Effect.sync(() => {
expect(result.prompt).toEqual(enabled ? sub : cfg)
expect(result.variant).toEqual(enabled ? subVariant : cfgVariant)
}),
),
),
)
for (const selection of [undefined, { model: null, provider: null, variant: null }]) {
it.live(
`inherits parent model and reasoning with selection ${JSON.stringify(selection)} and enabled ${enabled}`,
() =>
run({ agent: "worker", enabled, selection, variant: inherited }).pipe(
Effect.tap((result) =>
Effect.sync(() => {
expect(result.prompt).toEqual(parent)
expect(result.variant).toEqual(inherited)
expect(result.model).toEqual(parent)
expect(result.metadataVariant).toEqual(inherited)
}),
),
),
)

it.live(
`uses ${enabled ? "persisted" : "normal"} defaults on resume with selection ${JSON.stringify(selection)}`,
() =>
run({
agent: "pinned",
enabled,
selection,
resume: { id: sub.modelID, providerID: sub.providerID, variant: subVariant },
}).pipe(
Effect.tap((result) =>
Effect.sync(() => {
expect(result.prompt).toEqual(enabled ? sub : cfg)
expect(result.variant).toEqual(enabled ? subVariant : cfgVariant)
}),
),
),
)
}
}

it.live("allows a reasoning override on a resumed model", () =>
Expand Down Expand Up @@ -365,11 +388,21 @@ describe("tool.task model resolution", () => {
)
const def = yield* tool.init()
const fields = def.jsonSchema?.properties ?? {}
for (const field of ["model", "provider", "variant"]) expect(field in fields).toBe(enabled === true)
for (const field of ["model", "provider", "variant"]) {
expect(field in fields).toBe(enabled === true)
expect(def.jsonSchema?.required).not.toContain(field)
if (enabled) expect(fields[field]).toMatchObject({ anyOf: [{ type: "string" }, { type: "null" }] })
}
expect("background" in fields).toBe(background)
expect(def.description.includes("Experimental subagent model selection is enabled")).toBe(
enabled === true,
)
if (enabled) {
expect(def.description).toContain(
"Only override model, provider, or variant when the user explicitly requests it",
)
expect(def.description).toContain("Omit these fields, or send null")
}
}),
{ config: { experimental: { task_model_selection: enabled } } },
),
Expand Down
33 changes: 27 additions & 6 deletions packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -377,16 +377,30 @@ exports[`tool parameters JSON Schema (wire shape) task 1`] = `
"type": "string",
},
"model": {
"description": "Optional subagent model name or qualified provider/model ID from agent_manager_models. Choose a model suited to this task; omit to use the normal subagent model.",
"type": "string",
"anyOf": [
{
"type": "string",
},
{
"type": "null",
},
],
"description": "Optional subagent model name or qualified provider/model ID from agent_manager_models. Only set when the user explicitly requests a different model. Omit or send null to keep the normal subagent model.",
},
"prompt": {
"description": "The task for the agent to perform",
"type": "string",
},
"provider": {
"description": "Optional provider ID from agent_manager_models. Requires model; omit to prefer the current turn's provider, then Kilo Gateway.",
"type": "string",
"anyOf": [
{
"type": "string",
},
{
"type": "null",
},
],
"description": "Optional provider ID from agent_manager_models. Only set when the user explicitly requests a provider. Requires model; omit or send null to prefer the current turn's provider, then Kilo Gateway.",
},
"subagent_type": {
"description": "The type of specialized agent to use for this task",
Expand All @@ -397,8 +411,15 @@ exports[`tool parameters JSON Schema (wire shape) task 1`] = `
"type": "string",
},
"variant": {
"description": "Optional reasoning effort variant from agent_manager_models. Can be used without model to change only the normal subagent model's reasoning effort.",
"type": "string",
"anyOf": [
{
"type": "string",
},
{
"type": "null",
},
],
"description": "Optional reasoning effort override from agent_manager_models. Only set when the user explicitly requests a different reasoning effort. Omit or send null to keep the normal reasoning effort. Can be used without model.",
},
},
"required": [
Expand Down
Loading