Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions apps/kimi-code/src/cli/experimental-v2.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
/**
* Experimental agent-core-v2 engine gate for `kimi -p` (print mode).
* Experimental agent-core-v2 engine gate for the CLI surfaces.
*
* When the master switch `KIMI_CODE_EXPERIMENTAL_FLAG` is truthy, print mode
* routes to the native agent-core-v2 runner instead of the default v1
* harness (see `run-prompt.ts`). Read directly from the env (matching
* 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`) and the interactive TUI builds its harness through the
* SDK's v2-backed client (see `run-shell.ts`), both 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 harness.
* registry. Unset / any non-truthy value keeps the v1 path.
*
* Note: `kimi web` always boots kap-server (the agent-core-v2 engine
* server) — it no longer consults this switch.
* server) — it does not consult this switch.
*/

export const KIMI_V2_ENV = 'KIMI_CODE_EXPERIMENTAL_FLAG';
Expand Down
13 changes: 11 additions & 2 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { join } from 'node:path';

import {
createKimiHarness,
createKimiHarnessV2,
flushDiagnosticLogsSync,
log,
type KimiHarness,
type KimiHarnessOptions,
type TelemetryClient,
} from '@moonshot-ai/kimi-code-sdk';
import {
Expand All @@ -29,6 +31,7 @@ import { toTerminalHyperlink } from '#/utils/terminal-hyperlink';
import { restoreTerminalModes } from '#/utils/terminal-restore';

import type { CLIOptions } from './options';
import { isKimiV2Enabled } from './experimental-v2';
import { createCliTelemetryBootstrap, initializeCliTelemetry } from './telemetry';
import { createKimiCodeHostIdentity } from './version';

Expand Down Expand Up @@ -60,7 +63,7 @@ export async function runShell(
withContext: withTelemetryContext,
setContext: setTelemetryContext,
};
const harness = createKimiHarness({
const harnessOptions: KimiHarnessOptions = {
homeDir: telemetryBootstrap.homeDir,
identity: createKimiCodeHostIdentity(version),
skillDirs: opts.skillsDirs,
Expand All @@ -76,7 +79,13 @@ 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.
const harness = isKimiV2Enabled()
? createKimiHarnessV2(harnessOptions)
: createKimiHarness(harnessOptions);
log.info('kimi-code starting', {
version,
uiMode: CLI_UI_MODE,
Expand Down
96 changes: 85 additions & 11 deletions apps/kimi-code/test/cli/run-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const mocks = vi.hoisted(() => {
loadTuiConfig: vi.fn(),
detectTerminalTheme: vi.fn(),
kimiHarnessConstructor: vi.fn(),
kimiHarnessV2Constructor: vi.fn(),
harnessEnsureConfigFile: vi.fn(),
harnessGetConfig: vi.fn(async () => ({
providers: {},
Expand Down Expand Up @@ -67,6 +68,21 @@ const mocks = vi.hoisted(() => {

vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => {
const actual = await importOriginal<typeof import('@moonshot-ai/kimi-code-sdk')>();
const makeHarnessStub = (args: unknown[]) => {
const options = args[0] as { readonly homeDir?: string } | undefined;
const homeDir = options?.homeDir ?? '/tmp/kimi-code-test-home';
return {
homeDir,
auth: {
getCachedAccessToken: mocks.harnessGetCachedAccessToken,
},
ensureConfigFile: mocks.harnessEnsureConfigFile,
getConfig: mocks.harnessGetConfig,
getConfigDiagnostics: mocks.harnessGetConfigDiagnostics,
close: mocks.harnessClose,
track: mocks.harnessTrack,
};
};
return {
...actual,
resolveKimiHome: mocks.resolveKimiHome,
Expand All @@ -78,17 +94,11 @@ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => {
mocks.createKimiDeviceId(homeDir);
}
mocks.kimiHarnessConstructor(...args);
return {
homeDir,
auth: {
getCachedAccessToken: mocks.harnessGetCachedAccessToken,
},
ensureConfigFile: mocks.harnessEnsureConfigFile,
getConfig: mocks.harnessGetConfig,
getConfigDiagnostics: mocks.harnessGetConfigDiagnostics,
close: mocks.harnessClose,
track: mocks.harnessTrack,
};
return makeHarnessStub(args);
},
createKimiHarnessV2: (...args: unknown[]) => {
mocks.kimiHarnessV2Constructor(...args);
return makeHarnessStub(args);
},
};
});
Expand Down Expand Up @@ -163,6 +173,70 @@ describe('runShell', () => {
mocks.harnessCreatesDeviceIdOnConstruction = false;
});

const minimalCliOptions = {
session: undefined,
continue: false,
yolo: false,
auto: false,
plan: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
skillsDirs: [],
agent: undefined,
agentFiles: [],
};

function stubTuiStartup(): void {
mocks.loadTuiConfig.mockResolvedValue({
theme: 'dark',
editorCommand: null,
notifications: { enabled: true, condition: 'unfocused' },
});
mocks.tuiStart.mockResolvedValue(undefined);
}

function withEnv(patch: Record<string, string | undefined>, fn: () => Promise<void>): Promise<void> {
const saved: Record<string, string | undefined> = {};
for (const key of Object.keys(patch)) {
saved[key] = process.env[key];
const value = patch[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
return fn().finally(() => {
for (const key of Object.keys(patch)) {
const value = saved[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});
}

it('builds the v2 harness when the master experimental flag is set', async () => {
stubTuiStartup();
await withEnv({ KIMI_CODE_EXPERIMENTAL_FLAG: '1' }, 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 () => {
stubTuiStartup();
await withEnv({ KIMI_CODE_EXPERIMENTAL_FLAG: undefined }, 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',
Expand Down
14 changes: 10 additions & 4 deletions apps/kimi-code/test/cli/telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ vi.mock('@moonshot-ai/kimi-telemetry', () => ({
withTelemetryContext: vi.fn(),
}));

vi.mock('@moonshot-ai/kimi-code-oauth', () => ({
createKimiDeviceId: mocks.createKimiDeviceId,
KIMI_CODE_PROVIDER_NAME: 'managed:kimi-code',
}));
vi.mock('@moonshot-ai/kimi-code-oauth', async (importOriginal) => {
// Spread the real module: the SDK's v2 client pulls agent-core-v2 into the
// import graph, which subclasses KimiOAuthToolkit from this package.
const actual = await importOriginal<typeof import('@moonshot-ai/kimi-code-oauth')>();
return {
...actual,
createKimiDeviceId: mocks.createKimiDeviceId,
KIMI_CODE_PROVIDER_NAME: 'managed:kimi-code',
};
});

vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => {
const actual = await importOriginal<typeof import('@moonshot-ai/kimi-code-sdk')>();
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +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: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",
Expand Down
2 changes: 2 additions & 0 deletions packages/node-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@
},
"devDependencies": {
"@moonshot-ai/agent-core": "workspace:^",
"@moonshot-ai/agent-core-v2": "workspace:^",
"@moonshot-ai/kaos": "workspace:^",
"@moonshot-ai/kimi-code-oauth": "workspace:^",
"@moonshot-ai/klient": "workspace:^",
"@moonshot-ai/kosong": "workspace:^",
"@types/yazl": "^2.4.6",
"jimp": "^1.6.1"
Expand Down
6 changes: 4 additions & 2 deletions packages/node-sdk/scripts/build-dts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ const providerClientShimPath = path.join(dtsRoot, 'provider-clients.d.ts');
const tscBinPath = packageBinPath('typescript', 'bin/tsc');
const apiExtractorBinPath = packageBinPath('@microsoft/api-extractor', 'bin/api-extractor');

const packageDirs = new Set(['agent-core', 'kaos', 'kosong', 'node-sdk', 'oauth']);
const packageDirs = new Set(['agent-core', 'agent-core-v2', 'kaos', 'klient', 'kosong', 'node-sdk', 'oauth']);
const workspacePackages = new Map([
['@moonshot-ai/agent-core-v2', 'agent-core-v2'],
['@moonshot-ai/agent-core', 'agent-core'],
['@moonshot-ai/kaos', 'kaos'],
['@moonshot-ai/kimi-code-oauth', 'oauth'],
['@moonshot-ai/klient', 'klient'],
['@moonshot-ai/kosong', 'kosong'],
]);

Expand Down Expand Up @@ -105,7 +107,7 @@ async function rewriteWorkspaceSpecifiers() {
`import { GoogleGenAI as GenAIClient } from '${providerClientSpecifier}';`,
);
const updated = providerClientText.replaceAll(
/(["'])(#\/[^"']+|@moonshot-ai\/(?:agent-core|kaos|kimi-code-oauth|kosong)(?:\/[^"']+)?)\1/g,
/(["'])(#\/[^"']+|@moonshot-ai\/(?:agent-core-v2|agent-core|kaos|kimi-code-oauth|klient|kosong)(?:\/[^"']+)?)\1/g,
(_match, quote, specifier) => {
const resolved = resolveSpecifier({
currentFile: file,
Expand Down
5 changes: 5 additions & 0 deletions packages/node-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ export type { KimiHarnessRuntimeOptions } from '#/kimi-harness';
export { Session } from '#/session';
export { KimiAuthFacade } from '#/auth';
export { createKimiHarness, SDKRpcClient, type SDKRpcClientOptions } from '#/sdk-rpc-client';
export {
createKimiHarnessV2,
SDKRpcClientV2,
type SDKRpcClientV2Options,
} from '#/sdk-rpc-client-v2';
export {
createKimiConfigRpc,
KimiConfigRpcClient,
Expand Down
Loading
Loading