refactor(cli): drop v1 sdk and telemetry deps from v2 print

- run-v2-print: use core ITelemetryService + CloudAppender instead of
  kimi-telemetry; remove kimi-code-sdk import (auth via IOAuthToolkit,
  config path from bootstrap, hook result via structural type)
- prompt-render: replace SDK HookResultEvent with a structural type so
  the shared renderer does not depend on the v1 SDK event shape
- telemetry: revert initializeCliTelemetry to its original signature now
  that v2 no longer calls it; keep v1 callers and assertions untouched
- update run-prompt and v2-run-print tests for the new wiring
This commit is contained in:
haozhe.yang 2026-07-09 19:43:32 +08:00
parent c278cbbf78
commit ec50813f7e
9 changed files with 99 additions and 99 deletions

View file

@ -9,10 +9,20 @@
* event-filtering / completion flow intact.
*/
import type { HookResultEvent } from '@moonshot-ai/kimi-code-sdk';
import type { PromptOutputFormat } from './options';
/**
* Structural hook-result shape the renderer reads. Both the v1 SDK
* `HookResultEvent` and the v2 native `hook.result` `DomainEvent` satisfy it,
* so the renderer stays engine-agnostic without depending on either event
* definition.
*/
interface HookResultEventLike {
readonly hookEvent: string;
readonly content: string;
readonly blocked?: boolean;
}
export interface PromptOutput {
readonly columns?: number | undefined;
write(chunk: string): boolean;
@ -23,7 +33,7 @@ const PROMPT_BLOCK_INDENT = ' ';
export interface PromptTurnWriter {
writeAssistantDelta(delta: string): void;
writeHookResult(event: HookResultEvent): void;
writeHookResult(event: HookResultEventLike): void;
writeThinkingDelta(delta: string): void;
writeToolCall(toolCallId: string, name: string, args: unknown): void;
writeToolCallDelta(
@ -72,7 +82,7 @@ export class PromptTranscriptWriter implements PromptTurnWriter {
this.assistantWriter.write(delta);
}
writeHookResult(event: HookResultEvent): void {
writeHookResult(event: HookResultEventLike): void {
this.thinkingWriter.finish();
this.assistantWriter.finish();
this.assistantWriter.write(formatHookResultPlain(event));
@ -109,7 +119,7 @@ export class PromptJsonWriter implements PromptTurnWriter {
this.assistantText += delta;
}
writeHookResult(event: HookResultEvent): void {
writeHookResult(event: HookResultEventLike): void {
this.flushAssistant();
this.writeJsonLine({
role: 'assistant',
@ -263,15 +273,15 @@ function visibleCharWidth(char: string): number {
return char === '\t' ? 4 : 1;
}
function formatHookResultPlain(event: HookResultEvent): string {
function formatHookResultPlain(event: HookResultEventLike): string {
return `${formatHookResultTitle(event)}\n\n${formatHookResultBody(event)}`;
}
function formatHookResultTitle(event: HookResultEvent): string {
function formatHookResultTitle(event: HookResultEventLike): string {
return `${event.hookEvent} hook${event.blocked === true ? ' blocked' : ''}`;
}
function formatHookResultBody(event: HookResultEvent): string {
function formatHookResultBody(event: HookResultEventLike): string {
const content = event.content.trim();
return content.length === 0 ? '(empty)' : content;
}

View file

@ -185,10 +185,7 @@ export async function runPrompt(
restorePromptSessionPermission = restorePermission;
initializeCliTelemetry({
homeDir: harness.homeDir,
auth: harness.auth,
track: (event, properties) =>
properties === undefined ? harness.track(event) : harness.track(event, properties),
harness,
bootstrap: telemetryBootstrap,
config,
version,

View file

@ -112,10 +112,7 @@ export async function runShell(
});
initializeCliTelemetry({
homeDir: harness.homeDir,
auth: harness.auth,
track: (event, properties) =>
properties === undefined ? harness.track(event) : harness.track(event, properties),
harness,
bootstrap: telemetryBootstrap,
config,
version,

View file

@ -159,12 +159,7 @@ function createDefaultExportDeps(overrides: Partial<ExportDeps> = {}): ExportDep
await currentHarness.ensureConfigFile();
const config = await currentHarness.getConfig();
initializeCliTelemetry({
homeDir: currentHarness.homeDir,
auth: currentHarness.auth,
track: (event, properties) =>
properties === undefined
? currentHarness.track(event)
: currentHarness.track(event, properties),
harness: currentHarness,
bootstrap: currentTelemetryBootstrap,
config,
version: identity.version,

View file

@ -6,9 +6,9 @@ import {
resolveKimiHome,
type KimiConfig,
type TelemetryClient,
type TelemetryProperties,
} from '@moonshot-ai/kimi-code-sdk';
import type { PromptHarness } from './prompt-session';
import {
initializeTelemetry,
setTelemetryContext,
@ -27,11 +27,7 @@ export interface CliTelemetryBootstrap {
}
export interface InitializeCliTelemetryOptions {
readonly homeDir: string;
readonly auth: {
getCachedAccessToken(name: string): Promise<string | null | undefined>;
};
readonly track: (event: string, properties?: TelemetryProperties) => void;
readonly harness: PromptHarness;
readonly bootstrap: CliTelemetryBootstrap;
readonly config: Pick<KimiConfig, 'defaultModel' | 'telemetry'>;
readonly version: string;
@ -53,7 +49,7 @@ export function createCliTelemetryBootstrap(): CliTelemetryBootstrap {
export function initializeCliTelemetry(options: InitializeCliTelemetryOptions): void {
initializeTelemetry({
homeDir: options.homeDir,
homeDir: options.harness.homeDir,
deviceId: options.bootstrap.deviceId,
enabled: options.config.telemetry !== false,
appName: CLI_USER_AGENT_PRODUCT,
@ -62,10 +58,10 @@ export function initializeCliTelemetry(options: InitializeCliTelemetryOptions):
model: options.model ?? options.config.defaultModel,
sessionId: options.sessionId,
getAccessToken: async () =>
(await options.auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null,
(await options.harness.auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null,
});
if (options.bootstrap.firstLaunch) {
options.track('first_launch');
options.harness.track('first_launch');
}
}

View file

@ -16,6 +16,7 @@
*/
import {
CloudAppender,
IAgentGoalService,
IAgentLifecycleService,
IAgentPermissionModeService,
@ -25,35 +26,30 @@ import {
IAuthSummaryService,
IConfigService,
IEventBus,
IFileSystemStorageService,
IOAuthToolkit,
ISessionIndex,
ISessionLifecycleService,
ITelemetryService,
bootstrap,
ensureMainAgent,
hostRequestHeadersSeed,
logSeed,
resolveKimiHome,
resolveLoggingConfig,
type DomainEvent,
type IAgentScopeHandle,
type ISessionScopeHandle,
type Scope,
} from '@moonshot-ai/agent-core-v2';
import { createKimiDefaultHeaders } from '@moonshot-ai/kimi-code-oauth';
import {
KimiAuthFacade,
log,
resolveConfigPath,
type HookResultEvent,
type KimiConfig,
} from '@moonshot-ai/kimi-code-sdk';
import {
setCrashPhase,
shutdownTelemetry,
track as telemetryTrack,
withTelemetryContext,
} from '@moonshot-ai/kimi-telemetry';
import { createKimiDefaultHeaders, createKimiDeviceId } from '@moonshot-ai/kimi-code-oauth';
import { resolve } from 'pathe';
import { CLI_SHUTDOWN_TIMEOUT_MS, PROMPT_CLEANUP_TIMEOUT_MS } from '#/constant/app';
import {
CLI_SHUTDOWN_TIMEOUT_MS,
CLI_USER_AGENT_PRODUCT,
PROMPT_CLEANUP_TIMEOUT_MS,
} from '#/constant/app';
import {
formatGoalSummaryText,
@ -70,7 +66,6 @@ import {
raceWithTimeout,
requireConfiguredModel,
} from '../run-prompt';
import { createCliTelemetryBootstrap, initializeCliTelemetry } from '../telemetry';
import { createKimiCodeHostIdentity } from '../version';
import type { CLIOptions, PromptOutputFormat } from '../options';
@ -106,65 +101,51 @@ export async function runV2Print(
writeExperimentalVersion(version, outputFormat, stdout, stderr);
const telemetryBootstrap = createCliTelemetryBootstrap();
const homeDir = telemetryBootstrap.homeDir;
const configPath = resolveConfigPath({ homeDir });
const homeDir = resolveKimiHome();
let firstLaunch = false;
const deviceId = createKimiDeviceId(homeDir, {
onFirstLaunch: () => {
firstLaunch = true;
},
});
const logging = resolveLoggingConfig({ homeDir, env: process.env });
const identity = createKimiCodeHostIdentity(version);
const hostHeaders = createKimiDefaultHeaders({ homeDir, ...identity });
const { app } = bootstrap({ homeDir, configPath }, [
const { app } = bootstrap({ homeDir }, [
...logSeed(logging),
...hostRequestHeadersSeed(hostHeaders),
]);
const auth = new KimiAuthFacade({
homeDir,
configPath,
identity,
onRefresh: (outcome) => {
telemetryTrack('oauth_refresh', {
outcome: outcome.success ? 'success' : 'error',
...(outcome.success ? {} : { reason: outcome.reason }),
});
},
});
log.info('kimi-code starting', {
version,
uiMode: PROMPT_UI_MODE,
nodeVersion: process.version,
platform: `${process.platform}/${process.arch}`,
workDir,
});
const auth = app.accessor.get(IOAuthToolkit);
const configService = app.accessor.get(IConfigService);
await configService.ready;
const defaultModel = configService.get<string>('defaultModel') ?? undefined;
let telemetryConfig: KimiConfig['telemetry'];
let telemetryEnabled = true;
try {
telemetryConfig = configService.get<KimiConfig['telemetry']>('telemetry');
telemetryEnabled = configService.get('telemetry') !== false;
} catch {
telemetryConfig = undefined;
telemetryEnabled = true;
}
for (const diagnostic of configService.diagnostics()) {
if (diagnostic.severity === 'warning') {
stderr.write(`Warning: ${diagnostic.message}\n`);
}
}
const config = { defaultModel, telemetry: telemetryConfig };
let restorePermission = async (): Promise<void> => {};
let removeTerminationCleanup: (() => void) | undefined;
let cleanupPromise: Promise<void> | undefined;
let telemetryService: ITelemetryService | undefined;
const cleanup = async (): Promise<void> => {
const pending = (cleanupPromise ??= (async () => {
removeTerminationCleanup?.();
setCrashPhase('shutdown');
try {
await restorePermission();
} finally {
await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS });
if (telemetryService !== undefined) {
await raceWithTimeout(telemetryService.shutdown(), CLI_SHUTDOWN_TIMEOUT_MS);
}
app.dispose();
}
})());
@ -176,18 +157,25 @@ export async function runV2Print(
const resolved = await resolveNativeSession(app, opts, workDir, defaultModel, stderr);
restorePermission = resolved.restorePermission;
initializeCliTelemetry({
homeDir,
auth,
track: (event, properties) => telemetryTrack(event, properties),
bootstrap: telemetryBootstrap,
config,
version,
uiMode: PROMPT_UI_MODE,
model: resolved.telemetryModel,
sessionId: resolved.session.id,
});
setCrashPhase('runtime');
telemetryService = app.accessor.get(ITelemetryService);
if (telemetryEnabled) {
telemetryService.setAppender(
new CloudAppender({
storage: app.accessor.get(IFileSystemStorageService),
deviceId,
appName: CLI_USER_AGENT_PRODUCT,
version,
uiMode: PROMPT_UI_MODE,
model: resolved.telemetryModel,
getAccessToken: async () => (await auth.getCachedAccessToken()) ?? null,
env: process.env,
}),
);
}
telemetryService.setContext({ sessionId: resolved.session.id });
if (firstLaunch) {
telemetryService.track('first_launch');
}
const goalCreate = parseHeadlessGoalCreate(opts.prompt!);
if (goalCreate !== undefined) {
@ -214,7 +202,7 @@ export async function runV2Print(
}
writeResumeHint(resolved.session.id, outputFormat, stdout, stderr);
withTelemetryContext({ sessionId: resolved.session.id }).track('exit', {
telemetryService.withContext({ sessionId: resolved.session.id }).track('exit', {
duration_ms: Date.now() - startedAt,
});
} finally {
@ -369,8 +357,9 @@ async function runNativeTurn(
if (result.reason === 'completed') {
try {
await drainBackgroundTasks(app, session);
} catch (error) {
log.warn('drainBackgroundTasks failed', { error });
} catch {
// Draining is best-effort; a wedged background task must not fail the
// (already completed) turn. Swallow and proceed to finish.
}
writer.finish();
return;
@ -444,7 +433,7 @@ function dispatchNativeEvent(
writer.writeAssistantDelta(event.delta);
return;
case 'hook.result':
writer.writeHookResult(event as unknown as HookResultEvent);
writer.writeHookResult(event);
return;
case 'thinking.delta':
writer.writeThinkingDelta(event.delta);

View file

@ -105,10 +105,7 @@ export async function handleUpgradeCommand(version: string): Promise<void> {
await harness.ensureConfigFile();
const config = await harness.getConfig();
initializeCliTelemetry({
homeDir: harness.homeDir,
auth: harness.auth,
track: (event, properties) =>
properties === undefined ? harness.track(event) : harness.track(event, properties),
harness,
bootstrap: telemetryBootstrap,
config,
version,

View file

@ -396,8 +396,9 @@ describe('main entry command handling', () => {
}));
expect(mocks.harness.ensureConfigFile).toHaveBeenCalledTimes(1);
expect(mocks.initializeCliTelemetry).toHaveBeenCalledWith(expect.objectContaining({
homeDir: '/tmp/kimi-home',
track: expect.any(Function),
harness: expect.objectContaining({
homeDir: '/tmp/kimi-home',
}),
bootstrap: {
homeDir: '/tmp/kimi-home',
deviceId: 'device-id',

View file

@ -10,8 +10,11 @@ import {
IAuthSummaryService,
IConfigService,
IEventBus,
IFileSystemStorageService,
IOAuthToolkit,
ISessionIndex,
ISessionLifecycleService,
ITelemetryService,
type DomainEvent,
} from '@moonshot-ai/agent-core-v2';
@ -182,6 +185,21 @@ describe('runV2Print', () => {
},
],
[ISessionIndex, { list: vi.fn(async () => ({ items: [] })) }],
[IOAuthToolkit, { getCachedAccessToken: vi.fn(async () => undefined) }],
[IFileSystemStorageService, {}],
[
ITelemetryService,
(() => {
const svc = {
setAppender: vi.fn(),
setContext: vi.fn(),
track: vi.fn(),
shutdown: vi.fn(async () => {}),
withContext: vi.fn(() => svc),
};
return svc;
})(),
],
]);
const app = fakeScope('app', appServices);