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
6 changes: 3 additions & 3 deletions .agents/skills/agent-core-dev/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ Gate not-yet-public features behind `IFlagService.enabled(id)`, per the reposito

Highest wins; env is read live on every call (nothing cached):

1. Master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on.
2. Per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`) → forces on/off.
3. `[experimental]` config section per-flag override.
1. Per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`) → forces on/off.
2. `[experimental]` config section per-flag override.
3. Master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on.
4. Registry `default`.

`explain(id)` returns the winning `source` (`master-env` | `env` | `config` | `default`) plus the effective `configValue`. `explain(id)` returns `undefined` (and `enabled(id)` returns `false`) for an id that no domain has registered.
Expand Down
5 changes: 5 additions & 0 deletions .changeset/flag-config-over-master-env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Honor explicit `[experimental]` config entries over the `KIMI_CODE_EXPERIMENTAL_FLAG` master switch, so a flag set to `false` in `config.toml` stays off; per-feature `KIMI_CODE_EXPERIMENTAL_<NAME>` variables still override both.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo

## Experimental Features

- Gate a not-yet-public feature behind an experimental flag. Flags are env-driven and default off: `KIMI_CODE_EXPERIMENTAL_<NAME>` toggles one, `KIMI_CODE_EXPERIMENTAL_FLAG` enables all. Release by flipping the entry's `default` to `true`.
- Gate a not-yet-public feature behind an experimental flag. Flags are env-driven and default off: `KIMI_CODE_EXPERIMENTAL_<NAME>` toggles one, `KIMI_CODE_EXPERIMENTAL_FLAG` enables all. Precedence is per-flag env > `[experimental]` config > master env > the flag's `default`. Release by flipping the entry's `default` to `true`.
- `packages/agent-core` (v1): add the flag to the central registry at `packages/agent-core/src/flags/registry.ts`, then check it with `flags.enabled('my-feature')`.
- `packages/agent-core-v2` and kap-server modules: there is no central catalog — declare the flag in the owning domain via `registerFlagDefinition` at import time (see `packages/agent-core-v2/docs/flag.md`), then check it with `IFlagService.enabled(id)`. Current search-index-separation flags: `persistence_minidb_readmodel` (session read model, default on) and `search_worker` (global search worker host, default on).

Expand Down
4 changes: 2 additions & 2 deletions docs/en/configuration/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ Switches that control the behavior of subsystems such as telemetry, background t
| `KIMI_CODE_IDENTITY_SLUG` | Protocol identifier for the `User-Agent` product token sent to third-party providers and the MCP client name; takes higher priority than `[identity] slug`. Derived from the name when unset | Any non-empty string; normalized to lowercase with non-alphanumeric runs folded to `-` |
| `KIMI_CODE_BUILTIN_PRODUCT_SKILLS` | Whether the built-in skills documenting Kimi Code itself are offered to the model; takes higher priority than `builtin_product_skills` in `config.toml` (default enabled) | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` |
| `KIMI_CODE_TUI_FULL_SCREEN` | Enable the experimental fullscreen alternate-screen UI: scrollable transcript viewport, mouse text selection, clickable links, and Ctrl-Shift-F transcript search | `1` enables it; anything else keeps the regular inline UI |
| `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` | The [subagent model pool](./config-files.md#subagent-model-pool) is enabled by default in every launch mode, including the interactive TUI; set a falsy value to disable it; the master `KIMI_CODE_EXPERIMENTAL_FLAG=1` force-enables it | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` |
| `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` | The [subagent model pool](./config-files.md#subagent-model-pool) is enabled by default in every launch mode, including the interactive TUI; set a falsy value to disable it; the master `KIMI_CODE_EXPERIMENTAL_FLAG=1` also enables it | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` |
| `KIMI_CODE_EXPERIMENTAL_SUBAGENT_FORK` | Enable the experimental `fork` parameter on the `Agent` and `AgentSwarm` tools, letting the model start a subagent with a snapshot of the calling agent's conversation history instead of an empty context; the master `KIMI_CODE_EXPERIMENTAL_FLAG=1` also enables it | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` |
| `KIMI_MCP_STARTUP_TIMEOUT_MS` | Global default connection timeout (ms) for all MCP servers; takes higher priority than `[mcp] startup_timeout_ms` in `config.toml`, but a per-server `startupTimeoutMs` in `mcp.json` still wins (default `30000`) | Integer from `1` to `2147483647`; invalid values are ignored |
| `KIMI_MCP_TOOL_TIMEOUT_MS` | Global default single tool-call timeout (ms) for all MCP servers; takes higher priority than `[mcp] tool_timeout_ms` in `config.toml`, but a per-server `toolTimeoutMs` in `mcp.json` still wins (default `60000`) | Integer from `1` to `2147483647`; invalid values are ignored |
Expand All @@ -162,7 +162,7 @@ Switches that control the behavior of subsystems such as telemetry, background t
| `KIMI_WEB_SEARCH_API_KEY` | API key of the web search (`WebSearch`) service; replaces both the configured API key and OAuth credential when set | Non-blank string; blank values are ignored |
| `KIMI_WEB_FETCH_BASE_URL` | API URL of the web fetch (`FetchURL`) service; takes higher priority than `[services.moonshot_fetch] base_url`. Persisted credentials and custom headers are not forwarded to an env-selected endpoint. Without an env or config endpoint, signed-in users try the managed Kimi OAuth fetch service before direct local requests | Non-blank string; blank values are ignored |
| `KIMI_WEB_FETCH_API_KEY` | API key of the web fetch (`FetchURL`) service; replaces both the configured API key and OAuth credential when set | Non-blank string; blank values are ignored |
| `KIMI_CODE_EXPERIMENTAL_FLAG` | Enable all registered experimental features for this process; it does not select the agent engine | `1`, `true`, `yes`, `on` |
| `KIMI_CODE_EXPERIMENTAL_FLAG` | Enable all registered experimental features for this process; a per-feature `KIMI_CODE_EXPERIMENTAL_<NAME>` variable or an explicit entry in the `[experimental]` section of `config.toml` takes precedence over it; it does not select the agent engine | `1`, `true`, `yes`, `on` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Qualify the new precedence as v2-only

When KIMI_CODE_LEGACY_FLAG=1 selects the v1 engine, this unconditional precedence statement is false: packages/agent-core/src/flags/resolver.ts:47-52 still checks the truthy master switch before both the dedicated environment variable and config, so neither KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=0 nor an explicit [experimental] value can override it. Either apply the same ordering to the v1 resolver or document that the new precedence applies only to the default v2 engine; otherwise users following the adjacent legacy-mode option observe the opposite behavior.

AGENTS.md reference: AGENTS.md:L66-L68

Useful? React with 👍 / 👎.

| `KIMI_CODE_LEGACY_FLAG` | Use the legacy `agent-core` engine for `kimi`, `kimi -p`, `kimi doctor`, `kimi acp`, `kimi export`, and `kimi provider`; these commands use `agent-core-v2` by default | `1`, `true`, `yes`, `on` |
| `KIMI_SHELL_PATH` | Override the Git Bash path on Windows (used when auto-detection fails) | Absolute path |
| `KIMI_MODEL_MAX_COMPLETION_TOKENS` | Hard cap on `max_completion_tokens` per LLM step; applies to the `kimi` provider only | Positive integer; `0` or negative disables clamping |
Expand Down
4 changes: 2 additions & 2 deletions docs/zh/configuration/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ kimi
| `KIMI_CODE_IDENTITY_SLUG` | 协议标识,用于发给第三方 provider 的 `User-Agent` 产品名和 MCP 客户端名,优先级高于 `[identity] slug`。未设置时由名称派生 | 任意非空字符串;会转小写并将连续非字母数字字符折叠为 `-` |
| `KIMI_CODE_BUILTIN_PRODUCT_SKILLS` | 是否向模型提供介绍 Kimi Code 自身的内置 Skills,优先级高于 `config.toml` 的 `builtin_product_skills`(默认开启) | 真值:`1`/`true`/`yes`/`on`;假值:`0`/`false`/`no`/`off` |
| `KIMI_CODE_TUI_FULL_SCREEN` | 启用实验性的 fullscreen alternate-screen 界面:可滚动的 transcript 视口、鼠标选择文本、可点击链接、Ctrl-Shift-F 搜索 | `1` 开启;其他值保持常规内联界面 |
| `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` | [subagent 模型池](./config-files.md#subagent-模型池) 默认启用,在包括交互式 TUI 在内的所有启动方式下生效;设为假值可禁用;master `KIMI_CODE_EXPERIMENTAL_FLAG=1` 会强制启用本功能 | 真值:`1`/`true`/`yes`/`on`;假值:`0`/`false`/`no`/`off` |
| `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` | [subagent 模型池](./config-files.md#subagent-模型池) 默认启用,在包括交互式 TUI 在内的所有启动方式下生效;设为假值可禁用;master `KIMI_CODE_EXPERIMENTAL_FLAG=1` 也会启用本功能 | 真值:`1`/`true`/`yes`/`on`;假值:`0`/`false`/`no`/`off` |
| `KIMI_CODE_EXPERIMENTAL_SUBAGENT_FORK` | 在 `Agent` 和 `AgentSwarm` 工具上启用实验性的 `fork` 参数,让模型可以以调用方 Agent 对话历史的快照而不是空上下文启动 subagent;master `KIMI_CODE_EXPERIMENTAL_FLAG=1` 也会启用本功能 | 真值:`1`/`true`/`yes`/`on`;假值:`0`/`false`/`no`/`off` |
| `KIMI_MCP_STARTUP_TIMEOUT_MS` | 所有 MCP server 的全局默认连接超时(毫秒);优先级高于 `config.toml` 的 `[mcp] startup_timeout_ms`,但低于 `mcp.json` 中单个 server 的 `startupTimeoutMs`(默认 `30000`) | `1` 到 `2147483647` 的整数;非法值被忽略 |
| `KIMI_MCP_TOOL_TIMEOUT_MS` | 所有 MCP server 的全局默认单次工具调用超时(毫秒);优先级高于 `config.toml` 的 `[mcp] tool_timeout_ms`,但低于 `mcp.json` 中单个 server 的 `toolTimeoutMs`(默认 `60000`) | `1` 到 `2147483647` 的整数;非法值被忽略 |
Expand All @@ -162,7 +162,7 @@ kimi
| `KIMI_WEB_SEARCH_API_KEY` | 网页搜索(`WebSearch`)服务的 API 密钥;设置后同时替换配置中的 API 密钥和 OAuth 凭据 | 非空字符串;空白值被忽略 |
| `KIMI_WEB_FETCH_BASE_URL` | 网页抓取(`FetchURL`)服务的 API URL;优先级高于 `[services.moonshot_fetch] base_url`。文件中持久化的凭据和自定义 header 不会发送到环境变量指定的端点。环境变量和配置都没有指定端点时,已登录用户会先尝试 Kimi OAuth 托管抓取服务,再回退到本地直接请求 | 非空字符串;空白值被忽略 |
| `KIMI_WEB_FETCH_API_KEY` | 网页抓取(`FetchURL`)服务的 API 密钥;设置后同时替换配置中的 API 密钥和 OAuth 凭据 | 非空字符串;空白值被忽略 |
| `KIMI_CODE_EXPERIMENTAL_FLAG` | 在当前进程启用所有已注册的实验功能;不用于选择 Agent 引擎 | `1`、`true`、`yes`、`on` |
| `KIMI_CODE_EXPERIMENTAL_FLAG` | 在当前进程启用所有已注册的实验功能;单个功能的 `KIMI_CODE_EXPERIMENTAL_<NAME>` 变量或 `config.toml` 的 `[experimental]` 节中的显式配置优先于它;不用于选择 Agent 引擎 | `1`、`true`、`yes`、`on` |
| `KIMI_CODE_LEGACY_FLAG` | 让 `kimi`、`kimi -p`、`kimi doctor`、`kimi acp`、`kimi export` 和 `kimi provider` 使用旧版 `agent-core` 引擎;这些命令默认使用 `agent-core-v2` | `1`、`true`、`yes`、`on` |
| `KIMI_SHELL_PATH` | Windows 上覆盖 Git Bash 路径(自动探测失败时使用) | 绝对路径 |
| `KIMI_MODEL_MAX_COMPLETION_TOKENS` | 单步 LLM 请求的 `max_completion_tokens` 硬上限,仅对 `kimi` 供应商生效 | 正整数;`0` 或负数禁用 clamp |
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-core-v2/docs/flag.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ Gates not-yet-public features behind `IFlagService.enabled(id)`, per the reposit

Highest wins; env is read live on every call (nothing cached):

1. L1 master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on.
2. L2 per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`) → forces on/off.
3. L3 `[experimental]` config section per-flag override.
1. L1 per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`) → forces on/off.
2. L2 `[experimental]` config section per-flag override.
3. L3 master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on.
4. L4 registry `default`.

`explain(id)` returns the winning `source` (`master-env` | `env` | `config` | `default`) plus the effective `configValue`. `explain(id)` returns `undefined` (and `enabled(id)` returns `false`) for an id that no domain has registered.
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-core-v2/src/app/flag/flagService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ export class FlagService extends Disposable implements IFlagService {
const def = this.registry.get(id);
if (def === undefined) return undefined;
const configValue = this.configOverrides[def.id];
if (parseBooleanEnv(this.bootstrap.getEnv(MASTER_ENV)) === true) {
return this.state(def, true, 'master-env', configValue);
}
const override = parseBooleanEnv(this.bootstrap.getEnv(def.env));
if (override !== undefined) return this.state(def, override, 'env', configValue);
if (configValue !== undefined) return this.state(def, configValue, 'config', configValue);
if (parseBooleanEnv(this.bootstrap.getEnv(MASTER_ENV)) === true) {
return this.state(def, true, 'master-env', configValue);
}
return this.state(def, def.default, 'default', undefined);
}

Expand Down
32 changes: 30 additions & 2 deletions packages/agent-core-v2/test/app/flag/flag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,10 @@ describe('FlagService', () => {
expect(state?.configValue).toBe(false);
});

it('lets per-feature env override config', async () => {
it('lets per-feature env override config and the master env', async () => {
const { config, flags } = makeFlags({
KIMI_CODE_EXPERIMENTAL_EXAMPLE_FLAG: 'true',
[MASTER_ENV]: '1',
});
await config.set(EXPERIMENTAL_SECTION, { example_flag: false });
const state = flags.explain('example_flag');
Expand All @@ -128,14 +129,41 @@ describe('FlagService', () => {
expect(state?.configValue).toBe(false);
});

it('lets the master env switch force every flag on', async () => {
it('lets per-feature env force a flag off against config and the master env', async () => {
const { config, flags } = makeFlags({
KIMI_CODE_EXPERIMENTAL_EXAMPLE_FLAG: 'false',
[MASTER_ENV]: '1',
});
await config.set(EXPERIMENTAL_SECTION, { example_flag: true });
const state = flags.explain('example_flag');
expect(state?.enabled).toBe(false);
expect(state?.source).toBe('env');
expect(state?.configValue).toBe(true);
});

it('lets config override the master env', async () => {
const { config, flags } = makeFlags({ [MASTER_ENV]: '1' });
await config.set(EXPERIMENTAL_SECTION, { example_flag: false });
const state = flags.explain('example_flag');
expect(state?.enabled).toBe(false);
expect(state?.source).toBe('config');
expect(state?.configValue).toBe(false);
});

it('lets the master env switch turn flags on when nothing else is set', () => {
const { flags } = makeFlags({ [MASTER_ENV]: '1' });
const state = flags.explain('example_flag');
expect(state?.enabled).toBe(true);
expect(state?.source).toBe('master-env');
});

it('treats a falsy master env as unset', () => {
const { flags } = makeFlags({ [MASTER_ENV]: '0' });
const state = flags.explain('example_flag');
expect(state?.enabled).toBe(true);
expect(state?.source).toBe('default');
});

it('refreshes overrides when the experimental config section changes', async () => {
const { config, flags } = makeFlags();
expect(flags.enabled('example_flag')).toBe(true);
Expand Down
23 changes: 16 additions & 7 deletions packages/agent-core-v2/test/features/tower/towerFeature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,16 +192,25 @@ describe('tower flag — resolution', () => {
});
});

it('lets the master env override the dedicated env and config', async () => {
const { config, flags } = makeFlags({
[TOWER_FLAG_ENV]: 'false',
[MASTER_ENV]: 'true',
});
await config.set(EXPERIMENTAL_SECTION, { [TOWER_FLAG_ID]: false });
expect(flags.explain(TOWER_FLAG_ID)).toMatchObject({
it('lets the master env turn the flag on only when env and config are unset', async () => {
const masterOnly = makeFlags({ [MASTER_ENV]: 'true' });
expect(masterOnly.flags.explain(TOWER_FLAG_ID)).toMatchObject({
enabled: true,
source: 'master-env',
});

const configured = makeFlags({ [MASTER_ENV]: 'true' });
await configured.config.set(EXPERIMENTAL_SECTION, { [TOWER_FLAG_ID]: false });
expect(configured.flags.explain(TOWER_FLAG_ID)).toMatchObject({
enabled: false,
source: 'config',
configValue: false,
});

const overridden = makeFlags({ [TOWER_FLAG_ENV]: 'false', [MASTER_ENV]: 'true' });
expect(overridden.flags.explain(TOWER_FLAG_ID)).toMatchObject({
enabled: false,
source: 'env',
});
});
});
Loading