From 996b89c6917b792f48aa3c86ae557a47f79a5c61 Mon Sep 17 00:00:00 2001 From: AGSQ11 Date: Tue, 9 Jun 2026 14:20:25 +0300 Subject: [PATCH 01/11] feat(agent-core): add per-role and per-invocation model selection for subagents Add a [subagent_models] config.toml section that maps subagent profile names (coder, explore, plan) to model aliases, so different subagent roles can use different LLM models. Also add a 'model' parameter to the Agent tool so the parent agent can override the model on a per-call basis. Model resolution priority: 1. Per-invocation 'model' override (Agent tool parameter) 2. Role-based [subagent_models] config mapping 3. Parent model (default inheritance) Changes: - schema: add subagentModels field to KimiConfigSchema + patch schema - toml: add transform/serialize for subagent_models section - agent tool: add optional 'model' string parameter to input schema - subagent-host: wire model resolution in configureChild() - agent.md: document model selection behaviour - tests: parse, round-trip, merge, and empty config coverage --- packages/agent-core/src/config/schema.ts | 5 ++ packages/agent-core/src/config/toml.ts | 16 ++++ .../agent-core/src/session/subagent-host.ts | 15 +++- .../src/tools/builtin/collaboration/agent.md | 5 ++ .../src/tools/builtin/collaboration/agent.ts | 7 ++ .../agent-core/test/config/configs.test.ts | 90 +++++++++++++++++++ 6 files changed, 135 insertions(+), 3 deletions(-) diff --git a/packages/agent-core/src/config/schema.ts b/packages/agent-core/src/config/schema.ts index 094239b73..bf867b8ba 100644 --- a/packages/agent-core/src/config/schema.ts +++ b/packages/agent-core/src/config/schema.ts @@ -205,11 +205,15 @@ export const KimiConfigSchema = z.object({ background: BackgroundConfigSchema.optional(), experimental: ExperimentalConfigSchema.optional(), telemetry: z.boolean().optional(), + subagentModels: z.record(z.string(), z.string()).optional(), raw: z.record(z.string(), z.unknown()).optional(), }); export type KimiConfig = z.infer; +/** Maps subagent profile names (coder, explore, plan) to model aliases. */ +export type SubagentModels = Record; + const ProviderConfigPatchSchema = ProviderConfigSchema.partial(); const ModelAliasPatchSchema = ModelAliasSchema.partial(); const ThinkingConfigPatchSchema = ThinkingConfigSchema.partial(); @@ -244,6 +248,7 @@ export const KimiConfigPatchSchema = z background: BackgroundConfigPatchSchema.optional(), experimental: ExperimentalConfigPatchSchema.optional(), telemetry: z.boolean().optional(), + subagentModels: z.record(z.string(), z.string()).optional(), }) .strict(); diff --git a/packages/agent-core/src/config/toml.ts b/packages/agent-core/src/config/toml.ts index 56452e41a..637fe2ef3 100644 --- a/packages/agent-core/src/config/toml.ts +++ b/packages/agent-core/src/config/toml.ts @@ -135,6 +135,10 @@ export function transformTomlData(data: Record): Record — keys are profile names, values are model aliases. + // No per-entry transform needed; just clone the strings through. + result[targetKey] = cloneRecord(value); } else if (!isPlainObject(value)) { result[targetKey] = value; } @@ -307,6 +311,7 @@ export function configToTomlData(config: KimiConfig): Record { setSection(out, 'background', config.background, backgroundToToml); setSection(out, 'experimental', config.experimental, experimentalToToml); setSection(out, 'permission', config.permission, permissionToToml); + setSection(out, 'subagent_models', config.subagentModels, subagentModelsToToml); setHooks(out, config.hooks); return out; @@ -478,6 +483,17 @@ function experimentalToToml( return out; } +function subagentModelsToToml( + subagentModels: Record, + _raw: unknown, +): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(subagentModels)) { + setDefined(out, key, value); + } + return out; +} + function setHooks(out: Record, hooks: readonly HookDefConfig[] | undefined): void { if (hooks === undefined) { delete out['hooks']; diff --git a/packages/agent-core/src/session/subagent-host.ts b/packages/agent-core/src/session/subagent-host.ts index 1848c640a..79e18b2f2 100644 --- a/packages/agent-core/src/session/subagent-host.ts +++ b/packages/agent-core/src/session/subagent-host.ts @@ -81,6 +81,7 @@ export interface RunSubagentOptions { export interface SpawnSubagentOptions extends RunSubagentOptions { readonly profileName: string; + readonly model?: string; readonly swarmItem?: string; } @@ -122,7 +123,7 @@ export class SessionSubagentHost { const completion = this.runWithActiveChild(id, options, async (runOptions) => { this.emitSubagentSpawned(parent, id, profile.name, runOptions); try { - await this.configureChild(parent, agent, profile); + await this.configureChild(parent, agent, profile, options.model); return await this.runPromptTurn(parent, id, agent, profile.name, runOptions); } catch (error) { this.emitSubagentFailed(parent, id, runOptions, error); @@ -354,11 +355,19 @@ export class SessionSubagentHost { parent: Agent, child: Agent, profile: ResolvedAgentProfile, + modelOverride?: string, ): Promise { - // A subagent always inherits the parent agent's model. + // Resolve the effective model for this subagent: + // 1. Per-invocation override (LLM passes model= in the Agent tool call) + // 2. Role-based config ([subagent_models] in config.toml) + // 3. Inherit from parent (default behaviour) + const subagentModels = this.session.options.config?.subagentModels; + const effectiveModel = + modelOverride ?? subagentModels?.[profile.name] ?? parent.config.modelAlias; + child.config.update({ cwd: parent.config.cwd, - modelAlias: parent.config.modelAlias, + ...(effectiveModel !== undefined ? { modelAlias: effectiveModel } : {}), thinkingLevel: parent.config.thinkingLevel, }); diff --git a/packages/agent-core/src/tools/builtin/collaboration/agent.md b/packages/agent-core/src/tools/builtin/collaboration/agent.md index 6ff3f26a4..4ae6ca847 100644 --- a/packages/agent-core/src/tools/builtin/collaboration/agent.md +++ b/packages/agent-core/src/tools/builtin/collaboration/agent.md @@ -6,6 +6,11 @@ Writing the prompt: - Investigations (figure out X, find why Y): give the question, not prescribed steps — fixed steps become dead weight when the premise is wrong. - Do not delegate understanding. If the task hinges on a file path or line number, find it yourself first and write it into the prompt. +Model selection: +- By default, subagents inherit the parent agent's model. +- The user can configure per-role model aliases in config.toml under `[subagent_models]` (e.g., `coder = "gpt-5.2"`). When set, that role always uses the configured model. +- You can also pass `model` to override the model for a single invocation. The value must be a model alias defined in config.toml. This takes highest priority. Only use this when the user explicitly asks you to use a specific model for a task. + Usage notes: - When the task continues earlier work a subagent already did, prefer resuming that agent (pass its `resume` id) over spawning a fresh instance — the resumed agent keeps its prior context. - A subagent's result is only visible to you, not to the user. When the user needs to see what a subagent produced, summarize the relevant parts yourself in your own reply. diff --git a/packages/agent-core/src/tools/builtin/collaboration/agent.ts b/packages/agent-core/src/tools/builtin/collaboration/agent.ts index f5b4d9197..ef75295dd 100644 --- a/packages/agent-core/src/tools/builtin/collaboration/agent.ts +++ b/packages/agent-core/src/tools/builtin/collaboration/agent.ts @@ -75,6 +75,12 @@ export const AgentToolInputSchema = z.preprocess( .string() .optional() .describe('Optional agent ID to resume instead of creating a new instance'), + model: z + .string() + .optional() + .describe( + 'Model alias from config.toml to use for this subagent. Overrides the default model for the subagent role. Only used when spawning a new agent, not when resuming.', + ), run_in_background: z .boolean() .optional() @@ -195,6 +201,7 @@ export class AgentTool implements BuiltinTool { prompt: args.prompt, description: args.description, runInBackground, + model: args.model, signal: backgroundController?.signal ?? foregroundDeadline?.signal ?? signal, }; diff --git a/packages/agent-core/test/config/configs.test.ts b/packages/agent-core/test/config/configs.test.ts index dd3bb2eb8..3bc30a688 100644 --- a/packages/agent-core/test/config/configs.test.ts +++ b/packages/agent-core/test/config/configs.test.ts @@ -661,3 +661,93 @@ describe('config value env override helpers', () => { ).toBe(false); }); }); + +describe('subagentModels config', () => { + it('parses subagent_models from TOML', () => { + const toml = ` +[providers.openai] +type = "openai" +api_key = "sk-test" + +[providers.zhipu] +type = "openai" +api_key = "sk-zhipu" +base_url = "https://open.bigmodel.cn/api/paas/v4" + +[models.gpt-52] +provider = "openai" +model = "gpt-5.2" +max_context_size = 200000 + +[models.glm-47] +provider = "zhipu" +model = "glm-4.7" +max_context_size = 128000 + +[subagent_models] +coder = "gpt-52" +explore = "glm-47" +`; + const config = parseConfigString(toml, 'config.toml'); + + expect(config.subagentModels).toEqual({ + coder: 'gpt-52', + explore: 'glm-47', + }); + }); + + it('round-trips subagent_models through write and read', async () => { + const dir = makeTempDir(); + const configPath = join(dir, 'subagent-models.toml'); + const toml = ` +[providers.openai] +type = "openai" +api_key = "sk-test" + +[models.gpt-52] +provider = "openai" +model = "gpt-5.2" +max_context_size = 200000 + +[subagent_models] +coder = "gpt-52" +`; + await writeFile(configPath, toml, 'utf-8'); + const config = readConfigFile(configPath); + expect(config.subagentModels).toEqual({ coder: 'gpt-52' }); + + await writeConfigFile(configPath, config); + const reloaded = readConfigFile(configPath); + expect(reloaded.subagentModels).toEqual({ coder: 'gpt-52' }); + }); + + it('merges subagentModels via patch', () => { + const base = parseConfigString(` +[providers.openai] +type = "openai" +api_key = "sk-test" + +[models.gpt-52] +provider = "openai" +model = "gpt-5.2" +max_context_size = 200000 + +[subagent_models] +coder = "gpt-52" +`); + + const merged = mergeConfigPatch(base, { + subagentModels: { coder: 'glm-47', explore: 'gpt-52' }, + }); + + expect(merged.subagentModels).toEqual({ + coder: 'glm-47', + explore: 'gpt-52', + }); + }); + + it('allows undefined/empty subagent_models', () => { + const config = parseConfigString(''); + expect(config.subagentModels).toBeUndefined(); + }); +}); From 87a4590c1991d7a6f55c12abf7157ccd6f05e16a Mon Sep 17 00:00:00 2001 From: AGSQ11 Date: Tue, 9 Jun 2026 14:36:10 +0300 Subject: [PATCH 02/11] fix(agent-core): preserve subagent model through retry and resume paths retry() was resetting the child's model to parent.config.modelAlias, losing any [subagent_models] config that was applied during the initial spawn. Now retry() preserves the child's existing model since it is an automatic retry of the same agent instance. resume() now re-resolves the model using [subagent_models] config so that role-based model settings survive across resume boundaries. --- packages/agent-core/src/session/subagent-host.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/agent-core/src/session/subagent-host.ts b/packages/agent-core/src/session/subagent-host.ts index 79e18b2f2..1009d0dce 100644 --- a/packages/agent-core/src/session/subagent-host.ts +++ b/packages/agent-core/src/session/subagent-host.ts @@ -144,7 +144,14 @@ export class SessionSubagentHost { const completion = this.runWithActiveChild(agentId, options, async (runOptions) => { this.emitSubagentSpawned(parent, agentId, profileName, runOptions); try { - child.config.update({ modelAlias: parent.config.modelAlias }); + // Re-resolve the model using the same 3-tier priority as configureChild: + // [subagent_models] config → parent model. The per-invocation override + // does not survive across resume boundaries (no model param on resume). + const subagentModels = this.session.options.config?.subagentModels; + const effectiveModel = subagentModels?.[profileName] ?? parent.config.modelAlias; + child.config.update({ + ...(effectiveModel !== undefined ? { modelAlias: effectiveModel } : {}), + }); return await this.runPromptTurn(parent, agentId, child, profileName, runOptions); } catch (error) { this.emitSubagentFailed(parent, agentId, runOptions, error); @@ -160,7 +167,9 @@ export class SessionSubagentHost { const completion = this.runWithActiveChild(agentId, options, async (runOptions) => { try { runOptions.signal.throwIfAborted(); - child.config.update({ modelAlias: parent.config.modelAlias }); + // Preserve the child's current model — it was already resolved during the + // initial spawn (via [subagent_models] config or per-invocation override). + // Resetting to parent.config.modelAlias would lose a role-based model. this.emitSubagentStarted(parent, agentId); const turnId = child.turn.retry('agent-host'); if (turnId === null) { From 17fde535dc0a874ce1080639b1687e6d7b555eb0 Mon Sep 17 00:00:00 2001 From: AGSQ11 Date: Tue, 9 Jun 2026 15:02:47 +0300 Subject: [PATCH 03/11] feat(agent-core,tui): display model alias in background agent terminal output Thread the effective model alias through the subagent.spawned event so that the TUI can render it alongside the agent description. Also refactors model resolution into a single resolveSubagentModel() helper used consistently across spawn(), resume(), and retry() paths. --- .../tui/controllers/subagent-event-handler.ts | 1 + apps/kimi-code/src/tui/types.ts | 1 + .../src/tui/utils/background-agent-status.ts | 3 +- packages/agent-core/src/rpc/events.ts | 2 ++ .../agent-core/src/session/subagent-host.ts | 34 +++++++++---------- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/apps/kimi-code/src/tui/controllers/subagent-event-handler.ts b/apps/kimi-code/src/tui/controllers/subagent-event-handler.ts index 1fb2d71c5..321bd1881 100644 --- a/apps/kimi-code/src/tui/controllers/subagent-event-handler.ts +++ b/apps/kimi-code/src/tui/controllers/subagent-event-handler.ts @@ -361,6 +361,7 @@ export class SubAgentEventHandler { parentToolCallId: event.parentToolCallId, agentName: event.subagentName, description: typeof description === 'string' ? description : undefined, + modelAlias: event.modelAlias, }; } diff --git a/apps/kimi-code/src/tui/types.ts b/apps/kimi-code/src/tui/types.ts index 8bf127096..363a13a22 100644 --- a/apps/kimi-code/src/tui/types.ts +++ b/apps/kimi-code/src/tui/types.ts @@ -85,6 +85,7 @@ export interface BackgroundAgentMetadata { readonly parentToolCallId: string; readonly agentName?: string; readonly description?: string; + readonly modelAlias?: string; } export type BackgroundAgentStatusPhase = 'started' | 'completed' | 'failed'; diff --git a/apps/kimi-code/src/tui/utils/background-agent-status.ts b/apps/kimi-code/src/tui/utils/background-agent-status.ts index a54257a97..a2668ef41 100644 --- a/apps/kimi-code/src/tui/utils/background-agent-status.ts +++ b/apps/kimi-code/src/tui/utils/background-agent-status.ts @@ -28,7 +28,8 @@ export function formatBackgroundAgentTranscript( ? `${subject} completed in background` : `${subject} failed in background`; const tail = phase === 'failed' ? normalizeBackgroundField(extras?.error) : undefined; - const detailParts = [normalizeBackgroundField(meta.description), tail].filter( + const modelPart = meta.modelAlias !== undefined ? `model: ${meta.modelAlias}` : undefined; + const detailParts = [normalizeBackgroundField(meta.description), modelPart, tail].filter( (part): part is string => part !== undefined, ); diff --git a/packages/agent-core/src/rpc/events.ts b/packages/agent-core/src/rpc/events.ts index 69633d004..d4dd0e99c 100644 --- a/packages/agent-core/src/rpc/events.ts +++ b/packages/agent-core/src/rpc/events.ts @@ -211,6 +211,8 @@ export interface SubagentSpawnedEvent { readonly description?: string | undefined; readonly swarmIndex?: number; readonly runInBackground: boolean; + /** Model alias actually used for this subagent (after [subagent_models] resolution). */ + readonly modelAlias?: string | undefined; } export interface SubagentStartedEvent { diff --git a/packages/agent-core/src/session/subagent-host.ts b/packages/agent-core/src/session/subagent-host.ts index 1009d0dce..b5e6ab590 100644 --- a/packages/agent-core/src/session/subagent-host.ts +++ b/packages/agent-core/src/session/subagent-host.ts @@ -120,10 +120,11 @@ export class SessionSubagentHost { { type: 'sub', generate: parent.rawGenerate }, { parentAgentId: this.ownerAgentId, swarmItem: options.swarmItem }, ); + const effectiveModel = this.resolveSubagentModel(parent, profile.name, options.model); const completion = this.runWithActiveChild(id, options, async (runOptions) => { - this.emitSubagentSpawned(parent, id, profile.name, runOptions); + this.emitSubagentSpawned(parent, id, profile.name, runOptions, effectiveModel); try { - await this.configureChild(parent, agent, profile, options.model); + await this.configureChild(parent, agent, profile, effectiveModel); return await this.runPromptTurn(parent, id, agent, profile.name, runOptions); } catch (error) { this.emitSubagentFailed(parent, id, runOptions, error); @@ -141,14 +142,10 @@ export class SessionSubagentHost { async resume(agentId: string, options: RunSubagentOptions): Promise { options.signal.throwIfAborted(); const { parent, child, profileName } = await this.ensureIdleSubagent(agentId); + const effectiveModel = this.resolveSubagentModel(parent, profileName); const completion = this.runWithActiveChild(agentId, options, async (runOptions) => { - this.emitSubagentSpawned(parent, agentId, profileName, runOptions); + this.emitSubagentSpawned(parent, agentId, profileName, runOptions, effectiveModel); try { - // Re-resolve the model using the same 3-tier priority as configureChild: - // [subagent_models] config → parent model. The per-invocation override - // does not survive across resume boundaries (no model param on resume). - const subagentModels = this.session.options.config?.subagentModels; - const effectiveModel = subagentModels?.[profileName] ?? parent.config.modelAlias; child.config.update({ ...(effectiveModel !== undefined ? { modelAlias: effectiveModel } : {}), }); @@ -364,16 +361,8 @@ export class SessionSubagentHost { parent: Agent, child: Agent, profile: ResolvedAgentProfile, - modelOverride?: string, + effectiveModel?: string, ): Promise { - // Resolve the effective model for this subagent: - // 1. Per-invocation override (LLM passes model= in the Agent tool call) - // 2. Role-based config ([subagent_models] in config.toml) - // 3. Inherit from parent (default behaviour) - const subagentModels = this.session.options.config?.subagentModels; - const effectiveModel = - modelOverride ?? subagentModels?.[profile.name] ?? parent.config.modelAlias; - child.config.update({ cwd: parent.config.cwd, ...(effectiveModel !== undefined ? { modelAlias: effectiveModel } : {}), @@ -427,11 +416,21 @@ export class SessionSubagentHost { .catch(() => {}); } + private resolveSubagentModel( + parent: Agent, + profileName: string, + modelOverride?: string, + ): string | undefined { + const subagentModels = this.session.options.config?.subagentModels; + return modelOverride ?? subagentModels?.[profileName] ?? parent.config.modelAlias; + } + private emitSubagentSpawned( parent: Agent, childId: string, profileName: string, options: RunSubagentOptions, + modelAlias?: string, ): void { parent.emitEvent({ type: 'subagent.spawned', @@ -443,6 +442,7 @@ export class SessionSubagentHost { description: options.description, swarmIndex: options.swarmIndex, runInBackground: options.runInBackground, + modelAlias, }); parent.telemetry.track('subagent_created', { subagent_name: profileName, From e1cb9b4ce482109ddfd9bca4643690668a05b148 Mon Sep 17 00:00:00 2001 From: AGSQ11 Date: Tue, 9 Jun 2026 18:38:20 +0300 Subject: [PATCH 04/11] fix(agent-core): disable thinking on subagents when target model lacks support When a subagent is configured with a different model from its parent, check the target model's capabilities. If the model does not support thinking/reasoning, reset thinkingLevel to 'off' instead of blindly inheriting the parent's level. This prevents API errors like: Model grok-build-0.1 does not support parameter reasoningEffort. The check is best-effort: if provider resolution fails, we fall back to the parent's thinking level and let the first request fail with the usual configuration error. --- .../agent-core/src/session/subagent-host.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/agent-core/src/session/subagent-host.ts b/packages/agent-core/src/session/subagent-host.ts index b5e6ab590..8a1f3fb35 100644 --- a/packages/agent-core/src/session/subagent-host.ts +++ b/packages/agent-core/src/session/subagent-host.ts @@ -363,10 +363,30 @@ export class SessionSubagentHost { profile: ResolvedAgentProfile, effectiveModel?: string, ): Promise { + const targetModel = effectiveModel ?? parent.config.modelAlias; + let thinkingLevel = parent.config.thinkingLevel; + + // If the subagent is using a different model, check whether that model + // actually supports thinking/reasoning. Inheriting a non-zero thinking + // level from the parent will cause API errors on models that do not + // expose a reasoning/thinking parameter (e.g. grok-build-0.1). + if (targetModel !== undefined && targetModel !== parent.config.modelAlias) { + try { + const resolved = this.session.options.providerManager?.resolveProviderConfig(targetModel); + if (resolved !== undefined && !resolved.modelCapabilities.thinking) { + thinkingLevel = 'off'; + } + } catch { + // If the provider manager cannot resolve the model here, fall back + // to the parent's thinking level and let the first request fail + // loudly with the usual configuration error. + } + } + child.config.update({ cwd: parent.config.cwd, ...(effectiveModel !== undefined ? { modelAlias: effectiveModel } : {}), - thinkingLevel: parent.config.thinkingLevel, + thinkingLevel, }); const context = await prepareSystemPromptContext( From dfaf5f98d9b633cf01c8931fd734d42574bd690c Mon Sep 17 00:00:00 2001 From: AGSQ11 Date: Tue, 9 Jun 2026 18:43:57 +0300 Subject: [PATCH 05/11] chore: add changeset for subagent model selection --- .changeset/subagent-model-selection.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/subagent-model-selection.md diff --git a/.changeset/subagent-model-selection.md b/.changeset/subagent-model-selection.md new file mode 100644 index 000000000..f140a60d8 --- /dev/null +++ b/.changeset/subagent-model-selection.md @@ -0,0 +1,13 @@ +--- +"@moonshot-ai/agent-core": minor +"@moonshot-ai/kimi-code": minor +--- + +Add per-role and per-invocation model selection for subagents. + +A new `[subagent_models]` config.toml section maps subagent profile names +to model aliases so different roles (coder, explore, plan) can use +different LLM models. The Agent tool also accepts an optional `model` +parameter to override the model for a single invocation. When a subagent +uses a model that does not support thinking, the thinking level is +automatically disabled to avoid API errors. From 6225a318290f70d68279142e5203a084516c3462 Mon Sep 17 00:00:00 2001 From: AGSQ11 Date: Tue, 9 Jun 2026 18:47:44 +0300 Subject: [PATCH 06/11] docs: document [subagent_models] config and Agent tool model parameter Add section to config-files.md in both English and Chinese, explaining the 3-tier model resolution priority. Update the Agent tool description in tools.md to mention the optional parameter for per-invocation overrides. --- docs/en/configuration/config-files.md | 21 +++++++++++++++++++++ docs/en/reference/tools.md | 2 +- docs/zh/configuration/config-files.md | 21 +++++++++++++++++++++ docs/zh/reference/tools.md | 2 +- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/docs/en/configuration/config-files.md b/docs/en/configuration/config-files.md index 7cf622736..4ff60ae75 100644 --- a/docs/en/configuration/config-files.md +++ b/docs/en/configuration/config-files.md @@ -84,6 +84,7 @@ Fields in the config file fall into two categories: **top-level scalars** that d | `telemetry` | `boolean` | `true` | Whether anonymous telemetry is enabled; disabled only when explicitly set to `false` | | `providers` | `table` | `{}` | API provider table → [`providers`](#providers) | | `models` | `table` | — | Model alias table → [`models`](#models) | +| `subagent_models` | `table` | — | Subagent role-to-model mapping → [`subagent_models`](#subagent_models) | | `thinking` | `table` | — | Default parameters for Thinking mode → [`thinking`](#thinking) | | `loop_control` | `table` | — | Agent loop control parameters → [`loop_control`](#loop_control) | | `background` | `table` | — | Background task runtime parameters → [`background`](#background) | @@ -143,6 +144,26 @@ max_context_size = 1047576 You can also switch models temporarily without touching the config file — by setting `KIMI_MODEL_*` environment variables, the CLI synthesizes a temporary provider in memory that does not persist after restart. See [Define a model from environment variables](./env-vars.md#define-a-model-from-environment-variables-kimi_model). +## `subagent_models` + +`subagent_models` maps subagent profile names to model aliases, so different roles can use different LLMs. When a subagent is spawned, the model is resolved in this priority order: + +1. Per-invocation `model` parameter on the `Agent` tool (if the parent agent explicitly requests a model) +2. Role-based mapping in `[subagent_models]` (if the profile name has an entry) +3. Parent agent's model (default inheritance) + +When a subagent uses a model that does not support Thinking, the thinking level is automatically disabled to avoid API errors. + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `` | `string` | No | Model alias to use for the given profile; valid names are `coder`, `explore`, and `plan` | + +```toml +[subagent_models] +coder = "gpt-5.2" +explore = "glm-4.7" +``` + ## `thinking` `thinking` sets the global default behavior for Thinking mode. `mode = "off"` forces Thinking off even when the top-level `default_thinking = true`. diff --git a/docs/en/reference/tools.md b/docs/en/reference/tools.md index 0856ff19e..a15d8da54 100644 --- a/docs/en/reference/tools.md +++ b/docs/en/reference/tools.md @@ -89,7 +89,7 @@ Collaboration tools handle inter-Agent coordination, user interaction, and Skill | `AskUserQuestion` | Auto-allow | Ask the user a question to gather structured input | | `Skill` | Auto-allow | Invoke a registered inline Skill | -**`Agent`** delegates a subtask to a sub-Agent. Required parameters: `prompt` (complete task description) and `description` (a 3–5 word short summary). Optional parameters: `subagent_type` (defaults to `coder`), `resume` (ID of an existing Agent to resume; mutually exclusive with `subagent_type`), and `run_in_background` (defaults to false). Agent tasks have a fixed 30-minute timeout. In foreground mode the parent Agent waits for the sub-Agent to complete before continuing; in background mode a task ID is returned immediately and the result is automatically delivered back to the main Agent via a synthetic User message when done. When several foreground `Agent` calls run in the same step, the TUI groups them and shows each subagent's running, waiting, completed, or failed status with elapsed time. See [Agent & Sub-Agents](../customization/agents.md) for details. +**`Agent`** delegates a subtask to a sub-Agent. Required parameters: `prompt` (complete task description) and `description` (a 3–5 word short summary). Optional parameters: `subagent_type` (defaults to `coder`), `resume` (ID of an existing Agent to resume; mutually exclusive with `subagent_type`), `run_in_background` (defaults to false), and `model` (a model alias defined in `config.toml` to use for this subagent instead of the inherited model). Agent tasks have a fixed 30-minute timeout. In foreground mode the parent Agent waits for the sub-Agent to complete before continuing; in background mode a task ID is returned immediately and the result is automatically delivered back to the main Agent via a synthetic User message when done. When several foreground `Agent` calls run in the same step, the TUI groups them and shows each subagent's running, waiting, completed, or failed status with elapsed time. See [Agent & Sub-Agents](../customization/agents.md) for details. **`AgentSwarm`** launches subagents from a shared `prompt_template` and an `items` array, resumes existing subagents through `resume_agent_ids`, or combines both in one call. The template must contain the `{{item}}` placeholder; each item replaces that placeholder and launches one new subagent. Pass `subagent_type` to choose the profile used by every spawned subagent in the swarm, or omit it to use `coder`. Without `resume_agent_ids`, the tool requires at least 2 items; with `resume_agent_ids`, it can resume one or more existing subagents. The tool supports up to 128 total subagents, waits for all subagents to finish, and returns an aggregated report. In the TUI, foreground swarms show a live `Agent swarm` progress panel above the input box. In `manual` permission mode, `AgentSwarm` calls outside active swarm mode request approval unless a permission rule allows them; while swarm mode is active, `AgentSwarm` itself is auto-approved. Permission rules match `AgentSwarm` by tool name only — argument patterns such as `AgentSwarm(swarm)` are not supported. diff --git a/docs/zh/configuration/config-files.md b/docs/zh/configuration/config-files.md index 885840245..e0188eb54 100644 --- a/docs/zh/configuration/config-files.md +++ b/docs/zh/configuration/config-files.md @@ -84,6 +84,7 @@ timeout = 5 | `telemetry` | `boolean` | `true` | 是否启用匿名遥测;显式设为 `false` 时关闭 | | `providers` | `table` | `{}` | API 供应商表 → [`providers`](#providers) | | `models` | `table` | — | 模型别名表 → [`models`](#models) | +| `subagent_models` | `table` | — | 子 Agent 角色到模型的映射 → [`subagent_models`](#subagent_models) | | `thinking` | `table` | — | Thinking 模式默认参数 → [`thinking`](#thinking) | | `loop_control` | `table` | — | Agent 循环控制参数 → [`loop_control`](#loop_control) | | `background` | `table` | — | 后台任务运行参数 → [`background`](#background) | @@ -143,6 +144,26 @@ max_context_size = 1047576 无需修改配置文件也可以临时切换模型——通过 `KIMI_MODEL_*` 环境变量在内存里合成一个临时供应商,详见[用环境变量定义模型](./env-vars.md#用环境变量定义模型-kimi-model)。 +## `subagent_models` + +`subagent_models` 将子 Agent profile 名称映射到模型别名,让不同角色可以使用不同的 LLM。子 Agent 启动时,模型按以下优先级解析: + +1. `Agent` 工具的 `model` 参数(父 Agent 显式指定模型时) +2. `[subagent_models]` 中的角色映射(该 profile 有配置项时) +3. 父 Agent 的模型(默认继承) + +当子 Agent 使用的模型不支持 Thinking 时,Thinking 级别会自动关闭,以避免 API 报错。 + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `` | `string` | 否 | 该 profile 使用的模型别名;有效名称包括 `coder`、`explore`、`plan` | + +```toml +[subagent_models] +coder = "gpt-5.2" +explore = "glm-4.7" +``` + ## `thinking` `thinking` 设置 Thinking 模式的全局默认行为。`mode = "off"` 会强制关闭 Thinking,即使顶层 `default_thinking = true` 也不例外。 diff --git a/docs/zh/reference/tools.md b/docs/zh/reference/tools.md index 151ee273c..a8a2630f5 100644 --- a/docs/zh/reference/tools.md +++ b/docs/zh/reference/tools.md @@ -89,7 +89,7 @@ Plan 模式是一种受约束的工作状态:进入后 `Write` 与 `Edit` 只 | `AskUserQuestion` | 自动放行 | 向用户提问以获取结构化输入 | | `Skill` | 自动放行 | 调用已注册的 inline Skill | -**`Agent`** 将子任务委托给子 Agent 执行。必填参数:`prompt`(完整任务描述)和 `description`(3–5 个词的简短说明)。可选参数:`subagent_type`(默认 `coder`)、`resume`(恢复已有 Agent 的 ID,与 `subagent_type` 互斥)和 `run_in_background`(默认 false)。Agent 任务使用固定 30 分钟超时。前台模式下父 Agent 等待子 Agent 完成再继续;后台模式立即返回任务 ID,完成时通过合成 User 消息自动回到主 Agent。多个前台 `Agent` 调用在同一步运行时,TUI 会合并展示,并为每个子 Agent 显示运行、等待、完成或失败状态以及已耗时长。子 Agent 体系细节见 [Agent 与子 Agent](../customization/agents.md)。 +**`Agent`** 将子任务委托给子 Agent 执行。必填参数:`prompt`(完整任务描述)和 `description`(3–5 个词的简短说明)。可选参数:`subagent_type`(默认 `coder`)、`resume`(恢复已有 Agent 的 ID,与 `subagent_type` 互斥)、`run_in_background`(默认 false)和 `model`(在 `config.toml` 中定义的模型别名,用于代替继承的模型)。Agent 任务使用固定 30 分钟超时。前台模式下父 Agent 等待子 Agent 完成再继续;后台模式立即返回任务 ID,完成时通过合成 User 消息自动回到主 Agent。多个前台 `Agent` 调用在同一步运行时,TUI 会合并展示,并为每个子 Agent 显示运行、等待、完成或失败状态以及已耗时长。子 Agent 体系细节见 [Agent 与子 Agent](../customization/agents.md)。 **`AgentSwarm`** 可以从共享的 `prompt_template` 和 `items` 数组启动子 Agent,也可以通过 `resume_agent_ids` 恢复已有子 Agent,或在一次调用中同时使用两者。模板必须包含 `{{item}}` 占位符;每个 item 会替换该占位符,并启动一个新的子 Agent。传入 `subagent_type` 可以指定整个 swarm 中所有新启动的子 Agent 使用的 profile;省略时默认使用 `coder`。不传 `resume_agent_ids` 时,本工具要求至少 2 个 item;传入 `resume_agent_ids` 时,可以恢复 1 个或多个已有子 Agent。本工具最多支持 128 个子 Agent,会等待全部子 Agent 完成,并返回聚合报告。在 TUI 中,前台 swarm 会在输入框上方显示实时 `Agent swarm` 进度面板。在 `manual` 权限模式下,未处于 swarm mode 时调用 `AgentSwarm` 会触发审批,除非已有权限规则允许;swarm mode 已开启时,`AgentSwarm` 本身会自动放行。权限规则只能按工具名 `AgentSwarm` 匹配,不支持 `AgentSwarm(swarm)` 这类参数模式。 From 88a5f12a74be3f215a44efb8df779d4321e548c0 Mon Sep 17 00:00:00 2001 From: AGSQ11 Date: Tue, 9 Jun 2026 19:26:54 +0300 Subject: [PATCH 07/11] fix(agent-core): default thinking to off when model capabilities cannot be resolved When configureChild() cannot resolve a subagent model's capabilities (catch block), default thinkingLevel to 'off' instead of inheriting the parent's level. This prevents reasoningEffort API errors on models that do not support thinking parameters. --- packages/agent-core/src/session/subagent-host.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/agent-core/src/session/subagent-host.ts b/packages/agent-core/src/session/subagent-host.ts index 8a1f3fb35..c6100aa37 100644 --- a/packages/agent-core/src/session/subagent-host.ts +++ b/packages/agent-core/src/session/subagent-host.ts @@ -377,9 +377,11 @@ export class SessionSubagentHost { thinkingLevel = 'off'; } } catch { - // If the provider manager cannot resolve the model here, fall back - // to the parent's thinking level and let the first request fail - // loudly with the usual configuration error. + // If the provider manager cannot resolve the model here, default to + // 'off' rather than inheriting the parent's thinking level. Sending + // reasoning/thinking params to an unknown model is more likely to + // cause API errors than disabling them. + thinkingLevel = 'off'; } } From e293fbe843a21ce473f80043df7beb8587044621 Mon Sep 17 00:00:00 2001 From: AGSQ11 Date: Tue, 9 Jun 2026 19:39:52 +0300 Subject: [PATCH 08/11] fix(anthropic,agent-core): omit thinking param entirely when disabled When thinking is off, the Anthropic provider previously sent thinking: { type: 'disabled' } to the API. Some Anthropic-compatible backends (e.g. grok) reject any thinking parameter, including disabled. Now withThinking('off') deletes the thinking key from generation kwargs entirely, so the parameter is omitted from the request body. Also in configureChild: when a subagent uses a different model from its parent, default thinkingLevel to 'off' to avoid sending reasoning params to models that don't support them. --- .changeset/subagent-model-selection.md | 1 + .../agent-core/src/session/subagent-host.ts | 21 +++++-------------- packages/kosong/src/providers/anthropic.ts | 2 +- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/.changeset/subagent-model-selection.md b/.changeset/subagent-model-selection.md index f140a60d8..88ce98279 100644 --- a/.changeset/subagent-model-selection.md +++ b/.changeset/subagent-model-selection.md @@ -1,6 +1,7 @@ --- "@moonshot-ai/agent-core": minor "@moonshot-ai/kimi-code": minor +"@moonshot-ai/kosong": patch --- Add per-role and per-invocation model selection for subagents. diff --git a/packages/agent-core/src/session/subagent-host.ts b/packages/agent-core/src/session/subagent-host.ts index c6100aa37..6acb3174a 100644 --- a/packages/agent-core/src/session/subagent-host.ts +++ b/packages/agent-core/src/session/subagent-host.ts @@ -366,23 +366,12 @@ export class SessionSubagentHost { const targetModel = effectiveModel ?? parent.config.modelAlias; let thinkingLevel = parent.config.thinkingLevel; - // If the subagent is using a different model, check whether that model - // actually supports thinking/reasoning. Inheriting a non-zero thinking - // level from the parent will cause API errors on models that do not - // expose a reasoning/thinking parameter (e.g. grok-build-0.1). + // If the subagent is using a different model from its parent, do not + // inherit the parent's thinking/reasoning level. Different models have + // different thinking support (e.g. grok-build-0.1 rejects reasoningEffort), + // and the safest default for a model switch is 'off'. if (targetModel !== undefined && targetModel !== parent.config.modelAlias) { - try { - const resolved = this.session.options.providerManager?.resolveProviderConfig(targetModel); - if (resolved !== undefined && !resolved.modelCapabilities.thinking) { - thinkingLevel = 'off'; - } - } catch { - // If the provider manager cannot resolve the model here, default to - // 'off' rather than inheriting the parent's thinking level. Sending - // reasoning/thinking params to an unknown model is more likely to - // cause API errors than disabling them. - thinkingLevel = 'off'; - } + thinkingLevel = 'off'; } child.config.update({ diff --git a/packages/kosong/src/providers/anthropic.ts b/packages/kosong/src/providers/anthropic.ts index 29c1b3171..5e2cba4ae 100644 --- a/packages/kosong/src/providers/anthropic.ts +++ b/packages/kosong/src/providers/anthropic.ts @@ -1069,10 +1069,10 @@ export class AnthropicChatProvider implements ChatProvider { newBetas = newBetas.filter((b) => b !== INTERLEAVED_THINKING_BETA); } const clone = this._withGenerationKwargs({ - thinking: { type: 'disabled' }, betaFeatures: newBetas, }); delete clone._generationKwargs.output_config; + delete clone._generationKwargs.thinking; return clone; } From ca7a3e76b3739baa7cbaa9c576ec78c9d2b517e3 Mon Sep 17 00:00:00 2001 From: AGSQ11 Date: Fri, 12 Jun 2026 22:40:35 +0300 Subject: [PATCH 09/11] feat: support global and project-level sysprompt.md override --- .changeset/sysprompt-override.md | 6 ++ packages/agent-core/src/agent/index.ts | 16 ++--- packages/agent-core/src/profile/context.ts | 34 ++++++++++- packages/agent-core/test/agent/config.test.ts | 19 ++++++ .../agent-core/test/profile/context.test.ts | 58 ++++++++++++++++++- 5 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 .changeset/sysprompt-override.md diff --git a/.changeset/sysprompt-override.md b/.changeset/sysprompt-override.md new file mode 100644 index 000000000..a47165fb3 --- /dev/null +++ b/.changeset/sysprompt-override.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core": minor +"@moonshot-ai/kimi-code": minor +--- + +Support overriding the system prompt via `~/.kimi-code/sysprompt.md` or a project-level `.kimi-code/sysprompt.md` file. diff --git a/packages/agent-core/src/agent/index.ts b/packages/agent-core/src/agent/index.ts index 1b90276f9..3df8940f7 100644 --- a/packages/agent-core/src/agent/index.ts +++ b/packages/agent-core/src/agent/index.ts @@ -283,13 +283,15 @@ export class Agent { } useProfile(profile: ResolvedAgentProfile, context?: PreparedSystemPromptContext): void { - const systemPrompt = profile.systemPrompt({ - osEnv: this.kaos.osEnv, - cwd: this.config.cwd, - skills: this.skills?.registry, - cwdListing: context?.cwdListing, - agentsMd: context?.agentsMd, - }); + const systemPrompt = + context?.systemPromptOverride ?? + profile.systemPrompt({ + osEnv: this.kaos.osEnv, + cwd: this.config.cwd, + skills: this.skills?.registry, + cwdListing: context?.cwdListing, + agentsMd: context?.agentsMd, + }); this.config.update({ profileName: profile.name, systemPrompt }); this.tools.setActiveTools(profile.tools); } diff --git a/packages/agent-core/src/profile/context.ts b/packages/agent-core/src/profile/context.ts index e03b8469c..c4319fa8a 100644 --- a/packages/agent-core/src/profile/context.ts +++ b/packages/agent-core/src/profile/context.ts @@ -9,17 +9,45 @@ const AGENTS_MD_MAX_BYTES = 32 * 1024; const S_IFMT = 0o170000; const S_IFREG = 0o100000; -export type PreparedSystemPromptContext = Pick; +export type PreparedSystemPromptContext = Pick & { + systemPromptOverride?: string | undefined; +}; export async function prepareSystemPromptContext( kaos: Kaos, brandHome?: string, ): Promise { - const [cwdListing, agentsMd] = await Promise.all([ + const [cwdListing, agentsMd, systemPromptOverride] = await Promise.all([ listDirectory(kaos), loadAgentsMd(kaos, brandHome), + loadSystemPromptOverride(kaos, brandHome), ]); - return { cwdListing, agentsMd }; + return { cwdListing, agentsMd, systemPromptOverride }; +} + +/** + * Load a user-provided system prompt override. + * + * Looks for `.kimi-code/sysprompt.md` in the current working directory first, + * then falls back to `/sysprompt.md` (default `~/.kimi-code/sysprompt.md`). + * A project-level file fully replaces a global file; if neither exists the + * agent falls back to the profile's rendered system prompt. + */ +export async function loadSystemPromptOverride( + kaos: Kaos, + brandHome?: string, +): Promise { + const workDir = kaos.getcwd(); + const realHome = kaos.gethome(); + const brandDir = brandHome ?? join(realHome, '.kimi-code'); + + const project = await readAgentFile(kaos, join(workDir, '.kimi-code', 'sysprompt.md')); + if (project !== undefined) return project.content; + + const global = await readAgentFile(kaos, join(brandDir, 'sysprompt.md')); + if (global !== undefined) return global.content; + + return undefined; } export async function loadAgentsMd(kaos: Kaos, brandHome?: string): Promise { diff --git a/packages/agent-core/test/agent/config.test.ts b/packages/agent-core/test/agent/config.test.ts index 5fc9edf88..be95944cc 100644 --- a/packages/agent-core/test/agent/config.test.ts +++ b/packages/agent-core/test/agent/config.test.ts @@ -82,6 +82,25 @@ describe('Agent config', () => { await ctx.expectResumeMatches(); }); + it('useProfile uses a system prompt override when one is provided', async () => { + const ctx = testAgent(); + ctx.configure(); + const profile: ResolvedAgentProfile = { + name: 'test-profile', + systemPrompt: () => 'Profile system prompt.', + tools: ['Bash'], + }; + + ctx.agent.useProfile(profile, { systemPromptOverride: 'Override system prompt.' }); + + expect(ctx.newEvents()).toMatchInlineSnapshot(` + [wire] config.update { "profileName": "test-profile", "systemPrompt": "Override system prompt.", "time": "