Skip to content
Open
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
14 changes: 14 additions & 0 deletions .changeset/subagent-model-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@moonshot-ai/agent-core": minor
"@moonshot-ai/kimi-code": minor
"@moonshot-ai/kosong": patch
---

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.
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ export class SubAgentEventHandler {
parentToolCallId: event.parentToolCallId,
agentName: event.subagentName,
description: typeof description === 'string' ? description : undefined,
modelAlias: event.modelAlias,
};
}

Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/tui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 2 additions & 1 deletion apps/kimi-code/src/tui/utils/background-agent-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);

Expand Down
21 changes: 21 additions & 0 deletions docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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 |
| --- | --- | --- | --- |
| `<profile_name>` | `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`.
Expand Down
2 changes: 1 addition & 1 deletion docs/en/reference/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
21 changes: 21 additions & 0 deletions docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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 报错。

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `<profile_name>` | `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` 也不例外。
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/reference/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)` 这类参数模式。

Expand Down
5 changes: 5 additions & 0 deletions packages/agent-core/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof KimiConfigSchema>;

/** Maps subagent profile names (coder, explore, plan) to model aliases. */
export type SubagentModels = Record<string, string>;

const ProviderConfigPatchSchema = ProviderConfigSchema.partial();
const ModelAliasPatchSchema = ModelAliasSchema.partial();
const ThinkingConfigPatchSchema = ThinkingConfigSchema.partial();
Expand Down Expand Up @@ -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();

Expand Down
16 changes: 16 additions & 0 deletions packages/agent-core/src/config/toml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ export function transformTomlData(data: Record<string, unknown>): Record<string,
result[targetKey] = transformPlainObject(value);
} else if (targetKey === 'experimental' && isPlainObject(value)) {
result[targetKey] = cloneRecord(value);
} else if (targetKey === 'subagentModels' && isPlainObject(value)) {
// Record<string, string> — 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;
}
Expand Down Expand Up @@ -307,6 +311,7 @@ export function configToTomlData(config: KimiConfig): Record<string, unknown> {
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;
Expand Down Expand Up @@ -478,6 +483,17 @@ function experimentalToToml(
return out;
}

function subagentModelsToToml(
subagentModels: Record<string, string>,
_raw: unknown,
): Record<string, unknown> {
const out: Record<string, unknown> = {};
for (const [key, value] of Object.entries(subagentModels)) {
setDefined(out, key, value);
}
return out;
}

function setHooks(out: Record<string, unknown>, hooks: readonly HookDefConfig[] | undefined): void {
if (hooks === undefined) {
delete out['hooks'];
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-core/src/rpc/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
45 changes: 37 additions & 8 deletions packages/agent-core/src/session/subagent-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export interface RunSubagentOptions {

export interface SpawnSubagentOptions extends RunSubagentOptions {
readonly profileName: string;
readonly model?: string;
readonly swarmItem?: string;
}

Expand Down Expand Up @@ -119,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);
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);
Expand All @@ -140,10 +142,13 @@ export class SessionSubagentHost {
async resume(agentId: string, options: RunSubagentOptions): Promise<SubagentHandle> {
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 {
child.config.update({ modelAlias: 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);
Expand All @@ -159,7 +164,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) {
Expand Down Expand Up @@ -354,12 +361,23 @@ export class SessionSubagentHost {
parent: Agent,
child: Agent,
profile: ResolvedAgentProfile,
effectiveModel?: string,
): Promise<void> {
// A subagent always inherits the parent agent's model.
const targetModel = effectiveModel ?? parent.config.modelAlias;
let thinkingLevel = parent.config.thinkingLevel;

// 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) {
thinkingLevel = 'off';
}

child.config.update({
cwd: parent.config.cwd,
modelAlias: parent.config.modelAlias,
thinkingLevel: parent.config.thinkingLevel,
...(effectiveModel !== undefined ? { modelAlias: effectiveModel } : {}),
thinkingLevel,
});

const context = await prepareSystemPromptContext(
Expand Down Expand Up @@ -409,11 +427,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',
Expand All @@ -425,6 +453,7 @@ export class SessionSubagentHost {
description: options.description,
swarmIndex: options.swarmIndex,
runInBackground: options.runInBackground,
modelAlias,
});
parent.telemetry.track('subagent_created', {
subagent_name: profileName,
Expand Down
Loading