diff --git a/.changeset/default-v2-engine-cli.md b/.changeset/default-v2-engine-cli.md new file mode 100644 index 00000000000..cab868c44ec --- /dev/null +++ b/.changeset/default-v2-engine-cli.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": minor +--- + +Run the CLI surfaces (interactive TUI, `kimi -p`, `kimi doctor`, `kimi acp`, `kimi export`, `kimi provider`) on the agent-core-v2 engine by default, and drop the experimental `kimi acp-v2` command now that `kimi acp` uses the new engine directly. Set `KIMI_CODE_LEGACY_FLAG=1` to fall back to the legacy engine. diff --git a/apps/kimi-code/src/cli/commands.ts b/apps/kimi-code/src/cli/commands.ts index 4690912f2aa..a090df4d0f8 100644 --- a/apps/kimi-code/src/cli/commands.ts +++ b/apps/kimi-code/src/cli/commands.ts @@ -2,10 +2,8 @@ import { CLI_COMMAND_NAME } from '#/constant/app'; import { registerMigrateCommand } from '#/migration/index'; import { Command, InvalidArgumentError, Option } from 'commander'; -import { isAcpV2Enabled } from './experimental-v2'; import type { CLIOptions } from './options'; import { registerAcpCommand } from './sub/acp'; -import { registerAcpV2Command } from './sub/acp-v2'; import { registerDoctorCommand } from './sub/doctor'; import { registerExportCommand } from './sub/export'; import { registerLoginCommand } from './sub/login'; @@ -119,9 +117,6 @@ export function createProgram( registerProviderCommand(program); registerAcpCommand(program); registerWebCommand(program); - if (isAcpV2Enabled()) { - registerAcpV2Command(program); - } registerLoginCommand(program); registerDoctorCommand(program); registerVisCommand(program); diff --git a/apps/kimi-code/src/cli/experimental-v2.ts b/apps/kimi-code/src/cli/experimental-v2.ts index de40d76c2d3..09deacc9c2f 100644 --- a/apps/kimi-code/src/cli/experimental-v2.ts +++ b/apps/kimi-code/src/cli/experimental-v2.ts @@ -1,22 +1,17 @@ /** - * Experimental agent-core-v2 engine gate for the CLI surfaces. + * Agent engine routing gates for the CLI surfaces. * - * When the master switch `KIMI_CODE_EXPERIMENTAL_FLAG` is truthy, `kimi -p` - * (print mode) routes to the native agent-core-v2 runner (see - * `run-prompt.ts`), the interactive TUI builds its harness through the - * SDK's v2-backed client (see `run-shell.ts`), and `kimi doctor` validates - * config.toml against the v2 section registry (see `sub/doctor.ts` / - * `v2/validate-config.ts`), all instead of the default v1 engine. The - * master switch also enables every experimental feature flag in the engine. Read directly from the env (matching - * `cli/update/rollout.ts`) because the CLI must not depend on the core flag - * registry. Unset / any non-truthy value keeps the v1 path. + * `kimi -p`, the interactive TUI, and `kimi doctor` use the native + * agent-core-v2 path by default. A truthy `KIMI_CODE_LEGACY_FLAG` selects the + * legacy agent-core-backed path instead. `KIMI_CODE_EXPERIMENTAL_FLAG` remains + * the master switch for experimental features within either engine; it does + * not select the engine. * * Note: `kimi web` always boots kap-server (the agent-core-v2 engine * server) — it does not consult this switch. */ -export const KIMI_V2_ENV = 'KIMI_CODE_EXPERIMENTAL_FLAG'; -export const KIMI_ACP_V2_ENV = 'KIMI_CODE_EXPERIMENTAL_ACP_V2'; +export const KIMI_LEGACY_ENV = 'KIMI_CODE_LEGACY_FLAG'; const TRUTHY_VALUES = new Set(['1', 'true', 'yes', 'on']); @@ -27,14 +22,14 @@ function isTruthyEnv( return TRUTHY_VALUES.has((env[key] ?? '').trim().toLowerCase()); } -export function isKimiV2Enabled( +export function isLegacyEnabled( env: Readonly> = process.env, ): boolean { - return isTruthyEnv(KIMI_V2_ENV, env); + return isTruthyEnv(KIMI_LEGACY_ENV, env); } -export function isAcpV2Enabled( +export function isKimiV2Enabled( env: Readonly> = process.env, ): boolean { - return isTruthyEnv(KIMI_ACP_V2_ENV, env) || isKimiV2Enabled(env); + return !isLegacyEnabled(env); } diff --git a/apps/kimi-code/src/cli/prompt-session.ts b/apps/kimi-code/src/cli/prompt-session.ts index f6e3a240a9c..e4b4410af9a 100644 --- a/apps/kimi-code/src/cli/prompt-session.ts +++ b/apps/kimi-code/src/cli/prompt-session.ts @@ -3,11 +3,11 @@ * * `run-prompt.ts` only needs a small subset of the SDK `KimiHarness` / `Session` * API. Coding the print-mode driver against these narrow interfaces — instead of - * the concrete SDK classes — lets the same driver run on either the v1 engine - * (`createKimiHarness`, the default) or the experimental agent-core-v2 engine - * (`createPromptHarnessV2`, gated by `KIMI_CODE_EXPERIMENTAL_FLAG`). Both the - * v1 `KimiHarness` / `Session` and the v2 harness structurally satisfy these - * interfaces, so no adapter wrappers are needed on the v1 path. + * the concrete SDK classes — lets the same driver run on either the legacy + * engine (`createKimiHarness`) or the default agent-core-v2 engine + * (`createPromptHarnessV2`, selected unless `KIMI_CODE_LEGACY_FLAG` is truthy). + * Both the legacy `KimiHarness` / `Session` and the v2 harness structurally + * satisfy these interfaces, so no adapter wrappers are needed on the legacy path. */ import type { diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index fd795e838db..cd519b223ea 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -101,10 +101,9 @@ export async function runPrompt( io: PromptRunIO = {}, ): Promise { if (isKimiV2Enabled()) { - // The experimental agent-core-v2 engine runs on its own native DI service - // runtime (see v2/run-v2-print.ts); it does not share the v1 PromptHarness - // path below. Loaded lazily so the v2 module graph stays off the default - // (v1) path. + // The agent-core-v2 engine runs on its own native DI service runtime (see + // v2/run-v2-print.ts); it does not share the v1 PromptHarness path below. + // Loaded lazily so the v2 module graph stays off the legacy path. const { runV2Print } = await import('./v2/run-v2-print'); await runV2Print(opts, version, io); return; diff --git a/apps/kimi-code/src/cli/run-shell.ts b/apps/kimi-code/src/cli/run-shell.ts index 35b0ca9e01d..3d6c741cebd 100644 --- a/apps/kimi-code/src/cli/run-shell.ts +++ b/apps/kimi-code/src/cli/run-shell.ts @@ -81,9 +81,9 @@ export async function runShell( }, sessionStartedProperties: { yolo: opts.yolo, auto: opts.auto, plan: opts.plan, afk: false }, }; - // Experimental agent-core-v2 route (same master switch as `kimi -p`): the - // harness is the SDK's v2-backed client, so the whole TUI runs on the - // agent-core-v2 engine. + // The agent-core-v2 route is the default (same engine gate as `kimi -p`): + // the harness is the SDK's v2-backed client, so the whole TUI runs on the + // agent-core-v2 engine unless the legacy flag is set. const engineV2 = isKimiV2Enabled(); const harness = engineV2 ? createKimiHarnessV2(harnessOptions) diff --git a/apps/kimi-code/src/cli/sub/acp-v2.ts b/apps/kimi-code/src/cli/sub/acp-native.ts similarity index 78% rename from apps/kimi-code/src/cli/sub/acp-v2.ts rename to apps/kimi-code/src/cli/sub/acp-native.ts index d1aef65eaf1..2b9886769f6 100644 --- a/apps/kimi-code/src/cli/sub/acp-v2.ts +++ b/apps/kimi-code/src/cli/sub/acp-native.ts @@ -1,11 +1,9 @@ /** - * `kimi acp-v2` sub-command. + * Native `kimi acp` implementation. * * Starts the Agent Client Protocol (ACP) server backed directly by the * DI × Scope agent engine (`agent-core-v2`) over stdio, so ACP-compatible - * clients can drive a kimi-code session on the new engine. This is the v2 - * counterpart to `kimi acp` (which runs the legacy `@moonshot-ai/acp-adapter` - * over the SDK harness). + * clients can drive a kimi-code session on the default engine. * * Wire-up mirrors `kimi acp` for the parts that are host-independent: * - `--login` pivots into the shared device-code login flow (the entry point @@ -17,9 +15,8 @@ * `_meta['terminal-auth'].command` fallback. * * `@moonshot-ai/acp-server` (and its `agent-core-v2` engine) is loaded via a - * lazy dynamic import so the default CLI / `kimi acp` module graph stays free - * of the experimental v2 engine — mirroring the `kimi server run` v2 routing - * in `#/cli/sub/server/run.ts`. + * lazy dynamic import so parsing the CLI does not initialize the ACP engine — + * mirroring the `kimi server run` v2 routing in `#/cli/sub/server/run.ts`. */ import type { Command } from 'commander'; @@ -30,12 +27,10 @@ import { getDataDir } from '#/utils/paths'; import { runLoginFlow } from './login-flow'; -export function registerAcpV2Command(parent: Command): void { +export function registerNativeAcpCommand(parent: Command): void { parent - .command('acp-v2') - .description( - 'Run kimi-code as an Agent Client Protocol (ACP) server over stdio (experimental agent-core-v2 engine).', - ) + .command('acp') + .description('Run kimi-code as an Agent Client Protocol (ACP) server over stdio.') .option( '--login', 'Run the device-code login flow then exit (entry point for ACP terminal-auth).', @@ -70,7 +65,7 @@ export function registerAcpV2Command(parent: Command): void { }); process.exit(0); } catch (error) { - process.stderr.write(`acp-v2 server: fatal error: ${String(error)}\n`); + process.stderr.write(`acp server: fatal error: ${String(error)}\n`); process.exit(1); } }); diff --git a/apps/kimi-code/src/cli/sub/acp.ts b/apps/kimi-code/src/cli/sub/acp.ts index a98991464f5..4da7e892ea8 100644 --- a/apps/kimi-code/src/cli/sub/acp.ts +++ b/apps/kimi-code/src/cli/sub/acp.ts @@ -1,9 +1,9 @@ /** - * `kimi acp` sub-command. + * `kimi acp` sub-command routing and legacy implementation. * - * Starts the Agent Client Protocol (ACP) server over stdio so that - * ACP-compatible clients (editors, IDEs, custom front-ends) can drive - * a kimi-code session. + * By default the command delegates to the agent-core-v2 ACP server. A truthy + * `KIMI_CODE_LEGACY_FLAG` uses the SDK harness and `@moonshot-ai/acp-adapter` + * implementation below instead. * * Wire-up: * - A {@link KimiHarness} is constructed with the kimi-code host identity @@ -33,9 +33,16 @@ import { KIMI_CODE_HOME_ENV } from '#/constant/app'; import { createKimiCodeHostIdentity, getVersion } from '#/cli/version'; import { buildSkillSlashCommands } from '#/tui/commands/skills'; +import { isLegacyEnabled } from '../experimental-v2'; +import { registerNativeAcpCommand } from './acp-native'; import { runLoginFlow } from './login-flow'; export function registerAcpCommand(parent: Command): void { + if (!isLegacyEnabled()) { + registerNativeAcpCommand(parent); + return; + } + parent .command('acp') .description('Run kimi-code as an Agent Client Protocol (ACP) server over stdio.') diff --git a/apps/kimi-code/src/cli/sub/doctor.ts b/apps/kimi-code/src/cli/sub/doctor.ts index d6d5db3d1b9..8081c6e0e37 100644 --- a/apps/kimi-code/src/cli/sub/doctor.ts +++ b/apps/kimi-code/src/cli/sub/doctor.ts @@ -134,9 +134,9 @@ function resolveDeps(deps: Partial | DoctorDeps | undefined): Resolv deps?.validateConfigToml ?? (async (text, filePath) => { if (isKimiV2Enabled()) { - // Experimental v2 route (same master switch as `kimi -p`): validate - // with the agent-core-v2 section registry instead of the v1 schema. - // Loaded lazily so the v2 module graph stays off the default path. + // Default v2 route (same engine gate as `kimi -p`): validate with + // the agent-core-v2 section registry instead of the legacy schema. + // Loaded lazily so the v2 module graph stays off the legacy path. const { validateConfigTomlV2 } = await import('../v2/validate-config'); return validateConfigTomlV2(text, filePath); } diff --git a/apps/kimi-code/src/cli/sub/export.ts b/apps/kimi-code/src/cli/sub/export.ts index a93c3e27194..38796832c9b 100644 --- a/apps/kimi-code/src/cli/sub/export.ts +++ b/apps/kimi-code/src/cli/sub/export.ts @@ -15,6 +15,7 @@ import { } from '@moonshot-ai/kimi-telemetry'; import { createKimiHarness, + createKimiHarnessV2, type ExportSessionInput, type ExportSessionResult, type KimiHarness, @@ -30,6 +31,8 @@ import { detectInstallSource } from '#/cli/update/source'; import { createKimiCodeHostIdentity } from '#/cli/version'; import { detectShellEnvironment } from '#/utils/process/shell-env'; +import { isKimiV2Enabled } from '../experimental-v2'; + interface WritableLike { write(chunk: string): boolean; } @@ -120,15 +123,22 @@ export function registerExportCommand(parent: Command, deps?: Partial { - await handleExport(createDefaultExportDeps(deps), sessionId, options.output, { - yes: options.yes === true, - includeGlobalLog: options.includeGlobalLog !== false, - }); + const resolved = createDefaultExportDeps(deps); + try { + await handleExport(resolved, sessionId, options.output, { + yes: options.yes === true, + includeGlobalLog: options.includeGlobalLog !== false, + }); + } finally { + await resolved.close(); + } }, ); } -function createDefaultExportDeps(overrides: Partial = {}): ExportDeps { +function createDefaultExportDeps(overrides: Partial = {}): ExportDeps & { + readonly close: () => Promise; +} { let harness: KimiHarness | undefined; let telemetryBootstrap: ReturnType | undefined; let telemetryInitialized = false; @@ -145,7 +155,9 @@ function createDefaultExportDeps(overrides: Partial = {}): ExportDep }; const getHarness = (): KimiHarness => { const currentTelemetryBootstrap = getTelemetryBootstrap(); - harness ??= createKimiHarness({ + // Same engine gate as `kimi -p` / the TUI: the SDK's v2-backed harness by + // default, the legacy agent-core harness when KIMI_CODE_LEGACY_FLAG is set. + harness ??= (isKimiV2Enabled() ? createKimiHarnessV2 : createKimiHarness)({ homeDir: currentTelemetryBootstrap.homeDir, identity, telemetry: telemetryClient, @@ -197,6 +209,12 @@ function createDefaultExportDeps(overrides: Partial = {}): ExportDep stdout: overrides.stdout ?? process.stdout, stderr: overrides.stderr ?? process.stderr, exit: overrides.exit ?? ((code: number) => process.exit(code)), + // The v2 harness boots an engine whose watchers hold the event loop open; + // close it so a one-shot command can exit. No-op when the run never needed + // the harness. + close: async () => { + await harness?.close(); + }, }; } diff --git a/apps/kimi-code/src/cli/sub/provider.ts b/apps/kimi-code/src/cli/sub/provider.ts index bb61b0add3d..0b73d81f26a 100644 --- a/apps/kimi-code/src/cli/sub/provider.ts +++ b/apps/kimi-code/src/cli/sub/provider.ts @@ -24,6 +24,7 @@ import { catalogProviderModels, CatalogFetchError, createKimiHarness, + createKimiHarnessV2, DEFAULT_CATALOG_URL, resolveCatalogImport, type Catalog, @@ -36,6 +37,8 @@ import type { Command } from 'commander'; import { createKimiCodeHostIdentity, createKimiCodeUserAgent } from '#/cli/version'; import { fetchCatalogOrBuiltIn } from '#/utils/catalog-fetch'; +import { isKimiV2Enabled } from '../experimental-v2'; + interface WritableLike { write(chunk: string): boolean; } @@ -457,12 +460,17 @@ export function registerProviderCommand(parent: Command, deps?: Partial Promise): Promise => { + const runAction = async ( + resolved: ResolvedProviderDeps, + run: () => Promise, + ): Promise => { try { await run(); } catch (error) { resolved.stderr.write(`${errorMessage(error)}\n`); resolved.exit(1); + } finally { + await resolved.close(); } }; @@ -546,20 +554,30 @@ export function registerProviderCommand(parent: Command, deps?: Partial = {}): ProviderDeps { +type ResolvedProviderDeps = ProviderDeps & { readonly close: () => Promise }; + +function resolveDeps(overrides: Partial = {}): ResolvedProviderDeps { let harness: KimiHarness | undefined; const identity = createKimiCodeHostIdentity(); return { getHarness: overrides.getHarness ?? (() => { - harness ??= createKimiHarness({ identity }); + // Same engine gate as the TUI's `/provider` flow: the SDK's v2-backed + // harness by default, the legacy agent-core harness when + // KIMI_CODE_LEGACY_FLAG is set. + harness ??= (isKimiV2Enabled() ? createKimiHarnessV2 : createKimiHarness)({ identity }); return harness; }), stdout: overrides.stdout ?? process.stdout, stderr: overrides.stderr ?? process.stderr, env: overrides.env ?? process.env, exit: overrides.exit ?? ((code: number) => process.exit(code)), + // The v2 harness boots an engine whose watchers hold the event loop open; + // close it so a one-shot command can exit. No-op for injected harnesses. + close: async () => { + await harness?.close(); + }, }; } diff --git a/apps/kimi-code/src/cli/v2/run-v2-print.ts b/apps/kimi-code/src/cli/v2/run-v2-print.ts index aac6062fcef..6112d8bf67a 100644 --- a/apps/kimi-code/src/cli/v2/run-v2-print.ts +++ b/apps/kimi-code/src/cli/v2/run-v2-print.ts @@ -13,7 +13,7 @@ * - applies the print-mode background policy (config-driven, v1-aligned: * `exit` / `drain` / `steer`) before exiting. * - * Selected by `runPrompt` when `KIMI_CODE_EXPERIMENTAL_FLAG` is set. + * Selected by `runPrompt` unless `KIMI_CODE_LEGACY_FLAG` is truthy. */ import { readFile } from 'node:fs/promises'; diff --git a/apps/kimi-code/src/cli/v2/validate-config.ts b/apps/kimi-code/src/cli/v2/validate-config.ts index 89d14869b5b..04769d9250f 100644 --- a/apps/kimi-code/src/cli/v2/validate-config.ts +++ b/apps/kimi-code/src/cli/v2/validate-config.ts @@ -1,10 +1,10 @@ /** - * Experimental v2 config.toml validation for `kimi doctor`. + * V2 config.toml validation for `kimi doctor`. * - * Loaded lazily (dynamic import) by the doctor command only when the - * agent-core-v2 master switch (`KIMI_CODE_EXPERIMENTAL_FLAG`) is on, so the - * v2 module graph stays off the default (v1) doctor path. Validation uses the - * engine's own section registry instead of v1's whole-document strict schema: + * Loaded lazily (dynamic import) by the doctor command on the default + * agent-core-v2 path, so the v2 module graph stays off the legacy doctor path. + * Validation uses the engine's own section registry instead of the legacy + * whole-document strict schema: * importing the package root runs every built-in section's side-effect * registration ("import = register"), and `ConfigRegistry` is then * constructed directly — no DI container, no `ConfigService`, no file IO. diff --git a/apps/kimi-code/src/tui/commands/dispatch.ts b/apps/kimi-code/src/tui/commands/dispatch.ts index e5aef1639cf..a9eb40f0593 100644 --- a/apps/kimi-code/src/tui/commands/dispatch.ts +++ b/apps/kimi-code/src/tui/commands/dispatch.ts @@ -112,7 +112,7 @@ export interface SlashCommandHost { state: TUIState; session: Session | undefined; readonly harness: KimiHarness; - /** agent-core-v2 engine (KIMI_CODE_EXPERIMENTAL_FLAG); enables lazy session creation. */ + /** agent-core-v2 engine; enables lazy session creation. */ readonly engineV2: boolean; cancelInFlight: (() => void) | undefined; deferUserMessages: boolean; diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index a979de72195..845c48bf7ec 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -194,7 +194,7 @@ export interface KimiTUIStartupInput { readonly migrationPlan?: MigrationPlan | null; /** When true, run only the migration screen, then exit (the `kimi migrate` command). */ readonly migrateOnly?: boolean; - /** agent-core-v2 engine (KIMI_CODE_EXPERIMENTAL_FLAG); enables the startup workspace-trust prompt. */ + /** agent-core-v2 engine; enables the startup workspace-trust prompt. */ readonly engineV2?: boolean; } diff --git a/apps/kimi-code/test/cli/acp-v2.test.ts b/apps/kimi-code/test/cli/acp-native.test.ts similarity index 75% rename from apps/kimi-code/test/cli/acp-v2.test.ts rename to apps/kimi-code/test/cli/acp-native.test.ts index d26b0b25331..1649c94da1c 100644 --- a/apps/kimi-code/test/cli/acp-v2.test.ts +++ b/apps/kimi-code/test/cli/acp-native.test.ts @@ -1,5 +1,5 @@ /** - * `kimi acp-v2` + * `kimi acp` * * Verifies that the ACP v2 sub-command is registered on the program and that * the action wires `@moonshot-ai/acp-server`'s `runAcpServer` (the real server @@ -17,7 +17,8 @@ vi.mock('@moonshot-ai/acp-server', () => ({ import { runAcpServer } from '@moonshot-ai/acp-server'; -import { registerAcpV2Command } from '#/cli/sub/acp-v2'; +import { registerAcpCommand } from '#/cli/sub/acp'; +import { registerNativeAcpCommand } from '#/cli/sub/acp-native'; import { getDataDir } from '#/utils/paths'; class ExitCalled extends Error { @@ -26,11 +27,12 @@ class ExitCalled extends Error { } } -describe('kimi acp-v2', () => { +describe('kimi acp', () => { let exitSpy: ReturnType; let stderrSpy: ReturnType; beforeEach(() => { + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', ''); vi.mocked(runAcpServer).mockClear(); exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number | string | null) => { throw new ExitCalled(code); @@ -41,22 +43,36 @@ describe('kimi acp-v2', () => { afterEach(() => { exitSpy.mockRestore(); stderrSpy.mockRestore(); + vi.unstubAllEnvs(); }); - it('registers an `acp-v2` subcommand on the program', () => { + it('registers an `acp` subcommand on the program', () => { const program = new Command('kimi'); - registerAcpV2Command(program); + registerNativeAcpCommand(program); - const acpV2 = program.commands.find((c) => c.name() === 'acp-v2'); + const acpV2 = program.commands.find((c) => c.name() === 'acp'); expect(acpV2).toBeDefined(); expect(acpV2?.description()).toMatch(/Agent Client Protocol/); }); + it('uses the v2 server for the default `acp` command', async () => { + const program = new Command('kimi').exitOverride(); + registerAcpCommand(program); + + await expect(program.parseAsync(['node', 'kimi', 'acp'])).rejects.toThrow(ExitCalled); + + expect(runAcpServer).toHaveBeenCalledTimes(1); + expect(vi.mocked(runAcpServer).mock.calls[0]?.[0]).toEqual( + expect.objectContaining({ homeDir: getDataDir() }), + ); + expect(exitSpy).toHaveBeenCalledWith(0); + }); + it('invokes runAcpServer with the v2 host options and exits 0 on success', async () => { const program = new Command('kimi').exitOverride(); - registerAcpV2Command(program); + registerNativeAcpCommand(program); - await expect(program.parseAsync(['node', 'kimi', 'acp-v2'])).rejects.toThrow(ExitCalled); + await expect(program.parseAsync(['node', 'kimi', 'acp'])).rejects.toThrow(ExitCalled); expect(runAcpServer).toHaveBeenCalledTimes(1); const optsArg = vi.mocked(runAcpServer).mock.calls[0]?.[0]; @@ -74,9 +90,9 @@ describe('kimi acp-v2', () => { process.env['KIMI_CODE_HOME'] = '/tmp/kimi-debug'; try { const program = new Command('kimi').exitOverride(); - registerAcpV2Command(program); + registerNativeAcpCommand(program); - await expect(program.parseAsync(['node', 'kimi', 'acp-v2'])).rejects.toThrow(ExitCalled); + await expect(program.parseAsync(['node', 'kimi', 'acp'])).rejects.toThrow(ExitCalled); const optsArg = vi.mocked(runAcpServer).mock.calls[0]?.[0]; expect(optsArg).toEqual( @@ -99,9 +115,9 @@ describe('kimi acp-v2', () => { delete process.env['KIMI_CODE_HOME']; try { const program = new Command('kimi').exitOverride(); - registerAcpV2Command(program); + registerNativeAcpCommand(program); - await expect(program.parseAsync(['node', 'kimi', 'acp-v2'])).rejects.toThrow(ExitCalled); + await expect(program.parseAsync(['node', 'kimi', 'acp'])).rejects.toThrow(ExitCalled); const optsArg = vi.mocked(runAcpServer).mock.calls[0]?.[0] as { terminalAuthEnv?: unknown; @@ -118,9 +134,9 @@ describe('kimi acp-v2', () => { it('forwards process.argv[1] as terminalAuthLegacyCommand', async () => { const program = new Command('kimi').exitOverride(); - registerAcpV2Command(program); + registerNativeAcpCommand(program); - await expect(program.parseAsync(['node', 'kimi', 'acp-v2'])).rejects.toThrow(ExitCalled); + await expect(program.parseAsync(['node', 'kimi', 'acp'])).rejects.toThrow(ExitCalled); const optsArg = vi.mocked(runAcpServer).mock.calls[0]?.[0] as { terminalAuthLegacyCommand?: string; @@ -145,12 +161,12 @@ describe('kimi acp-v2', () => { }; }); vi.resetModules(); - const { registerAcpV2Command: freshRegister } = await import('#/cli/sub/acp-v2'); + const { registerNativeAcpCommand: freshRegister } = await import('#/cli/sub/acp-native'); try { const program = new Command('kimi').exitOverride(); freshRegister(program); - await expect(program.parseAsync(['node', 'kimi', 'acp-v2', '--login'])).rejects.toThrow( + await expect(program.parseAsync(['node', 'kimi', 'acp', '--login'])).rejects.toThrow( ExitCalled, ); diff --git a/apps/kimi-code/test/cli/acp.test.ts b/apps/kimi-code/test/cli/acp.test.ts index 85366252eaf..8633906c76f 100644 --- a/apps/kimi-code/test/cli/acp.test.ts +++ b/apps/kimi-code/test/cli/acp.test.ts @@ -30,6 +30,7 @@ describe('kimi acp', () => { let stderrSpy: ReturnType; beforeEach(() => { + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '1'); vi.mocked(runAcpServer).mockClear(); exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number | string | null) => { throw new ExitCalled(code); @@ -40,6 +41,7 @@ describe('kimi acp', () => { afterEach(() => { exitSpy.mockRestore(); stderrSpy.mockRestore(); + vi.unstubAllEnvs(); }); it('registers an `acp` subcommand on the program', () => { diff --git a/apps/kimi-code/test/cli/doctor.test.ts b/apps/kimi-code/test/cli/doctor.test.ts index 0e2024617c7..6422a9acc0a 100644 --- a/apps/kimi-code/test/cli/doctor.test.ts +++ b/apps/kimi-code/test/cli/doctor.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { Command } from 'commander'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { handleDoctor, @@ -14,11 +14,13 @@ import { let dir: string; beforeEach(async () => { + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', ''); dir = join(tmpdir(), `kimi-doctor-${Date.now()}-${Math.random().toString(36).slice(2)}`); await mkdir(dir, { recursive: true }); }); afterEach(async () => { + vi.unstubAllEnvs(); await rm(dir, { recursive: true, force: true }); }); @@ -101,6 +103,27 @@ describe('kimi doctor', () => { expect(out).toContain('built-in defaults will apply'); }); + it('uses the legacy validator when legacy wins over the experimental flag', async () => { + const configPath = join(dir, 'config.toml'); + const text = '[providers.kimi]\ntype = "kimi"\n'; + await writeFile(configPath, text, 'utf-8'); + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '1'); + vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '1'); + const validateConfigToml = vi.fn(async () => undefined); + const { deps } = makeDeps(); + + const code = await handleDoctor( + { + ...deps, + configRpc: { validateConfigToml } as unknown as NonNullable, + }, + { target: 'config' }, + ); + + expect(code).toBe(0); + expect(validateConfigToml).toHaveBeenCalledWith({ text, filePath: configPath }); + }); + it('checks only config.toml when the config target is selected', async () => { const { deps, stdout, stderr } = makeDeps(); @@ -269,13 +292,8 @@ max_context_size = "large" }); }); -describe('kimi doctor (v2 config validation)', () => { - beforeEach(() => { - process.env['KIMI_CODE_EXPERIMENTAL_FLAG'] = '1'; - }); - +describe('kimi doctor (default v2 config validation)', () => { afterEach(() => { - delete process.env['KIMI_CODE_EXPERIMENTAL_FLAG']; delete process.env['KIMI_LOOP_MAX_RETRIES_PER_STEP']; delete process.env['KIMI_LOOP_MAX_ATTEMPTS_PER_STEP']; }); diff --git a/apps/kimi-code/test/cli/export.test.ts b/apps/kimi-code/test/cli/export.test.ts index 14fdc019000..25f72ae1ec8 100644 --- a/apps/kimi-code/test/cli/export.test.ts +++ b/apps/kimi-code/test/cli/export.test.ts @@ -28,6 +28,7 @@ type CreateKimiDeviceId = typeof createKimiDeviceIdFn; const mocks = vi.hoisted(() => ({ kimiHarnessConstructor: vi.fn(), + kimiHarnessV2Constructor: vi.fn(), harnessEnsureConfigFile: vi.fn(), harnessGetConfig: vi.fn(async () => ({ providers: {}, @@ -36,6 +37,7 @@ const mocks = vi.hoisted(() => ({ })), harnessGetCachedAccessToken: vi.fn(), harnessExportSession: vi.fn(), + harnessClose: vi.fn(async () => {}), harnessTrack: vi.fn(), createKimiDeviceId: vi.fn(() => 'device-1'), initializeTelemetry: vi.fn(), @@ -49,26 +51,33 @@ const mocks = vi.hoisted(() => ({ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => { const actual = await importOriginal(); + const createFakeHarness = (options: { readonly homeDir?: string } | undefined) => { + const homeDir = options?.homeDir ?? '/tmp/kimi-export-home'; + if (mocks.harnessCreatesDeviceIdOnConstruction) { + mocks.createKimiDeviceId(homeDir); + } + return { + homeDir, + auth: { + getCachedAccessToken: mocks.harnessGetCachedAccessToken, + }, + ensureConfigFile: mocks.harnessEnsureConfigFile, + getConfig: mocks.harnessGetConfig, + track: mocks.harnessTrack, + exportSession: mocks.harnessExportSession, + close: mocks.harnessClose, + }; + }; return { ...actual, resolveKimiHome: mocks.resolveKimiHome, createKimiHarness: (...args: unknown[]) => { - const options = args[0] as { readonly homeDir?: string } | undefined; - const homeDir = options?.homeDir ?? '/tmp/kimi-export-home'; - if (mocks.harnessCreatesDeviceIdOnConstruction) { - mocks.createKimiDeviceId(homeDir); - } mocks.kimiHarnessConstructor(...args); - return { - homeDir, - auth: { - getCachedAccessToken: mocks.harnessGetCachedAccessToken, - }, - ensureConfigFile: mocks.harnessEnsureConfigFile, - getConfig: mocks.harnessGetConfig, - track: mocks.harnessTrack, - exportSession: mocks.harnessExportSession, - }; + return createFakeHarness(args[0] as { readonly homeDir?: string } | undefined); + }, + createKimiHarnessV2: (...args: unknown[]) => { + mocks.kimiHarnessV2Constructor(...args); + return createFakeHarness(args[0] as { readonly homeDir?: string } | undefined); }, }; }); @@ -93,10 +102,14 @@ vi.mock('@moonshot-ai/kimi-telemetry', () => ({ })); beforeEach(() => { + // Pin the legacy engine so the default-deps cases keep exercising the legacy + // SDK harness this suite asserts on; the routing cases below re-stub it. + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '1'); tmp = mkdtempSync(join(tmpdir(), 'kimi-export-')); }); afterEach(() => { + vi.unstubAllEnvs(); rmSync(tmp, { recursive: true, force: true }); vi.clearAllMocks(); mocks.harnessGetConfig.mockResolvedValue({ @@ -515,4 +528,64 @@ describe('kimi export', () => { mocks.harnessTrack.mock.invocationCallOrder[0]!, ); }); + + it('builds the v2 harness by default', async () => { + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', ''); + const program = new Command('kimi'); + const output = join(tmp, 'v2-engine.zip'); + mocks.harnessExportSession.mockResolvedValue(makeResult('ses_v2_engine', output)); + + registerExportCommand(program, { + cwd: () => tmp, + stdout: { + write: () => true, + }, + stderr: { + write: () => true, + }, + exit: ((code: number) => { + throw new ExitCalled(code); + }) as ExportDeps['exit'], + }); + + await program.parseAsync(['node', 'kimi', 'export', 'ses_v2_engine', '--output', output], { + from: 'node', + }); + + expect(mocks.kimiHarnessV2Constructor).toHaveBeenCalledTimes(1); + expect(mocks.kimiHarnessConstructor).not.toHaveBeenCalled(); + expect(mocks.harnessExportSession).toHaveBeenCalledWith( + expect.objectContaining({ id: 'ses_v2_engine', outputPath: output }), + ); + }); + + it('builds the legacy harness when the legacy flag is truthy', async () => { + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '1'); + const program = new Command('kimi'); + const output = join(tmp, 'legacy-engine.zip'); + mocks.harnessExportSession.mockResolvedValue(makeResult('ses_legacy_engine', output)); + + registerExportCommand(program, { + cwd: () => tmp, + stdout: { + write: () => true, + }, + stderr: { + write: () => true, + }, + exit: ((code: number) => { + throw new ExitCalled(code); + }) as ExportDeps['exit'], + }); + + await program.parseAsync(['node', 'kimi', 'export', 'ses_legacy_engine', '--output', output], { + from: 'node', + }); + + expect(mocks.kimiHarnessConstructor).toHaveBeenCalledTimes(1); + expect(mocks.kimiHarnessV2Constructor).not.toHaveBeenCalled(); + expect(mocks.harnessExportSession).toHaveBeenCalledWith( + expect.objectContaining({ id: 'ses_legacy_engine', outputPath: output }), + ); + }); }); diff --git a/apps/kimi-code/test/cli/goal-prompt.test.ts b/apps/kimi-code/test/cli/goal-prompt.test.ts index 89770deef7b..8f600525e0f 100644 --- a/apps/kimi-code/test/cli/goal-prompt.test.ts +++ b/apps/kimi-code/test/cli/goal-prompt.test.ts @@ -168,10 +168,10 @@ describe('runPrompt headless goal mode', () => { let savedExitCode: typeof process.exitCode; beforeEach(() => { - // Pin the experimental engine flag off so runPrompt stays on the v1 path - // this suite mocks, regardless of the host environment (matches - // run-prompt.test.ts). With the flag on, runPrompt dispatches to the - // native v2 runner, which ignores these mocks and hangs the test. + // Pin the legacy engine so runPrompt stays on the SDK path this suite + // mocks, regardless of the host environment. Without this flag, runPrompt + // dispatches to the native v2 runner, which ignores these mocks. + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '1'); vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', ''); savedExitCode = process.exitCode; mocks.experimentalFeatures = [{ id: 'micro_compaction', enabled: true }]; diff --git a/apps/kimi-code/test/cli/options.test.ts b/apps/kimi-code/test/cli/options.test.ts index 46549e21b47..95936fe5c11 100644 --- a/apps/kimi-code/test/cli/options.test.ts +++ b/apps/kimi-code/test/cli/options.test.ts @@ -501,14 +501,14 @@ describe('CLI options parsing', () => { expect(validateOptions(parse(['--agent-file', 'a.md']), {}).uiMode).toBe('shell'); }); - it('accepts the flags in prompt mode without the v2 engine flag', () => { + it('accepts the flags in prompt mode on the default v2 engine', () => { const opts = parse(['-p', 'hi', '--agent-file', 'a.md']); expect(validateOptions(opts, {}).uiMode).toBe('print'); }); - it('accepts the flags in prompt mode with the v2 engine flag', () => { + it('accepts the flags in prompt mode with the legacy engine flag', () => { const opts = parse(['-p', 'hi', '--agent', 'reviewer']); - expect(validateOptions(opts, { KIMI_CODE_EXPERIMENTAL_FLAG: '1' }).uiMode).toBe('print'); + expect(validateOptions(opts, { KIMI_CODE_LEGACY_FLAG: '1' }).uiMode).toBe('print'); }); }); @@ -594,27 +594,6 @@ describe('CLI options parsing', () => { ]); }); - it('registers acp-v2 when the experimental flag is enabled', () => { - const original = process.env['KIMI_CODE_EXPERIMENTAL_ACP_V2']; - process.env['KIMI_CODE_EXPERIMENTAL_ACP_V2'] = '1'; - try { - const program = createProgram( - '0.0.0', - () => {}, - () => {}, - ); - const commandNames: string[] = program.commands - .filter((command) => !command.name().startsWith('__')) - .map((command) => command.name()); - expect(commandNames).toContain('acp-v2'); - } finally { - if (original === undefined) { - delete process.env['KIMI_CODE_EXPERIMENTAL_ACP_V2']; - } else { - process.env['KIMI_CODE_EXPERIMENTAL_ACP_V2'] = original; - } - } - }); }); describe('rejected flags', () => { diff --git a/apps/kimi-code/test/cli/provider.test.ts b/apps/kimi-code/test/cli/provider.test.ts index ce2951e6401..618496a7dd9 100644 --- a/apps/kimi-code/test/cli/provider.test.ts +++ b/apps/kimi-code/test/cli/provider.test.ts @@ -18,6 +18,30 @@ import { type ProviderDeps, } from '#/cli/sub/provider'; +// Spy on the SDK harness factories so the default-deps engine routing can be +// asserted without booting a real engine. The real implementations stay in +// place for everything else the handlers use. +const harnessRouting = vi.hoisted(() => ({ + kimiHarnessConstructor: vi.fn(), + kimiHarnessV2Constructor: vi.fn(), + harness: undefined as unknown, +})); + +vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createKimiHarness: (...args: unknown[]) => { + harnessRouting.kimiHarnessConstructor(...args); + return harnessRouting.harness; + }, + createKimiHarnessV2: (...args: unknown[]) => { + harnessRouting.kimiHarnessV2Constructor(...args); + return harnessRouting.harness; + }, + }; +}); + class ExitCalled extends Error { constructor(public readonly code: number) { super(`exit(${code})`); @@ -29,6 +53,7 @@ interface FakeHarness { getConfig: () => Promise; setConfig: (patch: Partial) => Promise; removeProvider: (providerId: string) => Promise; + close: () => Promise; } function makeHarness(initial: KimiConfig): { @@ -80,6 +105,7 @@ function makeHarness(initial: KimiConfig): { if (removedDefault) persisted = { ...persisted, defaultModel: undefined }; return structuredClone(persisted); }, + close: async () => {}, }; return { harness, @@ -1093,3 +1119,48 @@ describe('kimi provider catalog add', () => { }); }); }); + +describe('kimi provider engine routing', () => { + beforeEach(() => { + harnessRouting.kimiHarnessConstructor.mockClear(); + harnessRouting.kimiHarnessV2Constructor.mockClear(); + harnessRouting.harness = makeHarness({ providers: {} } as KimiConfig).harness; + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + function registerWithDefaultHarness(program: Command): void { + registerProviderCommand(program, { + stdout: { write: () => true }, + stderr: { write: () => true }, + env: {}, + exit: ((code: number) => { + throw new ExitCalled(code); + }) as ProviderDeps['exit'], + }); + } + + it('builds the v2 harness by default', async () => { + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', ''); + const program = new Command('kimi'); + registerWithDefaultHarness(program); + + await program.parseAsync(['node', 'kimi', 'provider', 'list'], { from: 'node' }); + + expect(harnessRouting.kimiHarnessV2Constructor).toHaveBeenCalledTimes(1); + expect(harnessRouting.kimiHarnessConstructor).not.toHaveBeenCalled(); + }); + + it('builds the legacy harness when the legacy flag is truthy', async () => { + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '1'); + const program = new Command('kimi'); + registerWithDefaultHarness(program); + + await program.parseAsync(['node', 'kimi', 'provider', 'list'], { from: 'node' }); + + expect(harnessRouting.kimiHarnessConstructor).toHaveBeenCalledTimes(1); + expect(harnessRouting.kimiHarnessV2Constructor).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/kimi-code/test/cli/run-prompt.test.ts b/apps/kimi-code/test/cli/run-prompt.test.ts index d71e88b23f6..726a83e607b 100644 --- a/apps/kimi-code/test/cli/run-prompt.test.ts +++ b/apps/kimi-code/test/cli/run-prompt.test.ts @@ -176,10 +176,9 @@ vi.mock('@moonshot-ai/kimi-telemetry', () => ({ withTelemetryContext: mocks.withTelemetryContext, })); -// The experimental v2 engine is loaded via a dynamic import from run-prompt.ts -// when KIMI_CODE_EXPERIMENTAL_FLAG is set. Mock the native v2 runner so tests -// that flip that flag can exercise the dispatch without pulling in the real -// agent-core-v2 graph. +// The v2 engine is loaded via a dynamic import from run-prompt.ts when the +// legacy engine flag is absent. Mock the native v2 runner so routing tests can +// exercise the dispatch without pulling in the real agent-core-v2 graph. vi.mock('../../src/cli/v2/run-v2-print', () => ({ runV2Print: mocks.runV2Print, })); @@ -246,9 +245,9 @@ async function waitForAssertion(assertion: () => void): Promise { describe('runPrompt', () => { beforeEach(() => { - // Pin the experimental engine flag off so the default v1 path is - // deterministic regardless of the host environment. Tests that exercise the - // experimental path opt back in explicitly with `vi.stubEnv(..., '1')`. + // Pin the legacy engine for the SDK-mocked cases. The v2 routing cases below + // clear this flag explicitly. + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '1'); vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', ''); vi.stubEnv('KIMI_MODEL_OUTPUT_FORMAT', ''); }); @@ -1251,14 +1250,15 @@ describe('runPrompt', () => { expect(handler()).toBeNull(); }); - it('emits the version first in text mode when the experimental flag is enabled', async () => { - vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '1'); + it('emits the version first in text mode on the default v2 engine', async () => { + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', ''); + vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', ''); const stdout = writer(); const stderr = writer(); await runPrompt(opts(), '1.2.3-test', { stdout, stderr }); - // The experimental engine is selected and the version banner is the very + // The v2 engine is selected by default and the version banner is the very // first write, ahead of any assistant output or the resume hint. expect(mocks.runV2Print).toHaveBeenCalled(); expect(mocks.kimiHarnessConstructor).not.toHaveBeenCalled(); @@ -1267,7 +1267,8 @@ describe('runPrompt', () => { expect(stdout.text()).toBe('• hello world\n\n'); }); - it('emits the version first in stream-json mode when the experimental flag is enabled', async () => { + it('emits the version first in stream-json mode on the default v2 engine', async () => { + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', ''); vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '1'); const stdout = writer(); const stderr = writer(); @@ -1286,8 +1287,9 @@ describe('runPrompt', () => { expect(stderr.text()).toBe(''); }); - it('does not emit the version when the experimental flag is disabled', async () => { - vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '0'); + it('uses the legacy engine when legacy wins over the experimental flag', async () => { + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '1'); + vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '1'); const stdout = writer(); const stderr = writer(); diff --git a/apps/kimi-code/test/cli/run-shell.test.ts b/apps/kimi-code/test/cli/run-shell.test.ts index 5f0dc3f04d0..c4e95c1d1b5 100644 --- a/apps/kimi-code/test/cli/run-shell.test.ts +++ b/apps/kimi-code/test/cli/run-shell.test.ts @@ -1,7 +1,7 @@ import { execSync } from 'node:child_process'; import type { createKimiDeviceId as createKimiDeviceIdFn } from '@moonshot-ai/kimi-code-oauth'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { runShell } from '#/cli/run-shell'; @@ -156,8 +156,13 @@ vi.mock('node:child_process', () => ({ })); describe('runShell', () => { + beforeEach(() => { + vi.stubEnv('KIMI_CODE_LEGACY_FLAG', '1'); + }); + afterEach(() => { vi.clearAllMocks(); + vi.unstubAllEnvs(); mocks.harnessGetConfig.mockResolvedValue({ providers: {}, defaultModel: 'k2', @@ -219,24 +224,39 @@ describe('runShell', () => { }); } - it('builds the v2 harness when the master experimental flag is set', async () => { + it('builds the v2 harness by default', async () => { stubTuiStartup(); - await withEnv({ KIMI_CODE_EXPERIMENTAL_FLAG: '1' }, async () => { - await runShell(minimalCliOptions, '1.2.3-test'); - }); + await withEnv( + { KIMI_CODE_LEGACY_FLAG: undefined, KIMI_CODE_EXPERIMENTAL_FLAG: undefined }, + async () => { + await runShell(minimalCliOptions, '1.2.3-test'); + }, + ); expect(mocks.kimiHarnessV2Constructor).toHaveBeenCalledTimes(1); expect(mocks.kimiHarnessConstructor).not.toHaveBeenCalled(); }); - it('keeps the v1 harness when the master experimental flag is unset', async () => { + it('uses the legacy harness when the legacy flag is truthy', async () => { stubTuiStartup(); - await withEnv({ KIMI_CODE_EXPERIMENTAL_FLAG: undefined }, async () => { + await withEnv({ KIMI_CODE_LEGACY_FLAG: '1' }, async () => { await runShell(minimalCliOptions, '1.2.3-test'); }); expect(mocks.kimiHarnessConstructor).toHaveBeenCalledTimes(1); expect(mocks.kimiHarnessV2Constructor).not.toHaveBeenCalled(); }); + it('lets the legacy flag take priority over the experimental master switch', async () => { + stubTuiStartup(); + await withEnv( + { KIMI_CODE_LEGACY_FLAG: '1', KIMI_CODE_EXPERIMENTAL_FLAG: '1' }, + async () => { + await runShell(minimalCliOptions, '1.2.3-test'); + }, + ); + expect(mocks.kimiHarnessConstructor).toHaveBeenCalledTimes(1); + expect(mocks.kimiHarnessV2Constructor).not.toHaveBeenCalled(); + }); + it('constructs KimiHarness and KimiTUI with startup input', async () => { mocks.loadTuiConfig.mockResolvedValue({ theme: 'dark', diff --git a/docs/en/configuration/config-files.md b/docs/en/configuration/config-files.md index 27131197745..94d40a8b505 100644 --- a/docs/en/configuration/config-files.md +++ b/docs/en/configuration/config-files.md @@ -103,7 +103,7 @@ Fields in the config file fall into two categories: **top-level scalars** that d | `merge_all_available_skills` | `boolean` | `true` | Whether to merge Agent Skills from all available directories | | `extra_skill_dirs` | `array` | — | Extra skill search directories, layered on top of the default directories | | `extra_agent_dirs` | `array` | — | Extra custom agent search directories, layered on top of the default directories | -| `builtin_product_skills` | `boolean` | `true` | Whether the built-in skills that document Kimi Code itself are offered to the model: `update-config`, `custom-theme`, `mcp-config`, `check-kimi-code-docs`, and `import-from-cc-codex`. Turning them off trims their names and descriptions from the system prompt, at the cost of the guided flows for those tasks. Read by the `agent-core-v2` engine (`kimi web` and the `KIMI_CODE_EXPERIMENTAL_FLAG` paths); ignored on the default engine | +| `builtin_product_skills` | `boolean` | `true` | Whether the built-in skills that document Kimi Code itself are offered to the model: `update-config`, `custom-theme`, `mcp-config`, `check-kimi-code-docs`, and `import-from-cc-codex`. Turning them off trims their names and descriptions from the system prompt, at the cost of the guided flows for those tasks. Read by the default `agent-core-v2` engine; ignored when `KIMI_CODE_LEGACY_FLAG=1` selects the legacy engine | | `telemetry` | `boolean` | `true` | Whether anonymous telemetry is enabled; disabled only when explicitly set to `false` | | `providers` | `table` | `{}` | API provider table → [`providers`](#providers) | | `models` | `table` | — | Model alias table → [`models`](#models) | @@ -320,7 +320,7 @@ A name that contains no ASCII letters or digits (for example a purely Chinese na The identity is resolved once at startup and holds for the life of the process — it is announced to MCP servers and providers when connections are made, so it cannot change midway. Edits to this section take effect on the next start, for new sessions: a resumed session keeps the system prompt it was recorded with, since its past turns already speak under that identity. Likewise, an MCP OAuth authorization keeps the client registration it was granted under; reset that server's authentication to register under the new identity. -This section is read by the `agent-core-v2` engine, which currently backs `kimi web` and the `KIMI_CODE_EXPERIMENTAL_FLAG` paths. On the default `kimi` / `kimi -p` engine it is ignored. +This section is read by the default `agent-core-v2` engine. It is ignored by the legacy `kimi` / `kimi -p` path selected with `KIMI_CODE_LEGACY_FLAG=1`; `kimi web` always uses `agent-core-v2`. ## `tools` diff --git a/docs/en/configuration/env-vars.md b/docs/en/configuration/env-vars.md index 031d36a258c..4e519dda47f 100644 --- a/docs/en/configuration/env-vars.md +++ b/docs/en/configuration/env-vars.md @@ -143,7 +143,8 @@ Switches that control the behavior of subsystems such as telemetry, background t | `KIMI_WEB_SEARCH_API_KEY` | API key of the web search (`WebSearch`) service; replaces both the configured API key and OAuth credential when set | Non-blank string; blank values are ignored | | `KIMI_WEB_FETCH_BASE_URL` | API URL of the web fetch (`FetchURL`) service; takes higher priority than `[services.moonshot_fetch] base_url`. Persisted credentials and custom headers are not forwarded to an env-selected endpoint. Without an env or config endpoint, signed-in users try the managed Kimi OAuth fetch service before direct local requests | Non-blank string; blank values are ignored | | `KIMI_WEB_FETCH_API_KEY` | API key of the web fetch (`FetchURL`) service; replaces both the configured API key and OAuth credential when set | Non-blank string; blank values are ignored | -| `KIMI_CODE_EXPERIMENTAL_FLAG` | Enable all registered experimental features for this process | `1`, `true`, `yes`, `on` | +| `KIMI_CODE_EXPERIMENTAL_FLAG` | Enable all registered experimental features for this process; it does not select the agent engine | `1`, `true`, `yes`, `on` | +| `KIMI_CODE_LEGACY_FLAG` | Use the legacy `agent-core` engine for `kimi`, `kimi -p`, `kimi doctor`, `kimi acp`, `kimi export`, and `kimi provider`; these commands use `agent-core-v2` by default | `1`, `true`, `yes`, `on` | | `KIMI_SHELL_PATH` | Override the Git Bash path on Windows (used when auto-detection fails) | Absolute path | | `KIMI_MODEL_MAX_COMPLETION_TOKENS` | Hard cap on `max_completion_tokens` per LLM step; applies to the `kimi` provider only | Positive integer; `0` or negative disables clamping | | `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` | @@ -153,7 +154,7 @@ Switches that control the behavior of subsystems such as telemetry, background t | `KIMI_CODE_NO_AUTO_UPDATE` | Fully disable the update preflight — no check, background install, or prompt. Legacy alias `KIMI_CLI_NO_AUTO_UPDATE` is also honored | Truthy: `1`/`true`/`yes`/`on` | | `KIMI_DISABLE_CRON` | Disable the scheduled-task tool (`CronCreate` rejects new schedules; existing tasks do not fire) | `1` to disable | -The three `KIMI_CODE_IDENTITY_*` / `KIMI_CODE_BUILTIN_PRODUCT_SKILLS` variables are read by the `agent-core-v2` engine, which currently backs `kimi web` and the `KIMI_CODE_EXPERIMENTAL_FLAG` paths; the default `kimi` / `kimi -p` engine ignores them. +The three `KIMI_CODE_IDENTITY_*` / `KIMI_CODE_BUILTIN_PRODUCT_SKILLS` variables are read by the default `agent-core-v2` engine. The legacy `kimi` / `kimi -p` path selected with `KIMI_CODE_LEGACY_FLAG=1` ignores them. ## Diagnostic logs diff --git a/docs/en/customization/plugins.md b/docs/en/customization/plugins.md index aa592c34376..56d3c16c128 100644 --- a/docs/en/customization/plugins.md +++ b/docs/en/customization/plugins.md @@ -184,7 +184,7 @@ Use `systemPrompt` for a short inline instruction, or `systemPromptPath` to keep } ``` -System-prompt contributions take effect on both agent engines: the interactive TUI and `kimi -p` (the v1 engine), `kimi web`, and any CLI surface with `KIMI_CODE_EXPERIMENTAL_FLAG=1` (the v2 engine). +System-prompt contributions take effect on both agent engines. The interactive TUI, `kimi -p`, and `kimi web` use the v2 engine by default; setting `KIMI_CODE_LEGACY_FLAG=1` routes the local CLI surfaces to the legacy engine. Each field — the inline `systemPrompt` and the `systemPromptPath` file — is limited to 32 KB (UTF-8 bytes): oversized content is ignored and reported in the plugin diagnostics. Across all enabled plugins, one prompt build injects at most 64 KB of instructions; contributions beyond the budget are skipped with a warning, including a single plugin whose inline text and file together exceed that budget. diff --git a/docs/zh/configuration/config-files.md b/docs/zh/configuration/config-files.md index f26368d0283..4a81877cbca 100644 --- a/docs/zh/configuration/config-files.md +++ b/docs/zh/configuration/config-files.md @@ -103,7 +103,7 @@ timeout = 5 | `merge_all_available_skills` | `boolean` | `true` | 是否合并所有目录中的 Agent Skills | | `extra_skill_dirs` | `array` | — | 额外 Skill 搜索目录,叠加到默认目录之上 | | `extra_agent_dirs` | `array` | — | 额外自定义 Agent 搜索目录,叠加到默认目录之上 | -| `builtin_product_skills` | `boolean` | `true` | 是否向模型提供介绍 Kimi Code 自身的内置 Skills:`update-config`、`custom-theme`、`mcp-config`、`check-kimi-code-docs`、`import-from-cc-codex`。关闭后它们的名称和描述不再进入系统提示词,代价是失去这些任务的引导流程。本字段由 `agent-core-v2` 引擎读取(`kimi web` 和开启 `KIMI_CODE_EXPERIMENTAL_FLAG` 的路径),默认引擎会忽略 | +| `builtin_product_skills` | `boolean` | `true` | 是否向模型提供介绍 Kimi Code 自身的内置 Skills:`update-config`、`custom-theme`、`mcp-config`、`check-kimi-code-docs`、`import-from-cc-codex`。关闭后它们的名称和描述不再进入系统提示词,代价是失去这些任务的引导流程。默认的 `agent-core-v2` 引擎会读取本字段;设置 `KIMI_CODE_LEGACY_FLAG=1` 选择旧版引擎时会忽略 | | `telemetry` | `boolean` | `true` | 是否启用匿名遥测;显式设为 `false` 时关闭 | | `providers` | `table` | `{}` | API 供应商表 → [`providers`](#providers) | | `models` | `table` | — | 模型别名表 → [`models`](#models) | @@ -320,7 +320,7 @@ slug = "acme-dev" # 可选 身份在启动时解析一次,进程生命周期内保持不变——建立连接时它已宣告给 MCP 服务器和 provider,中途无法更换。修改本节配置在下次启动时对新会话生效;resume 的会话保留录制时的系统提示词,因为其历史轮次本就以原身份自称。同理,已完成的 MCP OAuth 授权保留其授予时的客户端注册;重置该服务器的认证即可在新身份下重新注册。 -本节由 `agent-core-v2` 引擎读取,目前 `kimi web` 和开启 `KIMI_CODE_EXPERIMENTAL_FLAG` 的路径使用该引擎。默认的 `kimi` / `kimi -p` 引擎会忽略此配置。 +本节由默认的 `agent-core-v2` 引擎读取。设置 `KIMI_CODE_LEGACY_FLAG=1` 后,旧版 `kimi` / `kimi -p` 路径会忽略此配置;`kimi web` 始终使用 `agent-core-v2`。 ## `tools` diff --git a/docs/zh/configuration/env-vars.md b/docs/zh/configuration/env-vars.md index c8848ff5157..8d44b78734e 100644 --- a/docs/zh/configuration/env-vars.md +++ b/docs/zh/configuration/env-vars.md @@ -143,7 +143,8 @@ kimi | `KIMI_WEB_SEARCH_API_KEY` | 网页搜索(`WebSearch`)服务的 API 密钥;设置后同时替换配置中的 API 密钥和 OAuth 凭据 | 非空字符串;空白值被忽略 | | `KIMI_WEB_FETCH_BASE_URL` | 网页抓取(`FetchURL`)服务的 API URL;优先级高于 `[services.moonshot_fetch] base_url`。文件中持久化的凭据和自定义 header 不会发送到环境变量指定的端点。环境变量和配置都没有指定端点时,已登录用户会先尝试 Kimi OAuth 托管抓取服务,再回退到本地直接请求 | 非空字符串;空白值被忽略 | | `KIMI_WEB_FETCH_API_KEY` | 网页抓取(`FetchURL`)服务的 API 密钥;设置后同时替换配置中的 API 密钥和 OAuth 凭据 | 非空字符串;空白值被忽略 | -| `KIMI_CODE_EXPERIMENTAL_FLAG` | 在当前进程启用所有已注册的实验功能 | `1`、`true`、`yes`、`on` | +| `KIMI_CODE_EXPERIMENTAL_FLAG` | 在当前进程启用所有已注册的实验功能;不用于选择 Agent 引擎 | `1`、`true`、`yes`、`on` | +| `KIMI_CODE_LEGACY_FLAG` | 让 `kimi`、`kimi -p`、`kimi doctor`、`kimi acp`、`kimi export` 和 `kimi provider` 使用旧版 `agent-core` 引擎;这些命令默认使用 `agent-core-v2` | `1`、`true`、`yes`、`on` | | `KIMI_SHELL_PATH` | Windows 上覆盖 Git Bash 路径(自动探测失败时使用) | 绝对路径 | | `KIMI_MODEL_MAX_COMPLETION_TOKENS` | 单步 LLM 请求的 `max_completion_tokens` 硬上限,仅对 `kimi` 供应商生效 | 正整数;`0` 或负数禁用 clamp | | `KIMI_MODEL_TEMPERATURE` | 每次请求的采样温度,仅对 `kimi` 供应商生效(全局生效,不依赖 `KIMI_MODEL_NAME`) | 数字,如 `0.3` | @@ -153,7 +154,7 @@ kimi | `KIMI_CODE_NO_AUTO_UPDATE` | 完全禁用更新预检——不检查、不后台安装、不提示。同时兼容旧名 `KIMI_CLI_NO_AUTO_UPDATE` | 真值:`1`/`true`/`yes`/`on` | | `KIMI_DISABLE_CRON` | 禁用定时任务工具(`CronCreate` 拒绝新计划,已有任务不触发) | `1` 表示禁用 | -`KIMI_CODE_IDENTITY_*` 和 `KIMI_CODE_BUILTIN_PRODUCT_SKILLS` 这三个变量由 `agent-core-v2` 引擎读取,目前 `kimi web` 和开启 `KIMI_CODE_EXPERIMENTAL_FLAG` 的路径使用该引擎;默认的 `kimi` / `kimi -p` 引擎会忽略它们。 +`KIMI_CODE_IDENTITY_*` 和 `KIMI_CODE_BUILTIN_PRODUCT_SKILLS` 这三个变量由默认的 `agent-core-v2` 引擎读取。设置 `KIMI_CODE_LEGACY_FLAG=1` 后,旧版 `kimi` / `kimi -p` 路径会忽略它们。 ## 诊断日志 diff --git a/docs/zh/customization/plugins.md b/docs/zh/customization/plugins.md index 60d7c13dfe6..cfccfa93538 100644 --- a/docs/zh/customization/plugins.md +++ b/docs/zh/customization/plugins.md @@ -184,7 +184,7 @@ Plugin 是一个带 manifest 的目录或 zip 文件。Manifest 可以放在以 } ``` -系统提示词贡献在两个 Agent 引擎上都生效:交互式 TUI 与 `kimi -p`(v1 引擎)、`kimi web`,以及 `KIMI_CODE_EXPERIMENTAL_FLAG=1` 时的所有 CLI 界面(v2 引擎)。 +系统提示词贡献在两个 Agent 引擎上都生效。交互式 TUI、`kimi -p` 和 `kimi web` 默认使用 v2 引擎;设置 `KIMI_CODE_LEGACY_FLAG=1` 后,本地 CLI 界面会改用旧版引擎。 `systemPrompt` 字段与 `systemPromptPath` 文件各限制为 32 KB(UTF-8 字节):超限内容会被忽略,并显示在 plugin 的 diagnostics 中。一次提示词构建最多注入所有已启用 plugin 合计 64 KB 的指令;超出预算的贡献会被跳过并给出警告——单个 plugin 的内联文本与文件合计超过该预算时同样整体跳过。 diff --git a/package.json b/package.json index a216fa01ba4..364283bccc5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build": "pnpm -r run build", "build:packages": "pnpm -r --filter './packages/*' run build", "dev:cli": "pnpm -C apps/kimi-code run dev", - "dev:cli:v2": "KIMI_CODE_EXPERIMENTAL_FLAG=1 pnpm -C apps/kimi-code run dev", + "dev:cli:legacy": "KIMI_CODE_LEGACY_FLAG=1 pnpm -C apps/kimi-code run dev", "dev:cli:marketplace": "KIMI_CODE_DEV_MARKETPLACE_URL=https://code.kimi.com/kimi-code/plugins/marketplace.json pnpm -C apps/kimi-code run dev", "dev:web": "pnpm -C apps/kimi-web run dev", "dev:server": "pnpm -C apps/kimi-code run dev:server", diff --git a/packages/agent-core/src/flags/registry.ts b/packages/agent-core/src/flags/registry.ts index 065994b25c7..55903c6d00b 100644 --- a/packages/agent-core/src/flags/registry.ts +++ b/packages/agent-core/src/flags/registry.ts @@ -41,15 +41,6 @@ export const FLAG_DEFINITIONS = [ default: false, surface: 'core', }, - { - id: 'acp-v2', - title: 'ACP server v2 (agent-core-v2 engine)', - description: - 'Expose the `kimi acp-v2` sub-command that runs the Agent Client Protocol server over the experimental agent-core-v2 engine.', - env: 'KIMI_CODE_EXPERIMENTAL_ACP_V2', - default: false, - surface: 'core', - }, ] as const satisfies readonly FlagDefinitionInput[]; /** Literal union of registered flag ids. */ diff --git a/packages/node-sdk/test/config.test.ts b/packages/node-sdk/test/config.test.ts index 9ed22fd1f18..3fae01040de 100644 --- a/packages/node-sdk/test/config.test.ts +++ b/packages/node-sdk/test/config.test.ts @@ -356,17 +356,6 @@ describe('KimiHarness config API', () => { enabled: false, source: 'default', }, - { - id: 'acp-v2', - title: 'ACP server v2 (agent-core-v2 engine)', - description: - 'Expose the `kimi acp-v2` sub-command that runs the Agent Client Protocol server over the experimental agent-core-v2 engine.', - surface: 'core', - env: 'KIMI_CODE_EXPERIMENTAL_ACP_V2', - defaultEnabled: false, - enabled: false, - source: 'default', - }, ]); });