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
19 changes: 15 additions & 4 deletions .agents/skills/agent-core-dev/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,18 @@ Each field is an `EnvBinding` — a string (env var name) or
section. Empty nested entries (no field resolved) are omitted, so a synthetic
entry like `__kimi_env__` only appears when at least one of its env vars is set.

`stripEnv(value, rawSnake?)` removes env-derived fields before `set`/`replace`
persists, so env overrides never leak into `config.toml`.
`stripEnv(value, raw?, getEnv?)` removes env-derived fields before `set`/`replace`
persists, so env overrides never leak into `config.toml`. `raw` is the section's
env-free camelCase base (already `fromToml`-normalized, so legacy key renames
are honored), and `getEnv` reads the live env bag. For fields that are **both
user-persistable and env-overridable**, register
`stripEnv: stripEnvBoundFields(sectionEnvBindings)` (from `#/app/config/config`)
— it derives the guard from the same bindings the read path uses: while a
field's env var resolves to a value, writes restore the field's raw-base value
(or drop it) instead of persisting an echoed env value; an env value that
fails the binding's `parse` owns nothing, so writes pass through. Env-only
fields/sections need no env check — strip them unconditionally (e.g. thinking's
`forcedEffort`, cron's whole-section `() => undefined`).

Business domains read `config.get('section')`; they never read env directly, and
never write their own env-merge logic.
Expand Down Expand Up @@ -217,11 +227,12 @@ This means registration order is never a correctness concern — you do not need
- **Read**: `transformTomlData(fileData, registry)` maps each top-level key to a domain and applies that domain's `fromToml` hook (or a plain key-casing pass when none is registered). Owner domains register their own normalization — e.g. provider `oauth`/`env`/`customHeaders`, permission `deny/allow/ask` → `rules`, `loop_control.max_steps_per_run` → `maxStepsPerTurn`, `experimental` keys preserved verbatim. When a section registers after the initial load, `ConfigService` re-applies its `fromToml` against the preserved snake_case raw value (see "Late registration"), so registration order is never a correctness concern.
- **Write**: `applySectionToToml(rawSnake, domain, value, registry)` applies the domain's `toToml` hook (or a plain camelCase→snake_case mapping) into a raw clone of the file, preserving unknown top-level keys and unknown sub-fields (lossless round-trip).

`ConfigService` keeps three views:
`ConfigService` keeps four views:

- `rawSnake` — snake_case clone of the file; the write base, never carries the env overlay.
- `raw` — camelCase, env-free; the read/set/replace base.
- `effective` — validated `raw` plus the env overlay; what `get()` returns.
- `validated` — validated `raw`, env-free; the base every live env re-application starts from, so a degraded or removed env value falls back to the file instead of a stale overlay.
- `effective` — `validated` plus the env overlay, recomputed on load/set; `get()`/`getAll()` re-apply the overlay on a fresh `validated` copy per read rather than caching it.

### `KIMI_MODEL_*` env overlay

Expand Down
7 changes: 7 additions & 0 deletions .changeset/config-env-overrides.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/agent-core-v2": patch
"@moonshot-ai/kimi-code": patch
---

Add environment variable overrides for agent loop and background task limits. Set KIMI_LOOP_MAX_STEPS_PER_TURN, KIMI_LOOP_MAX_RETRIES_PER_STEP, or KIMI_CODE_BACKGROUND_MAX_RUNNING_TASKS to take priority over the [loop_control] and [background] config.
6 changes: 6 additions & 0 deletions .changeset/config-env-persist-and-stale-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core-v2": patch
"@moonshot-ai/kimi-code": patch
---

Fix config environment overrides (such as KIMI_IMAGE_MAX_EDGE_PX or KIMI_SUBAGENT_TIMEOUT_MS) being persisted into config.toml by config API writes while the env var is set, and keeping the old value after the env var is changed to an invalid value or removed.
4 changes: 3 additions & 1 deletion docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ You can also switch models temporarily without touching the config file — by s
| `max_retries_per_step` | `integer` | `10` | Maximum retries after a step failure |
| `reserved_context_size` | `integer` | — | Number of tokens reserved for model output; automatic compaction is triggered when the remaining context window falls below this value |

`max_steps_per_turn` can be overridden by the `KIMI_LOOP_MAX_STEPS_PER_TURN` environment variable, and `max_retries_per_step` by `KIMI_LOOP_MAX_RETRIES_PER_STEP`; both take higher priority than the config file.

## `background`

`background` controls the concurrency behavior of background tasks (launched via the `Bash` tool or the `Agent` tool's `run_in_background=true` parameter).
Expand All @@ -227,7 +229,7 @@ You can also switch models temporarily without touching the config file — by s
| `print_wait_ceiling_s` | `integer` | `315360000` | In print mode (`kimi -p`), the wall-clock ceiling (seconds) for the wait/steer loop when `print_background_mode` is `"drain"` or `"steer"` (the default is 10 years — effectively unbounded). Has no effect outside print mode or when it is `"exit"` |
| `print_max_turns` | `integer` | `100000` | In print mode (`kimi -p`) with `print_background_mode = "steer"`, the maximum number of new turns that may be triggered by background-task completions, to keep the steering loop bounded (the default is effectively unbounded) |

`keep_alive_on_exit` can be overridden by the `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` environment variable, which takes higher priority than `config.toml`.
`keep_alive_on_exit` can be overridden by the `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` environment variable, and `max_running_tasks` by `KIMI_CODE_BACKGROUND_MAX_RUNNING_TASKS`; both take higher priority than `config.toml`.

In print mode (`kimi -p "<prompt>"`), Kimi Code stays alive after the main agent's turn as long as background tasks are still pending: each completion is fed back to the main agent as a synthetic user message, steering it into a new turn (`print_background_mode = "steer"` by default), and the run exits once a turn ends with nothing pending. The loop is bounded by `print_wait_ceiling_s` and `print_max_turns`, both effectively unbounded by default. Background work is never killed by a wall-clock cap in print mode either: background `Bash` tasks default to no timeout (`bash_task_timeout_s = 0`), and subagents run without a timeout (`[subagent] timeout_ms = 0`), so only the model itself stops a task. Set `print_background_mode` to `"drain"` to wait for tasks without feeding results back, or `"exit"` to end the run as soon as the main agent finishes.

Expand Down
3 changes: 3 additions & 0 deletions docs/en/configuration/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,14 @@ Switches that control the behavior of subsystems such as telemetry, background t
| --- | --- | --- |
| `KIMI_DISABLE_TELEMETRY` | Disable anonymous telemetry reporting | `1`, `true`, `yes`, `y` (case-insensitive) |
| `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` | Whether to keep background tasks when the session closes; takes higher priority than `config.toml`. The default is to stop them on exit | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` |
| `KIMI_CODE_BACKGROUND_MAX_RUNNING_TASKS` | Cap on concurrently running background tasks; takes higher priority than `[background] max_running_tasks` in `config.toml` (unset means no cap) | Positive integer; invalid values are ignored |
| `KIMI_IMAGE_MAX_EDGE_PX` | Longest-edge ceiling (px) for image compression; takes higher priority than `[image] max_edge_px` in `config.toml` (default `2000`) | Positive integer; invalid values are ignored |
| `KIMI_IMAGE_READ_BYTE_BUDGET` | Per-image byte budget for model-initiated image reads (`ReadMediaFile` default reads); takes higher priority than `[image] read_byte_budget` in `config.toml` (default `262144`, i.e. 256 KB) | Positive integer; invalid values are ignored |
| `KIMI_CODE_PLUGIN_MARKETPLACE_URL` | Override the plugin marketplace JSON loaded by `/plugins`; useful for dev loopback servers, staging CDN files, or alternate marketplace directories | `https://code.kimi.com/kimi-code/plugins/marketplace.json`; also accepts `http://`, `file://` URLs, and local paths |
| `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` | Cap how many AgentSwarm subagents run concurrently during the initial ramp; leave unset for no cap | Positive integer; invalid values fail fast |
| `KIMI_SUBAGENT_TIMEOUT_MS` | Maximum wall-clock time (ms) a single subagent (`Agent` / `AgentSwarm`) may run; takes higher priority than `[subagent] timeout_ms` in `config.toml` (default `7200000`, i.e. 2 hours) | Positive integer; invalid values fall back to the config or default |
| `KIMI_LOOP_MAX_STEPS_PER_TURN` | Maximum Agent steps per turn; takes higher priority than `[loop_control] max_steps_per_turn` in `config.toml` (unset or `0` means unlimited) | Non-negative integer; invalid values are ignored |
| `KIMI_LOOP_MAX_RETRIES_PER_STEP` | Maximum retries after a step failure; takes higher priority than `[loop_control] max_retries_per_step` in `config.toml` (default `10`) | Non-negative integer; invalid values are ignored |
| `KIMI_CODE_EXPERIMENTAL_FLAG` | Enable all registered experimental features for this process | `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: 3 additions & 1 deletion docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ display_name = "Kimi for Coding (custom)"
| `max_retries_per_step` | `integer` | `10` | 单步失败后的最大重试次数 |
| `reserved_context_size` | `integer` | — | 预留给模型输出的 token 数;上下文窗口剩余量低于此值时触发自动压缩 |

`max_steps_per_turn` 可被环境变量 `KIMI_LOOP_MAX_STEPS_PER_TURN` 覆盖,`max_retries_per_step` 可被 `KIMI_LOOP_MAX_RETRIES_PER_STEP` 覆盖,优先级均高于配置文件。

## `background`

`background` 控制后台任务(通过 `Bash` 工具或 `Agent` 工具的 `run_in_background=true` 参数启动)的并发数。
Expand All @@ -227,7 +229,7 @@ display_name = "Kimi for Coding (custom)"
| `print_wait_ceiling_s` | `integer` | `315360000` | print 模式(`kimi -p`)下,`print_background_mode` 为 `"drain"` 或 `"steer"` 时,等待/steer 循环的墙钟上限(秒;默认 10 年,近似不设限)。在非 print 模式或 `"exit"` 时无效 |
| `print_max_turns` | `integer` | `100000` | print 模式(`kimi -p`)且 `print_background_mode = "steer"` 时,允许由后台任务完成触发的新 turn 的最大数量,防止 steer 循环失控(默认值近似不设限) |

`keep_alive_on_exit` 可被环境变量 `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` 覆盖,优先级高于配置文件
`keep_alive_on_exit` 可被环境变量 `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` 覆盖,`max_running_tasks` 可被 `KIMI_CODE_BACKGROUND_MAX_RUNNING_TASKS` 覆盖,优先级均高于配置文件

在 print 模式(`kimi -p "<prompt>"`)下,只要还有未决的后台任务,Kimi Code 在主 agent 的 turn 结束后不会退出:每个任务完成都会以合成 user 消息回馈给主 agent,steer 出新的 turn(默认 `print_background_mode = "steer"`),直到某 turn 结束时没有任何未决任务才退出。该循环受 `print_wait_ceiling_s` 与 `print_max_turns` 约束,默认值都近似不设限。print 模式下后台工作也不会被墙钟超时杀掉:后台 `Bash` 任务默认无超时(`bash_task_timeout_s = 0`),子代理默认无超时(`[subagent] timeout_ms = 0`),只有模型自己能停止任务。将 `print_background_mode` 设为 `"drain"` 可等待任务结束但不回馈结果,设为 `"exit"` 则在主 agent 结束后立即退出。

Expand Down
3 changes: 3 additions & 0 deletions docs/zh/configuration/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,14 @@ kimi
| --- | --- | --- |
| `KIMI_DISABLE_TELEMETRY` | 关闭匿名遥测上报 | `1`、`true`、`yes`、`y`(不区分大小写) |
| `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` | 会话关闭时是否保留后台任务,优先级高于 `config.toml`。默认会在退出时停止后台任务 | 真值:`1`/`true`/`yes`/`on`;假值:`0`/`false`/`no`/`off` |
| `KIMI_CODE_BACKGROUND_MAX_RUNNING_TASKS` | 同时运行的后台任务数上限,优先级高于 `config.toml` 的 `[background] max_running_tasks`(不设置表示无上限) | 正整数;非法值被忽略 |
| `KIMI_IMAGE_MAX_EDGE_PX` | 图片压缩的最长边上限(像素),优先级高于 `config.toml` 的 `[image] max_edge_px`(默认 `2000`) | 正整数;非法值被忽略 |
| `KIMI_IMAGE_READ_BYTE_BUDGET` | 模型自行读图(`ReadMediaFile` 默认读取)的单图字节预算,优先级高于 `config.toml` 的 `[image] read_byte_budget`(默认 `262144`,即 256 KB) | 正整数;非法值被忽略 |
| `KIMI_CODE_PLUGIN_MARKETPLACE_URL` | 覆盖 `/plugins` 加载的 plugin marketplace JSON,适合 dev loopback server、测试 CDN 文件或替换 marketplace 目录 | `https://code.kimi.com/kimi-code/plugins/marketplace.json`;也接受 `http://`、`file://` URL 和本地路径 |
| `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` | 限制 AgentSwarm 初始提升并发阶段可同时运行的子 Agent 数量;不设置表示不限制 | 正整数;非法值会立即失败 |
| `KIMI_SUBAGENT_TIMEOUT_MS` | 单个子 Agent(`Agent` / `AgentSwarm`)可运行的最长时间(毫秒);优先级高于 `config.toml` 的 `[subagent] timeout_ms`(默认 `7200000`,即 2 小时) | 正整数;非法值回退到配置或默认值 |
| `KIMI_LOOP_MAX_STEPS_PER_TURN` | Agent 单轮最大步数;优先级高于 `config.toml` 的 `[loop_control] max_steps_per_turn`(不设或 `0` 表示无上限) | 非负整数;非法值被忽略 |
| `KIMI_LOOP_MAX_RETRIES_PER_STEP` | 单步失败后的最大重试次数;优先级高于 `config.toml` 的 `[loop_control] max_retries_per_step`(默认 `10`) | 非负整数;非法值被忽略 |
| `KIMI_CODE_EXPERIMENTAL_FLAG` | 在当前进程启用所有已注册的实验功能 | `1`、`true`、`yes`、`on` |
| `KIMI_SHELL_PATH` | Windows 上覆盖 Git Bash 路径(自动探测失败时使用) | 绝对路径 |
| `KIMI_MODEL_MAX_COMPLETION_TOKENS` | 单步 LLM 请求的 `max_completion_tokens` 硬上限,仅对 `kimi` 供应商生效 | 正整数;`0` 或负数禁用 clamp |
Expand Down
34 changes: 31 additions & 3 deletions packages/agent-core-v2/src/agent/loop/configSection.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
/**
* `loop` domain (L4) — `loopControl` config-section schema and TOML transforms.
* `loop` domain (L4) — `loopControl` config-section schema, env bindings, and
* TOML transforms.
*
* Owns the `[loop_control]` configuration section (step / retry / context-size
* limits) consumed by `AgentLoopService` (step + retry budgets) and `AgentProfileService`
* (context sizing), plus the snake_case ↔ camelCase TOML transforms (including
* the legacy `max_steps_per_run` → `maxStepsPerTurn` rename). Self-registered at
* module load via `registerConfigSection`.
* the legacy `max_steps_per_run` → `maxStepsPerTurn` rename). The step and retry
* budgets also accept operational env overrides (`KIMI_LOOP_MAX_STEPS_PER_TURN`
* / `KIMI_LOOP_MAX_RETRIES_PER_STEP`); `config` resolves each field as
* `env > config.toml > default` and re-applies the env binding on every read.
* Self-registered at module load via `registerConfigSection`.
*
* While a field's env var is set, `stripEnvBoundFields` restores its env-free
* raw value before `set`/`replace` persists, so an env override echoed
* back through a config write can never leak into `config.toml`.
*/

import { z } from 'zod';

import { type EnvBindings, envBindings, stripEnvBoundFields } from '#/app/config/config';
import { registerConfigSection } from '#/app/config/configSectionContributions';
import { plainObjectToToml, transformPlainObject } from '#/app/config/toml';

export const LOOP_CONTROL_SECTION = 'loopControl';

export const LOOP_MAX_STEPS_PER_TURN_ENV = 'KIMI_LOOP_MAX_STEPS_PER_TURN';
export const LOOP_MAX_RETRIES_PER_STEP_ENV = 'KIMI_LOOP_MAX_RETRIES_PER_STEP';

export const LoopControlSchema = z.object({
maxStepsPerTurn: z.number().int().min(0).optional(),
maxRetriesPerStep: z.number().int().min(0).optional(),
Expand All @@ -25,6 +37,20 @@ export const LoopControlSchema = z.object({

export type LoopControl = z.infer<typeof LoopControlSchema>;

function parseNonNegativeInt(raw: string): number | undefined {
const value = raw.trim();
if (value.length === 0 || !/^\d+$/.test(value)) return undefined;
const parsed = Number(value);
return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
}

export const loopControlEnvBindings: EnvBindings<LoopControl> = envBindings(LoopControlSchema, {
maxStepsPerTurn: { env: LOOP_MAX_STEPS_PER_TURN_ENV, parse: parseNonNegativeInt },
maxRetriesPerStep: { env: LOOP_MAX_RETRIES_PER_STEP_ENV, parse: parseNonNegativeInt },
});

export const stripLoopControlEnv = stripEnvBoundFields(loopControlEnvBindings);

export const loopControlFromToml = (rawSnake: unknown): unknown => {
if (rawSnake === null || typeof rawSnake !== 'object' || Array.isArray(rawSnake)) return rawSnake;
const out = transformPlainObject(rawSnake as Record<string, unknown>);
Expand All @@ -43,4 +69,6 @@ export const loopControlToToml = (value: unknown, rawSnake: unknown): unknown =>
registerConfigSection(LOOP_CONTROL_SECTION, LoopControlSchema, {
fromToml: loopControlFromToml,
toToml: loopControlToToml,
env: loopControlEnvBindings,
Comment thread
RealKai42 marked this conversation as resolved.
stripEnv: stripLoopControlEnv,
});
11 changes: 7 additions & 4 deletions packages/agent-core-v2/src/agent/media/configSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
* `KIMI_IMAGE_READ_BYTE_BUDGET`); `config` resolves each field as
* `env > config.toml > default` and re-applies the env binding on every read.
*
* No `stripEnv` is registered: nothing calls `set`/`replace` for `image`, and
* `raw`/`rawSnake` are always env-free (the env overlay lands only in
* `effective`), so an env override can never be written to `config.toml`.
* While a field's env var is set, `stripEnvBoundFields` restores its env-free
* raw value before `set`/`replace` persists, so an env override echoed
* back through a config write can never leak into `config.toml`.
*
* The compression support module (`#/agent/media/image-compress`) stays
* config-agnostic: `ImageConfigBridge` reads this env-resolved section and
Expand All @@ -22,7 +22,7 @@

import { z } from 'zod';

import { type EnvBindings, envBindings } from '#/app/config/config';
import { type EnvBindings, envBindings, stripEnvBoundFields } from '#/app/config/config';
import { registerConfigSection } from '#/app/config/configSectionContributions';

export const IMAGE_SECTION = 'image';
Expand All @@ -49,7 +49,10 @@ export const imageEnvBindings: EnvBindings<ImageConfig> = envBindings(ImageConfi
readByteBudget: { env: IMAGE_READ_BYTE_BUDGET_ENV, parse: parsePositiveInt },
});

export const stripImageEnv = stripEnvBoundFields(imageEnvBindings);

registerConfigSection(IMAGE_SECTION, ImageConfigSchema, {
defaultValue: {},
env: imageEnvBindings,
stripEnv: stripImageEnv,
});
Loading
Loading