diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index 71a7c13bb..e5374a079 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -101,6 +101,14 @@ export async function runPrompt( const stdout = io.stdout ?? process.stdout; const stderr = io.stderr ?? process.stderr; const promptProcess = io.process ?? process; + const outputFormat = opts.outputFormat ?? 'text'; + // The experimental agent-core-v2 engine (selected by KIMI_CODE_EXPERIMENTAL_FLAG) + // announces the running version as the very first output so headless consumers + // can identify which build produced the stream, in both `text` and `stream-json` + // formats. The default v1 path keeps its established output unchanged. + if (isKimiV2Enabled()) { + writeExperimentalVersion(version, outputFormat, stdout, stderr); + } const workDir = process.cwd(); const telemetryBootstrap = createCliTelemetryBootstrap(); const telemetryClient: TelemetryClient = { @@ -183,7 +191,6 @@ export async function runPrompt( }); setCrashPhase('runtime'); - const outputFormat = opts.outputFormat ?? 'text'; // Headless goal mode: `kimi -p "/goal "`. The goal driver keeps // 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 @@ -663,6 +670,30 @@ interface PromptJsonResumeMetaMessage { content: string; } +interface PromptJsonVersionMetaMessage { + role: 'meta'; + type: 'system.version'; + version: string; +} + +function writeExperimentalVersion( + version: string, + outputFormat: PromptOutputFormat, + stdout: PromptOutput, + stderr: PromptOutput, +): void { + if (outputFormat === 'stream-json') { + const message: PromptJsonVersionMetaMessage = { + role: 'meta', + type: 'system.version', + version, + }; + stdout.write(`${JSON.stringify(message)}\n`); + return; + } + stderr.write(`kimi version ${version}\n`); +} + function writeResumeHint( sessionId: string, outputFormat: PromptOutputFormat, diff --git a/apps/kimi-code/test/cli/run-prompt.test.ts b/apps/kimi-code/test/cli/run-prompt.test.ts index a4358e0a3..ba4d634ba 100644 --- a/apps/kimi-code/test/cli/run-prompt.test.ts +++ b/apps/kimi-code/test/cli/run-prompt.test.ts @@ -1,5 +1,5 @@ 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 { runPrompt } from '#/cli/run-prompt'; import { PROMPT_CLEANUP_TIMEOUT_MS } from '#/constant/app'; @@ -64,6 +64,19 @@ const mocks = vi.hoisted(() => { harnessClose: vi.fn(), harnessTrack: vi.fn(), harnessGetCachedAccessToken: vi.fn(), + createV2Harness: vi.fn(async () => ({ + homeDir: '/tmp/kimi-code-test-home', + auth: { getCachedAccessToken: mocks.harnessGetCachedAccessToken }, + ensureConfigFile: mocks.harnessEnsureConfigFile, + getConfig: mocks.harnessGetConfig, + getConfigDiagnostics: mocks.harnessGetConfigDiagnostics, + getExperimentalFeatures: mocks.harnessGetExperimentalFeatures, + createSession: mocks.harnessCreateSession, + resumeSession: mocks.harnessResumeSession, + listSessions: mocks.harnessListSessions, + close: mocks.harnessClose, + track: mocks.harnessTrack, + })), initializeTelemetry: vi.fn(), setCrashPhase: vi.fn(), shutdownTelemetry: vi.fn(), @@ -126,6 +139,14 @@ 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 it so tests that flip that flag +// can exercise the experimental path without pulling in the real agent-core-v2 +// graph. The returned harness mirrors the v1 mock shape above. +vi.mock('../../src/cli/v2/create-v2-harness', () => ({ + createV2Harness: mocks.createV2Harness, +})); + function opts(overrides: Partial[0]> = {}) { return { session: undefined, @@ -185,8 +206,16 @@ 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')`. + vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', ''); + }); + afterEach(() => { vi.clearAllMocks(); + vi.unstubAllEnvs(); mocks.eventHandlers.clear(); mocks.createKimiDeviceId.mockImplementation(() => 'device-1'); mocks.resolveKimiHome.mockImplementation( @@ -1030,4 +1059,51 @@ describe('runPrompt', () => { const handler = mocks.session.setQuestionHandler.mock.calls[0]![0] as () => unknown; 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'); + 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 + // first write, ahead of any assistant output or the resume hint. + expect(mocks.createV2Harness).toHaveBeenCalled(); + expect(mocks.kimiHarnessConstructor).not.toHaveBeenCalled(); + expect(stderr.write).toHaveBeenNthCalledWith(1, 'kimi version 1.2.3-test\n'); + expect(stderr.text().startsWith('kimi version 1.2.3-test\n')).toBe(true); + expect(stdout.text()).toBe('• hello world\n\n'); + }); + + it('emits the version first in stream-json mode when the experimental flag is enabled', async () => { + vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '1'); + const stdout = writer(); + const stderr = writer(); + + await runPrompt(opts({ outputFormat: 'stream-json' }), '1.2.3-test', { + stdout, + stderr, + }); + + expect(mocks.createV2Harness).toHaveBeenCalled(); + expect(mocks.kimiHarnessConstructor).not.toHaveBeenCalled(); + const lines = stdout.text().split('\n'); + expect(lines[0]).toBe( + '{"role":"meta","type":"system.version","version":"1.2.3-test"}', + ); + expect(stderr.text()).toBe(''); + }); + + it('does not emit the version when the experimental flag is disabled', async () => { + vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '0'); + const stdout = writer(); + const stderr = writer(); + + await runPrompt(opts(), '1.2.3-test', { stdout, stderr }); + + expect(mocks.createV2Harness).not.toHaveBeenCalled(); + expect(mocks.kimiHarnessConstructor).toHaveBeenCalled(); + expect(stderr.text()).not.toContain('kimi version'); + }); });