From 7613b478acf9e9a511f04f64213e0068e7ef0270 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Tue, 9 Jun 2026 10:23:58 +0800 Subject: [PATCH 1/4] feat: release experimental features --- .changeset/release-experimental-features.md | 6 ++++ apps/kimi-code/src/cli/goal-prompt.ts | 6 +--- apps/kimi-code/src/cli/run-prompt.ts | 4 +-- apps/kimi-code/src/tui/commands/registry.ts | 2 -- apps/kimi-code/src/tui/kimi-tui.ts | 4 +-- apps/kimi-code/test/cli/goal-prompt.test.ts | 18 +++++------ apps/kimi-code/test/tui/commands/goal.test.ts | 13 -------- .../test/tui/commands/registry.test.ts | 6 ++-- .../test/tui/commands/resolve.test.ts | 23 ++------------ .../test/tui/kimi-tui-startup.test.ts | 9 +++--- docs/en/configuration/config-files.md | 17 ++-------- docs/en/configuration/env-vars.md | 5 +-- docs/en/guides/goals.md | 10 +----- docs/en/reference/slash-commands.md | 21 +++---------- docs/zh/configuration/config-files.md | 17 ++-------- docs/zh/configuration/env-vars.md | 5 +-- docs/zh/guides/goals.md | 10 +----- docs/zh/reference/slash-commands.md | 21 +++---------- .../agent-core/src/agent/compaction/micro.ts | 4 --- .../agent-core/src/agent/injection/manager.ts | 1 - packages/agent-core/src/agent/tool/index.ts | 15 ++++----- packages/agent-core/src/agent/turn/index.ts | 6 ++-- packages/agent-core/src/session/rpc.ts | 10 ------ .../agent-core/src/skill/builtin/index.ts | 17 +++++----- packages/agent-core/src/skill/scanner.ts | 17 +++------- .../tools/builtin/collaboration/ask-user.ts | 14 +++------ .../test/agent/compaction/micro.test.ts | 8 +++-- packages/agent-core/test/agent/tool.test.ts | 14 +++------ .../agent-core/test/harness/runtime.test.ts | 6 ++-- packages/agent-core/test/session/goal.test.ts | 18 +++++------ packages/agent-core/test/session/init.test.ts | 6 ++-- .../test/skill/builtin-sub-skill.test.ts | 31 ++++++------------- .../agent-core/test/skill/scanner.test.ts | 10 +++--- .../agent-core/test/tools/ask-user.test.ts | 29 +++++++++++------ packages/agent-core/test/tools/goal.test.ts | 7 ++--- 35 files changed, 124 insertions(+), 286 deletions(-) create mode 100644 .changeset/release-experimental-features.md diff --git a/.changeset/release-experimental-features.md b/.changeset/release-experimental-features.md new file mode 100644 index 0000000000..7b729c1525 --- /dev/null +++ b/.changeset/release-experimental-features.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core": minor +"@moonshot-ai/kimi-code": minor +--- + +Make goals, swarm mode, background questions, micro compaction, and sub-skill discovery available without experimental opt-ins. diff --git a/apps/kimi-code/src/cli/goal-prompt.ts b/apps/kimi-code/src/cli/goal-prompt.ts index 7a685178f1..5ab0cfe859 100644 --- a/apps/kimi-code/src/cli/goal-prompt.ts +++ b/apps/kimi-code/src/cli/goal-prompt.ts @@ -48,11 +48,7 @@ const GOAL_PREFIX = /^\/goal(\s|$)/; * prompt). Non-create goal subcommands are not supported headless and fall * through to normal prompt handling. */ -export function parseHeadlessGoalCreate( - prompt: string, - flagEnabled: boolean, -): HeadlessGoalCreate | undefined { - if (!flagEnabled) return undefined; +export function parseHeadlessGoalCreate(prompt: string): HeadlessGoalCreate | undefined { const trimmed = prompt.trim(); if (!GOAL_PREFIX.test(trimmed)) return undefined; const args = trimmed.replace(/^\/goal/, '').trim(); diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index 32348cf6c9..0e40922444 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -20,7 +20,6 @@ import { import { resolve } from 'pathe'; import { CLI_SHUTDOWN_TIMEOUT_MS } from '#/constant/app'; -import { experimentalFeatureMap } from '#/utils/experimental-features'; import type { CLIOptions, PromptOutputFormat } from './options'; import { @@ -148,8 +147,7 @@ export async function runPrompt( // the turn-run alive across continuation turns, so the normal prompt-turn // waiter blocks until the goal is terminal; we then emit a summary and set a // distinct exit code. - const flagMap = experimentalFeatureMap(await harness.getExperimentalFeatures()); - const goalCreate = parseHeadlessGoalCreate(opts.prompt!, flagMap['goal_command'] === true); + const goalCreate = parseHeadlessGoalCreate(opts.prompt!); if (goalCreate !== undefined) { await runHeadlessGoal(session, goalCreate, goalModel, outputFormat, stdout, stderr); } else { diff --git a/apps/kimi-code/src/tui/commands/registry.ts b/apps/kimi-code/src/tui/commands/registry.ts index 6a75c42d6d..464cc770db 100644 --- a/apps/kimi-code/src/tui/commands/registry.ts +++ b/apps/kimi-code/src/tui/commands/registry.ts @@ -82,7 +82,6 @@ export const BUILTIN_SLASH_COMMANDS = [ aliases: [], description: 'Toggle swarm mode or run one task in swarm mode', priority: 100, - experimentalFlag: 'agent_swarm', completeArgs: swarmArgumentCompletions, availability: 'idle-only', }, @@ -179,7 +178,6 @@ export const BUILTIN_SLASH_COMMANDS = [ aliases: [], description: 'Start or manage an autonomous goal', priority: 80, - experimentalFlag: 'goal_command', // No argumentHint: the menu description stays as short as every other // command's. The subcommands (status/pause/resume/cancel/replace) surface in // the argument autocomplete list once the user types `/goal ` (see diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 6e0ade688d..870157b9ce 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -1041,9 +1041,7 @@ export class KimiTUI { async syncRuntimeState(session: Session = this.requireSession()): Promise { const [status, goalResult] = await Promise.all([ session.getStatus(), - isExperimentalFlagEnabled('goal_command') - ? session.getGoal() - : Promise.resolve({ goal: null }), + session.getGoal(), ]); this.setAppState({ sessionId: session.id, diff --git a/apps/kimi-code/test/cli/goal-prompt.test.ts b/apps/kimi-code/test/cli/goal-prompt.test.ts index b7966a5a38..30a6513329 100644 --- a/apps/kimi-code/test/cli/goal-prompt.test.ts +++ b/apps/kimi-code/test/cli/goal-prompt.test.ts @@ -40,19 +40,15 @@ describe('goalExitCode', () => { }); describe('parseHeadlessGoalCreate', () => { - it('returns undefined when the flag is disabled', () => { - expect(parseHeadlessGoalCreate('/goal Ship feature X', false)).toBeUndefined(); - }); - it('parses a create command into objective + replace', () => { - const result = parseHeadlessGoalCreate('/goal Ship feature X', true); + const result = parseHeadlessGoalCreate('/goal Ship feature X'); expect(result).toEqual({ objective: 'Ship feature X', replace: false }); }); it('returns undefined for non-goal prompts and non-create subcommands', () => { - expect(parseHeadlessGoalCreate('say hello', true)).toBeUndefined(); - expect(parseHeadlessGoalCreate('/goal status', true)).toBeUndefined(); - expect(parseHeadlessGoalCreate('/goal pause', true)).toBeUndefined(); + expect(parseHeadlessGoalCreate('say hello')).toBeUndefined(); + expect(parseHeadlessGoalCreate('/goal status')).toBeUndefined(); + expect(parseHeadlessGoalCreate('/goal pause')).toBeUndefined(); }); }); @@ -237,7 +233,7 @@ describe('runPrompt headless goal mode', () => { expect(stdout.text()).not.toContain('"goalId":null'); }); - it('treats /goal as a normal prompt when the flag is disabled', async () => { + it('creates a headless goal without reading experimental features', async () => { mocks.experimentalFeatures = []; const stdout = writer(); const stderr = writer(); @@ -246,8 +242,8 @@ describe('runPrompt headless goal mode', () => { stderr, process: { once: () => {}, off: () => {}, exit: () => undefined as never }, }); - expect(mocks.session.createGoal).not.toHaveBeenCalled(); - expect(mocks.session.prompt).toHaveBeenCalled(); + expect(mocks.session.createGoal).toHaveBeenCalled(); + expect(mocks.session.prompt).toHaveBeenCalledWith('Ship feature X'); }); it('validates the resumed session model before creating a headless goal', async () => { diff --git a/apps/kimi-code/test/tui/commands/goal.test.ts b/apps/kimi-code/test/tui/commands/goal.test.ts index 9b55fbe3bd..ace8e94b63 100644 --- a/apps/kimi-code/test/tui/commands/goal.test.ts +++ b/apps/kimi-code/test/tui/commands/goal.test.ts @@ -694,7 +694,6 @@ describe('dispatchInput /goal integration', () => { }); it('routes /goal through the real resolver, creates the goal, and sends the objective', async () => { - setExperimentalFeatures([{ id: 'goal_command', enabled: true }]); const { host, session } = makeHost(); dispatchInput(host, '/goal Ship feature X'); @@ -707,18 +706,6 @@ describe('dispatchInput /goal integration', () => { expect(host.sendNormalUserInput).toHaveBeenCalledWith('Ship feature X'); expect(host.sendNormalUserInput).not.toHaveBeenCalledWith('/goal Ship feature X'); }); - - it('treats /goal as a normal message when the flag is disabled', async () => { - setExperimentalFeatures([]); - const { host, session } = makeHost(); - - dispatchInput(host, '/goal Ship feature X'); - - await vi.waitFor(() => { - expect(host.sendNormalUserInput).toHaveBeenCalledWith('/goal Ship feature X'); - }); - expect(session.createGoal).not.toHaveBeenCalled(); - }); }); describe('goalArgumentCompletions', () => { diff --git a/apps/kimi-code/test/tui/commands/registry.test.ts b/apps/kimi-code/test/tui/commands/registry.test.ts index 510206edff..edfeaa106a 100644 --- a/apps/kimi-code/test/tui/commands/registry.test.ts +++ b/apps/kimi-code/test/tui/commands/registry.test.ts @@ -51,7 +51,7 @@ describe('built-in slash command registry', () => { it('keeps swarm mode changes and swarm tasks idle-only', () => { const swarm = findBuiltInSlashCommand('swarm'); expect(swarm).toBeDefined(); - expect((swarm as KimiSlashCommand).experimentalFlag).toBe('agent_swarm'); + expect((swarm as KimiSlashCommand).experimentalFlag).toBeUndefined(); expect(resolveSlashCommandAvailability(swarm!, 'on')).toBe('idle-only'); expect(resolveSlashCommandAvailability(swarm!, 'off')).toBe('idle-only'); expect(resolveSlashCommandAvailability(swarm!, 'Ship feature X')).toBe('idle-only'); @@ -99,10 +99,10 @@ describe('built-in slash command registry', () => { ]); }); - it('registers goal behind the goal_command flag with subcommand-aware availability', () => { + it('registers goal with subcommand-aware availability', () => { const goal = findBuiltInSlashCommand('goal'); expect(goal).toBeDefined(); - expect((goal as KimiSlashCommand).experimentalFlag).toBe('goal_command'); + expect((goal as KimiSlashCommand).experimentalFlag).toBeUndefined(); expect(resolveSlashCommandAvailability(goal!, '')).toBe('always'); expect(resolveSlashCommandAvailability(goal!, 'status')).toBe('always'); expect(resolveSlashCommandAvailability(goal!, 'pause')).toBe('always'); diff --git a/apps/kimi-code/test/tui/commands/resolve.test.ts b/apps/kimi-code/test/tui/commands/resolve.test.ts index b1a05bfe97..35624ab510 100644 --- a/apps/kimi-code/test/tui/commands/resolve.test.ts +++ b/apps/kimi-code/test/tui/commands/resolve.test.ts @@ -220,15 +220,7 @@ describe('resolveSlashCommandInput', () => { }); }); - it('treats /swarm as a normal message when agent_swarm is disabled', () => { - expect(resolve('/swarm on')).toEqual({ - kind: 'message', - input: '/swarm on', - }); - }); - - it('resolves /swarm when agent_swarm is enabled', () => { - setExperimentalFeatures([{ id: 'agent_swarm', enabled: true }]); + it('resolves /swarm without an experimental flag', () => { expect(resolve('/swarm Ship feature X')).toMatchObject({ kind: 'builtin', name: 'swarm', @@ -243,8 +235,7 @@ describe('goal command resolution', () => { setExperimentalFeatures([]); }); - it('resolves /goal to the builtin command when goal_command is enabled', () => { - setExperimentalFeatures([{ id: 'goal_command', enabled: true }]); + it('resolves /goal to the builtin command without an experimental flag', () => { expect(resolve('/goal Ship feature X')).toMatchObject({ kind: 'builtin', name: 'goal', @@ -252,16 +243,7 @@ describe('goal command resolution', () => { }); }); - it('treats /goal as a normal message when goal_command is disabled', () => { - setExperimentalFeatures([]); - expect(resolve('/goal Ship feature X')).toEqual({ - kind: 'message', - input: '/goal Ship feature X', - }); - }); - it('blocks goal creation while streaming', () => { - setExperimentalFeatures([{ id: 'goal_command', enabled: true }]); expect(resolve('/goal Ship feature X', { isStreaming: true })).toEqual({ kind: 'blocked', commandName: 'goal', @@ -270,7 +252,6 @@ describe('goal command resolution', () => { }); it('does not block status/pause/cancel/bare goal while streaming', () => { - setExperimentalFeatures([{ id: 'goal_command', enabled: true }]); for (const sub of ['status', 'pause', 'cancel']) { expect(resolve(`/goal ${sub}`, { isStreaming: true })).toMatchObject({ kind: 'builtin', diff --git a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts index 1d4396a169..130bdd3507 100644 --- a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts @@ -277,17 +277,18 @@ describe("KimiTUI startup", () => { expect(driver.state.appState.goal).toEqual(goal); }); - it("does not sync goal state while the goal flag is disabled", async () => { + it("syncs goal state regardless of the goal flag", async () => { + const goal = goalSnapshot(); const session = makeSession({ - getGoal: vi.fn(async () => ({ goal: goalSnapshot() })), + getGoal: vi.fn(async () => ({ goal })), }); const harness = makeHarness(session); const driver = makeDriver(harness, makeStartupInput()); await expect(driver.init()).resolves.toBe(false); - expect(session.getGoal).not.toHaveBeenCalled(); - expect(driver.state.appState.goal).toBeNull(); + expect(session.getGoal).toHaveBeenCalledOnce(); + expect(driver.state.appState.goal).toEqual(goal); }); it("clears goal state when closing the current session", async () => { diff --git a/docs/en/configuration/config-files.md b/docs/en/configuration/config-files.md index 1a414f8cba..687f3b5ebe 100644 --- a/docs/en/configuration/config-files.md +++ b/docs/en/configuration/config-files.md @@ -51,11 +51,6 @@ reserved_context_size = 50000 max_running_tasks = 4 keep_alive_on_exit = false -[experimental] -goal_command = false -micro_compaction = false -background_ask = false - [[permission.rules]] decision = "allow" pattern = "Read" @@ -89,7 +84,7 @@ Fields in the config file fall into two categories: **top-level scalars** that d | `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) | -| `experimental` | `table` | — | Persistent experimental feature toggles → [`experimental`](#experimental) | +| `experimental` | `table` | — | Reserved experimental feature opt-ins → [`experimental`](#experimental) | | `services` | `table` | — | Built-in external service configuration → [`services`](#services) | | `permission` | `table` | — | Initial permission rules → [`permission`](#permission) | | `hooks` | `array` | — | Lifecycle hooks; see [Hooks](../customization/hooks.md) | @@ -177,15 +172,7 @@ You can also switch models temporarily without touching the config file — by s ## `experimental` -`experimental` stores persistent opt-ins for features that are not public by default yet. You can edit this table directly or run `/experiments` in the TUI. The TUI panel stages changes locally until you confirm them, then writes `config.toml` and reloads the current session. Each TOML key is the experimental flag ID, for example `goal_command`. - -| Field | Type | Default | Description | -| --- | --- | --- | --- | -| `goal_command` | `boolean` | `false` | Enable `/goal` and goal-management tools | -| `micro_compaction` | `boolean` | `false` | Trim older large tool results from context while preserving recent conversation | -| `background_ask` | `boolean` | `false` | Allow `AskUserQuestion` to start a background question task when the Agent can continue working | - -Environment variables take priority over this table. `KIMI_CODE_EXPERIMENTAL_` overrides one feature, and `KIMI_CODE_EXPERIMENTAL_FLAG=1` enables all experimental features for that process — see the full list in [Environment variables → Runtime switches](./env-vars.md#runtime-switches). When a feature is controlled by the environment, `/experiments` shows it as locked. +`experimental` is reserved for persistent opt-ins to features that are not public by default. There are currently no user-facing experimental features, so you can omit this table or leave it empty. ## `services` diff --git a/docs/en/configuration/env-vars.md b/docs/en/configuration/env-vars.md index 9a61e23e0e..5f5606021a 100644 --- a/docs/en/configuration/env-vars.md +++ b/docs/en/configuration/env-vars.md @@ -125,10 +125,7 @@ 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` | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | | `KIMI_CODE_PLUGIN_MARKETPLACE_URL` | Override the plugin marketplace JSON loaded by `/plugins` | URL or local path | -| `KIMI_CODE_EXPERIMENTAL_FLAG` | Enable all experimental features for this process; takes higher priority than [`[experimental]`](./config-files.md#experimental) in `config.toml` | `1`, `true`, `yes`, `on` | -| `KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND` | Override [`[experimental].goal_command`](./config-files.md#experimental) for this process | Truthy or falsy | -| `KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION` | Override [`[experimental].micro_compaction`](./config-files.md#experimental) for this process | Truthy or falsy | -| `KIMI_CODE_EXPERIMENTAL_BACKGROUND_ASK` | Override [`[experimental].background_ask`](./config-files.md#experimental) for this process | Truthy or falsy | +| `KIMI_CODE_EXPERIMENTAL_FLAG` | Enable all registered experimental features for this process; currently no user-facing feature requires it | `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 | | `KIMI_MODEL_TEMPERATURE` | Sampling temperature for every request; applies to the `kimi` provider only (global — independent of `KIMI_MODEL_NAME`) | Number, e.g. `0.3` | diff --git a/docs/en/guides/goals.md b/docs/en/guides/goals.md index 49c6e27db7..82242df6fa 100644 --- a/docs/en/guides/goals.md +++ b/docs/en/guides/goals.md @@ -2,14 +2,6 @@ Goals keep Kimi Code working toward a defined outcome across turns. Unlike a normal prompt that says what to do next, a goal says what must become true. Use `/goal` when the task has a clear finish line, but the next useful step depends on what the agent learns while it works — for example, fixing a batch of failing tests or tracking down the root cause of a broken build. -::: info -`/goal` is still experimental. Start `kimi` with: - -```sh -KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1 kimi -``` -::: - ## Start a goal Write the objective after `/goal`: @@ -145,7 +137,7 @@ In `manual` permission mode, goal work may pause for tool call approval. For una In non-interactive prompt mode, only goal creation is supported: ```sh -KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1 kimi -p "/goal Fix the failing checkout test" +kimi -p "/goal Fix the failing checkout test" ``` Prompt mode exits with code `0` when the goal completes, `3` when it blocks, and `6` when it pauses. `/goal next` and other management commands are TUI controls. diff --git a/docs/en/reference/slash-commands.md b/docs/en/reference/slash-commands.md index 47538b0403..953352120f 100644 --- a/docs/en/reference/slash-commands.md +++ b/docs/en/reference/slash-commands.md @@ -17,7 +17,7 @@ Some commands are only available in the idle state. Executing these commands whi | `/provider` | — | Open the interactive provider manager to view, add, and remove configured providers. See [Platforms & Models — `/provider` and provider management](../configuration/providers.md#provider-与供应商管理) | Yes | | `/model` | — | Switch the LLM model used in the current session | Yes | | `/settings` | `/config` | Open the settings panel inside the TUI | Yes | -| `/experiments` | `/experimental` | Open the experimental feature panel. Confirm changes to persist them to `config.toml` and reload the current session | Yes | +| `/experiments` | `/experimental` | Open the experimental feature panel | Yes | | `/permission` | — | Select a permission mode | Yes | | `/editor` | — | Configure the external editor launched by `Ctrl-G` | Yes | | `/theme` | — | Switch the terminal UI color theme | Yes | @@ -48,26 +48,13 @@ Some commands are only available in the idle state. Executing these commands whi | `/plan clear` | — | Clear the current plan | No | | `/swarm on\|off` | — | Turn swarm mode on or off without sending a prompt. | Yes | | `/swarm ` | — | Turn swarm mode on, then send `` as a normal prompt. If the turn completes normally, swarm mode turns off automatically. In `manual` permission mode, Kimi Code asks whether to switch to `auto` before starting. | No | -| `/goal [...]` | — | Start or manage an autonomous goal (experimental feature; enable it from `/experiments`, `[experimental].goal_command`, or `KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1`) | See below | +| `/goal [...]` | — | Start or manage an autonomous goal | See below | ::: warning `/yolo` skips approval for regular tool calls. Please make sure you understand the potential risks before enabling it. Plan mode exit approval is not bypassed by `/yolo`; `Bash` inside Plan mode is still subject to the regular `/yolo` allow rules. ::: -## Autonomous Goal (Experimental) - -::: info -`/goal` is an experimental command. Enable it from `/experiments`, or write it in `~/.kimi-code/config.toml`: -```toml -[experimental] -goal_command = true -``` - -You can also override the setting for one process with an environment variable: -```sh -KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1 kimi -``` -::: +## Autonomous Goal `/goal` starts or manages goal mode: a persistent objective that Kimi Code works toward across automatically continuing turns. For usage guidance and examples, see [Goals](../guides/goals.md). @@ -100,7 +87,7 @@ If an upcoming goal needs to start with `manage`, put `--` after `next`: In non-interactive prompt mode, only the create forms start goal mode: ```sh -KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1 kimi -p "/goal Fix the failing checkout test" +kimi -p "/goal Fix the failing checkout test" ``` Prompt mode exits with code `0` when the goal completes, `3` when it blocks, and `6` when it pauses. Other `/goal` subcommands, including `next`, are TUI controls and are not handled by `kimi -p`. diff --git a/docs/zh/configuration/config-files.md b/docs/zh/configuration/config-files.md index 15b71e4cae..fca36f250f 100644 --- a/docs/zh/configuration/config-files.md +++ b/docs/zh/configuration/config-files.md @@ -51,11 +51,6 @@ reserved_context_size = 50000 max_running_tasks = 4 keep_alive_on_exit = false -[experimental] -goal_command = false -micro_compaction = false -background_ask = false - [[permission.rules]] decision = "allow" pattern = "Read" @@ -89,7 +84,7 @@ timeout = 5 | `thinking` | `table` | — | Thinking 模式默认参数 → [`thinking`](#thinking) | | `loop_control` | `table` | — | Agent 循环控制参数 → [`loop_control`](#loop_control) | | `background` | `table` | — | 后台任务运行参数 → [`background`](#background) | -| `experimental` | `table` | — | 持久化实验功能开关 → [`experimental`](#experimental) | +| `experimental` | `table` | — | 预留的实验功能 opt-in → [`experimental`](#experimental) | | `services` | `table` | — | 内置外部服务配置 → [`services`](#services) | | `permission` | `table` | — | 初始权限规则 → [`permission`](#permission) | | `hooks` | `array
` | — | 生命周期 hook,详见 [Hooks](../customization/hooks.md) | @@ -177,15 +172,7 @@ max_context_size = 1047576 ## `experimental` -`experimental` 存放尚未默认公开的功能开关。可以直接编辑这个表,也可以在 TUI 中运行 `/experiments`。TUI 面板会先暂存选择,确认后写入 `config.toml` 并重载当前会话。每个 TOML key 就是实验 flag ID,例如 `goal_command`。 - -| 字段 | 类型 | 默认值 | 说明 | -| --- | --- | --- | --- | -| `goal_command` | `boolean` | `false` | 启用 `/goal` 和 goal 管理工具 | -| `micro_compaction` | `boolean` | `false` | 清理较旧的大型工具结果内容,同时保留最近对话 | -| `background_ask` | `boolean` | `false` | 允许 `AskUserQuestion` 在 Agent 可以继续工作时启动后台提问任务 | - -环境变量优先级高于这个表。`KIMI_CODE_EXPERIMENTAL_` 可以覆盖单个功能,`KIMI_CODE_EXPERIMENTAL_FLAG=1` 会在当前进程启用所有实验功能——完整变量列表见[环境变量 → 运行时开关](./env-vars.md#运行时开关)。某个功能被环境变量控制时,`/experiments` 会显示为 locked。 +`experimental` 预留给尚未默认公开功能的持久化 opt-in。目前没有用户可见的实验功能,因此可以省略这个表,或保留为空表。 ## `services` diff --git a/docs/zh/configuration/env-vars.md b/docs/zh/configuration/env-vars.md index 36d88ca80e..ca4f3d9247 100644 --- a/docs/zh/configuration/env-vars.md +++ b/docs/zh/configuration/env-vars.md @@ -125,10 +125,7 @@ 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_PLUGIN_MARKETPLACE_URL` | 替换 `/plugins` 加载的 marketplace JSON | URL 或本地路径 | -| `KIMI_CODE_EXPERIMENTAL_FLAG` | 在当前进程启用所有实验功能,优先级高于 `config.toml` 的 [`[experimental]`](./config-files.md#experimental) | `1`、`true`、`yes`、`on` | -| `KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND` | 覆盖当前进程的 [`[experimental].goal_command`](./config-files.md#experimental) | 真值或假值 | -| `KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION` | 覆盖当前进程的 [`[experimental].micro_compaction`](./config-files.md#experimental) | 真值或假值 | -| `KIMI_CODE_EXPERIMENTAL_BACKGROUND_ASK` | 覆盖当前进程的 [`[experimental].background_ask`](./config-files.md#experimental) | 真值或假值 | +| `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 | | `KIMI_MODEL_TEMPERATURE` | 每次请求的采样温度,仅对 `kimi` 供应商生效(全局生效,不依赖 `KIMI_MODEL_NAME`) | 数字,如 `0.3` | diff --git a/docs/zh/guides/goals.md b/docs/zh/guides/goals.md index bb69878c3b..1e5dc04ae6 100644 --- a/docs/zh/guides/goals.md +++ b/docs/zh/guides/goals.md @@ -2,14 +2,6 @@ 目标(goal)让 Kimi Code 在多个轮次中持续朝一个明确结果工作——不同于普通提示词只说"下一步做什么",目标说的是"最终要达成什么状态"。当任务有清晰终点,但下一步取决于 Agent 工作中发现的信息时,使用 `/goal`,例如:修复一批失败的测试、追踪并修复构建失败的根因。 -::: info 说明 -`/goal` 仍是实验功能,需要在启动 `kimi` 时设置相应的环境变量: - -```sh -KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1 kimi -``` -::: - ## 开始目标 在 `/goal` 命令后写目标: @@ -145,7 +137,7 @@ Agent 有时会很快完成一个目标。如果一次只能安排一个目标 在非交互式 prompt 模式中,只支持创建目标: ```sh -KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1 kimi -p "/goal 修复 checkout 测试失败" +kimi -p "/goal 修复 checkout 测试失败" ``` Prompt 模式在目标完成时以退出码 `0` 退出,在目标阻塞时以 `3` 退出,在目标暂停时以 `6` 退出。`/goal next` 和其它管理命令都是 TUI 控制命令。 diff --git a/docs/zh/reference/slash-commands.md b/docs/zh/reference/slash-commands.md index 959df39b14..3b7c247d2b 100644 --- a/docs/zh/reference/slash-commands.md +++ b/docs/zh/reference/slash-commands.md @@ -17,7 +17,7 @@ | `/provider` | — | 打开交互式供应商管理器,查看、添加和删除已配置的供应商。详见[平台与模型 — `/provider` 与供应商管理](../configuration/providers.md#provider-与供应商管理) | 是 | | `/model` | — | 切换当前会话使用的 LLM 模型 | 是 | | `/settings` | `/config` | 打开 TUI 内的设置面板 | 是 | -| `/experiments` | `/experimental` | 打开实验功能面板。确认后把变更持久化到 `config.toml` 并重载当前会话 | 是 | +| `/experiments` | `/experimental` | 打开实验功能面板 | 是 | | `/permission` | — | 选择权限模式 | 是 | | `/editor` | — | 配置 `Ctrl-G` 调起的外部编辑器 | 是 | | `/theme` | — | 切换终端 UI 配色主题 | 是 | @@ -46,26 +46,13 @@ | `/plan clear` | — | 清除当前 plan 方案 | 否 | | `/swarm on\|off` | — | 开启或关闭 swarm mode,但不发送提示词。 | 是 | | `/swarm ` | — | 先开启 swarm mode,再把 `` 作为普通提示词发送。如果该轮次正常完成,swarm mode 会自动关闭。若当前是 `manual` 权限模式,启动前会提示是否切换到 `auto`。 | 否 | -| `/goal [...]` | — | 开始或管理目标模式(实验功能;可通过 `/experiments`、`[experimental].goal_command` 或 `KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1` 启用) | 见下文 | +| `/goal [...]` | — | 开始或管理目标模式 | 见下文 | ::: warning 注意 `/yolo` 会跳过普通工具调用的审批确认,使用前请确保了解可能的风险。Plan 模式的退出审批不会被 `/yolo` 跳过;Plan 模式下的 `Bash` 也按 `/yolo` 的普通放行规则处理。 ::: -## 目标模式(实验功能) - -::: info -`/goal` 是实验命令。可以通过 `/experiments` 启用,也可以写入 `~/.kimi-code/config.toml`: -```toml -[experimental] -goal_command = true -``` - -也可以用环境变量只覆盖当前进程: -```sh -KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1 kimi -``` -::: +## 目标模式 `/goal` 用于开始或管理目标模式:Kimi Code 会在自动续跑的轮次中持续朝一个持久目标工作。使用指导和示例见[使用目标模式](../guides/goals.md)。 @@ -98,7 +85,7 @@ KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1 kimi 在非交互式 prompt 模式中,只有创建形式会启动目标模式: ```sh -KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1 kimi -p "/goal 修复 checkout 测试失败" +kimi -p "/goal 修复 checkout 测试失败" ``` Prompt 模式在目标完成时以退出码 `0` 退出,在目标阻塞时以 `3` 退出,在目标暂停时以 `6` 退出。其它 `/goal` 子命令,包括 `next`,都是 TUI 控制命令,不由 `kimi -p` 处理。 diff --git a/packages/agent-core/src/agent/compaction/micro.ts b/packages/agent-core/src/agent/compaction/micro.ts index da352db697..dd6d5bcbe2 100644 --- a/packages/agent-core/src/agent/compaction/micro.ts +++ b/packages/agent-core/src/agent/compaction/micro.ts @@ -44,8 +44,6 @@ export class MicroCompaction { } detect(): void { - if (!this.agent.experimentalFlags.enabled('micro_compaction')) return; - const config = this.config; const { history, lastAssistantAt } = this.agent.context; const cacheAgeMs = lastAssistantAt === null ? null : Date.now() - lastAssistantAt; @@ -77,8 +75,6 @@ export class MicroCompaction { } compact(messages: readonly ContextMessage[]): readonly ContextMessage[] { - if (!this.agent.experimentalFlags.enabled('micro_compaction')) return messages; - const config = this.config; const result: ContextMessage[] = []; let i = 0; diff --git a/packages/agent-core/src/agent/injection/manager.ts b/packages/agent-core/src/agent/injection/manager.ts index d95f0cddc6..99c9cd07e1 100644 --- a/packages/agent-core/src/agent/injection/manager.ts +++ b/packages/agent-core/src/agent/injection/manager.ts @@ -69,7 +69,6 @@ export class InjectionManager { } private activeGoalInjector(): GoalInjector | null { - if (!this.agent.experimentalFlags.enabled('goal_command')) return null; return this.goalInjector; } } diff --git a/packages/agent-core/src/agent/tool/index.ts b/packages/agent-core/src/agent/tool/index.ts index fb16cca5e0..8643b207ec 100644 --- a/packages/agent-core/src/agent/tool/index.ts +++ b/packages/agent-core/src/agent/tool/index.ts @@ -375,9 +375,7 @@ export class ToolManager { this.enabledTools.has('TaskList') && this.enabledTools.has('TaskOutput') && this.enabledTools.has('TaskStop'); - const goalCommandEnabled = - this.agent.experimentalFlags.enabled('goal_command') && this.agent.type === 'main'; - const agentSwarmEnabled = this.agent.experimentalFlags.enabled('agent_swarm'); + const goalToolsEnabled = this.agent.type === 'main'; this.builtinTools = new Map( [ new b.ReadTool(kaos, workspace), @@ -392,11 +390,11 @@ export class ToolManager { new b.ReadMediaFileTool(kaos, workspace, modelCapabilities, videoUploader), new b.EnterPlanModeTool(this.agent), new b.ExitPlanModeTool(this.agent), - // Goal tools are main-agent-only and gated by the goal_command flag. - goalCommandEnabled && new b.CreateGoalTool(this.agent), - goalCommandEnabled && new b.GetGoalTool(this.agent), - goalCommandEnabled && new b.SetGoalBudgetTool(this.agent), - goalCommandEnabled && new b.UpdateGoalTool(this.agent), + // Goal tools are main-agent-only. + goalToolsEnabled && new b.CreateGoalTool(this.agent), + goalToolsEnabled && new b.GetGoalTool(this.agent), + goalToolsEnabled && new b.SetGoalBudgetTool(this.agent), + goalToolsEnabled && new b.UpdateGoalTool(this.agent), this.agent.rpc?.requestQuestion && new b.AskUserQuestionTool(this.agent), new b.TodoListTool(this.toolStore), new b.TaskListTool(background), @@ -417,7 +415,6 @@ export class ToolManager { }, ), this.agent.subagentHost && - agentSwarmEnabled && new b.AgentSwarmTool(this.agent.subagentHost, this.agent.swarmMode), toolServices?.webSearcher && new b.WebSearchTool(toolServices.webSearcher), toolServices?.urlFetcher && new b.FetchURLTool(toolServices.urlFetcher), diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index 8901ca3fde..871f5fe1b6 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -112,7 +112,7 @@ export class TurnFlow { /** Whether goal-mode runtime behavior (continuation, abnormal-end marking) applies. */ private get goalRuntimeEnabled(): boolean { - return this.agent.experimentalFlags.enabled('goal_command') && this.agent.type === 'main'; + return this.agent.type === 'main'; } // Returns the new turnId, or null if the turn was marked as resuming. @@ -571,8 +571,8 @@ export class TurnFlow { const deduper = new ToolCallDeduplicator({ telemetry: this.agent.telemetry }); await this.agent.mcp?.waitForInitialLoad(signal); // Surface the active goal at the start of the turn (append-only; no-op when - // goal mode is off). Each goal continuation is its own turn, so this re-injects - // the reminder once per turn rather than per step, preserving prompt caching. + // there is no active goal). Each goal continuation is its own turn, so this + // re-injects the reminder once per turn rather than per step, preserving prompt caching. await this.agent.injection.injectGoal(); while (true) { signal.throwIfAborted(); diff --git a/packages/agent-core/src/session/rpc.ts b/packages/agent-core/src/session/rpc.ts index 8cbee4ae54..b32853e3da 100644 --- a/packages/agent-core/src/session/rpc.ts +++ b/packages/agent-core/src/session/rpc.ts @@ -111,27 +111,22 @@ export class SessionAPIImpl implements PromisableMethods { // --- Goal lifecycle (delegates to the session goal store) ------------- createGoal(payload: CreateGoalPayload) { - this.assertGoalCommandEnabled(); return this.session.goals.createGoal({ ...payload, actor: 'user' }); } getGoal(_payload: EmptyPayload) { - this.assertGoalCommandEnabled(); return this.session.goals.getGoal(); } pauseGoal(payload: GoalControlPayload) { - this.assertGoalCommandEnabled(); return this.session.goals.pauseGoal({ actor: 'user', reason: payload.reason }); } resumeGoal(payload: GoalControlPayload) { - this.assertGoalCommandEnabled(); return this.session.goals.resumeGoal({ actor: 'user', reason: payload.reason }); } async cancelGoal(payload: GoalControlPayload) { - this.assertGoalCommandEnabled(); const snapshot = await this.session.goals.cancelGoal({ actor: 'user', reason: payload.reason, @@ -147,11 +142,6 @@ export class SessionAPIImpl implements PromisableMethods { return snapshot; } - private assertGoalCommandEnabled(): void { - if (this.session.experimentalFlags.enabled('goal_command')) return; - throw new KimiError(ErrorCodes.NOT_IMPLEMENTED, 'Goal command is disabled'); - } - async prompt({ agentId, ...payload }: AgentScopedPayload) { if (agentId === 'main') { await this.updatePromptMetadata(promptMetadataTextFromPayload(payload)); diff --git a/packages/agent-core/src/skill/builtin/index.ts b/packages/agent-core/src/skill/builtin/index.ts index 36a28eca80..ad18007b73 100644 --- a/packages/agent-core/src/skill/builtin/index.ts +++ b/packages/agent-core/src/skill/builtin/index.ts @@ -1,4 +1,3 @@ -import { flags } from '../../flags/resolver'; import type { SkillRegistry } from '../registry'; import { MCP_CONFIG_SKILL } from './mcp-config'; import { @@ -8,22 +7,20 @@ import { } from './sub-skill'; import { UPDATE_CONFIG_SKILL } from './update-config'; -type SubSkillFlagResolver = { - enabled(id: 'sub_skill'): boolean; +type RegisterBuiltinSkillsOptions = { + /** Kept for API compatibility; built-in skills are no longer gated by this option. */ + readonly experimentalFlags?: unknown; }; export function registerBuiltinSkills( registry: SkillRegistry, - options: { readonly experimentalFlags?: SubSkillFlagResolver } = {}, + _options: RegisterBuiltinSkillsOptions = {}, ): void { - const experimentalFlags = options.experimentalFlags ?? flags; registry.registerBuiltinSkill(MCP_CONFIG_SKILL); registry.registerBuiltinSkill(UPDATE_CONFIG_SKILL); - if (experimentalFlags.enabled('sub_skill')) { - registry.registerBuiltinSkill(SUB_SKILL_PARENT); - registry.registerBuiltinSkill(SUB_SKILL_REVIEW); - registry.registerBuiltinSkill(SUB_SKILL_CONSOLIDATE); - } + registry.registerBuiltinSkill(SUB_SKILL_PARENT); + registry.registerBuiltinSkill(SUB_SKILL_REVIEW); + registry.registerBuiltinSkill(SUB_SKILL_CONSOLIDATE); } export { diff --git a/packages/agent-core/src/skill/scanner.ts b/packages/agent-core/src/skill/scanner.ts index b0e02a9ba5..1e85dbbf23 100644 --- a/packages/agent-core/src/skill/scanner.ts +++ b/packages/agent-core/src/skill/scanner.ts @@ -1,7 +1,6 @@ import { promises as fs } from 'node:fs'; import path from 'pathe'; -import { flags } from '../flags/resolver'; import { SkillParseError, UnsupportedSkillTypeError, parseSkillFromFile } from './parser'; import type { SkillDefinition, SkillRoot, SkillSource, SkippedSkill } from './types'; import { normalizeSkillName } from './types'; @@ -13,10 +12,6 @@ const USER_GENERIC_DIRS = ['.agents/skills'] as const; const PROJECT_BRAND_DIRS = ['.kimi-code/skills'] as const; const PROJECT_GENERIC_DIRS = ['.agents/skills'] as const; -type SubSkillFlagResolver = { - enabled(id: 'sub_skill'): boolean; -}; - // Bounds recursion so a directory symlink cycle inside a skill root cannot // loop forever. Real skill trees are 1-3 levels deep. const MAX_SKILL_SCAN_DEPTH = 8; @@ -45,7 +40,8 @@ export interface ResolveSkillRootsOptions { export interface DiscoverSkillsOptions { readonly roots: readonly SkillRoot[]; - readonly experimentalFlags?: SubSkillFlagResolver; + /** Kept for API compatibility; sub-skill discovery is no longer gated by this option. */ + readonly experimentalFlags?: unknown; readonly onWarning?: (message: string, cause?: unknown) => void; readonly onSkippedByPolicy?: (skill: SkippedSkill) => void; readonly onDiscoveredSkill?: (skill: SkillDefinition) => void; @@ -142,7 +138,6 @@ export async function discoverSkills( const isFile = options.isFile ?? defaultIsFile; const isDir = options.isDir ?? defaultIsDir; const parse = options.parse ?? parseSkillFromFile; - const subSkillFlags = options.experimentalFlags ?? flags; const warn = options.onWarning ?? (() => {}); const skip = options.onSkippedByPolicy ?? (() => {}); const byName = new Map(); @@ -190,7 +185,7 @@ export async function discoverSkills( warn, skip, }); - if (skill !== undefined && hasSubSkillEnabled(skill, subSkillFlags)) { + if (skill !== undefined && hasSubSkillEnabled(skill)) { allowedSubSkillBundles.add(entry); } } @@ -399,11 +394,7 @@ async function parseAndRegister(input: { } } -function hasSubSkillEnabled( - skill: SkillDefinition, - experimentalFlags: SubSkillFlagResolver, -): boolean { - if (!experimentalFlags.enabled('sub_skill')) return false; +function hasSubSkillEnabled(skill: SkillDefinition): boolean { const nested = skill.metadata['metadata']; const nestedFlag = typeof nested === 'object' && nested !== null diff --git a/packages/agent-core/src/tools/builtin/collaboration/ask-user.ts b/packages/agent-core/src/tools/builtin/collaboration/ask-user.ts index 3d63456d6b..370867dc08 100644 --- a/packages/agent-core/src/tools/builtin/collaboration/ask-user.ts +++ b/packages/agent-core/src/tools/builtin/collaboration/ask-user.ts @@ -98,18 +98,14 @@ export class AskUserQuestionTool implements BuiltinTool { readonly name = 'AskUserQuestion' as const; readonly description: string; readonly parameters: Record; - private readonly backgroundEnabled: boolean; constructor(private readonly agent: Agent) { - this.backgroundEnabled = agent.experimentalFlags.enabled('background_ask'); - this.description = this.backgroundEnabled - ? `${DESCRIPTION}- Set background=true when you can keep working without the answer. This starts a background question task and returns a task_id immediately. The answer arrives automatically in a later turn — you do not need to poll, sleep, or check on it. Continue with other work; never fabricate or predict the answer.` - : DESCRIPTION; + this.description = `${DESCRIPTION}- Set background=true when you can keep working without the answer. This starts a background question task and returns a task_id immediately. The answer arrives automatically in a later turn — you do not need to poll, sleep, or check on it. Continue with other work; never fabricate or predict the answer.`; this.parameters = toInputJsonSchema(this.inputSchema()); } resolveExecution(args: AskUserQuestionInput): ToolExecution { - const isBackground = args.background === true && this.backgroundEnabled; + const isBackground = args.background === true; return { description: isBackground ? `Starting background question: ${questionDescription(args.questions)}` @@ -127,7 +123,7 @@ export class AskUserQuestionTool implements BuiltinTool { turnId, }: ExecutableToolContext, ): Promise { - if (args.background === true && this.backgroundEnabled) { + if (args.background === true) { return this.executeInBackground(args, { toolCallId, turnId, signal }); } @@ -135,9 +131,7 @@ export class AskUserQuestionTool implements BuiltinTool { } private inputSchema(): z.ZodType { - return this.backgroundEnabled - ? AskUserQuestionInputSchemaWithBackground - : AskUserQuestionInputBaseSchema; + return AskUserQuestionInputSchemaWithBackground; } private async executeQuestion( diff --git a/packages/agent-core/test/agent/compaction/micro.test.ts b/packages/agent-core/test/agent/compaction/micro.test.ts index 287e1cbddd..0dfff78537 100644 --- a/packages/agent-core/test/agent/compaction/micro.test.ts +++ b/packages/agent-core/test/agent/compaction/micro.test.ts @@ -485,7 +485,7 @@ describe('MicroCompaction', () => { expect(records.filter((record) => record.event === 'micro_compaction_applied')).toHaveLength(1); }); - it('leaves context unchanged when the micro_compaction flag is disabled', () => { + it('applies micro compaction even when the micro_compaction flag is disabled', () => { vi.stubEnv(MICRO_COMPACTION_FLAG_ENV, '0'); vi.useFakeTimers(); const persistence = new InMemoryAgentRecordPersistence(); @@ -494,6 +494,7 @@ describe('MicroCompaction', () => { keepRecentMessages: 0, minContentTokens: 1, cacheMissedThresholdMs: 60 * MINUTE, + minContextUsageRatio: 0, }, persistence, }); @@ -502,9 +503,10 @@ describe('MicroCompaction', () => { appendMicroToolExchange(ctx, 1, { output: 'result one' }); vi.setSystemTime(61 * MINUTE); + ctx.agent.microCompaction.detect(); - expect(toolTexts(ctx.agent.context.messages)).toEqual(['result one']); - expect(lastMicroCompactionCutoff(persistence.records)).toBeUndefined(); + expect(toolTexts(ctx.agent.context.messages)).toEqual([DEFAULT_MARKER]); + expect(lastMicroCompactionCutoff(persistence.records)).toBe(3); }); it('uses the custom marker at the minContentTokens boundary', () => { diff --git a/packages/agent-core/test/agent/tool.test.ts b/packages/agent-core/test/agent/tool.test.ts index 96a1230bf8..56a74a42c9 100644 --- a/packages/agent-core/test/agent/tool.test.ts +++ b/packages/agent-core/test/agent/tool.test.ts @@ -252,22 +252,16 @@ describe('Agent tools', () => { expect(managedBash!.description).toContain('run_in_background=true'); }); - it('gates AgentSwarm behind the agent_swarm flag', () => { + it('exposes AgentSwarm when a subagent host is available', () => { const subagentHost = {} as unknown as SessionSubagentHost; - const disabled = testAgent({ + const ctx = testAgent({ subagentHost, experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS), }); - disabled.configure({ tools: ['AgentSwarm'] }); - expect(disabled.agent.tools.loopTools.some((tool) => tool.name === 'AgentSwarm')).toBe(false); + ctx.configure({ tools: ['AgentSwarm'] }); - const enabled = testAgent({ - subagentHost, - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { agent_swarm: true }), - }); - enabled.configure({ tools: ['AgentSwarm'] }); - expect(enabled.agent.tools.loopTools.some((tool) => tool.name === 'AgentSwarm')).toBe(true); + expect(ctx.agent.tools.loopTools.some((tool) => tool.name === 'AgentSwarm')).toBe(true); }); it('routes registered user tools through tool.call request/response', async () => { diff --git a/packages/agent-core/test/harness/runtime.test.ts b/packages/agent-core/test/harness/runtime.test.ts index 732a2f8253..061f0b5bdb 100644 --- a/packages/agent-core/test/harness/runtime.test.ts +++ b/packages/agent-core/test/harness/runtime.test.ts @@ -145,7 +145,7 @@ goal_command = false expect(experimentalFeatureEnabled(core, 'goal_command')).toBe(true); }); - it('updates the shared experimental resolver without hot-refreshing materialized tools', async () => { + it('updates the shared experimental resolver while goal tools stay available', async () => { tmp = await mkdtemp(join(tmpdir(), 'kimi-core-runtime-')); const homeDir = join(tmp, 'home'); const workDir = join(tmp, 'work'); @@ -179,7 +179,7 @@ goal_command = false expect(session?.experimentalFlags.enabled('goal_command')).toBe(false); expect(mainAgent?.experimentalFlags.enabled('goal_command')).toBe(false); - expect(mainAgent?.tools.data().some((tool) => tool.name === 'CreateGoal')).toBe(false); + expect(mainAgent?.tools.data().some((tool) => tool.name === 'CreateGoal')).toBe(true); await core.setKimiConfig({ experimental: { @@ -189,7 +189,7 @@ goal_command = false expect(session?.experimentalFlags.enabled('goal_command')).toBe(true); expect(mainAgent?.experimentalFlags.enabled('goal_command')).toBe(true); - expect(mainAgent?.tools.data().some((tool) => tool.name === 'CreateGoal')).toBe(false); + expect(mainAgent?.tools.data().some((tool) => tool.name === 'CreateGoal')).toBe(true); await rpc.reloadSession({ sessionId: created.id }); const reloadedMainAgent = core.sessions.get(created.id)?.getReadyAgent('main'); diff --git a/packages/agent-core/test/session/goal.test.ts b/packages/agent-core/test/session/goal.test.ts index 8aabe5a243..335c97e6c3 100644 --- a/packages/agent-core/test/session/goal.test.ts +++ b/packages/agent-core/test/session/goal.test.ts @@ -714,7 +714,7 @@ describe('SessionAPIImpl.updateSessionMetadata goal reservation', () => { }); }); -describe('SessionAPIImpl goal flag gating', () => { +describe('SessionAPIImpl goal lifecycle', () => { function makeSession(sessionDir: string, goalEnabled: boolean): Session { return new Session({ id: 'goal-rpc-flag', @@ -728,22 +728,18 @@ describe('SessionAPIImpl goal flag gating', () => { }); } - it('rejects SDK goal creation when the flag is disabled', async () => { + it('allows SDK goal creation regardless of the scoped flag value', async () => { const sessionDir = await makeTempDir(); const session = makeSession(sessionDir, false); const api = new SessionAPIImpl(session); - let thrown: unknown; - try { - void api.createGoal({ objective: 'work' }); - } catch (error) { - thrown = error; - } - expect(thrown).toMatchObject({ code: ErrorCodes.NOT_IMPLEMENTED }); - expect(session.goals.getGoal().goal).toBeNull(); + const snapshot = await api.createGoal({ objective: 'work' }); + + expect(snapshot.objective).toBe('work'); + expect(api.getGoal({}).goal?.status).toBe('active'); }); - it('allows SDK goal creation when the flag is enabled', async () => { + it('allows SDK goal creation when the scoped flag is enabled', async () => { const sessionDir = await makeTempDir(); const session = makeSession(sessionDir, true); const api = new SessionAPIImpl(session); diff --git a/packages/agent-core/test/session/init.test.ts b/packages/agent-core/test/session/init.test.ts index af67a7fe23..8ab55f8329 100644 --- a/packages/agent-core/test/session/init.test.ts +++ b/packages/agent-core/test/session/init.test.ts @@ -549,7 +549,7 @@ describe('AgentAPI.startBtw', () => { } }); - it('uses session-scoped experimental flags for sub-skill discovery and builtins', async () => { + it('discovers sub-skills and builtins regardless of session-scoped experimental flags', async () => { const workDir = await makeTempDir(); const sessionDir = await makeTempDir(); const skillsRoot = join(workDir, 'skills'); @@ -583,8 +583,8 @@ describe('AgentAPI.startBtw', () => { try { const disabledSkills = await disabledSession.listSkills(); expect(disabledSkills.map((skill) => skill.name)).toContain('outer'); - expect(disabledSkills.map((skill) => skill.name)).not.toContain('inner'); - expect(disabledSkills.map((skill) => skill.name)).not.toContain('sub-skill.consolidate'); + expect(disabledSkills.map((skill) => skill.name)).toContain('inner'); + expect(disabledSkills.map((skill) => skill.name)).toContain('sub-skill.consolidate'); } finally { await disabledSession.close(); } diff --git a/packages/agent-core/test/skill/builtin-sub-skill.test.ts b/packages/agent-core/test/skill/builtin-sub-skill.test.ts index 6c8940fd50..c92a931ec8 100644 --- a/packages/agent-core/test/skill/builtin-sub-skill.test.ts +++ b/packages/agent-core/test/skill/builtin-sub-skill.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { FLAG_DEFINITIONS, FlagResolver } from '../../src/flags'; import { SkillRegistry, SUB_SKILL_CONSOLIDATE, @@ -24,9 +23,7 @@ describe('builtin skill: sub-skill', () => { it('registers through registerBuiltinSkills but stays out of the model skill listing', () => { const registry = new SkillRegistry(); - registerBuiltinSkills(registry, { - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { 'sub_skill': true }), - }); + registerBuiltinSkills(registry); expect(registry.getSkill('sub-skill')).toBeDefined(); expect( @@ -34,24 +31,20 @@ describe('builtin skill: sub-skill', () => { ).toBe(false); }); - it('remains visible in the full skill list for CLI display when enabled', () => { + it('remains visible in the full skill list for CLI display', () => { const registry = new SkillRegistry(); - registerBuiltinSkills(registry, { - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { 'sub_skill': true }), - }); + registerBuiltinSkills(registry); expect(registry.listSkills().some((skill) => skill.name === 'sub-skill')).toBe(true); }); - it('does not register sub-skill builtins when the scoped flag is disabled', () => { + it('registers sub-skill builtins even when an ignored scoped flag is disabled', () => { const registry = new SkillRegistry(); - registerBuiltinSkills(registry, { - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { 'sub_skill': false }), - }); + registerBuiltinSkills(registry, { experimentalFlags: { enabled: () => false } }); - expect(registry.getSkill('sub-skill')).toBeUndefined(); - expect(registry.getSkill('sub-skill.review')).toBeUndefined(); - expect(registry.getSkill('sub-skill.consolidate')).toBeUndefined(); + expect(registry.getSkill('sub-skill')).toBeDefined(); + expect(registry.getSkill('sub-skill.review')).toBeDefined(); + expect(registry.getSkill('sub-skill.consolidate')).toBeDefined(); }); }); @@ -69,9 +62,7 @@ describe('builtin skill: sub-skill.review', () => { it('registers through registerBuiltinSkills', () => { const registry = new SkillRegistry(); - registerBuiltinSkills(registry, { - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { 'sub_skill': true }), - }); + registerBuiltinSkills(registry); expect(registry.getSkill('sub-skill.review')).toBeDefined(); expect( @@ -99,9 +90,7 @@ describe('builtin skill: sub-skill.consolidate', () => { it('registers through registerBuiltinSkills', () => { const registry = new SkillRegistry(); - registerBuiltinSkills(registry, { - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { 'sub_skill': true }), - }); + registerBuiltinSkills(registry); expect(registry.getSkill('sub-skill.consolidate')).toBeDefined(); expect( diff --git a/packages/agent-core/test/skill/scanner.test.ts b/packages/agent-core/test/skill/scanner.test.ts index be89e1b1b8..b3d7f5e126 100644 --- a/packages/agent-core/test/skill/scanner.test.ts +++ b/packages/agent-core/test/skill/scanner.test.ts @@ -337,8 +337,7 @@ describe('discoverSkills shape and ordering', () => { expect(skills.map((s) => s.name)).toEqual(['alpha', 'beta', 'top']); }); - it('discovers nested SKILL.md files inside a skill bundle when has-sub-skill is enabled', async () => { - vi.stubEnv('KIMI_CODE_EXPERIMENTAL_SUB_SKILL', '1'); + it('discovers nested SKILL.md files inside a skill bundle when has-sub-skill is declared', async () => { const { repoDir } = await makeWorkspace(); const root = path.join(repoDir, '.kimi-code', 'skills'); await writeSkill(root, path.join('outer', 'SKILL.md'), [ @@ -368,7 +367,7 @@ describe('discoverSkills shape and ordering', () => { expect(skills.map((s) => s.name)).toEqual(['inner', 'outer']); }); - it('discovers nested SKILL.md files when sub-skill is enabled by scoped config', async () => { + it('discovers nested SKILL.md files even when scoped config is passed', async () => { const { repoDir } = await makeWorkspace(); const root = path.join(repoDir, '.kimi-code', 'skills'); await writeSkill(root, path.join('outer', 'SKILL.md'), [ @@ -423,7 +422,6 @@ describe('discoverSkills shape and ordering', () => { }); it('discovers nested SKILL.md files when has-sub-skill is nested under metadata', async () => { - vi.stubEnv('KIMI_CODE_EXPERIMENTAL_SUB_SKILL', '1'); const { repoDir } = await makeWorkspace(); const root = path.join(repoDir, '.kimi-code', 'skills'); await writeSkill(root, path.join('outer', 'SKILL.md'), [ @@ -450,7 +448,7 @@ describe('discoverSkills shape and ordering', () => { expect(skills.map((s) => s.name)).toEqual(['outer', 'outer.inner']); }); - it('treats sub-skill discovery as opt-in via the KIMI_CODE_EXPERIMENTAL_SUB_SKILL flag', async () => { + it('ignores disabled experimental flags for declared sub-skill discovery', async () => { const { repoDir } = await makeWorkspace(); const root = path.join(repoDir, '.kimi-code', 'skills'); await writeSkill(root, path.join('outer', 'SKILL.md'), [ @@ -476,7 +474,7 @@ describe('discoverSkills shape and ordering', () => { experimentalFlags: new FlagResolver({ KIMI_CODE_EXPERIMENTAL_SUB_SKILL: '0' }), }); - expect(skills.map((s) => s.name)).toEqual(['outer']); + expect(skills.map((s) => s.name)).toEqual(['outer', 'outer.inner']); }); it('skips node_modules when scanning nested directories', async () => { diff --git a/packages/agent-core/test/tools/ask-user.test.ts b/packages/agent-core/test/tools/ask-user.test.ts index 80cd2e18c9..da38492b72 100644 --- a/packages/agent-core/test/tools/ask-user.test.ts +++ b/packages/agent-core/test/tools/ask-user.test.ts @@ -113,7 +113,7 @@ describe('AskUserQuestionTool', () => { expect(labelSchema.description).toContain("append '(Recommended)'"); }); - it('builds the background-question schema from the agent scoped resolver', () => { + it('always builds the background-question schema', () => { vi.stubEnv(MASTER_ENV, '1'); const enabledAgent = { rpc: { requestQuestion: vi.fn() }, @@ -137,8 +137,8 @@ describe('AskUserQuestionTool', () => { expect(enabledTool.description).toContain('Set background=true'); expect(JSON.stringify(enabledTool.parameters)).toContain('background'); - expect(disabledTool.description).not.toContain('Set background=true'); - expect(JSON.stringify(disabledTool.parameters)).not.toContain('background'); + expect(disabledTool.description).toContain('Set background=true'); + expect(JSON.stringify(disabledTool.parameters)).toContain('background'); }); it.each(['manual', 'yolo'] as const)( @@ -253,12 +253,16 @@ describe('AskUserQuestionTool', () => { }); }); - it('downgrades background=true to an inline question when the experimental flag is off', async () => { + it('starts background questions even when experimental env is off', async () => { vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '0'); vi.stubEnv('KIMI_CODE_EXPERIMENTAL_BACKGROUND_ASK', '0'); + let resolveQuestion!: (result: QuestionResult) => void; + const questionResult = new Promise((resolve) => { + resolveQuestion = resolve; + }); const { manager } = createBackgroundManager(); - const requestQuestion = vi.fn(async () => ({ Postgres: true })); + const requestQuestion = vi.fn(async () => questionResult); const agent = { rpc: { requestQuestion }, telemetry: { track: vi.fn() }, @@ -266,19 +270,24 @@ describe('AskUserQuestionTool', () => { experimentalFlags: new FlagResolver(), } as unknown as Agent; const tool = new AskUserQuestionTool(agent); - expect(tool.description).not.toContain('Set background=true'); + expect(tool.description).toContain('Set background=true'); const result = await executeTool(tool, { turnId: '0', - toolCallId: 'call_bg_disabled', + toolCallId: 'call_bg_enabled', args: { ...input(), background: true }, signal, }); expect(result.isError).toBe(false); - expect(result.output).toBe(JSON.stringify({ answers: { Postgres: true } })); - expect(requestQuestion).toHaveBeenCalled(); - expect(manager.list()).toHaveLength(0); + expect(result.output).toContain('task_id: question-'); + const outputText = typeof result.output === 'string' ? result.output : ''; + const taskId = /task_id: (?question-[0-9a-z]{8})/.exec(outputText)?.groups?.['taskId']; + expect(taskId).toBeDefined(); + expect(manager.getTask(taskId!)).toMatchObject({ status: 'running' }); + + resolveQuestion({ answers: { Postgres: true } }); + await manager.wait(taskId!); }); it('returns a dismissed message when every question is dismissed', async () => { diff --git a/packages/agent-core/test/tools/goal.test.ts b/packages/agent-core/test/tools/goal.test.ts index 3c458cd4e3..f254d70ab7 100644 --- a/packages/agent-core/test/tools/goal.test.ts +++ b/packages/agent-core/test/tools/goal.test.ts @@ -310,14 +310,13 @@ describe('ToolManager goal tool registration', () => { return ctxAgent.agent.tools.loopTools.map((tool) => tool.name); } - it('omits goal tools when the flag is disabled', () => { + it('exposes goal tools to the main agent even when the flag is disabled', () => { const names = loopToolNames('main', false); - expect(names).not.toContain('CreateGoal'); - expect(names).not.toContain('GetGoal'); + expect(names).toEqual(expect.arrayContaining(['CreateGoal', 'GetGoal'])); expect(names).not.toContain('SetGoalBudget'); }); - it('exposes goal tools to the main agent when the flag is enabled', () => { + it('exposes goal tools to the main agent when the scoped flag is enabled', () => { const names = loopToolNames('main', true); expect(names).toEqual(expect.arrayContaining(['CreateGoal', 'GetGoal'])); expect(names).not.toContain('SetGoalBudget'); From 463fa9ac72e56abc7eebcb6c9c3d1e256bba459e Mon Sep 17 00:00:00 2001 From: liruifengv Date: Tue, 9 Jun 2026 10:40:12 +0800 Subject: [PATCH 2/4] refactor: remove redundant goal runtime gate --- packages/agent-core/src/agent/turn/index.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index 12c608a0f8..7a869af577 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -110,11 +110,6 @@ export class TurnFlow { return this.agent.homedir ? basename(this.agent.homedir) : this.agent.type; } - /** Whether goal-mode runtime behavior (continuation, abnormal-end marking) applies. */ - private get goalRuntimeEnabled(): boolean { - return true; - } - // Returns the new turnId, or null if the turn was marked as resuming. prompt(input: readonly ContentPart[], origin: PromptOrigin = USER_PROMPT_ORIGIN): number | null { this.agent.records.logRecord({ @@ -295,7 +290,7 @@ export class TurnFlow { this.activeTurn.controller.signal === signal; try { const initialGoalStatus = this.agent.goal.getGoal().goal?.status; - if (this.goalRuntimeEnabled && initialGoalStatus === 'active') { + if (initialGoalStatus === 'active') { return await this.driveGoal(firstTurnId, input, origin, signal); } const end = await this.runOneTurn(firstTurnId, input, origin, signal, true); @@ -303,7 +298,6 @@ export class TurnFlow { initialGoalStatus === 'paused' || initialGoalStatus === 'blocked'; const currentGoalStatus = this.agent.goal.getGoal().goal?.status; if ( - this.goalRuntimeEnabled && resumedFromPausedOrBlocked && currentGoalStatus === 'active' && end.event.reason !== 'cancelled' && From 6be9b97ea0c78168ddab8f2c5871655533e558cd Mon Sep 17 00:00:00 2001 From: liruifengv Date: Tue, 9 Jun 2026 10:49:18 +0800 Subject: [PATCH 3/4] refactor: remove unused skill flag plumbing --- packages/agent-core/src/session/index.ts | 3 +- .../agent-core/src/skill/builtin/index.ts | 10 +----- packages/agent-core/src/skill/registry.ts | 9 ----- packages/agent-core/src/skill/scanner.ts | 2 -- .../test/skill/builtin-sub-skill.test.ts | 4 +-- .../agent-core/test/skill/scanner.test.ts | 35 ++----------------- 6 files changed, 7 insertions(+), 56 deletions(-) diff --git a/packages/agent-core/src/session/index.ts b/packages/agent-core/src/session/index.ts index 4f4708362e..0c1950bb32 100644 --- a/packages/agent-core/src/session/index.ts +++ b/packages/agent-core/src/session/index.ts @@ -157,7 +157,6 @@ export class Session { this.persistenceKaos = options.persistenceKaos ?? options.kaos; this.skills = new SkillRegistry({ sessionId: options.id, - experimentalFlags: this.experimentalFlags, }); this.mcp = new McpConnectionManager({ oauthService: new McpOAuthService({ kimiHomeDir: options.kimiHomeDir }), @@ -411,7 +410,7 @@ export class Session { builtinDir: this.options.skills?.builtinDir, }); await this.skills.loadRoots(roots); - registerBuiltinSkills(this.skills, { experimentalFlags: this.experimentalFlags }); + registerBuiltinSkills(this.skills); } private async loadMcpServers(): Promise { diff --git a/packages/agent-core/src/skill/builtin/index.ts b/packages/agent-core/src/skill/builtin/index.ts index ad18007b73..1f928ee84f 100644 --- a/packages/agent-core/src/skill/builtin/index.ts +++ b/packages/agent-core/src/skill/builtin/index.ts @@ -7,15 +7,7 @@ import { } from './sub-skill'; import { UPDATE_CONFIG_SKILL } from './update-config'; -type RegisterBuiltinSkillsOptions = { - /** Kept for API compatibility; built-in skills are no longer gated by this option. */ - readonly experimentalFlags?: unknown; -}; - -export function registerBuiltinSkills( - registry: SkillRegistry, - _options: RegisterBuiltinSkillsOptions = {}, -): void { +export function registerBuiltinSkills(registry: SkillRegistry): void { registry.registerBuiltinSkill(MCP_CONFIG_SKILL); registry.registerBuiltinSkill(UPDATE_CONFIG_SKILL); registry.registerBuiltinSkill(SUB_SKILL_PARENT); diff --git a/packages/agent-core/src/skill/registry.ts b/packages/agent-core/src/skill/registry.ts index f392aa1500..44f37d84d1 100644 --- a/packages/agent-core/src/skill/registry.ts +++ b/packages/agent-core/src/skill/registry.ts @@ -1,4 +1,3 @@ -import { flags } from '../flags/resolver'; import { expandSkillParameters, skillArgumentNames } from './parser'; import { discoverSkills, type DiscoverSkillsOptions } from './scanner'; import type { SkillDefinition, SkillRoot, SkillSource, SkippedSkill } from './types'; @@ -7,10 +6,6 @@ import { escapeXmlAttr } from '../utils/xml-escape'; const LISTING_DESC_MAX = 250; -type SubSkillFlagResolver = { - enabled(id: 'sub_skill'): boolean; -}; - export class SkillNotFoundError extends Error { readonly skillName: string; @@ -23,7 +18,6 @@ export class SkillNotFoundError extends Error { export interface SkillRegistryOptions { readonly discover?: typeof discoverSkills; - readonly experimentalFlags?: SubSkillFlagResolver; readonly onWarning?: (message: string, cause?: unknown) => void; readonly sessionId?: string; } @@ -34,13 +28,11 @@ export class SkillRegistry { private readonly roots: string[] = []; private readonly skipped: SkippedSkill[] = []; private readonly discoverImpl: typeof discoverSkills; - private readonly experimentalFlags: SubSkillFlagResolver; private readonly onWarning: (message: string, cause?: unknown) => void; readonly sessionId?: string; constructor(options: SkillRegistryOptions = {}) { this.discoverImpl = options.discover ?? discoverSkills; - this.experimentalFlags = options.experimentalFlags ?? flags; this.onWarning = options.onWarning ?? (() => {}); this.sessionId = options.sessionId; } @@ -52,7 +44,6 @@ export class SkillRegistry { const skills = await this.discoverImpl({ roots, - experimentalFlags: this.experimentalFlags, onWarning: this.onWarning, onSkippedByPolicy: (skill) => this.skipped.push(skill), onDiscoveredSkill: (skill) => { diff --git a/packages/agent-core/src/skill/scanner.ts b/packages/agent-core/src/skill/scanner.ts index 1e85dbbf23..6e60e8f0eb 100644 --- a/packages/agent-core/src/skill/scanner.ts +++ b/packages/agent-core/src/skill/scanner.ts @@ -40,8 +40,6 @@ export interface ResolveSkillRootsOptions { export interface DiscoverSkillsOptions { readonly roots: readonly SkillRoot[]; - /** Kept for API compatibility; sub-skill discovery is no longer gated by this option. */ - readonly experimentalFlags?: unknown; readonly onWarning?: (message: string, cause?: unknown) => void; readonly onSkippedByPolicy?: (skill: SkippedSkill) => void; readonly onDiscoveredSkill?: (skill: SkillDefinition) => void; diff --git a/packages/agent-core/test/skill/builtin-sub-skill.test.ts b/packages/agent-core/test/skill/builtin-sub-skill.test.ts index c92a931ec8..3cee3dc995 100644 --- a/packages/agent-core/test/skill/builtin-sub-skill.test.ts +++ b/packages/agent-core/test/skill/builtin-sub-skill.test.ts @@ -38,9 +38,9 @@ describe('builtin skill: sub-skill', () => { expect(registry.listSkills().some((skill) => skill.name === 'sub-skill')).toBe(true); }); - it('registers sub-skill builtins even when an ignored scoped flag is disabled', () => { + it('registers every sub-skill builtin', () => { const registry = new SkillRegistry(); - registerBuiltinSkills(registry, { experimentalFlags: { enabled: () => false } }); + registerBuiltinSkills(registry); expect(registry.getSkill('sub-skill')).toBeDefined(); expect(registry.getSkill('sub-skill.review')).toBeDefined(); diff --git a/packages/agent-core/test/skill/scanner.test.ts b/packages/agent-core/test/skill/scanner.test.ts index b3d7f5e126..2ac4b1f4ec 100644 --- a/packages/agent-core/test/skill/scanner.test.ts +++ b/packages/agent-core/test/skill/scanner.test.ts @@ -4,7 +4,6 @@ import path from 'pathe'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { FLAG_DEFINITIONS, FlagResolver } from '../../src/flags'; import { discoverSkills, resolveSkillRoots, SkillRegistry, type SkillRoot } from '../../src/skill'; const tempDirs: string[] = []; @@ -367,35 +366,6 @@ describe('discoverSkills shape and ordering', () => { expect(skills.map((s) => s.name)).toEqual(['inner', 'outer']); }); - it('discovers nested SKILL.md files even when scoped config is passed', async () => { - const { repoDir } = await makeWorkspace(); - const root = path.join(repoDir, '.kimi-code', 'skills'); - await writeSkill(root, path.join('outer', 'SKILL.md'), [ - '---', - 'name: outer', - 'description: Parent skill', - 'has-sub-skill: true', - '---', - '', - 'Outer body.', - ]); - await writeSkill(root, path.join('outer', 'references', 'inner', 'SKILL.md'), [ - '---', - 'name: inner', - 'description: Nested skill', - '---', - '', - 'Inner body.', - ]); - - const skills = await discoverSkills({ - roots: [{ path: root, source: 'user' }], - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { 'sub_skill': true }), - }); - - expect(skills.map((s) => s.name)).toEqual(['inner', 'outer']); - }); - it('does not discover nested SKILL.md files when the parent bundle disables sub-skills', async () => { const { repoDir } = await makeWorkspace(); const root = path.join(repoDir, '.kimi-code', 'skills'); @@ -448,7 +418,7 @@ describe('discoverSkills shape and ordering', () => { expect(skills.map((s) => s.name)).toEqual(['outer', 'outer.inner']); }); - it('ignores disabled experimental flags for declared sub-skill discovery', async () => { + it('ignores disabled experimental env for declared sub-skill discovery', async () => { const { repoDir } = await makeWorkspace(); const root = path.join(repoDir, '.kimi-code', 'skills'); await writeSkill(root, path.join('outer', 'SKILL.md'), [ @@ -469,9 +439,10 @@ describe('discoverSkills shape and ordering', () => { 'Inner body.', ]); + vi.stubEnv('KIMI_CODE_EXPERIMENTAL_SUB_SKILL', '0'); + const skills = await discoverSkills({ roots: [{ path: root, source: 'user' }], - experimentalFlags: new FlagResolver({ KIMI_CODE_EXPERIMENTAL_SUB_SKILL: '0' }), }); expect(skills.map((s) => s.name)).toEqual(['outer', 'outer.inner']); From c94a876fbc9fb89d81436870a15413e0b4b68056 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Tue, 9 Jun 2026 11:39:12 +0800 Subject: [PATCH 4/4] feat: keep micro compaction opt-out --- .changeset/enable-micro-compaction.md | 6 ++ .changeset/release-experimental-features.md | 2 +- .changeset/template-agent-swarm.md | 2 +- apps/kimi-code/test/cli/goal-prompt.test.ts | 4 +- .../test/tui/commands/experiments.test.ts | 22 +++--- .../test/tui/commands/reload.test.ts | 4 +- .../test/tui/commands/resolve.test.ts | 2 - .../dialogs/experiments-selector.test.ts | 70 ++++++------------- .../test/tui/kimi-tui-message-flow.test.ts | 4 +- .../test/tui/kimi-tui-startup.test.ts | 4 +- docs/en/configuration/config-files.md | 11 ++- docs/en/configuration/env-vars.md | 3 +- docs/zh/configuration/config-files.md | 11 ++- docs/zh/configuration/env-vars.md | 3 +- .../agent-core/src/agent/compaction/micro.ts | 4 ++ packages/agent-core/src/flags/registry.ts | 34 +-------- packages/agent-core/test/agent/basic.test.ts | 2 +- .../test/agent/compaction/micro.test.ts | 12 ++-- .../test/agent/injection/goal.test.ts | 15 +--- packages/agent-core/test/agent/turn.test.ts | 2 - .../agent-core/test/config/configs.test.ts | 8 --- .../test/harness/goal-session.test.ts | 8 +-- .../agent-core/test/harness/runtime.test.ts | 38 +++++----- packages/agent-core/test/session/init.test.ts | 5 +- .../agent-core/test/skill/scanner.test.ts | 4 +- .../agent-core/test/tools/ask-user.test.ts | 32 ++------- packages/agent-core/test/tools/goal.test.ts | 21 ++---- packages/node-sdk/test/config.test.ts | 28 +++----- 28 files changed, 123 insertions(+), 238 deletions(-) create mode 100644 .changeset/enable-micro-compaction.md diff --git a/.changeset/enable-micro-compaction.md b/.changeset/enable-micro-compaction.md new file mode 100644 index 0000000000..bb529d5410 --- /dev/null +++ b/.changeset/enable-micro-compaction.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core": minor +"@moonshot-ai/kimi-code": minor +--- + +Enable micro compaction by default while keeping its opt-out flag. diff --git a/.changeset/release-experimental-features.md b/.changeset/release-experimental-features.md index 7b729c1525..ccad2acf9b 100644 --- a/.changeset/release-experimental-features.md +++ b/.changeset/release-experimental-features.md @@ -3,4 +3,4 @@ "@moonshot-ai/kimi-code": minor --- -Make goals, swarm mode, background questions, micro compaction, and sub-skill discovery available without experimental opt-ins. +Make goals, background questions, and sub-skill discovery available without experimental opt-ins. diff --git a/.changeset/template-agent-swarm.md b/.changeset/template-agent-swarm.md index 6896ebdcb5..c8e10f91cc 100644 --- a/.changeset/template-agent-swarm.md +++ b/.changeset/template-agent-swarm.md @@ -5,4 +5,4 @@ "@moonshot-ai/kimi-code": minor --- -Add swarm agent runs with SDK/TUI controls, live progress, and rate-limit-aware retries. +Add the `/swarm` command for running agent swarms with live progress and rate-limit-aware retries. diff --git a/apps/kimi-code/test/cli/goal-prompt.test.ts b/apps/kimi-code/test/cli/goal-prompt.test.ts index f3c93eb2c0..e8a9955d7d 100644 --- a/apps/kimi-code/test/cli/goal-prompt.test.ts +++ b/apps/kimi-code/test/cli/goal-prompt.test.ts @@ -102,7 +102,7 @@ const mocks = vi.hoisted(() => { session, eventHandlers, mainEvent, - experimentalFeatures: [{ id: 'goal_command', enabled: true }], + experimentalFeatures: [{ id: 'micro_compaction', enabled: true }], sessions: [] as Array<{ readonly id: string; readonly workDir: string }>, }; }); @@ -160,7 +160,7 @@ describe('runPrompt headless goal mode', () => { beforeEach(() => { savedExitCode = process.exitCode; - mocks.experimentalFeatures = [{ id: 'goal_command', enabled: true }]; + mocks.experimentalFeatures = [{ id: 'micro_compaction', enabled: true }]; mocks.sessions = []; mocks.session.createGoal.mockClear(); mocks.session.getStatus.mockResolvedValue({ permission: 'auto', model: 'k2' } as never); diff --git a/apps/kimi-code/test/tui/commands/experiments.test.ts b/apps/kimi-code/test/tui/commands/experiments.test.ts index 6e823f9950..4c82330965 100644 --- a/apps/kimi-code/test/tui/commands/experiments.test.ts +++ b/apps/kimi-code/test/tui/commands/experiments.test.ts @@ -15,13 +15,13 @@ function feature( overrides: Partial = {}, ): ExperimentalFeatureState { return { - id: 'goal_command', - title: 'Goal command', - description: 'Enable goal mode.', - surface: 'both', - env: 'KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND', - defaultEnabled: false, - enabled: false, + id: 'micro_compaction', + title: 'Micro compaction', + description: 'Trim older tool results.', + surface: 'core', + env: 'KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION', + defaultEnabled: true, + enabled: true, source: 'default', ...overrides, }; @@ -40,7 +40,7 @@ function makeHost() { harness: { setConfig: vi.fn(async () => ({ providers: {} })), getExperimentalFeatures: vi.fn(async () => [ - feature({ enabled: true, source: 'config', configValue: true }), + feature({ enabled: false, source: 'config', configValue: false }), ]), }, session, @@ -77,14 +77,14 @@ describe('experimental feature command handlers', () => { const host = makeHost(); await applyExperimentalFeatureChanges(host, [ - { id: 'goal_command', enabled: true }, + { id: 'micro_compaction', enabled: false }, ]); expect(host.harness.setConfig).toHaveBeenCalledWith({ - experimental: { 'goal_command': true }, + experimental: { 'micro_compaction': false }, }); expect(host.harness.getExperimentalFeatures).toHaveBeenCalledOnce(); - expect(isExperimentalFlagEnabled('goal_command')).toBe(true); + expect(isExperimentalFlagEnabled('micro_compaction')).toBe(false); expect(host.refreshSlashCommandAutocomplete).toHaveBeenCalled(); expect(host.restoreEditor).toHaveBeenCalled(); expect(host.session.reloadSession).toHaveBeenCalledOnce(); diff --git a/apps/kimi-code/test/tui/commands/reload.test.ts b/apps/kimi-code/test/tui/commands/reload.test.ts index 5d6b41f550..317d2bea7e 100644 --- a/apps/kimi-code/test/tui/commands/reload.test.ts +++ b/apps/kimi-code/test/tui/commands/reload.test.ts @@ -79,7 +79,7 @@ auto_install = false expect(host.harness.getConfig).toHaveBeenCalledWith({ reload: true }); expect(host.harness.getExperimentalFeatures).toHaveBeenCalledOnce(); expect(host.refreshSlashCommandAutocomplete).toHaveBeenCalledOnce(); - expect(isExperimentalFlagEnabled('goal_command')).toBe(true); + expect(isExperimentalFlagEnabled('micro_compaction')).toBe(true); expect(host.state.appState.theme).toBe('light'); expect(host.state.appState.availableModels).toEqual({ fresh: { provider: 'test', model: 'fresh-model', maxContextSize: 1000 }, @@ -128,7 +128,7 @@ function makeHost({ test: { type: 'kimi', apiKey: 'test-key' }, }, })), - getExperimentalFeatures: vi.fn(async () => [{ id: 'goal_command', enabled: true }]), + getExperimentalFeatures: vi.fn(async () => [{ id: 'micro_compaction', enabled: true }]), }, setAppState: vi.fn((patch: Record) => { Object.assign(state.appState, patch); diff --git a/apps/kimi-code/test/tui/commands/resolve.test.ts b/apps/kimi-code/test/tui/commands/resolve.test.ts index 35624ab510..cf009b158c 100644 --- a/apps/kimi-code/test/tui/commands/resolve.test.ts +++ b/apps/kimi-code/test/tui/commands/resolve.test.ts @@ -58,7 +58,6 @@ describe('resolveSlashCommandInput', () => { }); it('blocks idle-only built-ins while streaming', () => { - setExperimentalFeatures([{ id: 'agent_swarm', enabled: true }]); expect(resolve('/new', { isStreaming: true })).toEqual({ kind: 'blocked', commandName: 'new', @@ -107,7 +106,6 @@ describe('resolveSlashCommandInput', () => { }); it('blocks model and session pickers while compacting', () => { - setExperimentalFeatures([{ id: 'agent_swarm', enabled: true }]); expect(resolve('/sessions', { isCompacting: true })).toEqual({ kind: 'blocked', commandName: 'sessions', diff --git a/apps/kimi-code/test/tui/components/dialogs/experiments-selector.test.ts b/apps/kimi-code/test/tui/components/dialogs/experiments-selector.test.ts index 96fd9526bb..ce402d3dd3 100644 --- a/apps/kimi-code/test/tui/components/dialogs/experiments-selector.test.ts +++ b/apps/kimi-code/test/tui/components/dialogs/experiments-selector.test.ts @@ -9,7 +9,6 @@ import { darkColors } from '#/tui/theme/colors'; const ANSI = /\u001B\[[0-9;]*m/g; const ESC = String.fromCodePoint(27); -const DOWN = `${ESC}[B`; const ENTER = '\r'; function strip(text: string): string { @@ -20,13 +19,13 @@ function feature( overrides: Partial = {}, ): ExperimentalFeatureState { return { - id: 'goal_command', - title: 'Goal command', - description: 'Enable goal mode.', - surface: 'both', - env: 'KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND', - defaultEnabled: false, - enabled: false, + id: 'micro_compaction', + title: 'Micro compaction', + description: 'Trim older tool results.', + surface: 'core', + env: 'KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION', + defaultEnabled: true, + enabled: true, source: 'default', ...overrides, }; @@ -41,14 +40,6 @@ describe('ExperimentsSelectorComponent', () => { const selector = new ExperimentsSelectorComponent({ features: [ feature({ enabled: true, source: 'config', configValue: true }), - feature({ - id: 'background_ask', - title: 'Background questions', - description: 'Ask questions without blocking the current turn.', - env: 'KIMI_CODE_EXPERIMENTAL_BACKGROUND_ASK', - enabled: false, - source: 'env', - }), ], colors: darkColors, onApply: vi.fn(), @@ -59,24 +50,17 @@ describe('ExperimentsSelectorComponent', () => { expect(out).toContain(' Experimental features (type to search)'); expect(out).toContain(' ↑↓ navigate · Space toggle · Enter apply · Esc cancel'); - expect(out).toContain(' ❯ Goal command enabled'); - expect(out).toContain(' id goal_command · config · KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND'); - expect(out).toContain(' Enable goal mode.'); - expect(out).toContain(' Background questions disabled'); - expect(out).toContain(' id background_ask · locked by KIMI_CODE_EXPERIMENTAL_BACKGROUND_ASK'); + expect(out).toContain(' ❯ Micro compaction enabled'); + expect(out).toContain(' id micro_compaction · config · KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION'); + expect(out).toContain(' Trim older tool results.'); expect(out).toContain(' [ Apply changes and reload ] no changes'); }); it('drafts changes with Space and applies them with Enter', () => { const onApply = vi.fn<(changes: readonly ExperimentalFeatureDraftChange[]) => void>(); - const first = feature({ id: 'goal_command', title: 'Goal command' }); - const second = feature({ - id: 'micro_compaction', - title: 'Micro compaction', - env: 'KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION', - }); + const first = feature(); const selector = new ExperimentsSelectorComponent({ - features: [first, second], + features: [first], colors: darkColors, onApply, onCancel: vi.fn(), @@ -85,19 +69,16 @@ describe('ExperimentsSelectorComponent', () => { selector.handleInput(' '); expect(onApply).not.toHaveBeenCalled(); - expect(text(selector)).toContain(' ❯ Goal command enabled'); + expect(text(selector)).toContain(' ❯ Micro compaction disabled'); expect(text(selector)).toContain( - ' id goal_command · default · KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND · modified', + ' id micro_compaction · default · KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION · modified', ); expect(text(selector)).toContain(' [ Apply changes and reload ] 1 change'); - selector.handleInput(DOWN); - selector.handleInput(' '); selector.handleInput(ENTER); expect(onApply).toHaveBeenCalledWith([ - { id: 'goal_command', enabled: true }, - { id: 'micro_compaction', enabled: true }, + { id: 'micro_compaction', enabled: false }, ]); }); @@ -118,7 +99,7 @@ describe('ExperimentsSelectorComponent', () => { selector.handleInput(' '); selector.handleInput(ENTER); - expect(text(selector)).toContain(' ❯ Goal command enabled'); + expect(text(selector)).toContain(' ❯ Micro compaction enabled'); expect(text(selector)).toContain(' [ Apply changes and reload ] no changes'); expect(onApply).not.toHaveBeenCalled(); }); @@ -126,26 +107,17 @@ describe('ExperimentsSelectorComponent', () => { it('filters by typing and clears the query before cancelling', () => { const onCancel = vi.fn(); const selector = new ExperimentsSelectorComponent({ - features: [ - feature({ id: 'goal_command', title: 'Goal command' }), - feature({ - id: 'background_ask', - title: 'Background questions', - env: 'KIMI_CODE_EXPERIMENTAL_BACKGROUND_ASK', - }), - ], + features: [feature()], colors: darkColors, onApply: vi.fn(), onCancel, }); - selector.handleInput('b'); - selector.handleInput('a'); + selector.handleInput('m'); + selector.handleInput('i'); selector.handleInput('c'); - selector.handleInput('k'); - expect(text(selector)).toContain('Search: back'); - expect(text(selector)).toContain('Background questions'); - expect(text(selector)).not.toContain('Goal command'); + expect(text(selector)).toContain('Search: mic'); + expect(text(selector)).toContain('Micro compaction'); selector.handleInput(ESC); expect(onCancel).not.toHaveBeenCalled(); diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index c7266fae21..42200cfa04 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -2120,9 +2120,7 @@ command = "vim" }); it('renders an ended marker when a one-shot /swarm task exits', async () => { - const { driver, session } = await makeDriver(undefined, { - getExperimentalFeatures: vi.fn(async () => [{ id: 'agent_swarm', enabled: true }]), - }); + const { driver, session } = await makeDriver(undefined); driver.state.appState.permissionMode = 'auto'; driver.handleUserInput('/swarm Ship feature X'); diff --git a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts index 5f7d5d954a..6b0caf0c94 100644 --- a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts @@ -263,7 +263,7 @@ describe("KimiTUI startup", () => { }); const harness = makeHarness(session, { listSessions: vi.fn(async () => [{ id: "ses-latest" }]), - getExperimentalFeatures: vi.fn(async () => [{ id: "goal_command", enabled: true }]), + getExperimentalFeatures: vi.fn(async () => [{ id: "micro_compaction", enabled: true }]), }); const driver = makeDriver(harness, makeStartupInput({ continue: true })); @@ -293,7 +293,7 @@ describe("KimiTUI startup", () => { getGoal: vi.fn(async () => ({ goal })), }); const harness = makeHarness(session, { - getExperimentalFeatures: vi.fn(async () => [{ id: "goal_command", enabled: true }]), + getExperimentalFeatures: vi.fn(async () => [{ id: "micro_compaction", enabled: true }]), }); const driver = makeDriver(harness, makeStartupInput()) as unknown as RuntimeStateDriver; diff --git a/docs/en/configuration/config-files.md b/docs/en/configuration/config-files.md index 687f3b5ebe..bd7cefa456 100644 --- a/docs/en/configuration/config-files.md +++ b/docs/en/configuration/config-files.md @@ -51,6 +51,9 @@ reserved_context_size = 50000 max_running_tasks = 4 keep_alive_on_exit = false +[experimental] +micro_compaction = true + [[permission.rules]] decision = "allow" pattern = "Read" @@ -84,7 +87,7 @@ Fields in the config file fall into two categories: **top-level scalars** that d | `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) | -| `experimental` | `table` | — | Reserved experimental feature opt-ins → [`experimental`](#experimental) | +| `experimental` | `table` | — | Experimental feature overrides → [`experimental`](#experimental) | | `services` | `table` | — | Built-in external service configuration → [`services`](#services) | | `permission` | `table` | — | Initial permission rules → [`permission`](#permission) | | `hooks` | `array
` | — | Lifecycle hooks; see [Hooks](../customization/hooks.md) | @@ -172,7 +175,11 @@ You can also switch models temporarily without touching the config file — by s ## `experimental` -`experimental` is reserved for persistent opt-ins to features that are not public by default. There are currently no user-facing experimental features, so you can omit this table or leave it empty. +`experimental` stores persistent overrides for experimental-feature flags. Currently, `micro_compaction` is the only user-facing entry and defaults to `true`; set it to `false` only when you need to disable automatic trimming of older large tool results. + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `micro_compaction` | `boolean` | `true` | Trim older large tool results from context while preserving recent conversation | ## `services` diff --git a/docs/en/configuration/env-vars.md b/docs/en/configuration/env-vars.md index 5f5606021a..50acb9121b 100644 --- a/docs/en/configuration/env-vars.md +++ b/docs/en/configuration/env-vars.md @@ -125,7 +125,8 @@ 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` | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | | `KIMI_CODE_PLUGIN_MARKETPLACE_URL` | Override the plugin marketplace JSON loaded by `/plugins` | URL or local path | -| `KIMI_CODE_EXPERIMENTAL_FLAG` | Enable all registered experimental features for this process; currently no user-facing feature requires it | `1`, `true`, `yes`, `on` | +| `KIMI_CODE_EXPERIMENTAL_FLAG` | Enable all registered experimental features for this process; `micro_compaction` is already enabled by default | `1`, `true`, `yes`, `on` | +| `KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION` | Override [`[experimental].micro_compaction`](./config-files.md#experimental) for this process | Truthy or falsy | | `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 | | `KIMI_MODEL_TEMPERATURE` | Sampling temperature for every request; applies to the `kimi` provider only (global — independent of `KIMI_MODEL_NAME`) | Number, e.g. `0.3` | diff --git a/docs/zh/configuration/config-files.md b/docs/zh/configuration/config-files.md index fca36f250f..fc19dc3c0b 100644 --- a/docs/zh/configuration/config-files.md +++ b/docs/zh/configuration/config-files.md @@ -51,6 +51,9 @@ reserved_context_size = 50000 max_running_tasks = 4 keep_alive_on_exit = false +[experimental] +micro_compaction = true + [[permission.rules]] decision = "allow" pattern = "Read" @@ -84,7 +87,7 @@ timeout = 5 | `thinking` | `table` | — | Thinking 模式默认参数 → [`thinking`](#thinking) | | `loop_control` | `table` | — | Agent 循环控制参数 → [`loop_control`](#loop_control) | | `background` | `table` | — | 后台任务运行参数 → [`background`](#background) | -| `experimental` | `table` | — | 预留的实验功能 opt-in → [`experimental`](#experimental) | +| `experimental` | `table` | — | 实验功能覆盖 → [`experimental`](#experimental) | | `services` | `table` | — | 内置外部服务配置 → [`services`](#services) | | `permission` | `table` | — | 初始权限规则 → [`permission`](#permission) | | `hooks` | `array
` | — | 生命周期 hook,详见 [Hooks](../customization/hooks.md) | @@ -172,7 +175,11 @@ max_context_size = 1047576 ## `experimental` -`experimental` 预留给尚未默认公开功能的持久化 opt-in。目前没有用户可见的实验功能,因此可以省略这个表,或保留为空表。 +`experimental` 存放实验功能 flag 的持久化覆盖。目前 `micro_compaction` 是唯一用户可见的字段,默认值为 `true`;只有在需要关闭自动清理较旧的大型工具结果时,才需要把它设为 `false`。 + +| 字段 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `micro_compaction` | `boolean` | `true` | 清理较旧的大型工具结果内容,同时保留最近对话 | ## `services` diff --git a/docs/zh/configuration/env-vars.md b/docs/zh/configuration/env-vars.md index ca4f3d9247..2f95c93ae2 100644 --- a/docs/zh/configuration/env-vars.md +++ b/docs/zh/configuration/env-vars.md @@ -125,7 +125,8 @@ 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_PLUGIN_MARKETPLACE_URL` | 替换 `/plugins` 加载的 marketplace JSON | URL 或本地路径 | -| `KIMI_CODE_EXPERIMENTAL_FLAG` | 在当前进程启用所有已注册的实验功能;目前没有用户可见功能需要它 | `1`、`true`、`yes`、`on` | +| `KIMI_CODE_EXPERIMENTAL_FLAG` | 在当前进程启用所有已注册的实验功能;`micro_compaction` 已默认开启 | `1`、`true`、`yes`、`on` | +| `KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION` | 覆盖当前进程的 [`[experimental].micro_compaction`](./config-files.md#experimental) | 真值或假值 | | `KIMI_SHELL_PATH` | Windows 上覆盖 Git Bash 路径(自动探测失败时使用) | 绝对路径 | | `KIMI_MODEL_MAX_COMPLETION_TOKENS` | 单步 LLM 请求的 `max_completion_tokens` 硬上限,仅对 `kimi` 供应商生效 | 正整数;`0` 或负数禁用 clamp | | `KIMI_MODEL_TEMPERATURE` | 每次请求的采样温度,仅对 `kimi` 供应商生效(全局生效,不依赖 `KIMI_MODEL_NAME`) | 数字,如 `0.3` | diff --git a/packages/agent-core/src/agent/compaction/micro.ts b/packages/agent-core/src/agent/compaction/micro.ts index dd6d5bcbe2..da352db697 100644 --- a/packages/agent-core/src/agent/compaction/micro.ts +++ b/packages/agent-core/src/agent/compaction/micro.ts @@ -44,6 +44,8 @@ export class MicroCompaction { } detect(): void { + if (!this.agent.experimentalFlags.enabled('micro_compaction')) return; + const config = this.config; const { history, lastAssistantAt } = this.agent.context; const cacheAgeMs = lastAssistantAt === null ? null : Date.now() - lastAssistantAt; @@ -75,6 +77,8 @@ export class MicroCompaction { } compact(messages: readonly ContextMessage[]): readonly ContextMessage[] { + if (!this.agent.experimentalFlags.enabled('micro_compaction')) return messages; + const config = this.config; const result: ContextMessage[] = []; let i = 0; diff --git a/packages/agent-core/src/flags/registry.ts b/packages/agent-core/src/flags/registry.ts index d4a200488a..16f88d5921 100644 --- a/packages/agent-core/src/flags/registry.ts +++ b/packages/agent-core/src/flags/registry.ts @@ -12,44 +12,12 @@ import type { FlagDefinitionInput } from './types'; * not equal the master switch 'KIMI_CODE_EXPERIMENTAL_FLAG'; `id` must not be 'flag'. */ export const FLAG_DEFINITIONS = [ - { - id: 'goal_command', - title: 'Goal command', - description: 'Enable /goal and goal-management tools for longer autonomous tasks.', - env: 'KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND', - default: false, - surface: 'both', - }, { id: 'micro_compaction', title: 'Micro compaction', description: 'Trim older large tool results from context while keeping recent conversation intact.', env: 'KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION', - default: false, - surface: 'core', - }, - { - id: 'background_ask', - title: 'Background questions', - description: 'Allow AskUserQuestion to return a background task when the agent can continue working.', - env: 'KIMI_CODE_EXPERIMENTAL_BACKGROUND_ASK', - default: false, - surface: 'core', - }, - { - id: 'agent_swarm', - title: 'Agent swarm', - description: 'Enable the AgentSwarm tool and /swarm command.', - env: 'KIMI_CODE_EXPERIMENTAL_AGENT_SWARM', - default: false, - surface: 'both', - }, - { - id: 'sub_skill', - title: 'Sub-skill', - description: 'Enable discovery of nested skills inside skill bundles that declare has-sub-skill.', - env: 'KIMI_CODE_EXPERIMENTAL_SUB_SKILL', - default: false, + default: true, surface: 'core', }, ] as const satisfies readonly FlagDefinitionInput[]; diff --git a/packages/agent-core/test/agent/basic.test.ts b/packages/agent-core/test/agent/basic.test.ts index 87cf617e34..6e947631ea 100644 --- a/packages/agent-core/test/agent/basic.test.ts +++ b/packages/agent-core/test/agent/basic.test.ts @@ -9,7 +9,7 @@ it('creates an independent agent with a scoped experimental flag resolver', () = experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS), }); - expect(ctx.agent.experimentalFlags.enabled('goal_command')).toBe(false); + expect(ctx.agent.experimentalFlags.enabled('micro_compaction')).toBe(true); }); it('runs a text-only agent turn from prompt to completion', async () => { diff --git a/packages/agent-core/test/agent/compaction/micro.test.ts b/packages/agent-core/test/agent/compaction/micro.test.ts index 0dfff78537..bcdeb327e1 100644 --- a/packages/agent-core/test/agent/compaction/micro.test.ts +++ b/packages/agent-core/test/agent/compaction/micro.test.ts @@ -6,7 +6,7 @@ import { AGENT_WIRE_PROTOCOL_VERSION, InMemoryAgentRecordPersistence, } from '../../../src/agent/records'; -import { FLAG_DEFINITIONS, MASTER_ENV } from '../../../src/flags'; +import { FLAG_DEFINITIONS, FlagResolver, MASTER_ENV } from '../../../src/flags'; import { estimateTokensForMessages } from '../../../src/utils/tokens'; import { recordingTelemetry, type TelemetryRecord } from '../../fixtures/telemetry'; import { testAgent, type TestAgentContext } from '../harness/agent'; @@ -35,6 +35,10 @@ describe('MicroCompaction', () => { vi.stubEnv(MICRO_COMPACTION_FLAG_ENV, '1'); }); + it('defaults the micro_compaction flag on', () => { + expect(new FlagResolver({}, FLAG_DEFINITIONS).enabled('micro_compaction')).toBe(true); + }); + it('truncates old tool results after cache miss', () => { vi.useFakeTimers(); const ctx = testAgent({ @@ -485,7 +489,7 @@ describe('MicroCompaction', () => { expect(records.filter((record) => record.event === 'micro_compaction_applied')).toHaveLength(1); }); - it('applies micro compaction even when the micro_compaction flag is disabled', () => { + it('leaves context unchanged when the micro_compaction flag is disabled', () => { vi.stubEnv(MICRO_COMPACTION_FLAG_ENV, '0'); vi.useFakeTimers(); const persistence = new InMemoryAgentRecordPersistence(); @@ -505,8 +509,8 @@ describe('MicroCompaction', () => { vi.setSystemTime(61 * MINUTE); ctx.agent.microCompaction.detect(); - expect(toolTexts(ctx.agent.context.messages)).toEqual([DEFAULT_MARKER]); - expect(lastMicroCompactionCutoff(persistence.records)).toBe(3); + expect(toolTexts(ctx.agent.context.messages)).toEqual(['result one']); + expect(lastMicroCompactionCutoff(persistence.records)).toBeUndefined(); }); it('uses the custom marker at the minContentTokens boundary', () => { diff --git a/packages/agent-core/test/agent/injection/goal.test.ts b/packages/agent-core/test/agent/injection/goal.test.ts index 68299f21ea..53e0c924c2 100644 --- a/packages/agent-core/test/agent/injection/goal.test.ts +++ b/packages/agent-core/test/agent/injection/goal.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import type { Agent } from '../../../src/agent'; import { GoalMode } from '../../../src/agent/goal'; @@ -6,8 +6,6 @@ import { GoalInjector } from '../../../src/agent/injection/goal'; import { InMemoryAgentRecordPersistence } from '../../../src/agent/records'; import { testAgent } from '../harness/agent'; -const GOAL_FLAG = 'KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND'; - function makeStore() { const agent = { records: { logRecord: () => {} }, @@ -191,12 +189,6 @@ describe('GoalInjector content', () => { }); describe('InjectionManager goal integration', () => { - const original = process.env[GOAL_FLAG]; - afterEach(() => { - if (original === undefined) delete process.env[GOAL_FLAG]; - else process.env[GOAL_FLAG] = original; - }); - function goalReminderRecords(persistence: InMemoryAgentRecordPersistence) { return persistence.records.filter( (r) => @@ -206,7 +198,6 @@ describe('InjectionManager goal integration', () => { } it('main-agent injectGoal writes a context.append_message with origin.variant goal', async () => { - process.env[GOAL_FLAG] = 'true'; const store = makeStore(); await store.createGoal({ objective: 'Ship feature X' }); const persistence = new InMemoryAgentRecordPersistence(); @@ -222,7 +213,6 @@ describe('InjectionManager goal integration', () => { }); it('the per-step inject() loop does NOT add a goal reminder (boundary cadence)', async () => { - process.env[GOAL_FLAG] = 'true'; const store = makeStore(); await store.createGoal({ objective: 'Ship feature X' }); const persistence = new InMemoryAgentRecordPersistence(); @@ -239,7 +229,6 @@ describe('InjectionManager goal integration', () => { }); it('injectGoal is append-only across boundaries (one record per call, prefix untouched)', async () => { - process.env[GOAL_FLAG] = 'true'; const store = makeStore(); await store.createGoal({ objective: 'Ship feature X' }); const persistence = new InMemoryAgentRecordPersistence(); @@ -255,7 +244,6 @@ describe('InjectionManager goal integration', () => { }); it('writes no goal record when there is no active goal', async () => { - process.env[GOAL_FLAG] = 'true'; const store = makeStore(); const persistence = new InMemoryAgentRecordPersistence(); const ctx = testAgent({ type: 'main', goal: store, persistence }); @@ -267,7 +255,6 @@ describe('InjectionManager goal integration', () => { }); it('subagent injectGoal does not add a goal reminder', async () => { - process.env[GOAL_FLAG] = 'true'; const store = makeStore(); await store.createGoal({ objective: 'Ship feature X' }); const persistence = new InMemoryAgentRecordPersistence(); diff --git a/packages/agent-core/test/agent/turn.test.ts b/packages/agent-core/test/agent/turn.test.ts index b54873e158..8afa05ecc6 100644 --- a/packages/agent-core/test/agent/turn.test.ts +++ b/packages/agent-core/test/agent/turn.test.ts @@ -16,7 +16,6 @@ import { import { describe, expect, it, vi } from 'vitest'; import { HookEngine } from '../../src/session/hooks'; -import { FLAG_DEFINITIONS, FlagResolver } from '../../src/flags'; import type { AgentOptions } from '../../src/agent'; import type { Logger, LogPayload } from '../../src/logging'; import type { @@ -335,7 +334,6 @@ describe('Agent turn flow', () => { }); const ctx = testAgent({ subagentHost, - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { agent_swarm: true }), }); ctx.configure({ tools: ['AgentSwarm'] }); await ctx.rpc.setPermission({ mode: 'yolo' }); diff --git a/packages/agent-core/test/config/configs.test.ts b/packages/agent-core/test/config/configs.test.ts index 745aa93f0c..551980f9da 100644 --- a/packages/agent-core/test/config/configs.test.ts +++ b/packages/agent-core/test/config/configs.test.ts @@ -263,25 +263,19 @@ oauth = { storage = "file", key = "oauth/kimi-code-env-1234", oauth_host = "http const configPath = join(dir, 'experimental.toml'); const toml = ` [experimental] -goal_command = true micro_compaction = false -background_ask = true `; const config = parseConfigString(toml, configPath); expect(config.experimental).toEqual({ - 'goal_command': true, 'micro_compaction': false, - 'background_ask': true, }); await writeConfigFile(configPath, config); const text = await readFile(configPath, 'utf-8'); expect(text).toContain('[experimental]'); - expect(text).toContain('goal_command = true'); expect(text).toContain('micro_compaction = false'); - expect(text).toContain('background_ask = true'); expect(parseConfigString(text, configPath).experimental).toEqual(config.experimental); }); @@ -503,7 +497,6 @@ describe('harness config schema and patch merge', () => { it('deep-merges experimental config patches', () => { const base = parseConfigString(` [experimental] -goal_command = true micro_compaction = false `); @@ -514,7 +507,6 @@ micro_compaction = false }); expect(merged.experimental).toEqual({ - 'goal_command': true, 'micro_compaction': true, }); }); diff --git a/packages/agent-core/test/harness/goal-session.test.ts b/packages/agent-core/test/harness/goal-session.test.ts index 79bd8a9d4c..b59219d273 100644 --- a/packages/agent-core/test/harness/goal-session.test.ts +++ b/packages/agent-core/test/harness/goal-session.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'pathe'; import { APIStatusError, type ProviderConfig } from '@moonshot-ai/kosong'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { ProviderManager } from '../../src/session/provider-manager'; import type { AgentOptions } from '../../src/agent'; @@ -15,7 +15,6 @@ import { SessionAPIImpl } from '../../src/session/rpc'; import { createScriptedGenerate } from '../agent/harness/scripted-generate'; import { testKaos } from '../fixtures/test-kaos'; -const GOAL_FLAG = 'KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND'; const MOCK_PROVIDER = { type: 'kimi', apiKey: 'test-key', model: 'mock-model' } as const satisfies ProviderConfig; const tempDirs: string[] = []; @@ -26,12 +25,7 @@ function track(session: Session): Session { return session; } -beforeEach(() => { - process.env[GOAL_FLAG] = 'true'; -}); - afterEach(async () => { - delete process.env[GOAL_FLAG]; // Close sessions first so their async metadata/wire writes settle before the // temp dirs are removed (otherwise rm races with a write -> ENOTEMPTY). await Promise.allSettled(openSessions.splice(0).map((s) => s.close())); diff --git a/packages/agent-core/test/harness/runtime.test.ts b/packages/agent-core/test/harness/runtime.test.ts index 061f0b5bdb..f660e2e992 100644 --- a/packages/agent-core/test/harness/runtime.test.ts +++ b/packages/agent-core/test/harness/runtime.test.ts @@ -74,16 +74,14 @@ describe('KimiCore runtime config', () => { for (const def of FLAG_DEFINITIONS) { vi.stubEnv(def.env, '0'); } - vi.stubEnv(requiredFlagEnv('goal_command'), '1'); - vi.stubEnv(requiredFlagEnv('background_ask'), '1'); + vi.stubEnv(requiredFlagEnv('micro_compaction'), '1'); void new KimiCore(async () => ({}) as never, { homeDir }); await getRootLogger().flushGlobal(); const text = await readFile(resolveGlobalLogPath(homeDir), 'utf-8'); expect(text).toContain('experimental flags enabled'); - expect(text).toContain('goal_command'); - expect(text).toContain('background_ask'); + expect(text).toContain('micro_compaction'); expect(text.match(/experimental flags enabled/g)).toHaveLength(1); }); @@ -97,16 +95,14 @@ describe('KimiCore runtime config', () => { join(firstHome, 'config.toml'), ` [experimental] -goal_command = true -background_ask = false +micro_compaction = true `, ); await writeFile( join(secondHome, 'config.toml'), ` [experimental] -goal_command = false -background_ask = true +micro_compaction = false `, ); clearExperimentalEnv(); @@ -114,10 +110,8 @@ background_ask = true const first = new KimiCore(async () => ({}) as never, { homeDir: firstHome }); const second = new KimiCore(async () => ({}) as never, { homeDir: secondHome }); - expect(experimentalFeatureEnabled(first, 'goal_command')).toBe(true); - expect(experimentalFeatureEnabled(first, 'background_ask')).toBe(false); - expect(experimentalFeatureEnabled(second, 'goal_command')).toBe(false); - expect(experimentalFeatureEnabled(second, 'background_ask')).toBe(true); + expect(experimentalFeatureEnabled(first, 'micro_compaction')).toBe(true); + expect(experimentalFeatureEnabled(second, 'micro_compaction')).toBe(false); }); it('updates the scoped experimental resolver after setKimiConfig', async () => { @@ -128,21 +122,21 @@ background_ask = true join(homeDir, 'config.toml'), ` [experimental] -goal_command = false +micro_compaction = false `, ); clearExperimentalEnv(); const core = new KimiCore(async () => ({}) as never, { homeDir }); - expect(experimentalFeatureEnabled(core, 'goal_command')).toBe(false); + expect(experimentalFeatureEnabled(core, 'micro_compaction')).toBe(false); await core.setKimiConfig({ experimental: { - 'goal_command': true, + 'micro_compaction': true, }, }); - expect(experimentalFeatureEnabled(core, 'goal_command')).toBe(true); + expect(experimentalFeatureEnabled(core, 'micro_compaction')).toBe(true); }); it('updates the shared experimental resolver while goal tools stay available', async () => { @@ -155,7 +149,7 @@ goal_command = false join(homeDir, 'config.toml'), `${baseModelConfig()} [experimental] -goal_command = false +micro_compaction = false `, ); clearExperimentalEnv(); @@ -177,18 +171,18 @@ goal_command = false const session = core.sessions.get(created.id); const mainAgent = session?.getReadyAgent('main'); - expect(session?.experimentalFlags.enabled('goal_command')).toBe(false); - expect(mainAgent?.experimentalFlags.enabled('goal_command')).toBe(false); + expect(session?.experimentalFlags.enabled('micro_compaction')).toBe(false); + expect(mainAgent?.experimentalFlags.enabled('micro_compaction')).toBe(false); expect(mainAgent?.tools.data().some((tool) => tool.name === 'CreateGoal')).toBe(true); await core.setKimiConfig({ experimental: { - 'goal_command': true, + 'micro_compaction': true, }, }); - expect(session?.experimentalFlags.enabled('goal_command')).toBe(true); - expect(mainAgent?.experimentalFlags.enabled('goal_command')).toBe(true); + expect(session?.experimentalFlags.enabled('micro_compaction')).toBe(true); + expect(mainAgent?.experimentalFlags.enabled('micro_compaction')).toBe(true); expect(mainAgent?.tools.data().some((tool) => tool.name === 'CreateGoal')).toBe(true); await rpc.reloadSession({ sessionId: created.id }); diff --git a/packages/agent-core/test/session/init.test.ts b/packages/agent-core/test/session/init.test.ts index 8ab55f8329..d6b844bfb6 100644 --- a/packages/agent-core/test/session/init.test.ts +++ b/packages/agent-core/test/session/init.test.ts @@ -9,7 +9,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import type { Agent, AgentOptions } from '../../src/agent'; import { trimTrailingOpenToolExchange } from '../../src/agent/context/projector'; -import { FLAG_DEFINITIONS, FlagResolver } from '../../src/flags'; import { ProviderManager } from '../../src/session/provider-manager'; import type { ResolvedAgentProfile } from '../../src/profile'; import type { SDKSessionRPC } from '../../src/rpc'; @@ -549,7 +548,7 @@ describe('AgentAPI.startBtw', () => { } }); - it('discovers sub-skills and builtins regardless of session-scoped experimental flags', async () => { + it('discovers sub-skills and builtins', async () => { const workDir = await makeTempDir(); const sessionDir = await makeTempDir(); const skillsRoot = join(workDir, 'skills'); @@ -577,7 +576,6 @@ describe('AgentAPI.startBtw', () => { homedir: sessionDir, rpc: createSessionRpc([]), skills: { explicitDirs: [skillsRoot] }, - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { 'sub_skill': false }), }); try { @@ -595,7 +593,6 @@ describe('AgentAPI.startBtw', () => { homedir: sessionDir, rpc: createSessionRpc([]), skills: { explicitDirs: [skillsRoot] }, - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { 'sub_skill': true }), }); try { diff --git a/packages/agent-core/test/skill/scanner.test.ts b/packages/agent-core/test/skill/scanner.test.ts index 2ac4b1f4ec..d6e67a9d1c 100644 --- a/packages/agent-core/test/skill/scanner.test.ts +++ b/packages/agent-core/test/skill/scanner.test.ts @@ -418,7 +418,7 @@ describe('discoverSkills shape and ordering', () => { expect(skills.map((s) => s.name)).toEqual(['outer', 'outer.inner']); }); - it('ignores disabled experimental env for declared sub-skill discovery', async () => { + it('discovers declared sub-skills without a feature flag', async () => { const { repoDir } = await makeWorkspace(); const root = path.join(repoDir, '.kimi-code', 'skills'); await writeSkill(root, path.join('outer', 'SKILL.md'), [ @@ -439,8 +439,6 @@ describe('discoverSkills shape and ordering', () => { 'Inner body.', ]); - vi.stubEnv('KIMI_CODE_EXPERIMENTAL_SUB_SKILL', '0'); - const skills = await discoverSkills({ roots: [{ path: root, source: 'user' }], }); diff --git a/packages/agent-core/test/tools/ask-user.test.ts b/packages/agent-core/test/tools/ask-user.test.ts index da38492b72..9af2b0d55f 100644 --- a/packages/agent-core/test/tools/ask-user.test.ts +++ b/packages/agent-core/test/tools/ask-user.test.ts @@ -3,7 +3,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import type { Agent } from '../../src/agent'; import type { PermissionMode } from '../../src/agent/permission'; import { ErrorCodes, KimiError } from '../../src/errors'; -import { FLAG_DEFINITIONS, FlagResolver, MASTER_ENV } from '../../src/flags'; import type { QuestionRequest, QuestionResult } from '../../src/rpc'; import { AskUserQuestionInputSchema, @@ -58,7 +57,6 @@ function makeTool( permission: { mode: options.mode ?? 'manual' }, rpc: { requestQuestion }, telemetry: { track: telemetryTrack }, - experimentalFlags: new FlagResolver(), } as unknown as Agent; return { tool: new AskUserQuestionTool(agent), requestQuestion, telemetryTrack }; } @@ -114,31 +112,16 @@ describe('AskUserQuestionTool', () => { }); it('always builds the background-question schema', () => { - vi.stubEnv(MASTER_ENV, '1'); - const enabledAgent = { - rpc: { requestQuestion: vi.fn() }, - telemetry: { track: vi.fn() }, - background: createBackgroundManager().manager, - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { - 'background_ask': true, - }), - } as unknown as Agent; - const disabledAgent = { + const agent = { rpc: { requestQuestion: vi.fn() }, telemetry: { track: vi.fn() }, background: createBackgroundManager().manager, - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { - 'background_ask': false, - }), } as unknown as Agent; - const enabledTool = new AskUserQuestionTool(enabledAgent); - const disabledTool = new AskUserQuestionTool(disabledAgent); + const tool = new AskUserQuestionTool(agent); - expect(enabledTool.description).toContain('Set background=true'); - expect(JSON.stringify(enabledTool.parameters)).toContain('background'); - expect(disabledTool.description).toContain('Set background=true'); - expect(JSON.stringify(disabledTool.parameters)).toContain('background'); + expect(tool.description).toContain('Set background=true'); + expect(JSON.stringify(tool.parameters)).toContain('background'); }); it.each(['manual', 'yolo'] as const)( @@ -216,7 +199,6 @@ describe('AskUserQuestionTool', () => { rpc: { requestQuestion }, telemetry: { track: telemetryTrack }, background: manager, - experimentalFlags: new FlagResolver(), } as unknown as Agent; const tool = new AskUserQuestionTool(agent); expect(tool.description).toContain('Set background=true'); @@ -253,10 +235,7 @@ describe('AskUserQuestionTool', () => { }); }); - it('starts background questions even when experimental env is off', async () => { - vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '0'); - vi.stubEnv('KIMI_CODE_EXPERIMENTAL_BACKGROUND_ASK', '0'); - + it('starts background questions without an experimental flag', async () => { let resolveQuestion!: (result: QuestionResult) => void; const questionResult = new Promise((resolve) => { resolveQuestion = resolve; @@ -267,7 +246,6 @@ describe('AskUserQuestionTool', () => { rpc: { requestQuestion }, telemetry: { track: vi.fn() }, background: manager, - experimentalFlags: new FlagResolver(), } as unknown as Agent; const tool = new AskUserQuestionTool(agent); expect(tool.description).toContain('Set background=true'); diff --git a/packages/agent-core/test/tools/goal.test.ts b/packages/agent-core/test/tools/goal.test.ts index ca9bde0ff6..b44dc25de2 100644 --- a/packages/agent-core/test/tools/goal.test.ts +++ b/packages/agent-core/test/tools/goal.test.ts @@ -3,7 +3,6 @@ import { describe, expect, it } from 'vitest'; import type { Agent } from '../../src/agent'; import { GoalMode } from '../../src/agent/goal'; import { ErrorCodes } from '../../src/errors'; -import { FLAG_DEFINITIONS, FlagResolver } from '../../src/flags'; import { compileToolArgsValidator, validateToolArgs } from '../../src/tools/args-validator'; import { CreateGoalTool, @@ -288,12 +287,9 @@ describe('UpdateGoalTool', () => { }); describe('ToolManager goal tool registration', () => { - function loopToolNames(type: 'main' | 'sub', goalEnabled: boolean): readonly string[] { + function loopToolNames(type: 'main' | 'sub'): readonly string[] { const ctxAgent = testAgent({ type, - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { - goal_command: goalEnabled, - }), }); // configure() gives the agent a provider so builtin tools can initialize. ctxAgent.configure({ tools: ['Read', 'CreateGoal', 'GetGoal', 'SetGoalBudget'] }); @@ -302,20 +298,14 @@ describe('ToolManager goal tool registration', () => { return ctxAgent.agent.tools.loopTools.map((tool) => tool.name); } - it('exposes goal tools to the main agent even when the flag is disabled', () => { - const names = loopToolNames('main', false); - expect(names).toEqual(expect.arrayContaining(['CreateGoal', 'GetGoal'])); - expect(names).not.toContain('SetGoalBudget'); - }); - - it('exposes goal tools to the main agent when the scoped flag is enabled', () => { - const names = loopToolNames('main', true); + it('exposes goal tools to the main agent', () => { + const names = loopToolNames('main'); expect(names).toEqual(expect.arrayContaining(['CreateGoal', 'GetGoal'])); expect(names).not.toContain('SetGoalBudget'); }); it('does not expose goal tools to subagents even when enabled', () => { - const names = loopToolNames('sub', true); + const names = loopToolNames('sub'); expect(names).not.toContain('CreateGoal'); expect(names).not.toContain('GetGoal'); expect(names).not.toContain('SetGoalBudget'); @@ -326,9 +316,6 @@ describe('ToolManager goal tool registration', () => { const ctxAgent = testAgent({ type: 'main', goal: store, - experimentalFlags: new FlagResolver({}, FLAG_DEFINITIONS, { - goal_command: true, - }), }); ctxAgent.configure({ tools: ['Read', 'CreateGoal', 'GetGoal', 'SetGoalBudget', 'UpdateGoal'] }); ctxAgent.agent.tools.initializeBuiltinTools(); diff --git a/packages/node-sdk/test/config.test.ts b/packages/node-sdk/test/config.test.ts index 5b4f790d48..bf2fafc13a 100644 --- a/packages/node-sdk/test/config.test.ts +++ b/packages/node-sdk/test/config.test.ts @@ -320,38 +320,32 @@ describe('KimiHarness config API', () => { it('returns experimental feature metadata through the harness', async () => { vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '0'); - vi.stubEnv('KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND', ''); vi.stubEnv('KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION', ''); - vi.stubEnv('KIMI_CODE_EXPERIMENTAL_BACKGROUND_ASK', ''); const homeDir = await makeTempDir(); await writeFile( join(homeDir, 'config.toml'), ` [experimental] -goal_command = true -background_ask = false +micro_compaction = false `, 'utf-8', ); const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY }); const features = await harness.getExperimentalFeatures(); - const goalCommand = features.find((feature) => feature.id === 'goal_command'); + const microCompaction = features.find((feature) => feature.id === 'micro_compaction'); - expect(goalCommand).toMatchObject({ - id: 'goal_command', - title: 'Goal command', - enabled: true, + expect(microCompaction).toMatchObject({ + id: 'micro_compaction', + title: 'Micro compaction', + enabled: false, source: 'config', - configValue: true, - env: 'KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND', + configValue: false, + env: 'KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION', }); - expect(features).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: 'goal_command', enabled: true }), - expect.objectContaining({ id: 'background_ask', enabled: false }), - ]), - ); + expect(features).toEqual([ + expect.objectContaining({ id: 'micro_compaction', enabled: false }), + ]); }); it('can create the default config scaffold without selecting a model', async () => {