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
2 changes: 1 addition & 1 deletion .agents/skills/agent-core-dev/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ pass `ConfigTarget.Memory` for a per-run override that is never written to disk.
- `src/kosong/model/thinking.ts` (owner domain, not `config`) — the `resolveThinkingEffort` helper and the authoritative `ThinkingConfig` type (the `thinking` section itself registers from `src/app/kosongConfig/configSection.ts`).
- `src/app/config/configPure.ts` — `isPlainObject`, `deepMerge`, `omitUndefined`, `describeUnknownError`.

A domain that owns a section keeps the schema in its own `configSection.ts` (e.g. `src/app/flag/flag.ts` for `experimental`, `src/agent/loop/configSection.ts` for `loopControl`). Exception: kosong-owned sections (`providers`, `models`, `thinking`) — kosong is a pure, persistence-free abstraction layer that defines only the types (`src/kosong/{provider,model}`); the section constants, the zod schemas (re-derived from those types and compile-time pinned via `AssertExact<Equal<z.infer<typeof Schema>, Type>>`, see `_base/utils/typeEquality.ts`), the registrations, env bindings, and TOML transforms all live in the persistence wrapper `src/app/kosongConfig/configSection.ts`. (`modelCatalog` has no kosong-side type at all — its section is fully self-contained in `app/kosongConfig`, types derived from the schema.) A cross-section env overlay (e.g. the `KIMI_MODEL_*` synthesis in `src/app/kosongConfig/envOverlay.ts`) lives in the wrapper too and is registered via module-level `registerConfigOverlay`. The session subagent domain owns two sections in `src/session/subagent/configSection.ts`: `[subagent]` (`timeout_ms` on disk) and `[secondary_model]` (`default_model` plus the `[secondary_model.models]` pool, with a lone legacy v1 `model` key honored as a fallback default below `default_model`); neither carries a cross-section overlay. Cross-field pool validation (default present / in-pool / every key resolvable) runs at session creation in `subagentModelsValidationService.ts`, not in the schema. The two-way sync between config sections and kosong's in-memory registries is owned by `IKosongConfigService` (`src/app/kosongConfig/kosongConfigService.ts`).
A domain that owns a section keeps the schema in its own `configSection.ts` (e.g. `src/app/flag/flag.ts` for `experimental`, `src/agent/loop/configSection.ts` for `loopControl`). Exception: kosong-owned sections (`providers`, `models`, `thinking`) — kosong is a pure, persistence-free abstraction layer that defines only the types (`src/kosong/{provider,model}`); the section constants, the zod schemas (re-derived from those types and compile-time pinned via `AssertExact<Equal<z.infer<typeof Schema>, Type>>`, see `_base/utils/typeEquality.ts`), the registrations, env bindings, and TOML transforms all live in the persistence wrapper `src/app/kosongConfig/configSection.ts`. (`modelCatalog` has no kosong-side type at all — its section is fully self-contained in `app/kosongConfig`, types derived from the schema.) A cross-section env overlay (e.g. the `KIMI_MODEL_*` synthesis in `src/app/kosongConfig/envOverlay.ts`) lives in the wrapper too and is registered via module-level `registerConfigOverlay`. The session subagent domain owns two sections in `src/session/subagent/configSection.ts`: `[subagent]` (`timeout_ms` on disk) and `[secondary_model]` (`default_model` plus the `[secondary_model.models]` pool, with a lone legacy v1 `model` key honored as a fallback default below `default_model`); neither carries a cross-section overlay. Pool problems (default missing / not in-pool / unresolvable key) never block session creation: `resolveEffectiveSubagentModelPool` in the same `configSection.ts` filters broken entries, falls back to the first surviving entry, and reports issues that surface as startup warnings; a spawn that still binds a broken model fails lazily at spawn time. The two-way sync between config sections and kosong's in-memory registries is owned by `IKosongConfigService` (`src/app/kosongConfig/kosongConfigService.ts`).

## Scope

Expand Down
5 changes: 5 additions & 0 deletions .changeset/secondary-model-pool-soft-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix sessions failing to open when the subagent model pool contains an entry that no longer resolves to a configured model.
6 changes: 4 additions & 2 deletions docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ Constraints between the fields:
- `default_effort` is section-wide: every spawn binds it regardless of the chosen pool entry (or the forced model). For per-entry efforts, leave it unset and use model variants (see below).
- `primary` is a reserved alias (see below) and cannot be a pool key.

Pool aliases reference the current `[models]` table: if a provider is later deleted or logged out, or its refreshed model list no longer contains an alias, session startup fails with a configuration error naming the broken alias — fix or remove the entry to recover. The `[secondary_model]` section itself is never rewritten automatically.
Pool aliases reference the current `[models]` table: if a provider is later deleted or logged out, or its refreshed model list no longer contains an alias, the broken entry is skipped instead of blocking startup — the session opens with a warning naming the alias, and the pool carries on with the remaining entries. When `default_model` itself breaks, the first remaining entry becomes the effective default; when no entry survives, subagents inherit the caller's model. Only a spawn that still ends up bound to a broken model — an explicit `model` request naming it, or a `force` pin — fails with an error naming the entry. The `[secondary_model]` section itself is never rewritten automatically.

In the interactive TUI, the [`/secondary-model`](../reference/slash-commands.md) command (alias `/subagent-model`) opens a model selector: the choice is written to `default_model` (when a models table exists and the picked alias is not in it, an entry with an empty description is added), and newly spawned subagents pick up the new default immediately — no session restart needed.

Expand Down Expand Up @@ -293,10 +293,12 @@ Two prerequisites:
Note the asymmetry between the main agent and pool-bound subagents: for the main agent, a configured global `[thinking].effort` overrides the variant's `default_effort`; for subagents the variant's `default_effort` wins over the global value, and only `[secondary_model].default_effort` outranks it. Value and fallback rules follow the [`[models]` entry's `default_effort`](#models).

::: warning Note
Configuration errors fail loudly instead of falling back silently. Session creation, resume, and fork all fail at startup when:
Pool problems never block session startup. Session creation, resume, and fork succeed with a startup warning when:

- `default_model` is missing, is not a pool key, or a pool key does not resolve to a configured [`[models]`](#models) entry;
- `force` is set without `default_model`, or combined with a `models` table.

Unresolvable entries are skipped: they disappear from the model choices advertised to the main agent, and a broken `default_model` falls back to the first remaining entry. Only a spawn that still binds a broken model — an explicit `model` request naming it, or a `force` pin — fails with an error naming the entry.
:::

## `thinking`
Expand Down
6 changes: 4 additions & 2 deletions docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ default_model = "kimi-code/kimi-for-coding-highspeed"
- `default_effort` 是节级设置:无论派生绑定到池中哪个条目(或 force 固定的模型)都生效。想按条目区分档位时不要设置它,改用下文的模型「变体」。
- `primary` 是保留字(含义见下文),不能作为池中 key。

池别名引用的是 `[models]` 表的当前内容:如果之后删除供应商、登出账号,或其刷新后的模型列表不再包含某个别名,会话启动时会报出指明失效别名的配置错误,修正或移除对应条目即可恢复。系统不会自动改写 `[secondary_model]` 节。
池别名引用的是 `[models]` 表的当前内容:如果之后删除供应商、登出账号,或其刷新后的模型列表不再包含某个别名,失效条目会被跳过而不是阻塞启动——会话照常打开并给出指明该别名的警告,池继续以剩余条目工作。当 `default_model` 本身失效时,剩余的第一个条目成为实际默认值;当所有条目都失效时,subagent 继承调用方的模型。只有仍然绑定到失效模型的派生——显式以 `model` 参数指定它,或 `force` 固定——才会报错并指明该条目。系统不会自动改写 `[secondary_model]` 节。

在交互式 TUI 中,也可以用 [`/secondary-model`](../reference/slash-commands.md) 命令(别名 `/subagent-model`)打开模型选择器:选择后写入 `default_model`(已有 models 表而所选别名不在其中时,会一并补一条空描述条目),之后派生的 subagent 立即按新默认值绑定,无需重启会话。

Expand Down Expand Up @@ -292,10 +292,12 @@ k3-max = "同一模型的 max Thinking 档位。适合最难的子任务。"
另外注意 main agent 与 subagent 的不对称:对 main agent,全局 `[thinking].effort` 一旦设置就压过变体的 `default_effort`;对绑定池内别名的 subagent,变体的 `default_effort` 优先于全局值,只有 `[secondary_model].default_effort` 的优先级更高。取值与回落规则同 [`[models]` 条目的 `default_effort`](#models)。

::: warning 注意
配置错误一律直接报错,不做静默回退。出现以下情况时,会话的创建、恢复(resume)与 fork 都会在启动时失败
池配置问题不会阻塞会话启动。出现以下情况时,会话的创建、恢复(resume)与 fork 都会正常进行,并给出启动警告

- `default_model` 缺失、不是池中 key,或池中 key 无法解析到已配置的 [`[models]`](#models) 条目;
- `force` 未搭配 `default_model`,或与 `models` 表同时使用。

无法解析的条目会被跳过:它们不再出现在提供给 main agent 的模型选项中,`default_model` 失效时回落到剩余的第一个条目。只有仍然绑定到失效模型的派生——显式以 `model` 参数指定它,或 `force` 固定——才会报错并指明该条目。
:::

## `thinking`
Expand Down
5 changes: 4 additions & 1 deletion packages/agent-core-v2/src/agent/tools/agent/agentTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
import { ILogService } from '#/_base/log/log';
import { IConfigService } from '#/app/config/config';
import { IFlagService } from '#/app/flag/flag';
import { IModelCatalog } from '#/kosong/model/catalog';
import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle';
import { isSubagentMeta, subagentLabels, subagentParentAgentId } from '#/session/agentLifecycle/subagentMetadata';
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
Expand Down Expand Up @@ -84,7 +85,7 @@ export class SubagentTool implements ISubagentTool {
readonly name: string = 'Agent';

get parameters(): Record<string, unknown> {
const parameters = exposesSubagentModelChoice(this.config, this.flags)
const parameters = exposesSubagentModelChoice(this.config, this.flags, this.modelCatalog)
? SUBAGENT_TOOL_PARAMETERS
: SUBAGENT_TOOL_PARAMETERS_NO_MODEL;
return this.flags.enabled(SUBAGENT_FORK_FLAG_ID)
Expand All @@ -110,6 +111,7 @@ export class SubagentTool implements ISubagentTool {
@ILogService private readonly log: ILogService,
@IConfigService private readonly config: IConfigService,
@IFlagService private readonly flags: IFlagService,
@IModelCatalog private readonly modelCatalog: IModelCatalog,
@AgentToolContribution private readonly contributions: CollectionView<AgentToolContribution>,
) {
this.callerAgentId = scopeContext.agentId;
Expand Down Expand Up @@ -150,6 +152,7 @@ export class SubagentTool implements ISubagentTool {
this.config,
this.flags,
this.profile.data().modelAlias,
this.modelCatalog,
);
if (modelLines !== undefined) {
description += `\n\n${modelLines}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Error2, ErrorCodes } from '#/errors';
import { toInputJsonSchema } from '#/tool/input-schema';
import { IConfigService } from '#/app/config/config';
import { IFlagService } from '#/app/flag/flag';
import { IModelCatalog } from '#/kosong/model/catalog';
import { ISessionSwarmService, type SessionSwarmTask } from '#/features/swarm/session/sessionSwarm';
import { IAgentProfileService } from '#/agent/profile/profile';
import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext';
Expand Down Expand Up @@ -73,7 +74,7 @@ export class AgentSwarmTool implements IAgentSwarmTool {
readonly name = 'AgentSwarm' as const;

get parameters(): Record<string, unknown> {
const parameters = exposesSubagentModelChoice(this.config, this.flags)
const parameters = exposesSubagentModelChoice(this.config, this.flags, this.modelCatalog)
? AGENT_SWARM_PARAMETERS
: AGENT_SWARM_PARAMETERS_NO_MODEL;
return this.flags.enabled(SUBAGENT_FORK_FLAG_ID)
Expand All @@ -89,6 +90,7 @@ export class AgentSwarmTool implements IAgentSwarmTool {
@IAgentSwarmService private readonly swarmMode: IAgentSwarmService,
@IConfigService private readonly config: IConfigService,
@IFlagService private readonly flags: IFlagService,
@IModelCatalog private readonly modelCatalog: IModelCatalog,
@ISessionSubagentService private readonly subagents: ISessionSubagentService,
@IAgentProfileService private readonly profile: IAgentProfileService,
) {
Expand All @@ -104,6 +106,7 @@ export class AgentSwarmTool implements IAgentSwarmTool {
this.config,
this.flags,
this.profile.data().modelAlias,
this.modelCatalog,
);
return modelLines === undefined ? description : `${description}\n\n${modelLines}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export class TowerSpawnTool implements ITowerSpawnTool {
this.flags,
{ modelAlias: own.modelAlias, thinkingLevel: own.thinkingLevel },
args.kind === 'reviewer' ? 'primary' : undefined,
this.modelCatalog,
);
let handle: SubagentHandle;
try {
Expand Down
2 changes: 0 additions & 2 deletions packages/agent-core-v2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,8 +478,6 @@ export * from '#/session/subagent/subagent';
export * from '#/session/subagent/subagentService';
export * from '#/session/subagent/spawn';
import '#/session/subagent/flag';
export * from '#/session/subagent/subagentModelsValidation';
import '#/session/subagent/subagentModelsValidationService';
export * from '#/agent/tools/agent/subagent-task';
export { AGENT_RUN_PROMPT_ORIGIN } from '#/session/subagent/runAgentTurn';
export * from '#/session/subagent/mirrorAgentRun';
Expand Down
Loading
Loading