diff --git a/.agents/skills/gen-changesets/SKILL.md b/.agents/skills/gen-changesets/SKILL.md index 3ab4458ba99..e37dce80164 100644 --- a/.agents/skills/gen-changesets/SKILL.md +++ b/.agents/skills/gen-changesets/SKILL.md @@ -5,7 +5,7 @@ description: Use when generating changesets in the kimi-code repository — deci # Generate Changesets -The only user-facing published package is the CLI: `@moonshot-ai/kimi-code`. All other `@moonshot-ai/*` packages (sdk, agent-core, kosong, kaos, oauth, telemetry, and so on) are internal. +The only user-facing published package is the CLI: `@moonshot-ai/kimi-code`. All other `@moonshot-ai/*` packages (sdk, kosong, kaos, oauth, telemetry, and so on) are internal. ## 1. Whether to Write diff --git a/.agents/skills/write-tui/SKILL.md b/.agents/skills/write-tui/SKILL.md index 3952b84b5c5..027b574cf09 100644 --- a/.agents/skills/write-tui/SKILL.md +++ b/.agents/skills/write-tui/SKILL.md @@ -68,7 +68,7 @@ Themes are managed centrally under `src/tui/theme/`: - `bundle.ts` — packs `colors`, `styles`, `markdownTheme` into a `KimiTUIThemeBundle`. - `index.ts` / `detect.ts` — theme type and auto/dark/light resolution. -> **Keep the color-token set in sync.** `ColorPalette` in `colors.ts` is the source of truth for color tokens. When you add, rename, or remove one, update its mirrors in the same change: the custom-theme JSON schema (`apps/kimi-code/src/tui/theme/theme-schema.json`), the token tables in the custom-theme docs (`docs/en/customization/themes.md` and `docs/zh/customization/themes.md`), and the token table in the `custom-theme` built-in skill (`packages/agent-core/src/skill/builtin/custom-theme.md`). +> **Keep the color-token set in sync.** `ColorPalette` in `colors.ts` is the source of truth for color tokens. When you add, rename, or remove one, update its mirrors in the same change: the custom-theme JSON schema (`apps/kimi-code/src/tui/theme/theme-schema.json`), the token tables in the custom-theme docs (`docs/en/customization/themes.md` and `docs/zh/customization/themes.md`), and the token table in the `custom-theme` built-in skill (`packages/agent-core-v2/src/features/skill/catalog/builtin/custom-theme.md`). Apply / switch flow: diff --git a/.changeset/README.md b/.changeset/README.md index 88fa0f78977..ce6afc094f8 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -15,7 +15,6 @@ Current publishable packages: All other workspace packages are private internal packages, are not published to npm, and are excluded via `ignore` in `.changeset/config.json`: -- `@moonshot-ai/agent-core` - `@moonshot-ai/kaos` - `@moonshot-ai/kimi-code-oauth` - `@moonshot-ai/kimi-telemetry` @@ -145,7 +144,7 @@ The root-level `pnpm run publish` first runs typecheck, lint, sherif, test, buil - Changeset files must be committed to the repository — release PRs are only triggered after they're merged. - Release PRs require human review and merge; they will not publish automatically. - Do not add release changesets for private internal packages; only select `@moonshot-ai/kimi-code` and `@moonshot-ai/kimi-code-sdk`. -- If a change in an underlying internal package alters user-visible behavior or public API of a publishable package, add a changeset to the affected publishable package. For example, when a bug fixed in `@moonshot-ai/agent-core` resolves an issue CLI users encounter, add a changeset to `@moonshot-ai/kimi-code` describing the user-visible fix. +- If a change in an underlying internal package alters user-visible behavior or public API of a publishable package, add a changeset to the affected publishable package. For example, when a bug fixed in `@moonshot-ai/kosong` resolves an issue CLI users encounter, add a changeset to `@moonshot-ai/kimi-code` describing the user-visible fix. - `@moonshot-ai/kimi-code` is the official CLI package name; after a global install it provides the `kimi` command. - Make sure each publishable package on npm has a Trusted Publisher configured. diff --git a/AGENTS.md b/AGENTS.md index b7481516411..f22bab43ca1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,11 +14,10 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo ## Project Map -- `apps/kimi-code`: the CLI / TUI application. It consumes core capabilities through `@moonshot-ai/kimi-code-sdk` and must not depend directly on `@moonshot-ai/agent-core`. When writing or modifying its terminal UI, use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`). +- `apps/kimi-code`: the CLI / TUI application. It consumes core capabilities through `@moonshot-ai/kimi-code-sdk` and must not depend directly on engine packages. When writing or modifying its terminal UI, use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`). - the browser web UI: **its source no longer lives in this repo.** It is developed in the code-app repo (`apps/web`) and shipped as the committed, prebuilt bundle `apps/kimi-code/dist-web` (gitignored, force-added), synced from code-app with `KIMI_CODE_REPO= pnpm run sync:web` — sync and commit the bundle in the same change whenever the web UI should ship differently. `apps/kimi-code/scripts/check-web-assets.mjs` guards packaging against a missing bundle. To hack on the web UI against this repo's server, run `pnpm dev:server` here and point code-app's `pnpm dev:web` at it via `KIMI_SERVER_URL`. - `apps/vis`, `apps/vis/server`, `apps/vis/web`: visual debugging tools for sessions and replays. - `apps/kimi-inspect`: web inspector for the kap-server `/api/v1/debug` RPC surface — workspace/session browser, per-session transcript chat, per-scope Service panels, and the DI unit inspection view. See `apps/kimi-inspect/AGENTS.md`. -- `packages/agent-core`: the unified agent engine, including Agent, Session, profile, skills, tools, plan, permission, background, records, the in-process DI service layer (`src/services/`), and other core capabilities. See `packages/agent-core/AGENTS.md`. - `packages/agent-core-v2`: the DI × Scope agent engine (the v2 port behind kap-server). Four `LifecycleScope` tiers — `App` / `Workspace` / `Session` / `Agent` (`app/scopes.ts`) — plus the L3 unit layer (`Service`/`Fiber` units, collection contribution points, the Feature seam in `src/features/`); there is no App-level session lifecycle facade — callers compose `ISessionIndex` → `IWorkspaceLifecycleService.handlerFor` → the handler. - `packages/node-sdk`: the public TypeScript SDK and harness. - `packages/kosong`: the LLM / provider abstraction layer. @@ -64,7 +63,6 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo ## Experimental Features - Gate a not-yet-public feature behind an experimental flag. Flags are env-driven and default off: `KIMI_CODE_EXPERIMENTAL_` toggles one, `KIMI_CODE_EXPERIMENTAL_FLAG` enables all. Precedence is per-flag env > `[experimental]` config > master env > the flag's `default`. Release by flipping the entry's `default` to `true`. - - `packages/agent-core` (v1): add the flag to the central registry at `packages/agent-core/src/flags/registry.ts`, then check it with `flags.enabled('my-feature')`. - `packages/agent-core-v2` and kap-server modules: there is no central catalog — declare the flag in the owning domain via `registerFlagDefinition` at import time, then check it with `IFlagService.enabled(id)`. Current search-index-separation flags: `persistence_minidb_readmodel` (session read model, default on) and `search_worker` (global search worker host, default on). ## Where to Update Instructions diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff90b934b38..a5e50c77806 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,12 +60,12 @@ All commits and PR titles must follow [Conventional Commits](https://www.convent | Type | Use for | Example | |----------|---------------------------------------------|-------------------------------------------| -| feat | A new feature | feat(agent-core): add tool dedup | +| feat | A new feature | feat(agent-core-v2): add tool dedup | | fix | A bug fix | fix(tui): correct status bar alignment | | docs | Documentation only | docs: clarify install instructions | | chore | Tooling / housekeeping | chore: bump dependencies | | refactor | Internal refactor without behavior change | refactor(kosong): extract retry helper | -| test | Adding or improving tests | test(agent-core): cover skill resolver | +| test | Adding or improving tests | test(agent-core-v2): cover skill resolver | | ci | CI / build pipeline changes | ci: cache pnpm store | | build | Build system / artifact changes | build(native): add win32-arm64 target | | perf | Performance improvement | perf(session): batch event flushes | diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md index 160e6d76c18..92ea4107b07 100644 --- a/CONTRIBUTING.zh-CN.md +++ b/CONTRIBUTING.zh-CN.md @@ -60,12 +60,12 @@ pnpm install | 类型 | 用途 | 示例 | |----------|------------------------------------------|----------------------------------------| -| feat | 新功能 | feat(agent-core): add tool dedup | +| feat | 新功能 | feat(agent-core-v2): add tool dedup | | fix | bug 修复 | fix(tui): correct status bar alignment | | docs | 仅文档 | docs: clarify install instructions | | chore | 工具 / 杂务 | chore: bump dependencies | | refactor | 无行为变更的内部重构 | refactor(kosong): extract retry helper | -| test | 新增或改进测试 | test(agent-core): cover skill resolver | +| test | 新增或改进测试 | test(agent-core-v2): cover skill resolver | | ci | CI / 构建流水线变更 | ci: cache pnpm store | | build | 构建系统 / 产物变更 | build(native): add win32-arm64 target | | perf | 性能优化 | perf(session): batch event flushes | diff --git a/apps/kimi-code/AGENTS.md b/apps/kimi-code/AGENTS.md index 857dc09e930..630252ff330 100644 --- a/apps/kimi-code/AGENTS.md +++ b/apps/kimi-code/AGENTS.md @@ -42,7 +42,7 @@ Main directories: - `reverse-rpc` converts SDK approval/question requests into the data shape a UI panel/dialog needs, and converts the user's choice back into an SDK response. - `theme` is the single source of truth for colors and styles. Components must not bypass the theme system and use chalk named colors directly. - `utils` holds utility functions with no UI-state dependency. Logic that needs `TUIState` or a component instance must not live under app-level `src/utils`. -- `apps/kimi-code` may only use core capabilities through `@moonshot-ai/kimi-code-sdk`. Do not import `@moonshot-ai/agent-core` directly in app code. +- `apps/kimi-code` may only use core capabilities through `@moonshot-ai/kimi-code-sdk`. Do not import engine packages directly in app code. ## TUI Coding Conventions diff --git a/apps/kimi-code/scripts/dev.mjs b/apps/kimi-code/scripts/dev.mjs index 124413a03ea..3934f34e7b2 100644 --- a/apps/kimi-code/scripts/dev.mjs +++ b/apps/kimi-code/scripts/dev.mjs @@ -52,7 +52,7 @@ const child = spawn( tsxCli, // Use the dev tsconfig whose `include` covers packages/*/src, so tsx's // esbuild transform sees `experimentalDecorators: true` for DI parameter - // decorators in agent-core. Mirrors `dev:server` in package.json. + // decorators in agent-core-v2. Mirrors `dev:server` in package.json. '--tsconfig', resolve(APP_ROOT, 'tsconfig.dev.json'), '--import', diff --git a/apps/kimi-code/scripts/plugin-manifest-version.mjs b/apps/kimi-code/scripts/plugin-manifest-version.mjs index 1fd768af54a..a7a73f1ba94 100644 --- a/apps/kimi-code/scripts/plugin-manifest-version.mjs +++ b/apps/kimi-code/scripts/plugin-manifest-version.mjs @@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; // Read a local plugin directory's declared version from its manifest, mirroring -// the plugin loader's precedence (packages/agent-core/src/plugin/manifest.ts): +// the plugin loader's precedence (packages/agent-core-v2/src/app/plugin/manager.ts): // `kimi.plugin.json` is authoritative once it exists, and `.kimi-plugin/plugin.json` // is only consulted when the root manifest is absent. Returns undefined when no // manifest is present or the chosen manifest has no version — callers then leave diff --git a/apps/kimi-code/src/cli/experimental-v2.ts b/apps/kimi-code/src/cli/experimental-v2.ts deleted file mode 100644 index 8ccb56ebdda..00000000000 --- a/apps/kimi-code/src/cli/experimental-v2.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Agent engine routing gates for the CLI surfaces. - * - * `kimi -p` and the interactive TUI 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_LEGACY_ENV = 'KIMI_CODE_LEGACY_FLAG'; - -const TRUTHY_VALUES = new Set(['1', 'true', 'yes', 'on']); - -function isTruthyEnv( - key: string, - env: Readonly>, -): boolean { - return TRUTHY_VALUES.has((env[key] ?? '').trim().toLowerCase()); -} - -export function isLegacyEnabled( - env: Readonly> = process.env, -): boolean { - return isTruthyEnv(KIMI_LEGACY_ENV, env); -} - -export function isKimiV2Enabled( - env: Readonly> = process.env, -): boolean { - return !isLegacyEnabled(env); -} diff --git a/apps/kimi-code/src/cli/prompt-session.ts b/apps/kimi-code/src/cli/prompt-session.ts index e4b4410af9a..668fa3ddfd6 100644 --- a/apps/kimi-code/src/cli/prompt-session.ts +++ b/apps/kimi-code/src/cli/prompt-session.ts @@ -1,35 +1,13 @@ -/** - * Minimal harness/session surface consumed by `kimi -p` (print mode). - * - * `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 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 { - ApprovalHandler, ConfigDiagnostics, - CreateGoalInput, CreateSessionOptions, - Event, - GetCronTasksResult, - GoalSnapshot, - GoalToolResult, KimiAuthFacade, KimiConfig, ListSessionsOptions, - PermissionMode, - PromptInput, - QuestionHandler, ResumeSessionInput, - SessionStatus, + Session, SessionSummary, TelemetryProperties, - Unsubscribe, } from '@moonshot-ai/kimi-code-sdk'; export interface PromptHarness { @@ -42,25 +20,7 @@ export interface PromptHarness { getConfig(): Promise>; getConfigDiagnostics(): Promise; listSessions(options: ListSessionsOptions): Promise; - createSession(options: CreateSessionOptions): Promise; - resumeSession(input: ResumeSessionInput): Promise; + createSession(options: CreateSessionOptions): Promise; + resumeSession(input: ResumeSessionInput): Promise; close(): Promise; } - -export interface PromptSession { - readonly id: string; - readonly workDir: string; - - getStatus(): Promise; - setModel(model: string): Promise; - setPermission(mode: PermissionMode): Promise; - setApprovalHandler(handler: ApprovalHandler | undefined): void; - setQuestionHandler(handler: QuestionHandler | undefined): void; - onEvent(listener: (event: Event) => void): Unsubscribe; - prompt(input: string | PromptInput): Promise; - waitForBackgroundTasksOnPrint(): Promise; - handlePrintMainTurnCompleted?(): Promise<'finish' | 'continue'>; - createGoal(input: CreateGoalInput): Promise; - getGoal(): Promise; - getCronTasks(): Promise; -} diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index cd519b223ea..65a49446872 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -1,63 +1,8 @@ -import { - setCrashPhase, - setTelemetryContext, - shutdownTelemetry, - track, - withTelemetryContext, -} from '@moonshot-ai/kimi-telemetry'; -import chalk from 'chalk'; -import { - createKimiHarness, - log, - type Event, - type GoalSnapshot, - type SessionStatus, - type TelemetryClient, -} from '@moonshot-ai/kimi-code-sdk'; -import { resolve } from 'pathe'; +import type { CLIOptions } from './options'; -import { CLI_SHUTDOWN_TIMEOUT_MS, PROMPT_CLEANUP_TIMEOUT_MS } from '#/constant/app'; - -import { resolveAgentProfileSelection } from './agent-selection'; -import { isKimiV2Enabled } from './experimental-v2'; -import { resolveOutputFormat } from './options'; -import type { CLIOptions, PromptOutputFormat } from './options'; -import { - formatGoalSummaryText, - goalExitCode, - goalSummaryJson, - parseHeadlessGoalCreate, - type HeadlessGoalCreate, -} from './goal-prompt'; -import type { PromptHarness, PromptSession } from './prompt-session'; -import { PromptJsonWriter, PromptTranscriptWriter, writeResumeHint } from './prompt-render'; -import { createCliTelemetryBootstrap, initializeCliTelemetry } from './telemetry'; -import { createKimiCodeHostIdentity } from './version'; - -/** - * Await `promise`, but stop waiting after `timeoutMs`. - * - * The timeout only bounds how long we WAIT — it does not change the outcome: - * - if `promise` settles first, its result is propagated (a rejection throws), - * so a cleanup step that actually fails in time still surfaces; - * - if the timeout wins, we resolve (give up waiting) and swallow the abandoned - * promise's eventual late rejection so it can't surface as an unhandled - * rejection. - * - * Used to bound shutdown so a wedged cleanup step can't keep a completed - * headless run alive, without silently swallowing a cleanup that fails fast. The - * timer stays ref'd so a cleanup step that suspends on an unref'd handle (e.g. - * telemetry's retry backoff when the network is blocked) can't drain the event - * loop and exit 0 before the rejection propagates — the timer keeps the loop - * alive until it fires, then gives the rejection a chance to surface. A wedged - * cleanup is still bounded by `timeoutMs`, so this can't hang the run forever. - */ export async function raceWithTimeout(promise: Promise, timeoutMs: number): Promise { let timedOut = false; let timer: ReturnType | undefined; - // Attach the catch eagerly (synchronously) so `promise` is always consumed and - // a late rejection can never become an unhandled rejection. Before the timeout - // wins, the handler rethrows so a real cleanup failure still propagates. const guarded = promise.catch((error: unknown) => { if (timedOut) return; throw error; @@ -92,321 +37,13 @@ export interface PromptProcess { exit(code?: number): never | void; } -const PROMPT_UI_MODE = 'print'; -const PROMPT_MAIN_AGENT_ID = 'main'; - export async function runPrompt( opts: CLIOptions, version: string, io: PromptRunIO = {}, ): Promise { - if (isKimiV2Enabled()) { - // 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; - } - - const startedAt = Date.now(); - const stdout = io.stdout ?? process.stdout; - const stderr = io.stderr ?? process.stderr; - const promptProcess = io.process ?? process; - const outputFormat = resolveOutputFormat(opts); - const workDir = process.cwd(); - const telemetryBootstrap = createCliTelemetryBootstrap(); - const telemetryClient: TelemetryClient = { - track, - withContext: withTelemetryContext, - setContext: setTelemetryContext, - }; - const harness = await createPromptHarness({ - homeDir: telemetryBootstrap.homeDir, - identity: createKimiCodeHostIdentity(version), - uiMode: PROMPT_UI_MODE, - skillDirs: opts.skillsDirs, - telemetry: telemetryClient, - onOAuthRefresh: (outcome) => { - if (outcome.success) { - track('oauth_refresh', { outcome: 'success' }); - return; - } - track('oauth_refresh', { outcome: 'error', reason: outcome.reason }); - }, - sessionStartedProperties: { yolo: false, plan: false, afk: true }, - }); - log.info('kimi-code starting', { - version, - uiMode: PROMPT_UI_MODE, - nodeVersion: process.version, - platform: `${process.platform}/${process.arch}`, - workDir, - }); - let restorePromptSessionPermission = async (): Promise => {}; - let removeTerminationCleanup: (() => void) | undefined; - let cleanupPromise: Promise | undefined; - const cleanupPromptRun = async (): Promise => { - const pending = (cleanupPromise ??= (async () => { - removeTerminationCleanup?.(); - setCrashPhase('shutdown'); - try { - await restorePromptSessionPermission(); - } finally { - await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS }); - await harness.close(); - } - })()); - // Bound cleanup so a wedged shutdown step (e.g. a SessionEnd hook, MCP - // shutdown, or a connection blackholed by a restrictive firewall) cannot - // keep a completed headless run alive forever. The cleanup keeps running in - // the background if it overruns; the caller (`kimi -p`) force-exits shortly - // after, so any straggling work is torn down with the process. - await raceWithTimeout(pending, PROMPT_CLEANUP_TIMEOUT_MS); - }; - removeTerminationCleanup = installPromptTerminationCleanup(promptProcess, cleanupPromptRun); - - try { - await harness.ensureConfigFile(); - const config = await harness.getConfig(); - for (const warning of (await harness.getConfigDiagnostics()).warnings) { - stderr.write(`Warning: ${warning}\n`); - } - const { session, restorePermission, telemetryModel, goalModel } = - await resolvePromptSession( - harness, - opts, - workDir, - config.defaultModel, - stderr, - (restorePermission) => { - restorePromptSessionPermission = restorePermission; - }, - ); - restorePromptSessionPermission = restorePermission; - - initializeCliTelemetry({ - harness, - bootstrap: telemetryBootstrap, - config, - version, - uiMode: PROMPT_UI_MODE, - model: telemetryModel, - sessionId: session.id, - }); - setCrashPhase('runtime'); - - // 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 - // distinct exit code. - const goalCreate = parseHeadlessGoalCreate(opts.prompt!); - if (goalCreate !== undefined) { - await runHeadlessGoal(session, goalCreate, goalModel, outputFormat, stdout, stderr); - } else { - await runPromptTurn( - session as PrintTurnSession, - opts.prompt!, - outputFormat, - stdout, - stderr, - ); - } - writeResumeHint(session.id, outputFormat, stdout, stderr); - - withTelemetryContext({ sessionId: session.id }).track('exit', { - duration_ms: Date.now() - startedAt, - }); - } finally { - await cleanupPromptRun(); - } -} - -async function createPromptHarness( - options: Parameters[0], -): Promise { - // The v2 engine is dispatched earlier in `runPrompt` (see the - // `isKimiV2Enabled()` branch) and never reaches here; this is the v1 path. - return createKimiHarness(options); -} - -async function runHeadlessGoal( - session: PromptSession, - goal: HeadlessGoalCreate, - model: string | undefined, - outputFormat: PromptOutputFormat, - stdout: PromptOutput, - stderr: PromptOutput, -): Promise { - requireConfiguredModel(model); - await session.createGoal({ - objective: goal.objective, - replace: goal.replace, - }); - let completedSnapshot: GoalSnapshot | null = null; - const unsubscribeGoalEvents = session.onEvent((event) => { - if ( - event.type === 'goal.updated' && - event.agentId === 'main' && - event.change?.kind === 'completion' && - event.snapshot !== null - ) { - completedSnapshot = event.snapshot; - } - }); - try { - // The objective is sent as the normal prompt; goal continuation keeps the - // turn alive until a terminal state is reached. - await runPromptTurn( - session as PrintTurnSession, - goal.objective, - outputFormat, - stdout, - stderr, - ); - } finally { - unsubscribeGoalEvents(); - const snapshot = completedSnapshot ?? (await session.getGoal()).goal; - if (outputFormat === 'stream-json') { - stdout.write(`${JSON.stringify(goalSummaryJson(snapshot))}\n`); - } else { - stderr.write(`${formatGoalSummaryText(snapshot)}\n`); - } - // Map the terminal goal status to a distinct, non-fatal exit code. A turn - // that threw (error / cancellation) already propagates its own exit path. - if (snapshot !== null && snapshot.status !== 'complete') { - process.exitCode = goalExitCode(snapshot.status); - } - } -} - -interface ResolvedPromptSession { - readonly session: PromptSession; - readonly resumed: boolean; - readonly restorePermission: () => Promise; - readonly telemetryModel?: string; - readonly goalModel?: string; -} - -async function resolvePromptSession( - harness: PromptHarness, - opts: CLIOptions, - workDir: string, - defaultModel: string | undefined, - stderr: PromptOutput, - setRestorePermission: (restorePermission: () => Promise) => void, -): Promise { - // `--agent`/`--agent-file` are creation-only: validateOptions rejects them - // together with --session/--continue, so resume paths never forward a - // profile — the bound agent is restored from the session itself. - if (opts.session !== undefined) { - const sessions = await harness.listSessions({ sessionId: opts.session, workDir }); - const target = sessions[0]; - if (target === undefined) { - throw new Error(`Session "${opts.session}" not found.`); - } - if (resolve(target.workDir) !== resolve(workDir)) { - stderr.write( - `${chalk.hex('#E8A838')( - `Session "${opts.session}" was created under a different directory.\n` + - ` cd "${target.workDir}" && kimi -r ${opts.session}`, - )}\n\n`, - ); - throw new Error( - `Session "${opts.session}" was created under a different directory.`, - ); - } - const session = await harness.resumeSession({ - id: opts.session, - additionalDirs: opts.addDirs?.length ? opts.addDirs : undefined, - }); - const status = await session.getStatus(); - const restorePermission = await forcePromptPermission( - session, - status.permission, - setRestorePermission, - ); - if (opts.model !== undefined) { - await session.setModel(opts.model); - } - installHeadlessHandlers(session); - return { - session, - resumed: true, - restorePermission, - telemetryModel: configuredModel(opts.model, status.model, defaultModel), - goalModel: configuredModel(opts.model, status.model), - }; - } - - if (opts.continue) { - const sessions = await harness.listSessions({ workDir }); - const previous = sessions[0]; - if (previous !== undefined) { - const session = await harness.resumeSession({ - id: previous.id, - additionalDirs: opts.addDirs?.length ? opts.addDirs : undefined, - }); - const status = await session.getStatus(); - const restorePermission = await forcePromptPermission( - session, - status.permission, - setRestorePermission, - ); - if (opts.model !== undefined) { - await session.setModel(opts.model); - } - installHeadlessHandlers(session); - return { - session, - resumed: true, - restorePermission, - telemetryModel: configuredModel(opts.model, status.model, defaultModel), - goalModel: configuredModel(opts.model, status.model), - }; - } - stderr.write(`No sessions to continue under "${workDir}"; starting a fresh session.\n`); - } - - const agentProfile = await resolveAgentProfileSelection(opts, workDir); - const model = requireConfiguredModel(opts.model, defaultModel); - const session = await harness.createSession({ - workDir, - model, - permission: 'auto', - additionalDirs: opts.addDirs?.length ? opts.addDirs : undefined, - agentProfile, - agentFiles: opts.agentFiles?.length ? opts.agentFiles : undefined, - drainAgentTasksOnStop: true, - }); - installHeadlessHandlers(session); - return { - session, - resumed: false, - restorePermission: async () => {}, - telemetryModel: model, - goalModel: model, - }; -} - -async function forcePromptPermission( - session: PromptSession, - previousPermission: SessionStatus['permission'], - setRestorePermission: (restorePermission: () => Promise) => void, -): Promise<() => Promise> { - let overridePermission: Promise | undefined; - const restorePermission = async () => { - await overridePermission?.catch(() => {}); - if (previousPermission !== 'auto') { - await session.setPermission(previousPermission); - } - }; - setRestorePermission(restorePermission); - if (previousPermission !== 'auto') { - overridePermission = session.setPermission('auto'); - await overridePermission; - } - return restorePermission; + const { runV2Print } = await import('./v2/run-v2-print'); + await runV2Print(opts, version, io); } export function requireConfiguredModel(...models: readonly (string | undefined)[]): string { @@ -423,11 +60,6 @@ export function configuredModel(...models: readonly (string | undefined)[]): str return models.find((model) => model !== undefined && model.trim().length > 0); } -function installHeadlessHandlers(session: PromptSession): void { - session.setApprovalHandler(() => ({ decision: 'approved' })); - session.setQuestionHandler(() => null); -} - export function installPromptTerminationCleanup( promptProcess: PromptProcess, cleanup: () => Promise, @@ -460,227 +92,3 @@ export function signalExitCode(signal: NodeJS.Signals): number { if (signal === 'SIGHUP') return 129; return 143; } - -type PrintTurnSession = PromptSession & - Required>; - -function runPromptTurn( - session: PrintTurnSession, - prompt: string, - outputFormat: PromptOutputFormat, - stdout: PromptOutput, - stderr: PromptOutput, -): Promise { - let activeTurnId: number | undefined; - let activeAgentId: string | undefined; - const outputWriter = - outputFormat === 'stream-json' - ? new PromptJsonWriter(stdout) - : new PromptTranscriptWriter(stdout, stderr); - let settled = false; - let unsubscribe: (() => void) | undefined; - // A `kimi -p` run is not done just because the model ended a turn: an active - // goal drives continuation turns on its own, and a scheduled cron task fires - // later from an idle session — both trigger new turns after `end_turn`. While - // either is pending, something must keep the event loop alive: the cron - // scheduler's tick is deliberately unref'd, so without a ref'd handle the - // process would drain and exit before the next turn is ever triggered. This - // no-op interval is that handle; finish() always clears it. - let keepAliveTimer: NodeJS.Timeout | undefined; - const holdEventLoop = (): void => { - keepAliveTimer ??= setInterval(() => {}, 60_000); - }; - const releaseEventLoop = (): void => { - if (keepAliveTimer === undefined) return; - clearInterval(keepAliveTimer); - keepAliveTimer = undefined; - }; - - return new Promise((resolve, reject) => { - const finish = (error?: Error): void => { - if (settled) return; - settled = true; - releaseEventLoop(); - unsubscribe?.(); - outputWriter.finish(); - if (error !== undefined) { - reject(error); - return; - } - resolve(); - }; - - // Re-evaluates whether the run can settle now that the main agent is idle. - // The run outlives a completed turn while a goal is still active (the goal - // driver launches the next continuation turn itself) or while cron tasks - // with a future fire remain (their fire steers a fresh turn when idle). - // Called on turn.ended and on a terminal goal.updated — the latter covers - // the driver blocking a goal on a hard budget, which emits no further - // turn.ended. Only when neither is pending do we drain background tasks - // and settle. - const evaluateRunCompletion = async (): Promise => { - try { - const { goal } = await session.getGoal(); - if (settled || activeTurnId !== undefined) return; - if (goal?.status === 'active') { - holdEventLoop(); - return; - } - const { tasks } = await session.getCronTasks(); - if (settled || activeTurnId !== undefined) return; - // A task whose expression has no future fire can never trigger a - // turn; don't hold the run open for it. - if (tasks.some((task) => task.nextFireAt !== null)) { - holdEventLoop(); - return; - } - await finishCompletedTurn(); - } catch (error) { - finish(error instanceof Error ? error : new Error(String(error))); - } - }; - - unsubscribe = session.onEvent((event) => { - if (event.type === 'error') { - if (event.agentId !== PROMPT_MAIN_AGENT_ID) { - return; - } - finish(new Error(`${event.code}: ${event.message}`)); - return; - } - if (event.type === 'turn.started') { - if (event.agentId !== PROMPT_MAIN_AGENT_ID) { - return; - } - activeTurnId = event.turnId; - activeAgentId = event.agentId; - return; - } - if ( - event.type === 'goal.updated' && - event.agentId === PROMPT_MAIN_AGENT_ID && - activeTurnId === undefined && - event.snapshot !== null && - event.snapshot.status !== 'active' - ) { - void evaluateRunCompletion(); - return; - } - if ( - activeTurnId === undefined || - activeAgentId === undefined || - !hasTurnId(event) || - event.turnId !== activeTurnId || - event.agentId !== activeAgentId - ) { - return; - } - switch (event.type) { - case 'turn.step.started': - case 'turn.step.interrupted': - outputWriter.flushAssistant(); - return; - case 'turn.step.retrying': - outputWriter.discardAssistant(); - outputWriter.writeRetrying(event); - return; - case 'assistant.delta': - outputWriter.writeAssistantDelta(event.delta); - return; - case 'hook.result': - outputWriter.writeHookResult(event); - return; - case 'thinking.delta': - outputWriter.writeThinkingDelta(event.delta); - return; - case 'tool.call.started': - outputWriter.writeToolCall(event.toolCallId, event.name, event.args); - return; - case 'tool.call.delta': - outputWriter.writeToolCallDelta(event.toolCallId, event.name, event.argumentsPart); - return; - case 'tool.result': - outputWriter.writeToolResult(event.toolCallId, event.output); - return; - case 'tool.progress': - if (event.update.text !== undefined && event.update.text.length > 0) { - stderr.write( - event.update.text.endsWith('\n') ? event.update.text : `${event.update.text}\n`, - ); - } - return; - case 'turn.ended': - if (event.reason === 'completed') { - outputWriter.flushAssistant(); - activeTurnId = undefined; - activeAgentId = undefined; - void evaluateRunCompletion(); - return; - } - finish(new Error(formatTurnEndedFailure(event))); - return; - case 'agent.status.updated': - case 'background.task.started': - case 'background.task.terminated': - case 'compaction.blocked': - case 'compaction.cancelled': - case 'compaction.completed': - case 'compaction.started': - case 'cron.fired': - case 'goal.updated': - case 'mcp.server.status': - case 'session.meta.updated': - case 'skill.activated': - case 'subagent.completed': - case 'subagent.failed': - case 'subagent.spawned': - case 'subagent.started': - case 'subagent.suspended': - case 'tool.list.updated': - case 'turn.step.completed': - case 'warning': - return; - } - }); - - session.prompt(prompt).catch((error: unknown) => { - finish(error instanceof Error ? error : new Error(String(error))); - }); - - async function finishCompletedTurn(): Promise { - // Flush the buffered assistant message before the end-of-turn policy - // runs: in stream-json mode the final message is only emitted by - // finish(), so a long drain/steer wait would otherwise withhold the main - // turn's result until the run exits. - outputWriter.flushAssistant(); - try { - const action = await session.handlePrintMainTurnCompleted(); - if (action === 'continue') { - // Stay alive: a still-pending background task will, on completion, - // steer the main agent into a new turn whose events we keep mapping. - // Do not finish yet. - holdEventLoop(); - return; - } - } catch (error) { - log.warn('handlePrintMainTurnCompleted failed', { error }); - } - finish(); - } - }); -} - -function hasTurnId(event: Event): event is Event & { readonly turnId: number } { - return 'turnId' in event; -} - -function formatTurnEndedFailure(event: Extract): string { - if (event.error?.code === 'provider.filtered') { - return 'Provider safety policy blocked the response.'; - } - if (event.error !== undefined) return `${event.error.code}: ${event.error.message}`; - if (event.reason === 'blocked') { - return 'Prompt hook blocked the request.'; - } - return `Prompt turn ended with reason: ${event.reason}`; -} diff --git a/apps/kimi-code/src/cli/run-shell.ts b/apps/kimi-code/src/cli/run-shell.ts index 4845c47a479..c81f57bc514 100644 --- a/apps/kimi-code/src/cli/run-shell.ts +++ b/apps/kimi-code/src/cli/run-shell.ts @@ -3,7 +3,6 @@ import { homedir } from 'node:os'; import { createKimiHarness, - createKimiHarnessV2, flushDiagnosticLogsSync, log, type KimiHarness, @@ -32,7 +31,6 @@ import { resolveCommandPath } from '#/utils/process/resolve-command'; import type { CLIOptions } from './options'; import { resolveAgentProfileSelection } from './agent-selection'; -import { isKimiV2Enabled } from './experimental-v2'; import { createCliTelemetryBootstrap, initializeCliTelemetry } from './telemetry'; import { createKimiCodeHostIdentity } from './version'; @@ -81,13 +79,7 @@ export async function runShell( }, sessionStartedProperties: { yolo: opts.yolo, auto: opts.auto, plan: opts.plan, afk: false }, }; - // 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) - : createKimiHarness(harnessOptions); + const harness = createKimiHarness(harnessOptions); startupTrace('harness:created'); log.info('kimi-code starting', { version, @@ -139,7 +131,6 @@ export async function runShell( startupNotice: configWarning, migrationPlan, migrateOnly: runOptions.migrateOnly, - engineV2, telemetryDisabled: config.telemetry === false, }); diff --git a/apps/kimi-code/src/cli/sub/export.ts b/apps/kimi-code/src/cli/sub/export.ts index 38796832c9b..79c13154358 100644 --- a/apps/kimi-code/src/cli/sub/export.ts +++ b/apps/kimi-code/src/cli/sub/export.ts @@ -15,7 +15,6 @@ import { } from '@moonshot-ai/kimi-telemetry'; import { createKimiHarness, - createKimiHarnessV2, type ExportSessionInput, type ExportSessionResult, type KimiHarness, @@ -31,8 +30,6 @@ 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; } @@ -155,9 +152,7 @@ function createDefaultExportDeps(overrides: Partial = {}): ExportDep }; const getHarness = (): KimiHarness => { const currentTelemetryBootstrap = getTelemetryBootstrap(); - // 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)({ + harness ??= createKimiHarness({ homeDir: currentTelemetryBootstrap.homeDir, identity, telemetry: telemetryClient, diff --git a/apps/kimi-code/src/cli/sub/fork.ts b/apps/kimi-code/src/cli/sub/fork.ts index 50711f33a1b..17802cc2a8a 100644 --- a/apps/kimi-code/src/cli/sub/fork.ts +++ b/apps/kimi-code/src/cli/sub/fork.ts @@ -8,7 +8,6 @@ import { } from '@moonshot-ai/kimi-telemetry'; import { createKimiHarness, - createKimiHarnessV2, type KimiHarness, type SessionSummary, type TelemetryClient, @@ -18,7 +17,6 @@ import type { Command } from 'commander'; import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE } from '#/constant/app'; import { createCliTelemetryBootstrap, initializeCliTelemetry } from '#/cli/telemetry'; import { createKimiCodeHostIdentity } from '#/cli/version'; -import { isKimiV2Enabled } from '../experimental-v2'; interface WritableLike { write(chunk: string): boolean; @@ -121,7 +119,7 @@ function createDefaultForkDeps(overrides: Partial = {}): ForkDeps & { }; const getHarness = (): KimiHarness => { const currentTelemetryBootstrap = getTelemetryBootstrap(); - harness ??= (isKimiV2Enabled() ? createKimiHarnessV2 : createKimiHarness)({ + harness ??= createKimiHarness({ homeDir: currentTelemetryBootstrap.homeDir, identity, telemetry: telemetryClient, diff --git a/apps/kimi-code/src/cli/sub/provider.ts b/apps/kimi-code/src/cli/sub/provider.ts index 0b73d81f26a..6d901198bc3 100644 --- a/apps/kimi-code/src/cli/sub/provider.ts +++ b/apps/kimi-code/src/cli/sub/provider.ts @@ -24,7 +24,6 @@ import { catalogProviderModels, CatalogFetchError, createKimiHarness, - createKimiHarnessV2, DEFAULT_CATALOG_URL, resolveCatalogImport, type Catalog, @@ -37,8 +36,6 @@ 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; } @@ -409,7 +406,7 @@ export async function handleCatalogAdd( // Always restore `[thinking]` from what was there before — including // `undefined`. Persisting `enabled: false` when the user never set it would - // make `resolveThinkingEffort` (agent-core/src/agent/config/thinking.ts) treat + // make `resolveThinkingEffort` (agent-core-v2/src/kosong/model/thinking.ts) treat // it as an explicit "off" request and silently disable thinking, even for // thinking-capable models. config.thinking = previousThinking; @@ -563,10 +560,7 @@ function resolveDeps(overrides: Partial = {}): ResolvedProviderDep getHarness: overrides.getHarness ?? (() => { - // 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 }); + harness ??= createKimiHarness({ identity }); return harness; }), stdout: overrides.stdout ?? process.stdout, diff --git a/apps/kimi-code/src/cli/sub/session.ts b/apps/kimi-code/src/cli/sub/session.ts index a15fc2063ee..e25f572488d 100644 --- a/apps/kimi-code/src/cli/sub/session.ts +++ b/apps/kimi-code/src/cli/sub/session.ts @@ -9,7 +9,6 @@ import { setTelemetryContext, track, withTelemetryContext } from '@moonshot-ai/kimi-telemetry'; import { createKimiHarness, - createKimiHarnessV2, type KimiHarness, type ListSessionsOptions, type SessionSummary, @@ -20,8 +19,6 @@ import type { Command } from 'commander'; import { createCliTelemetryBootstrap } from '#/cli/telemetry'; import { createKimiCodeHostIdentity } from '#/cli/version'; -import { isKimiV2Enabled } from '../experimental-v2'; - interface WritableLike { write(chunk: string): boolean; } @@ -106,7 +103,7 @@ function createDefaultSessionListDeps( setContext: setTelemetryContext, }; const getHarness = (): KimiHarness => { - harness ??= (isKimiV2Enabled() ? createKimiHarnessV2 : createKimiHarness)({ + harness ??= createKimiHarness({ homeDir: createCliTelemetryBootstrap().homeDir, identity, telemetry: telemetryClient, diff --git a/apps/kimi-code/src/cli/telemetry.ts b/apps/kimi-code/src/cli/telemetry.ts index 3c3b63d8a14..666ea0c51b0 100644 --- a/apps/kimi-code/src/cli/telemetry.ts +++ b/apps/kimi-code/src/cli/telemetry.ts @@ -75,13 +75,8 @@ export interface InitializeServerTelemetryOptions { * Bootstrap telemetry for the `kimi web` host. * * Mirrors {@link initializeCliTelemetry}: mints the device id, reads config to - * honor the `telemetry` toggle and pick up the default model, attaches the - * sink with `ui_mode = "web"`, and returns a {@link TelemetryClient} the - * caller hands to `startServer` via `coreProcessOptions.telemetry`. That wires - * the same real client into `KimiCore`, so agent-core events emitted inside the - * server process (`mcp_connected`, `session_load_failed`, plan-mode / cron - * events, …) actually leave the process carrying the enriched context - * (`app_name` / `version` / `ui_mode` / `model` / platform fields). + * honor the `telemetry` toggle and pick up the default model, and attaches the + * sink with `ui_mode = "web"`. * * The returned client wraps the `@moonshot-ai/kimi-telemetry` module * functions, so the module-level `track` / `withTelemetryContext` (used to 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 f329c0e06c2..63900cfb19c 100644 --- a/apps/kimi-code/src/cli/v2/run-v2-print.ts +++ b/apps/kimi-code/src/cli/v2/run-v2-print.ts @@ -12,8 +12,6 @@ * `Turn.result` for authoritative completion, * - applies the print-mode background policy (config-driven, v1-aligned: * `exit` / `drain` / `steer`) before exiting. - * - * Selected by `runPrompt` unless `KIMI_CODE_LEGACY_FLAG` is truthy. */ import { readFile } from 'node:fs/promises'; diff --git a/apps/kimi-code/src/tui/commands/add-dir.ts b/apps/kimi-code/src/tui/commands/add-dir.ts index 228b71d14c8..8d76e20f35e 100644 --- a/apps/kimi-code/src/tui/commands/add-dir.ts +++ b/apps/kimi-code/src/tui/commands/add-dir.ts @@ -23,10 +23,6 @@ export async function handleAddDirCommand(host: SlashCommandHost, args: string): } if (session === undefined) { - if (!host.engineV2) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); - return; - } // The path-adding form needs a live session; lazy-create it on first use // (the read-only `list`/bare forms above tolerate a missing session). session = await host.ensureSession(); diff --git a/apps/kimi-code/src/tui/commands/btw.ts b/apps/kimi-code/src/tui/commands/btw.ts index 72b3b5806b0..a36c3e018cd 100644 --- a/apps/kimi-code/src/tui/commands/btw.ts +++ b/apps/kimi-code/src/tui/commands/btw.ts @@ -14,9 +14,9 @@ export async function handleBtwCommand(host: SlashCommandHost, args: string): Pr try { const agentId = await session.startBtw(); - const activations = host.engineV2 - ? extractInlineSkillActivations(prompt, host.skillCommandMap, { includeLeading: true }) - : []; + const activations = extractInlineSkillActivations(prompt, host.skillCommandMap, { + includeLeading: true, + }); host.btwPanelController.open( agentId, prompt, diff --git a/apps/kimi-code/src/tui/commands/config.ts b/apps/kimi-code/src/tui/commands/config.ts index 10086bf8123..e099058e545 100644 --- a/apps/kimi-code/src/tui/commands/config.ts +++ b/apps/kimi-code/src/tui/commands/config.ts @@ -131,10 +131,6 @@ async function applyPlanMode(host: SlashCommandHost, session: Session, enabled: export async function handleYoloCommand(host: SlashCommandHost, args: string): Promise { const session = host.session; - if (session === undefined && !host.engineV2) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); - return; - } // v2 session-less: the chosen mode is recorded in appState and passed to the // lazy-created session; apply the runtime permission only when one exists. @@ -179,10 +175,6 @@ export async function handleYoloCommand(host: SlashCommandHost, args: string): P export async function handleAutoCommand(host: SlashCommandHost, args: string): Promise { const session = host.session; - if (session === undefined && !host.engineV2) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); - return; - } // v2 session-less: the chosen mode is recorded in appState and passed to the // lazy-created session; apply the runtime permission only when one exists. @@ -498,7 +490,7 @@ async function performModelSwitch( persist: boolean, ): Promise { let session = host.session; - if (session === undefined && host.engineV2) { + if (session === undefined) { // A first prompt may still be inside lazy creation: wait it out so the // switch lands on the new session instead of being overwritten by its // assembly. @@ -900,9 +892,6 @@ async function applyPermissionChoice(host: SlashCommandHost, mode: PermissionMod try { if (host.session !== undefined) { await host.session.setPermission(mode); - } else if (!host.engineV2) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); - return; } // v2 session-less: the chosen mode is recorded in appState and passed to // the lazy-created session. diff --git a/apps/kimi-code/src/tui/commands/dispatch.ts b/apps/kimi-code/src/tui/commands/dispatch.ts index afe3b88be43..3f83d130c5a 100644 --- a/apps/kimi-code/src/tui/commands/dispatch.ts +++ b/apps/kimi-code/src/tui/commands/dispatch.ts @@ -120,8 +120,6 @@ export interface SlashCommandHost { state: TUIState; session: Session | undefined; readonly harness: KimiHarness; - /** agent-core-v2 engine; enables lazy session creation. */ - readonly engineV2: boolean; cancelInFlight: (() => void) | undefined; deferUserMessages: boolean; @@ -229,20 +227,17 @@ export function dispatchInput(host: SlashCommandHost, text: string): void { if (parseSlashInput(text) !== null) { // A leading skill command combined with further inline skill tokens // (`/skill:a args /skill:b`) is one grouped submission on the v2 engine. - if (host.engineV2 && dispatchInlineSkillCombo(host, text)) { + if (dispatchInlineSkillCombo(host, text)) { return; } void executeSlashCommand(host, text); return; } - // Inline skill tokens anywhere in a plain prompt (v2 engine only); on the - // legacy engine they keep their plain-text meaning. - if (host.engineV2) { - const activations = extractInlineSkillActivations(text, host.skillCommandMap); - if (activations.length > 0) { - void host.sendInlineSkillUserInput(text, activations); - return; - } + // Inline skill tokens anywhere in a plain prompt activate the skills. + const activations = extractInlineSkillActivations(text, host.skillCommandMap); + if (activations.length > 0) { + void host.sendInlineSkillUserInput(text, activations); + return; } host.sendNormalUserInput(text); } @@ -271,7 +266,6 @@ function dispatchInlineSkillCombo(host: SlashCommandHost, text: string): boolean pluginCommandMap: host.pluginCommandMap, isStreaming: false, isCompacting: false, - engineV2: host.engineV2, }); if (intent.kind !== 'skill' && intent.kind !== 'message') return false; @@ -309,7 +303,6 @@ async function executeSlashCommand(host: SlashCommandHost, input: string): Promi pluginCommandMap: host.pluginCommandMap, isStreaming: host.state.appState.streamingPhase !== 'idle', isCompacting: host.state.appState.isCompacting, - engineV2: host.engineV2, }); switch (intent.kind) { @@ -405,16 +398,11 @@ async function executeSlashCommand(host: SlashCommandHost, input: string): Promi } /** - * Lazy-create the session for a slash command that needs one (v2 engine). - * v1 keeps the historical "no active session" error; on v2 a missing session - * means the TUI started session-less, so commands create it on first use. - * Returns undefined (error already shown) when creation fails. + * Lazy-create the session for a slash command that needs one (v2 engine). A + * missing session means the TUI started session-less, so commands create it + * on first use. Returns undefined (error already shown) when creation fails. */ async function ensureSessionForCommand(host: SlashCommandHost): Promise { - if (!host.engineV2) { - host.showError(LLM_NOT_SET_MESSAGE); - return undefined; - } return host.ensureSession(); } diff --git a/apps/kimi-code/src/tui/commands/info.ts b/apps/kimi-code/src/tui/commands/info.ts index 8de248ca8d7..af7f30b7cdc 100644 --- a/apps/kimi-code/src/tui/commands/info.ts +++ b/apps/kimi-code/src/tui/commands/info.ts @@ -178,7 +178,7 @@ export async function showStatusReport(host: SlashCommandHost): Promise { permissionMode: appState.permissionMode, planMode: appState.planMode, towerMode: appState.towerMode, - towerAvailable: host.engineV2 && isExperimentalFlagEnabled('tower'), + towerAvailable: isExperimentalFlagEnabled('tower'), contextUsage: appState.contextUsage, contextTokens: appState.contextTokens, maxContextTokens: appState.maxContextTokens, @@ -198,12 +198,10 @@ export async function showMcpServers(host: SlashCommandHost): Promise { try { if (host.session !== undefined) { servers = await host.session.listMcpServers(); - } else if (host.engineV2) { + } else { // v2 session-less: the MCP connection set is workspace-scoped, so it is // inspectable before the first session exists. servers = await host.harness.listWorkspaceMcpServers(host.state.appState.workDir); - } else { - servers = await host.requireSession().listMcpServers(); } } catch (error) { host.showError(`Failed to load MCP servers: ${formatErrorMessage(error)}`); diff --git a/apps/kimi-code/src/tui/commands/plugins.ts b/apps/kimi-code/src/tui/commands/plugins.ts index 6ee5b731f94..4b54ee8087d 100644 --- a/apps/kimi-code/src/tui/commands/plugins.ts +++ b/apps/kimi-code/src/tui/commands/plugins.ts @@ -10,7 +10,6 @@ import { } from '@moonshot-ai/kimi-code-sdk'; import { Markdown, Spacer } from '@moonshot-ai/pi-tui'; -import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui'; import { PluginInstallTrustConfirmComponent, PluginMcpSelectorComponent, @@ -82,16 +81,12 @@ type PluginApi = Pick< >; /** - * Resolve the plugin-management API. On the v2 engine plugin state is - * app-global, so a session-less startup still gets a working `/plugins` - * through the harness's global facade; on v1 (and once a session exists) the - * session's own API is used. + * Resolve the plugin-management API. Plugin state is app-global, so a + * session-less startup still gets a working `/plugins` through the harness's + * global facade; once a session exists the session's own API is used. */ async function resolvePluginApi(host: SlashCommandHost): Promise { if (host.session !== undefined) return host.session; - if (!host.engineV2) { - throw new Error(NO_ACTIVE_SESSION_MESSAGE); - } return { listPlugins: () => host.harness.listPlugins(), installPlugin: (source) => host.harness.installPlugin(source), @@ -215,16 +210,12 @@ export async function handlePluginsCommand(host: SlashCommandHost, rawArgs: stri * Resolve the capability API. Like plugin state, capability state is * app-global on the v2 engine, so a session-less startup still gets * readiness and installs through the harness's global facade; with a live - * session the session's own API is used (v1 included, where the capability - * surface then reports itself unavailable). + * session the session's own API is used. */ type CapabilityApi = Pick; async function resolveCapabilityApi(host: SlashCommandHost): Promise { if (host.session !== undefined) return host.session; - if (!host.engineV2) { - throw new Error(NO_ACTIVE_SESSION_MESSAGE); - } return host.harness; } @@ -263,12 +254,10 @@ async function showPluginsPicker( } let capabilities: readonly CapabilityStatus[] = []; - if (host.engineV2) { - try { - capabilities = await (await resolveCapabilityApi(host)).listCapabilities(); - } catch (error) { - log.warn('capability status unavailable', { error }); - } + try { + capabilities = await (await resolveCapabilityApi(host)).listCapabilities(); + } catch (error) { + log.warn('capability status unavailable', { error }); } const installedIds = new Set(plugins.map((plugin) => plugin.id)); @@ -354,10 +343,9 @@ async function loadMarketplaceCatalog( source: string | undefined, capabilities: readonly CapabilityStatus[], ): Promise { - const builtInEntries = - host.engineV2 && isDefaultMarketplaceCatalog(source) - ? capabilities.map(capabilityMarketplaceEntry) - : undefined; + const builtInEntries = isDefaultMarketplaceCatalog(source) + ? capabilities.map(capabilityMarketplaceEntry) + : undefined; let marketplace: PluginMarketplace; let catalog: PluginMarketplace; try { @@ -483,8 +471,8 @@ const CAPABILITY_POLL_ATTEMPTS = 260; // ~3 minutes of runtime setup budget /** Client-injected v2 entries install their runtime and plugin together. * Trust keys on the parser-proof `builtIn` flag — the `capability:` * source string stays purely diagnostic. */ -function isCapabilityEntry(host: SlashCommandHost, entry: PluginMarketplaceEntry): boolean { - return host.engineV2 && entry.builtIn === true; +function isCapabilityEntry(entry: PluginMarketplaceEntry): boolean { + return entry.builtIn === true; } /** @@ -492,11 +480,8 @@ function isCapabilityEntry(host: SlashCommandHost, entry: PluginMarketplaceEntry * is answering membership by running `listCapabilities()`, which fires every * entry's detector (seconds of probes) just to print one hint line. */ -function isCapabilityPluginId(host: SlashCommandHost, id: string): boolean { - return ( - host.engineV2 && - (id === 'kimi-cu' || id === 'kimi-cu-win' || id === 'kimi-webbridge') - ); +function isCapabilityPluginId(id: string): boolean { + return id === 'kimi-cu' || id === 'kimi-cu-win' || id === 'kimi-webbridge'; } /** Poll a background capability install until it settles (or we run out of budget). */ @@ -717,7 +702,7 @@ async function handlePluginsPanelSelection( await showPluginsPicker(host, { initialTab: 'installed' }); return; case 'install': - if (isCapabilityEntry(host, selection.entry)) { + if (isCapabilityEntry(selection.entry)) { await installCapabilityFromPanel(host, panel, selection.entry); return; } @@ -773,7 +758,7 @@ async function handlePluginMcpSelection( async function removePlugin(host: SlashCommandHost, id: string): Promise { await (await resolvePluginApi(host)).removePlugin(id); host.showStatus(`Removed ${id}.`); - if (isCapabilityPluginId(host, id)) { + if (isCapabilityPluginId(id)) { host.showStatus( 'Note: the runtime binaries were left untouched, but Kimi Code plugin wiring is disabled for new sessions. Restart Kimi Code before reinstalling from the Official tab.', ); diff --git a/apps/kimi-code/src/tui/commands/provider.ts b/apps/kimi-code/src/tui/commands/provider.ts index 3a2f22e8ee5..492072a9145 100644 --- a/apps/kimi-code/src/tui/commands/provider.ts +++ b/apps/kimi-code/src/tui/commands/provider.ts @@ -306,7 +306,7 @@ export async function setDefaultModel( effort, model === undefined ? undefined : effectiveModelForHost(host, model), ); - if (host.session === undefined && host.engineV2) { + if (host.session === undefined) { // A first prompt may still be inside lazy creation: wait it out so the // pick lands on the new session instead of racing its assembly (same // coordination as the /model path). diff --git a/apps/kimi-code/src/tui/commands/registry.ts b/apps/kimi-code/src/tui/commands/registry.ts index 7888de62777..ea6aa0bb195 100644 --- a/apps/kimi-code/src/tui/commands/registry.ts +++ b/apps/kimi-code/src/tui/commands/registry.ts @@ -201,7 +201,6 @@ export const BUILTIN_SLASH_COMMANDS = [ // for the previous one to finish. availability: 'always', experimentalFlag: 'tower', - requiresEngineV2: true, }, { name: 'model', diff --git a/apps/kimi-code/src/tui/commands/reload.ts b/apps/kimi-code/src/tui/commands/reload.ts index 608c947c40e..2fa34312902 100644 --- a/apps/kimi-code/src/tui/commands/reload.ts +++ b/apps/kimi-code/src/tui/commands/reload.ts @@ -30,8 +30,7 @@ export async function handleReloadCommand(host: SlashCommandHost): Promise const config = await host.harness.getConfig({ reload: true }); setExperimentalFeatures(await host.harness.getExperimentalFeatures()); - const sessionlessV2 = session === undefined && host.engineV2; - if (sessionlessV2) { + if (session === undefined) { // Session-less v2: rebuild the workspace-level dynamic commands too, so // skill/plugin changes apply before the first session exists. await host.refreshSkillCommands(); @@ -45,9 +44,7 @@ export async function handleReloadCommand(host: SlashCommandHost): Promise // Still session-less on the v2 engine: refresh the lazy defaults too, so // defaults edited externally (config.toml, a newly added default model) // reach the first lazy-created session instead of staying stale. - if (sessionlessV2) { - await host.hydrateLazyConfigDefaults(); - } + await host.hydrateLazyConfigDefaults(); host.showStatus( 'Runtime and TUI config reloaded; no active session.', 'success', diff --git a/apps/kimi-code/src/tui/commands/resolve.ts b/apps/kimi-code/src/tui/commands/resolve.ts index de1d89a575a..7bcc942ad17 100644 --- a/apps/kimi-code/src/tui/commands/resolve.ts +++ b/apps/kimi-code/src/tui/commands/resolve.ts @@ -51,7 +51,6 @@ export interface ResolveSlashCommandInput { readonly pluginCommandMap: ReadonlyMap; readonly isStreaming: boolean; readonly isCompacting: boolean; - readonly engineV2: boolean; } export function resolveSlashCommandInput(options: ResolveSlashCommandInput): SlashCommandIntent { @@ -62,8 +61,7 @@ export function resolveSlashCommandInput(options: ResolveSlashCommandInput): Sla // `command` is a literal union where only some members carry `experimentalFlag`; widen to read it. if ( command !== undefined && - isExperimentalFlagEnabled((command as KimiSlashCommand).experimentalFlag) && - (!(command as KimiSlashCommand).requiresEngineV2 || options.engineV2) + isExperimentalFlagEnabled((command as KimiSlashCommand).experimentalFlag) ) { const busyReason = slashCommandBusyReason(options); if ( diff --git a/apps/kimi-code/src/tui/commands/session.ts b/apps/kimi-code/src/tui/commands/session.ts index df853fde56f..0f773209b7e 100644 --- a/apps/kimi-code/src/tui/commands/session.ts +++ b/apps/kimi-code/src/tui/commands/session.ts @@ -33,10 +33,6 @@ export async function handleTitleCommand(host: SlashCommandHost, args: string): let session = host.session; if (session === undefined) { - if (!host.engineV2) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); - return; - } // Setting a title needs a live session; lazy-create it on first use (the // bare read-only form above works session-less). session = await host.ensureSession(); diff --git a/apps/kimi-code/src/tui/commands/tower.ts b/apps/kimi-code/src/tui/commands/tower.ts index 78d2ac02b64..890a1c3fa4f 100644 --- a/apps/kimi-code/src/tui/commands/tower.ts +++ b/apps/kimi-code/src/tui/commands/tower.ts @@ -1,10 +1,6 @@ import type { Session } from '@moonshot-ai/kimi-code-sdk'; -import { - NO_ACTIVE_SESSION_MESSAGE, - TOWER_STATUS_PROMPT, - TOWER_TEARDOWN_PROMPT, -} from '../constant/kimi-tui'; +import { TOWER_STATUS_PROMPT, TOWER_TEARDOWN_PROMPT } from '../constant/kimi-tui'; import { formatErrorMessage } from '../utils/event-payload'; import type { SlashCommandHost } from './dispatch'; @@ -88,10 +84,6 @@ async function setTowerMode( async function requireSessionEnsured(host: SlashCommandHost): Promise { if (host.session !== undefined) return host.session; - if (!host.engineV2) { - host.showError(NO_ACTIVE_SESSION_MESSAGE); - return undefined; - } // v2 session-less: lazy-create the session, then toggle — the same path // the first prompt takes. return host.ensureSession(); diff --git a/apps/kimi-code/src/tui/commands/types.ts b/apps/kimi-code/src/tui/commands/types.ts index b317e5d076e..9380bbee388 100644 --- a/apps/kimi-code/src/tui/commands/types.ts +++ b/apps/kimi-code/src/tui/commands/types.ts @@ -11,8 +11,6 @@ export interface KimiSlashCommand extends SlashCom /** When set, the command is hidden from the palette and blocked unless this flag is enabled. * A plain string: the gating flag may live in either engine's registry (v1 core or v2 domain). */ readonly experimentalFlag?: string; - /** When set, the command is hidden and unresolved on the legacy (v1) engine. */ - readonly requiresEngineV2?: boolean; /** * Generic argument autocompletion. `argumentPrefix` is the text typed after * `/ `; return suggestions or `null`. Declared as a plain function diff --git a/apps/kimi-code/src/tui/components/messages/tool-call.ts b/apps/kimi-code/src/tui/components/messages/tool-call.ts index 449b2c9abf7..96a2beb1551 100644 --- a/apps/kimi-code/src/tui/components/messages/tool-call.ts +++ b/apps/kimi-code/src/tui/components/messages/tool-call.ts @@ -201,8 +201,8 @@ const PLAN_SAVED_TO_RE = /\nPlan saved to: ([^\n]+)\n/; /** * Parses the ExitPlanMode result content string to recover the approval outcome * and optional plan path. Core-side templates live in - * `packages/agent-core/src/tools/builtin/planning/exit-plan-mode.ts` and - * `.../agent/permission/policies/exit-plan-mode-review-ask.ts`: + * `packages/agent-core-v2/src/features/plan/tools/exit-plan-mode/exitPlanModeTool.ts` and + * `packages/agent-core-v2/src/features/plan/exitPlanModeReview.ts`: * - Approved output starts with 'Exited plan mode.' and selected options * are reported as 'Selected approach: