kfc-code-cli 0.0.1-alpha.1 → 0.0.1-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/dist/main.mjs +1856 -632
  3. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -10,7 +10,7 @@ import path, { basename, dirname, extname, isAbsolute, join, normalize, posix, r
10
10
  import { finished, pipeline } from "node:stream/promises";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { ZodError, z } from "zod";
13
- import { createHash, randomBytes, randomUUID } from "node:crypto";
13
+ import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
14
14
  import { execFile, execSync, spawn, spawnSync } from "node:child_process";
15
15
  import { createInterface } from "node:readline";
16
16
  import * as nodeOs from "node:os";
@@ -34,7 +34,7 @@ import { writeFile as writeFile$1 } from "fs/promises";
34
34
  import { AsyncLocalStorage } from "node:async_hooks";
35
35
  import { ZipFile } from "yazl";
36
36
  import pino from "pino";
37
- import { CombinedAutocompleteProvider, Container, Editor, Image, Key, Markdown, ProcessTerminal, Spacer, TUI, Text, fuzzyFilter, fuzzyMatch, getCapabilities, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
37
+ import { CombinedAutocompleteProvider, Container, Editor, Image, Key, Markdown, ProcessTerminal, Spacer, TUI, Text, decodeKittyPrintable, fuzzyFilter, fuzzyMatch, getCapabilities, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
38
38
  import chalk from "chalk";
39
39
  import { highlight } from "cli-highlight";
40
40
  //#region ../../packages/kimi-core/src/storage/fs/durability.ts
@@ -904,7 +904,7 @@ var BaseContextState = class {
904
904
  ...input.finishReason !== void 0 ? { finish_reason: input.finishReason } : {}
905
905
  });
906
906
  this.openSteps.delete(input.uuid);
907
- if (input.usage !== void 0) this._tokenCountWithPending += input.usage.input_tokens + input.usage.output_tokens;
907
+ if (input.usage !== void 0) this._tokenCountWithPending = input.usage.input_tokens + input.usage.output_tokens;
908
908
  }
909
909
  async appendContentPart(input) {
910
910
  this.assertNotBroken();
@@ -2092,6 +2092,19 @@ async function atomicWrite(filePath, content, _syncOverride) {
2092
2092
  }
2093
2093
  }
2094
2094
  //#endregion
2095
+ //#region ../../packages/kimi-core/src/session/session-runtime-slot.ts
2096
+ function createSessionRuntimeSlot(initial) {
2097
+ let current = initial;
2098
+ return {
2099
+ current() {
2100
+ return current;
2101
+ },
2102
+ replace(next) {
2103
+ current = next;
2104
+ }
2105
+ };
2106
+ }
2107
+ //#endregion
2095
2108
  //#region ../../packages/kimi-core/src/soul/adapters.ts
2096
2109
  /**
2097
2110
  * Widen `ToolCall.args` (`unknown`) into the `Record<string, unknown>`
@@ -2176,11 +2189,16 @@ function adaptToolResult(r) {
2176
2189
  if (r.isError === true) payload.isError = true;
2177
2190
  return payload;
2178
2191
  }
2192
+ const DEFAULT_RESERVED_CONTEXT_SIZE$1 = 5e4;
2193
+ const DEFAULT_RESERVED_CONTEXT_FRACTION$1 = .25;
2194
+ function defaultReservedContextSize$1(maxContextSize) {
2195
+ return Math.max(0, Math.min(DEFAULT_RESERVED_CONTEXT_SIZE$1, Math.floor(maxContextSize * DEFAULT_RESERVED_CONTEXT_FRACTION$1)));
2196
+ }
2179
2197
  /** Returns true when either trigger condition fires. */
2180
2198
  function shouldCompact(context, config) {
2181
2199
  if (config === void 0) return false;
2182
2200
  const triggerRatio = config.triggerRatio ?? .85;
2183
- const reservedContextSize = config.reservedContextSize ?? 5e4;
2201
+ const reservedContextSize = config.reservedContextSize ?? defaultReservedContextSize$1(config.maxContextSize);
2184
2202
  const tokens = context.tokenCountWithPending;
2185
2203
  if (tokens >= config.maxContextSize * triggerRatio) return true;
2186
2204
  if (tokens + reservedContextSize >= config.maxContextSize) return true;
@@ -2736,6 +2754,206 @@ async function executePendingCalls(step, pending, deferred) {
2736
2754
  await context.appendToolResult(toolCallByProviderId.get(toolCall.id), toolCall.id, adapted);
2737
2755
  }
2738
2756
  }
2757
+ //#endregion
2758
+ //#region ../../packages/kimi-core/src/hooks/engine.ts
2759
+ /**
2760
+ * Synthesises a stable id for `hook.resolved` emissions. Hooks are
2761
+ * registered without an intrinsic id; we derive one from
2762
+ * `event:type:matcher`, suffixed with the hook's position inside
2763
+ * `this.hooks` (its registration order). The `registrationIndex` is
2764
+ * stable across multiple `executeHooks` calls — using the per-call
2765
+ * `settled[]` index instead would hand the same id to different hooks
2766
+ * across different dispatches, breaking client-side correlation.
2767
+ */
2768
+ function hookId(hook, registrationIndex) {
2769
+ return `${hook.event}:${hook.type}:${hook.matcher ?? ""}:${registrationIndex}`;
2770
+ }
2771
+ var HookEngine = class {
2772
+ deps;
2773
+ /**
2774
+ * Mutable executor registry. Seeded from `deps.executors` at
2775
+ * construction so the constructor arg can stay `ReadonlyMap`-typed
2776
+ * while `registerExecutor` still lets callers install additional
2777
+ * executors (e.g. `WireHookExecutor`) after the engine is wired.
2778
+ */
2779
+ executors;
2780
+ hooks = [];
2781
+ /**
2782
+ * Phase 18 L3-2 — invalid-regex warn dedupe. Each distinct invalid
2783
+ * matcher fires `onInvalidMatcher` once per engine instance so a
2784
+ * misconfigured block-action hook doesn't flood logs on every
2785
+ * tool call.
2786
+ */
2787
+ warnedInvalidMatchers = /* @__PURE__ */ new Set();
2788
+ constructor(deps) {
2789
+ this.deps = deps;
2790
+ this.executors = new Map(deps.executors);
2791
+ }
2792
+ register(hook) {
2793
+ this.hooks.push(hook);
2794
+ }
2795
+ /**
2796
+ * Phase 18 L2-4 — install / replace an executor for a given `type`
2797
+ * label at runtime. Used by the wire layer to bolt a
2798
+ * `WireHookExecutor` onto an engine that was constructed with only
2799
+ * the `command` executor. Replacing an existing entry is silent;
2800
+ * callers wanting a conflict check can probe `hasExecutor(type)`.
2801
+ */
2802
+ registerExecutor(type, executor) {
2803
+ this.executors.set(type, executor);
2804
+ }
2805
+ /** Returns true when an executor has been registered under `type`. */
2806
+ hasExecutor(type) {
2807
+ return this.executors.has(type);
2808
+ }
2809
+ unregister(hook) {
2810
+ const idx = this.hooks.indexOf(hook);
2811
+ if (idx !== -1) this.hooks.splice(idx, 1);
2812
+ }
2813
+ list(event) {
2814
+ if (event === void 0) return [...this.hooks];
2815
+ return this.hooks.filter((h) => h.event === event);
2816
+ }
2817
+ /**
2818
+ * Pre-filters hooks by both `event` and `matcher` regex against the
2819
+ * target value (tool name for tool-scoped events). Exported-ish via
2820
+ * `executeHooks` — v2 §9-C.3 requires "getMatchingHooks(event, input)
2821
+ * then concurrent execute" ordering.
2822
+ */
2823
+ getMatchingHooks(event, input) {
2824
+ const matcherValue = extractMatcherValue(input);
2825
+ return this.hooks.filter((h) => {
2826
+ if (h.event !== event) return false;
2827
+ return this.matchesTarget(h, matcherValue);
2828
+ });
2829
+ }
2830
+ matchesTarget(hook, value) {
2831
+ const matcher = hook.matcher;
2832
+ if (matcher === void 0 || matcher === "") return true;
2833
+ let re;
2834
+ try {
2835
+ re = new RegExp(matcher);
2836
+ } catch {
2837
+ if (!this.warnedInvalidMatchers.has(matcher)) {
2838
+ this.warnedInvalidMatchers.add(matcher);
2839
+ this.deps.onInvalidMatcher?.(hook, matcher);
2840
+ }
2841
+ return false;
2842
+ }
2843
+ return re.test(value);
2844
+ }
2845
+ async executeHooks(event, input, signal) {
2846
+ const deduped = dedupeByCommand(this.getMatchingHooks(event, input));
2847
+ if (deduped.length === 0) {
2848
+ this.deps.sink?.emit({
2849
+ type: "hook.triggered",
2850
+ event,
2851
+ matchers: [],
2852
+ matched_count: 0
2853
+ });
2854
+ return {
2855
+ blockAction: false,
2856
+ additionalContext: []
2857
+ };
2858
+ }
2859
+ this.deps.sink?.emit({
2860
+ type: "hook.triggered",
2861
+ event,
2862
+ matchers: deduped.map((h) => h.matcher ?? ""),
2863
+ matched_count: deduped.length
2864
+ });
2865
+ const settled = await Promise.allSettled(deduped.map((hook) => {
2866
+ const executor = this.executors.get(hook.type);
2867
+ if (executor === void 0) return Promise.resolve(void 0);
2868
+ return executor.execute(hook, input, signal);
2869
+ }));
2870
+ let blockAction = false;
2871
+ let reason;
2872
+ const additionalContext = [];
2873
+ for (const [i, result] of settled.entries()) {
2874
+ const hook = deduped[i];
2875
+ const registrationIndex = hook !== void 0 ? this.hooks.indexOf(hook) : -1;
2876
+ if (result.status === "rejected") {
2877
+ if (hook !== void 0) {
2878
+ this.deps.onExecutorError?.(hook, result.reason instanceof Error ? result.reason : new Error(String(result.reason)));
2879
+ this.deps.sink?.emit({
2880
+ type: "hook.resolved",
2881
+ hook_id: hookId(hook, registrationIndex),
2882
+ outcome: "error"
2883
+ });
2884
+ }
2885
+ continue;
2886
+ }
2887
+ const value = result.value;
2888
+ if (hook !== void 0) {
2889
+ const outcome = value?.ok === false ? "error" : value?.blockAction === true ? "blocked" : "ok";
2890
+ this.deps.sink?.emit({
2891
+ type: "hook.resolved",
2892
+ hook_id: hookId(hook, registrationIndex),
2893
+ outcome
2894
+ });
2895
+ }
2896
+ if (value === void 0) continue;
2897
+ if (value.blockAction) {
2898
+ blockAction = true;
2899
+ if (value.reason !== void 0) reason = value.reason;
2900
+ }
2901
+ if (value.additionalContext !== void 0) additionalContext.push(value.additionalContext);
2902
+ }
2903
+ return {
2904
+ blockAction,
2905
+ reason,
2906
+ additionalContext
2907
+ };
2908
+ }
2909
+ };
2910
+ /**
2911
+ * Collapse hook configs that share the same `command` string. `wire` hooks
2912
+ * (no `command` field) key on their subscription id so different wire
2913
+ * subscriptions still run in parallel; identical subscriptions collapse
2914
+ * like identical commands.
2915
+ */
2916
+ function dedupeByCommand(hooks) {
2917
+ const seen = /* @__PURE__ */ new Map();
2918
+ for (const hook of hooks) {
2919
+ const key = hookDedupeKey(hook);
2920
+ if (!seen.has(key)) seen.set(key, hook);
2921
+ }
2922
+ return [...seen.values()];
2923
+ }
2924
+ function hookDedupeKey(hook) {
2925
+ if (hook.type === "command") return `command:${hook.command}`;
2926
+ if (hook.type === "wire") return `wire:${hook.subscriptionId}`;
2927
+ return `unknown:${JSON.stringify(hook)}`;
2928
+ }
2929
+ /**
2930
+ * Extracts the string fed to a hook's matcher regex. Event-dependent:
2931
+ *
2932
+ * - `PreToolUse` / `PostToolUse` / `OnToolFailure` — tool name
2933
+ * (mirrors Python's `matcher_value=toolCall.name` contract).
2934
+ * - `UserPromptSubmit` — the prompt text itself (Python parity).
2935
+ * - `Stop` — the turn reason (`done` / `cancelled` / `error`), so
2936
+ * hooks can filter e.g. `/^error$/`.
2937
+ * - `Notification` — the notification type string, so a single hook
2938
+ * can subscribe to an entire notification class via regex.
2939
+ */
2940
+ function extractMatcherValue(input) {
2941
+ switch (input.event) {
2942
+ case "PreToolUse":
2943
+ case "PostToolUse":
2944
+ case "OnToolFailure": return input.toolCall.name;
2945
+ case "UserPromptSubmit": return input.prompt;
2946
+ case "Stop": return input.reason;
2947
+ case "Notification": return input.notificationType;
2948
+ case "StopFailure": return input.error;
2949
+ case "SubagentStart":
2950
+ case "SubagentStop": return input.agentName;
2951
+ case "SessionStart":
2952
+ case "SessionEnd":
2953
+ case "PreCompact":
2954
+ case "PostCompact": return "";
2955
+ }
2956
+ }
2739
2957
  const AgentToolInputSchema = z.preprocess((input) => {
2740
2958
  if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
2741
2959
  const record = input;
@@ -3043,7 +3261,7 @@ var EnterPlanModeTool = class {
3043
3261
  content: `Failed to enter plan mode: ${error instanceof Error ? error.message : "Failed to enter plan mode."}`
3044
3262
  };
3045
3263
  }
3046
- return { content: ENTERED_PLAN_MODE_MESSAGE };
3264
+ return { content: enteredPlanModeMessage(this.deps.getPlanFilePath?.() ?? null) };
3047
3265
  }
3048
3266
  if (this.deps.requestApproval === void 0) return {
3049
3267
  isError: true,
@@ -3067,18 +3285,32 @@ var EnterPlanModeTool = class {
3067
3285
  content: `Failed to enter plan mode: ${error instanceof Error ? error.message : "Failed to enter plan mode."}`
3068
3286
  };
3069
3287
  }
3070
- return { content: ENTERED_PLAN_MODE_MESSAGE };
3288
+ return { content: enteredPlanModeMessage(this.deps.getPlanFilePath?.() ?? null) };
3071
3289
  }
3072
3290
  };
3073
- const ENTERED_PLAN_MODE_MESSAGE = [
3074
- "Plan mode is now active. Your workflow:",
3075
- "",
3076
- "1. Use read-only tools (Read, Grep, Glob) to investigate the codebase.",
3077
- "2. Design a concrete, step-by-step plan.",
3078
- "3. When the plan is ready, call ExitPlanMode with the full plan text.",
3079
- "",
3080
- "Do NOT use Edit, Write, or Bash (non-readonly) while plan mode is active."
3081
- ].join("\n");
3291
+ function enteredPlanModeMessage(planPath) {
3292
+ if (planPath === null) return [
3293
+ "Plan mode is now active. Your workflow:",
3294
+ "",
3295
+ "1. Use read-only tools (Read, Grep, Glob) to investigate the codebase.",
3296
+ "2. Design a concrete, step-by-step plan.",
3297
+ "3. When the plan is ready, call ExitPlanMode with the full plan text in the `plan` parameter.",
3298
+ "",
3299
+ "Do NOT use Write or Edit while plan mode is active in this host; no plan file path is available."
3300
+ ].join("\n");
3301
+ return [
3302
+ "Plan mode is now active. Your workflow:",
3303
+ "",
3304
+ `Plan file: ${planPath}`,
3305
+ "",
3306
+ "1. Use read-only tools (Read, Grep, Glob) to investigate the codebase.",
3307
+ "2. Design a concrete, step-by-step plan.",
3308
+ "3. Write the plan to the plan file with Write or Edit.",
3309
+ "4. When the plan is ready, call ExitPlanMode for user approval.",
3310
+ "",
3311
+ "Do NOT edit files other than the plan file while plan mode is active."
3312
+ ].join("\n");
3313
+ }
3082
3314
  //#endregion
3083
3315
  //#region ../../packages/kimi-core/src/tools/builtin/planning/exit-plan-mode.ts
3084
3316
  /**
@@ -3109,7 +3341,7 @@ const ENTERED_PLAN_MODE_MESSAGE = [
3109
3341
  * simulate a rejection.
3110
3342
  */
3111
3343
  const ExitPlanModeInputSchema = z.object({
3112
- plan: z.string().min(1).optional().describe("The finalised plan to present to the user. Markdown is rendered in the UI."),
3344
+ plan: z.string().min(1).optional().describe("Legacy inline fallback only. In the default CLI runtime, write the plan file first; the host reads that file and ignores this field."),
3113
3345
  options: z.array(z.object({
3114
3346
  label: z.string().min(1).max(80),
3115
3347
  description: z.string()
@@ -3122,7 +3354,8 @@ const DESCRIPTION$4 = `Use this tool when you are in plan mode and are ready to
3122
3354
 
3123
3355
  **Usage notes:**
3124
3356
  - Only call this tool after you have fully investigated the request and designed a concrete plan.
3125
- - If the host told you to write the plan to a plan file, write that file first and call ExitPlanMode after it is complete. Otherwise pass the full plan text as \`plan\`.
3357
+ - In the default CLI runtime, write the plan to the current plan file first; this tool reads the file and presents it for approval.
3358
+ - Only pass \`plan\` when running in a host that has no plan-file support.
3126
3359
  - When presenting multiple approaches, pass 2-3 concrete options and do not use reserved labels like "Approve", "Reject", "Reject and Exit", or "Revise"; the host adds those controls.
3127
3360
  - Do NOT call this tool to ask clarifying questions (use AskUserQuestion for that). Call ExitPlanMode only when the plan is genuinely final.`;
3128
3361
  var ExitPlanModeTool = class {
@@ -3223,13 +3456,19 @@ var ExitPlanModeTool = class {
3223
3456
  }
3224
3457
  };
3225
3458
  }
3226
- if (source === null || source.plan.trim().length === 0) return {
3227
- ok: false,
3228
- error: {
3229
- isError: true,
3230
- content: source?.path !== void 0 ? `No plan file found. Write your plan to ${source.path} first, then call ExitPlanMode.` : "No plan file found. Write your plan first, then call ExitPlanMode."
3231
- }
3232
- };
3459
+ if (source === null || source.plan.trim().length === 0) {
3460
+ if (args.plan !== void 0 && args.plan.trim().length > 0) return {
3461
+ ok: true,
3462
+ plan: args.plan
3463
+ };
3464
+ return {
3465
+ ok: false,
3466
+ error: {
3467
+ isError: true,
3468
+ content: source?.path !== void 0 ? `No plan file found. Write your plan to ${source.path} first, then call ExitPlanMode.` : "No plan file found. Write your plan first, then call ExitPlanMode."
3469
+ }
3470
+ };
3471
+ }
3233
3472
  return {
3234
3473
  ok: true,
3235
3474
  plan: source.plan,
@@ -3253,6 +3492,44 @@ function formatPlanForOutput(plan, path) {
3253
3492
  return `Plan mode deactivated. All tools are now available.\n${path !== void 0 ? `Plan saved to: ${path}\n\n` : ""}## Approved Plan:\n${plan}`;
3254
3493
  }
3255
3494
  //#endregion
3495
+ //#region ../../packages/kimi-core/src/soul-plus/approval/approval-runtime.ts
3496
+ /**
3497
+ * Thrown when a cross-process / team forwarding hook is invoked before
3498
+ * the TeamDaemon wiring lands (Slice 2.6+). Callers should treat this as
3499
+ * an unrecoverable misconfiguration in Slice 2.3.
3500
+ */
3501
+ var NotImplementedError = class extends Error {
3502
+ constructor(feature) {
3503
+ super(`${feature} not implemented in Slice 2.3 (deferred to TeamDaemon)`);
3504
+ this.name = "NotImplementedError";
3505
+ }
3506
+ };
3507
+ /**
3508
+ * Default stub used when no wire/UI integration is required (e.g. unit
3509
+ * tests of unrelated layers, or embedder harnesses that bypass approvals
3510
+ * entirely via their own `beforeToolCall`). Every request is immediately
3511
+ * approved and no records are written.
3512
+ *
3513
+ * For the real implementation see `./wired-approval-runtime.ts`.
3514
+ */
3515
+ var AlwaysAllowApprovalRuntime = class {
3516
+ async request(_req, _signal) {
3517
+ return {
3518
+ approved: true,
3519
+ response: "approved"
3520
+ };
3521
+ }
3522
+ async recoverPendingOnStartup() {}
3523
+ resolve(_requestId, _response) {}
3524
+ cancelBySource(_source) {}
3525
+ async ingestRemoteRequest(_data) {
3526
+ throw new NotImplementedError("ApprovalRuntime.ingestRemoteRequest");
3527
+ }
3528
+ resolveRemote(_data) {
3529
+ throw new NotImplementedError("ApprovalRuntime.resolveRemote");
3530
+ }
3531
+ };
3532
+ //#endregion
3256
3533
  //#region ../../packages/kimi-core/src/soul-plus/compaction/tokens.ts
3257
3534
  /**
3258
3535
  * Token-estimation helper used by `CompactionOrchestrator` to seed
@@ -3308,7 +3585,7 @@ var CompactionOrchestrator = class {
3308
3585
  signal.throwIfAborted();
3309
3586
  const messages = this.deps.contextState.buildMessages();
3310
3587
  const preCompactTokens = this.deps.contextState.tokenCountWithPending;
3311
- const summary = await this.deps.compactionProvider.run(messages, signal, customInstruction !== void 0 ? { userInstructions: customInstruction } : void 0);
3588
+ const summary = await (this.deps.runtimeSlot?.current().compactionProvider ?? this.deps.compactionProvider).run(messages, signal, customInstruction !== void 0 ? { userInstructions: customInstruction } : void 0);
3312
3589
  signal.throwIfAborted();
3313
3590
  const baselineInit = await this.deps.journalCapability.readSessionInitialized();
3314
3591
  const runtime = this.deps.runtimeStateProvider();
@@ -3537,6 +3814,375 @@ var SoulLifecycleGate = class {
3537
3814
  }
3538
3815
  };
3539
3816
  //#endregion
3817
+ //#region ../../packages/kimi-core/src/soul-plus/plan/plan-slugs.ts
3818
+ /**
3819
+ * Plan-file slug generator — Phase 18 §D.1.
3820
+ *
3821
+ * Ports Python `kimi_cli/tools/plan/heroes.py`: each session gets a
3822
+ * human-readable slug composed of three Marvel/DC hero names joined by
3823
+ * `-`. The slug is cached per `sessionId` so repeated lookups are
3824
+ * idempotent. When the randomly-assembled slug collides with the
3825
+ * caller-supplied `existingSlugs` set 20 times in a row, we append the
3826
+ * first 8 chars of `sessionId` as a tiebreaker (matches the Python
3827
+ * fallback path `{slug}-{session_id[:8]}`).
3828
+ *
3829
+ * Note: uses `crypto.randomInt` for uniform secure randomness, matching
3830
+ * Python `secrets.choice`.
3831
+ */
3832
+ const HERO_NAMES = [
3833
+ "iron-man",
3834
+ "spider-man",
3835
+ "captain-america",
3836
+ "thor",
3837
+ "hulk",
3838
+ "black-widow",
3839
+ "hawkeye",
3840
+ "black-panther",
3841
+ "doctor-strange",
3842
+ "scarlet-witch",
3843
+ "vision",
3844
+ "falcon",
3845
+ "war-machine",
3846
+ "ant-man",
3847
+ "wasp",
3848
+ "captain-marvel",
3849
+ "gamora",
3850
+ "star-lord",
3851
+ "groot",
3852
+ "rocket",
3853
+ "drax",
3854
+ "mantis",
3855
+ "nebula",
3856
+ "shang-chi",
3857
+ "moon-knight",
3858
+ "ms-marvel",
3859
+ "she-hulk",
3860
+ "echo",
3861
+ "wolverine",
3862
+ "cyclops",
3863
+ "storm",
3864
+ "jean-grey",
3865
+ "rogue",
3866
+ "beast",
3867
+ "nightcrawler",
3868
+ "colossus",
3869
+ "shadowcat",
3870
+ "jubilee",
3871
+ "cable",
3872
+ "deadpool",
3873
+ "bishop",
3874
+ "magik",
3875
+ "iceman",
3876
+ "archangel",
3877
+ "psylocke",
3878
+ "dazzler",
3879
+ "forge",
3880
+ "havok",
3881
+ "polaris",
3882
+ "emma-frost",
3883
+ "namor",
3884
+ "silver-surfer",
3885
+ "adam-warlock",
3886
+ "nova",
3887
+ "quasar",
3888
+ "sentry",
3889
+ "blue-marvel",
3890
+ "spectrum",
3891
+ "squirrel-girl",
3892
+ "cloak",
3893
+ "dagger",
3894
+ "punisher",
3895
+ "elektra",
3896
+ "luke-cage",
3897
+ "iron-fist",
3898
+ "jessica-jones",
3899
+ "daredevil",
3900
+ "blade",
3901
+ "ghost-rider",
3902
+ "morbius",
3903
+ "venom",
3904
+ "carnage",
3905
+ "silk",
3906
+ "spider-gwen",
3907
+ "miles-morales",
3908
+ "america-chavez",
3909
+ "kate-bishop",
3910
+ "yelena-belova",
3911
+ "white-tiger",
3912
+ "moon-girl",
3913
+ "devil-dinosaur",
3914
+ "amadeus-cho",
3915
+ "riri-williams",
3916
+ "kamala-khan",
3917
+ "sam-alexander",
3918
+ "nova-prime",
3919
+ "medusa",
3920
+ "black-bolt",
3921
+ "crystal",
3922
+ "karnak",
3923
+ "gorgon",
3924
+ "lockjaw",
3925
+ "quake",
3926
+ "mockingbird",
3927
+ "bobbi-morse",
3928
+ "maria-hill",
3929
+ "nick-fury",
3930
+ "phil-coulson",
3931
+ "winter-soldier",
3932
+ "us-agent",
3933
+ "patriot",
3934
+ "speed",
3935
+ "wiccan",
3936
+ "hulkling",
3937
+ "stature",
3938
+ "yellowjacket",
3939
+ "tigra",
3940
+ "hellcat",
3941
+ "valkyrie",
3942
+ "sif",
3943
+ "beta-ray-bill",
3944
+ "hercules",
3945
+ "wonder-man",
3946
+ "taskmaster",
3947
+ "domino",
3948
+ "cannonball",
3949
+ "sunspot",
3950
+ "wolfsbane",
3951
+ "warpath",
3952
+ "multiple-man",
3953
+ "banshee",
3954
+ "siryn",
3955
+ "monet",
3956
+ "rictor",
3957
+ "shatterstar",
3958
+ "longshot",
3959
+ "daken",
3960
+ "x-23",
3961
+ "fantomex",
3962
+ "batman",
3963
+ "superman",
3964
+ "wonder-woman",
3965
+ "flash",
3966
+ "aquaman",
3967
+ "green-lantern",
3968
+ "martian-manhunter",
3969
+ "cyborg",
3970
+ "hawkgirl",
3971
+ "green-arrow",
3972
+ "black-canary",
3973
+ "zatanna",
3974
+ "constantine",
3975
+ "shazam",
3976
+ "blue-beetle",
3977
+ "booster-gold",
3978
+ "firestorm",
3979
+ "atom",
3980
+ "hawkman",
3981
+ "plastic-man",
3982
+ "red-tornado",
3983
+ "starfire",
3984
+ "raven",
3985
+ "beast-boy",
3986
+ "robin",
3987
+ "nightwing",
3988
+ "batgirl",
3989
+ "batwoman",
3990
+ "red-hood",
3991
+ "signal",
3992
+ "orphan",
3993
+ "spoiler",
3994
+ "catwoman",
3995
+ "huntress",
3996
+ "supergirl",
3997
+ "superboy",
3998
+ "power-girl",
3999
+ "steel",
4000
+ "stargirl",
4001
+ "wildcat",
4002
+ "doctor-fate",
4003
+ "mister-terrific",
4004
+ "hourman",
4005
+ "sandman",
4006
+ "spectre",
4007
+ "phantom-stranger",
4008
+ "swamp-thing",
4009
+ "animal-man",
4010
+ "deadman",
4011
+ "vixen",
4012
+ "black-lightning",
4013
+ "static",
4014
+ "icon",
4015
+ "rocket-dc",
4016
+ "captain-atom",
4017
+ "fire",
4018
+ "ice",
4019
+ "elongated-man",
4020
+ "metamorpho",
4021
+ "black-hawk",
4022
+ "crimson-avenger",
4023
+ "doctor-mid-nite",
4024
+ "jakeem-thunder",
4025
+ "mister-miracle",
4026
+ "big-barda",
4027
+ "orion",
4028
+ "lightray",
4029
+ "forager",
4030
+ "killer-frost",
4031
+ "jessica-cruz",
4032
+ "simon-baz",
4033
+ "john-stewart",
4034
+ "guy-gardner",
4035
+ "kyle-rayner",
4036
+ "hal-jordan",
4037
+ "wally-west",
4038
+ "barry-allen",
4039
+ "jay-garrick",
4040
+ "impulse",
4041
+ "kid-flash",
4042
+ "donna-troy",
4043
+ "tempest",
4044
+ "aqualad",
4045
+ "miss-martian",
4046
+ "terra",
4047
+ "jericho",
4048
+ "ravager",
4049
+ "red-star",
4050
+ "pantha",
4051
+ "argent",
4052
+ "damage",
4053
+ "jade",
4054
+ "obsidian",
4055
+ "cyclone",
4056
+ "atom-smasher",
4057
+ "maxima",
4058
+ "starman",
4059
+ "liberty-belle",
4060
+ "dove",
4061
+ "hawk",
4062
+ "blue-devil",
4063
+ "creeper",
4064
+ "ragman",
4065
+ "thunder"
4066
+ ];
4067
+ const MAX_ATTEMPTS = 20;
4068
+ const slugCache = /* @__PURE__ */ new Map();
4069
+ function pickHero() {
4070
+ return HERO_NAMES[randomInt(HERO_NAMES.length)] ?? HERO_NAMES[0] ?? "kimi";
4071
+ }
4072
+ function assembleSlug() {
4073
+ return `${pickHero()}-${pickHero()}-${pickHero()}`;
4074
+ }
4075
+ /**
4076
+ * TODO(Slice 18-3): callers must scan `<KIMI_CODE_HOME>/plans/` for
4077
+ * previously-assigned slugs and pass them as `existingSlugs`. The
4078
+ * current call sites pass a literal empty `Set()` as a placeholder,
4079
+ * which means collision avoidance is only effective within a single
4080
+ * process (via `slugCache`) — two sessions that start after a process
4081
+ * restart can still collide. The plans directory scan is Slice 18-3
4082
+ * scope because it depends on PlanFileManager wiring in the host.
4083
+ */
4084
+ function generatePlanSlug(sessionId, existingSlugs) {
4085
+ const cached = slugCache.get(sessionId);
4086
+ if (cached !== void 0) return cached;
4087
+ let slug = "";
4088
+ let collided = true;
4089
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
4090
+ slug = assembleSlug();
4091
+ if (!existingSlugs.has(slug)) {
4092
+ collided = false;
4093
+ break;
4094
+ }
4095
+ }
4096
+ if (collided) slug = `${slug}-${sessionId.slice(0, 8)}`;
4097
+ slugCache.set(sessionId, slug);
4098
+ return slug;
4099
+ }
4100
+ //#endregion
4101
+ //#region ../../packages/kimi-core/src/soul-plus/plan/plan-session-controller.ts
4102
+ /**
4103
+ * Session-scoped owner for the plan artifact.
4104
+ *
4105
+ * Plan mode is not just a boolean: it needs a stable markdown file under
4106
+ * `<KIMI_CODE_HOME>/plans/`, a persisted slug for crash/restart recovery,
4107
+ * and one write surface shared by ExitPlanMode, /plan, and Write/Edit gates.
4108
+ */
4109
+ var PlanSessionController = class {
4110
+ constructor(deps) {
4111
+ this.deps = deps;
4112
+ }
4113
+ isAvailable() {
4114
+ return this.deps.paths !== void 0 && this.deps.sessionMeta !== void 0;
4115
+ }
4116
+ getPlanFilePath() {
4117
+ const paths = this.deps.paths;
4118
+ const slug = this.getActiveSlug();
4119
+ if (paths === void 0 || slug === null) return null;
4120
+ return join(paths.home, "plans", `${slug}.md`);
4121
+ }
4122
+ async ensurePlanFilePath(source = "system", reason = "plan mode activated") {
4123
+ const paths = this.deps.paths;
4124
+ const sessionMeta = this.deps.sessionMeta;
4125
+ if (paths === void 0 || sessionMeta === void 0) return null;
4126
+ const active = this.getActiveSlug();
4127
+ if (active !== null) return join(paths.home, "plans", `${active}.md`);
4128
+ const slug = generatePlanSlug(`${this.deps.sessionId}:${randomUUID()}`, await readExistingPlanSlugs(paths));
4129
+ await sessionMeta.setPlanSlug(slug, source, reason);
4130
+ return join(paths.home, "plans", `${slug}.md`);
4131
+ }
4132
+ async readCurrentPlan() {
4133
+ const path = this.getPlanFilePath();
4134
+ if (path === null) return null;
4135
+ try {
4136
+ return {
4137
+ plan: await readFile(path, "utf8"),
4138
+ path
4139
+ };
4140
+ } catch (error) {
4141
+ if (isEnoent(error)) return {
4142
+ plan: "",
4143
+ path
4144
+ };
4145
+ throw error;
4146
+ }
4147
+ }
4148
+ async writeCurrentPlan(content) {
4149
+ const path = await this.ensurePlanFilePath();
4150
+ if (path === null) throw new Error("Plan file is unavailable for this session.");
4151
+ await mkdir(dirname(path), { recursive: true });
4152
+ await atomicWrite(path, content);
4153
+ return Buffer.byteLength(content, "utf8");
4154
+ }
4155
+ async clearCurrentPlan() {
4156
+ const path = this.getPlanFilePath();
4157
+ if (path === null) return null;
4158
+ await rm(path, { force: true });
4159
+ return path;
4160
+ }
4161
+ async deactivatePlanMode() {
4162
+ const sessionMeta = this.deps.sessionMeta;
4163
+ if (sessionMeta === void 0 || this.getActiveSlug() === null) return;
4164
+ await sessionMeta.clearPlanSlug("system", "plan mode deactivated");
4165
+ }
4166
+ getActiveSlug() {
4167
+ const raw = this.deps.sessionMeta?.get().plan_slug;
4168
+ if (typeof raw !== "string") return null;
4169
+ const trimmed = raw.trim();
4170
+ return trimmed.length > 0 ? trimmed : null;
4171
+ }
4172
+ };
4173
+ async function readExistingPlanSlugs(paths) {
4174
+ try {
4175
+ const entries = await readdir(join(paths.home, "plans"), { withFileTypes: true });
4176
+ return new Set(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => basename(entry.name, ".md")));
4177
+ } catch (error) {
4178
+ if (isEnoent(error)) return /* @__PURE__ */ new Set();
4179
+ throw error;
4180
+ }
4181
+ }
4182
+ function isEnoent(error) {
4183
+ return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
4184
+ }
4185
+ //#endregion
3540
4186
  //#region ../../packages/kimi-core/src/utils/logger.ts
3541
4187
  const noopLogger$3 = {
3542
4188
  debug: () => {},
@@ -3802,10 +4448,12 @@ var DefaultSessionControl = class {
3802
4448
  turnManager;
3803
4449
  contextState;
3804
4450
  sessionJournal;
4451
+ setPlanModeOverride;
3805
4452
  constructor(deps) {
3806
4453
  this.turnManager = deps.turnManager;
3807
4454
  this.contextState = deps.contextState;
3808
4455
  this.sessionJournal = deps.sessionJournal;
4456
+ this.setPlanModeOverride = deps.setPlanModeOverride;
3809
4457
  }
3810
4458
  async compact(customInstruction) {
3811
4459
  await this.turnManager.triggerCompaction(customInstruction);
@@ -3819,6 +4467,10 @@ var DefaultSessionControl = class {
3819
4467
  }
3820
4468
  }
3821
4469
  async setPlanMode(enabled) {
4470
+ if (this.setPlanModeOverride !== void 0) {
4471
+ await this.setPlanModeOverride(enabled);
4472
+ return;
4473
+ }
3822
4474
  await this.contextState.applyConfigChange({
3823
4475
  type: "plan_mode_changed",
3824
4476
  enabled
@@ -3893,6 +4545,10 @@ var SessionMetaService = class {
3893
4545
  if (this.meta.plan_slug === trimmed) return;
3894
4546
  await this.applyPatch({ plan_slug: trimmed }, source, reason);
3895
4547
  };
4548
+ clearPlanSlug = async (source, reason) => {
4549
+ if (this.meta.plan_slug === void 0 || this.meta.plan_slug.length === 0) return;
4550
+ await this.applyPatch({ plan_slug: "" }, source, reason);
4551
+ };
3896
4552
  /**
3897
4553
  * Overwrite the in-memory view with replay-derived fields (dirty-exit
3898
4554
  * path). Does not emit any event and does not write wire — the caller
@@ -4078,6 +4734,39 @@ function escapeXml(input) {
4078
4734
  //#endregion
4079
4735
  //#region ../../packages/kimi-core/src/soul-plus/errors.ts
4080
4736
  /**
4737
+ * Business-error sentinel classes — Phase 18 A.11 / A.12 / A.13
4738
+ * (v2 §3.1 wire error code table).
4739
+ *
4740
+ * Lives in `src/` rather than the test harness so production paths
4741
+ * (KosongAdapter implementations, wire server) can throw these
4742
+ * directly; the wire layer maps them onto the canonical JSON-RPC
4743
+ * style error codes:
4744
+ *
4745
+ * -32001 LLM not configured (missing `default_model`)
4746
+ * -32002 LLM capability mismatch (image_in / video_in / audio_in
4747
+ * rejected by the selected model)
4748
+ * -32003 Provider-level failure (network, rate-limit, 5xx, etc.)
4749
+ *
4750
+ * These classes extend `Error` so `instanceof` works across module
4751
+ * boundaries; the wire mapper in `classifyBusinessError` drops back
4752
+ * to a `/provider|backend|upstream/i` string heuristic when callers
4753
+ * throw vanilla `Error`s (e.g. fixture adapters that haven't been
4754
+ * migrated to `ProviderError`).
4755
+ */
4756
+ /**
4757
+ * Phase 18 A.11 — no default LLM configured. Surfaced when a
4758
+ * session is created without a `model` AND config has no
4759
+ * `default_model` fallback. Host code can throw this at any point
4760
+ * the missing-model is observed; the wire layer maps it to code
4761
+ * -32001.
4762
+ */
4763
+ var LLMNotSetError = class extends Error {
4764
+ constructor(message = "No LLM configured") {
4765
+ super(message);
4766
+ this.name = "LLMNotSetError";
4767
+ }
4768
+ };
4769
+ /**
4081
4770
  * Phase 18 A.12 — the selected LLM does not accept the input
4082
4771
  * modality the client supplied (image / video / audio). Thrown by
4083
4772
  * the wire prompt handler before the turn is armed so the client
@@ -4503,7 +5192,7 @@ var StubChildLifecycle = class {
4503
5192
  * `SoulRegistry.runSubagentTurn`.
4504
5193
  */
4505
5194
  async function runSubagentTurn(deps, agentId, request, signal) {
4506
- const { store, typeRegistry, parentTools, parentRuntime, parentEventBus, parentSessionJournal, parentModel, sessionDir, pathConfig, sessionId, parentSessionId, toolExecutionScopeFactory } = deps;
5195
+ const { store, typeRegistry, parentTools, parentRuntime, runtimeSlot, parentEventBus, parentSessionJournal, parentModel, sessionDir, pathConfig, sessionId, parentSessionId, toolExecutionScopeFactory } = deps;
4507
5196
  const typeDef = typeRegistry.resolve(request.agentName);
4508
5197
  const childTools = typeRegistry.resolveToolSet(request.agentName, parentTools);
4509
5198
  await store.createInstance({
@@ -4588,7 +5277,7 @@ async function runSubagentTurn(deps, agentId, request, signal) {
4588
5277
  ...childSystemPrompt !== void 0 ? { initialSystemPrompt: childSystemPrompt } : {}
4589
5278
  });
4590
5279
  const childTurnId = `${agentId}_turn`;
4591
- const baseChildRuntime = { kosong: parentRuntime.kosong };
5280
+ const baseChildRuntime = { kosong: runtimeSlot?.current().runtime.kosong ?? parentRuntime.kosong };
4592
5281
  const contentCollector = [];
4593
5282
  let baseSink;
4594
5283
  if (parentEventBus !== void 0 && childJournalWriter !== void 0) {
@@ -5353,6 +6042,138 @@ function buildBeforeToolCall(options) {
5353
6042
  };
5354
6043
  }
5355
6044
  //#endregion
6045
+ //#region ../../packages/kimi-core/src/soul-plus/plan/plan-mode-checker.ts
6046
+ /**
6047
+ * Shared error message surface used by Write / Edit so the LLM always
6048
+ * sees a consistent "here's the exit" hint regardless of which tool
6049
+ * tripped the block.
6050
+ */
6051
+ function planModeWriteBlockMessage(planPath) {
6052
+ return `Plan mode is active. You may only write to the current plan file: ${planPath ?? "(no plan file selected yet)"}. Call ExitPlanMode to exit plan mode before editing other files.`;
6053
+ }
6054
+ /**
6055
+ * Shared error message surface for Bash mutation blocks.
6056
+ */
6057
+ function planModeBashBlockMessage() {
6058
+ return "Plan mode is active. This command would mutate the filesystem; plan mode forbids that. Call ExitPlanMode to exit plan mode before running mutation commands.";
6059
+ }
6060
+ /**
6061
+ * Classify a shell command as "mutation" vs read-only. Used exclusively
6062
+ * by the plan-mode hard block — the detector is conservative: the tests
6063
+ * pin seven concrete cases (rm, redirect `>`, `>>`, sed -i, git commit,
6064
+ * plus `ls` / `cat` that must NOT trip the gate). The detector leans
6065
+ * toward under-blocking (unknown-first-word = allow) so plan-mode
6066
+ * doesn't interfere with legitimate read-only explorations.
6067
+ *
6068
+ * This deliberately does NOT attempt to be a full shell parser. Known
6069
+ * and accepted false positives:
6070
+ * - Quoted strings containing `>` are blocked (e.g.
6071
+ * `echo "hello > world"`) because the regex does not track quote
6072
+ * state. Plan-mode callers can work around this by avoiding
6073
+ * redirect-like substrings in quoted output.
6074
+ * - `&>` / `&>>` (bash stderr+stdout combined redirect) is *not*
6075
+ * detected; those are legitimate file writes but the policy's
6076
+ * 7-case test pin does not cover them.
6077
+ *
6078
+ * Stricter parsing is out of scope for Phase 18; hosts that need
6079
+ * tighter policy should wrap or replace this helper.
6080
+ */
6081
+ function isMutatingBashCommand(command) {
6082
+ const trimmed = command.trim();
6083
+ if (trimmed.length === 0) return false;
6084
+ if (/(^|[^&0-9])>{1,2}\s*(?!&)\S/.test(trimmed)) return true;
6085
+ const tokens = trimmed.split(/\s+/);
6086
+ const firstToken = tokens[0] ?? "";
6087
+ if (new Set([
6088
+ "rm",
6089
+ "rmdir",
6090
+ "mv",
6091
+ "cp",
6092
+ "mkdir",
6093
+ "touch",
6094
+ "chmod",
6095
+ "chown",
6096
+ "ln",
6097
+ "tee",
6098
+ "dd",
6099
+ "truncate",
6100
+ "shred",
6101
+ "patch"
6102
+ ]).has(firstToken)) return true;
6103
+ if (firstToken === "sed" && (/\s-i(\b|\s|=)/.test(` ${trimmed}`) || trimmed.includes("--in-place"))) return true;
6104
+ if (firstToken === "git") {
6105
+ const gitMutating = new Set([
6106
+ "commit",
6107
+ "push",
6108
+ "reset",
6109
+ "checkout",
6110
+ "rebase",
6111
+ "merge",
6112
+ "tag",
6113
+ "stash",
6114
+ "cherry-pick",
6115
+ "revert",
6116
+ "pull",
6117
+ "add",
6118
+ "rm",
6119
+ "mv",
6120
+ "clone",
6121
+ "init",
6122
+ "restore",
6123
+ "apply",
6124
+ "clean",
6125
+ "gc",
6126
+ "prune"
6127
+ ]);
6128
+ const sub = tokens[1];
6129
+ if (sub !== void 0 && gitMutating.has(sub)) return true;
6130
+ if (sub === "branch" && /\s-[dD](\b|\s)/.test(trimmed)) return true;
6131
+ }
6132
+ if (new Set([
6133
+ "npm",
6134
+ "yarn",
6135
+ "pnpm",
6136
+ "pip",
6137
+ "pip3",
6138
+ "poetry",
6139
+ "cargo",
6140
+ "gem",
6141
+ "uv"
6142
+ ]).has(firstToken)) {
6143
+ const sub = tokens[1] ?? "";
6144
+ if (new Set([
6145
+ "install",
6146
+ "i",
6147
+ "add",
6148
+ "remove",
6149
+ "rm",
6150
+ "uninstall",
6151
+ "update",
6152
+ "upgrade",
6153
+ "publish",
6154
+ "link",
6155
+ "unlink"
6156
+ ]).has(sub)) return true;
6157
+ }
6158
+ return false;
6159
+ }
6160
+ //#endregion
6161
+ //#region ../../packages/kimi-core/src/soul-plus/plan/plan-tool-call-policy.ts
6162
+ function inspectPlanModeToolCall(toolName, args, planMode) {
6163
+ if (planMode === void 0 || !planMode.isActive()) return { kind: "none" };
6164
+ if (toolName !== "Write" && toolName !== "Edit") return { kind: "none" };
6165
+ if (!isRecord$9(args) || typeof args["path"] !== "string") return { kind: "none" };
6166
+ const planPath = planMode.controller.getPlanFilePath();
6167
+ if (planPath !== null && resolve(args["path"]) === resolve(planPath)) return { kind: "allow_plan_file" };
6168
+ return {
6169
+ kind: "block",
6170
+ reason: planModeWriteBlockMessage(planPath)
6171
+ };
6172
+ }
6173
+ function isRecord$9(value) {
6174
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6175
+ }
6176
+ //#endregion
5356
6177
  //#region ../../packages/kimi-core/src/soul-plus/tool-call/use-case.ts
5357
6178
  var DefaultToolCallUseCase = class {
5358
6179
  constructor(deps) {
@@ -5384,8 +6205,15 @@ var DefaultToolCallUseCase = class {
5384
6205
  block: true,
5385
6206
  reason: hookResult.reason
5386
6207
  };
5387
- const permissionResult = await permissionClosure(btcCtx, signal);
5388
- if (permissionResult?.block === true) return permissionResult;
6208
+ const planModeDecision = inspectPlanModeToolCall(btcCtx.toolCall.name, btcCtx.args, this.deps.planMode);
6209
+ if (planModeDecision.kind === "block") return {
6210
+ block: true,
6211
+ reason: planModeDecision.reason
6212
+ };
6213
+ if (planModeDecision.kind !== "allow_plan_file") {
6214
+ const permissionResult = await permissionClosure(btcCtx, signal);
6215
+ if (permissionResult?.block === true) return permissionResult;
6216
+ }
5389
6217
  const display = context.toolsByName?.get(btcCtx.toolCall.name)?.display;
5390
6218
  const activityDescription = display?.getActivityDescription?.(btcCtx.args);
5391
6219
  const userFacingName = display?.getUserFacingName?.(btcCtx.args);
@@ -5404,7 +6232,6 @@ var DefaultToolCallUseCase = class {
5404
6232
  }
5405
6233
  });
5406
6234
  if (wireUuid !== void 0) btcCtx.toolCallByProviderId?.set(btcCtx.toolCall.id, wireUuid);
5407
- return permissionResult;
5408
6235
  };
5409
6236
  }
5410
6237
  buildAfterToolCall(context, toolsByName) {
@@ -5528,10 +6355,12 @@ var DefaultToolExecutionScopeFactory = class {
5528
6355
  const transcript = new ContextStateToolTranscriptRecorder();
5529
6356
  const resultPolicy = this.deps.resultPolicy ?? new ArchiveToolResultPolicy();
5530
6357
  const streaming = new ToolStreamingPrefetchScope();
6358
+ const planMode = this.deps.planMode;
5531
6359
  const useCase = new DefaultToolCallUseCase({
5532
6360
  policy: this.deps.policy,
5533
6361
  transcript,
5534
6362
  resultPolicy,
6363
+ ...planMode !== void 0 ? { planMode } : {},
5535
6364
  consumeToolOutcome: (toolCallId) => {
5536
6365
  const outcome = outcomes.get(toolCallId);
5537
6366
  outcomes.delete(toolCallId);
@@ -5545,6 +6374,8 @@ var DefaultToolExecutionScopeFactory = class {
5545
6374
  inputSchema: inner.inputSchema,
5546
6375
  async execute(toolCallId, args, signal, onUpdate, ctx) {
5547
6376
  try {
6377
+ const planModeResult = await maybeExecutePlanFileTool(inner.name, args, planMode);
6378
+ if (planModeResult !== void 0) return planModeResult;
5548
6379
  return await inner.execute(toolCallId, args, signal, onUpdate, ctx);
5549
6380
  } catch (error) {
5550
6381
  if (isAbortLike(error, signal)) outcomes.set(toolCallId, { kind: "abort" });
@@ -5600,6 +6431,19 @@ var DefaultToolExecutionScopeFactory = class {
5600
6431
  };
5601
6432
  }
5602
6433
  };
6434
+ function createPlanModeOnlyToolExecutionScopeFactory(deps) {
6435
+ return { create(actor) {
6436
+ return {
6437
+ actor,
6438
+ buildSoulConfig({ tools }) {
6439
+ return { tools: tools.map((tool) => wrapPlanModeTool(tool, deps.planMode)) };
6440
+ },
6441
+ wrapRuntime(runtime) {
6442
+ return runtime;
6443
+ }
6444
+ };
6445
+ } };
6446
+ }
5603
6447
  /**
5604
6448
  * Explicit test/embedder escape hatch matching the historical
5605
6449
  * `runSubagentTurn` behavior: no hooks, no approval, no permission rule walk,
@@ -5635,6 +6479,109 @@ function createWalOnlyToolExecutionScopeFactory() {
5635
6479
  };
5636
6480
  } };
5637
6481
  }
6482
+ function wrapPlanModeTool(inner, planMode) {
6483
+ const wrapped = {
6484
+ name: inner.name,
6485
+ description: inner.description,
6486
+ inputSchema: inner.inputSchema,
6487
+ async execute(toolCallId, args, signal, onUpdate, ctx) {
6488
+ const planModeResult = await maybeExecutePlanFileTool(inner.name, args, planMode);
6489
+ if (planModeResult !== void 0) return planModeResult;
6490
+ return inner.execute(toolCallId, args, signal, onUpdate, ctx);
6491
+ }
6492
+ };
6493
+ copyToolMetadata(inner, wrapped);
6494
+ return wrapped;
6495
+ }
6496
+ function copyToolMetadata(inner, wrapped) {
6497
+ if (inner.maxResultSizeChars !== void 0) wrapped.maxResultSizeChars = inner.maxResultSizeChars;
6498
+ if (inner.display !== void 0) wrapped.display = inner.display;
6499
+ if (inner.isConcurrencySafe !== void 0) wrapped.isConcurrencySafe = inner.isConcurrencySafe.bind(inner);
6500
+ }
6501
+ async function maybeExecutePlanFileTool(toolName, args, planMode) {
6502
+ if (!isRecord$8(args) || typeof args["path"] !== "string") return void 0;
6503
+ const decision = inspectPlanModeToolCall(toolName, args, planMode);
6504
+ if (decision.kind === "none") return void 0;
6505
+ if (decision.kind === "block") return {
6506
+ isError: true,
6507
+ content: decision.reason
6508
+ };
6509
+ if (planMode === void 0) return void 0;
6510
+ const inputPath = args["path"];
6511
+ if (toolName === "Write") {
6512
+ const content = args["content"];
6513
+ if (typeof content !== "string") return void 0;
6514
+ const bytesWritten = await planMode.controller.writeCurrentPlan(content);
6515
+ return {
6516
+ content: `Wrote ${String(bytesWritten)} bytes to ${inputPath}`,
6517
+ output: { bytesWritten }
6518
+ };
6519
+ }
6520
+ const oldString = args["old_string"];
6521
+ const newString = args["new_string"];
6522
+ if (typeof oldString !== "string" || typeof newString !== "string") return void 0;
6523
+ const current = await planMode.controller.readCurrentPlan();
6524
+ if (current === null || current.plan.length === 0) return {
6525
+ isError: true,
6526
+ content: "The current plan file does not exist yet. Use Write to create it before calling Edit."
6527
+ };
6528
+ const replaceAll = args["replace_all"] === true;
6529
+ const replacement = replacePlanText(current.plan, oldString, newString, replaceAll, inputPath);
6530
+ if (!replacement.ok) return replacement.error;
6531
+ await planMode.controller.writeCurrentPlan(replacement.content);
6532
+ return {
6533
+ content: `Replaced ${String(replacement.replacementCount)} occurrence${replacement.replacementCount === 1 ? "" : "s"} in ${inputPath}`,
6534
+ output: { replacementCount: replacement.replacementCount }
6535
+ };
6536
+ }
6537
+ function replacePlanText(content, oldString, newString, replaceAll, path) {
6538
+ if (!replaceAll) {
6539
+ let count = 0;
6540
+ let pos = 0;
6541
+ while (pos < content.length) {
6542
+ const idx = content.indexOf(oldString, pos);
6543
+ if (idx === -1) break;
6544
+ count++;
6545
+ pos = idx + oldString.length;
6546
+ }
6547
+ if (count === 0) return {
6548
+ ok: false,
6549
+ error: {
6550
+ isError: true,
6551
+ content: `old_string not found in ${path}`
6552
+ }
6553
+ };
6554
+ if (count > 1) return {
6555
+ ok: false,
6556
+ error: {
6557
+ isError: true,
6558
+ content: `old_string is not unique in ${path} (found ${String(count)} occurrences)`
6559
+ }
6560
+ };
6561
+ return {
6562
+ ok: true,
6563
+ content: content.replace(oldString, newString),
6564
+ replacementCount: 1
6565
+ };
6566
+ }
6567
+ const parts = content.split(oldString);
6568
+ const replacementCount = parts.length - 1;
6569
+ if (replacementCount === 0) return {
6570
+ ok: false,
6571
+ error: {
6572
+ isError: true,
6573
+ content: `old_string not found in ${path}`
6574
+ }
6575
+ };
6576
+ return {
6577
+ ok: true,
6578
+ content: parts.join(newString),
6579
+ replacementCount
6580
+ };
6581
+ }
6582
+ function isRecord$8(value) {
6583
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6584
+ }
5638
6585
  function isAbortLike(error, signal) {
5639
6586
  if (signal.aborted) return true;
5640
6587
  if (error instanceof Error && error.name === "AbortError") return true;
@@ -5788,6 +6735,7 @@ function withPlanFileFooter(body, planFilePath) {
5788
6735
  return `${body}\n\nPlan file: ${planFilePath}`;
5789
6736
  }
5790
6737
  function fullReminder(planFilePath) {
6738
+ if (planFilePath === void 0 || planFilePath.length === 0) return inlineFullReminder();
5791
6739
  return withPlanFileFooter([
5792
6740
  "Plan mode is active. You MUST NOT make any edits (with the exception of the current plan file), run non-readonly tools, or otherwise make changes to the system. This supersedes any other instructions you have received.",
5793
6741
  "",
@@ -5811,6 +6759,7 @@ function fullReminder(planFilePath) {
5811
6759
  ].join("\n"), planFilePath);
5812
6760
  }
5813
6761
  function sparseReminder(planFilePath) {
6762
+ if (planFilePath === void 0 || planFilePath.length === 0) return inlineSparseReminder();
5814
6763
  return withPlanFileFooter([
5815
6764
  "Plan mode still active (see full instructions earlier).",
5816
6765
  "Read-only except the current plan file.",
@@ -5822,6 +6771,7 @@ function sparseReminder(planFilePath) {
5822
6771
  ].join(" "), planFilePath);
5823
6772
  }
5824
6773
  function reentryReminder(planFilePath) {
6774
+ if (planFilePath === void 0 || planFilePath.length === 0) return inlineReentryReminder();
5825
6775
  return withPlanFileFooter([
5826
6776
  "Plan mode is active. You MUST NOT make any edits (with the exception of the current plan file), run non-readonly tools, or otherwise make changes to the system. This supersedes any other instructions you have received.",
5827
6777
  "",
@@ -5838,6 +6788,50 @@ function reentryReminder(planFilePath) {
5838
6788
  "Your turn must end with either AskUserQuestion (to clarify requirements) or ExitPlanMode (to request plan approval)."
5839
6789
  ].join("\n"), planFilePath);
5840
6790
  }
6791
+ function inlineFullReminder() {
6792
+ return [
6793
+ "Plan mode is active. You MUST NOT make any edits, run non-readonly tools, or otherwise make changes to the system. This supersedes any other instructions you have received.",
6794
+ "",
6795
+ "Workflow:",
6796
+ " 1. Understand — explore the codebase with Glob, Grep, Read.",
6797
+ " 2. Design — converge on the best approach; consider trade-offs but aim for a single recommendation.",
6798
+ " 3. Review — re-read key files to verify understanding.",
6799
+ " 4. Exit — call ExitPlanMode with the complete plan text in the `plan` parameter.",
6800
+ "",
6801
+ "## Handling multiple approaches",
6802
+ "Keep it focused: at most 2-3 meaningfully different approaches. Do NOT pad with minor variations — if one approach is clearly superior, just propose that one.",
6803
+ "When the best approach depends on user preferences, constraints, or context you don't have, use AskUserQuestion to clarify first.",
6804
+ "When you do include multiple approaches in the plan, you MUST pass them as the `options` parameter when calling ExitPlanMode, so the user can select which approach to execute at approval time.",
6805
+ "",
6806
+ "AskUserQuestion is for clarifying missing requirements or user preferences that affect the plan.",
6807
+ "Never ask about plan approval via text or AskUserQuestion.",
6808
+ "Your turn must end with either AskUserQuestion (to clarify requirements or preferences) or ExitPlanMode (to request plan approval). Do NOT end your turn any other way."
6809
+ ].join("\n");
6810
+ }
6811
+ function inlineSparseReminder() {
6812
+ return [
6813
+ "Plan mode still active (see full instructions earlier).",
6814
+ "Read-only; no plan file path is available in this host.",
6815
+ "Call ExitPlanMode with the complete plan text in the `plan` parameter when ready.",
6816
+ "Use AskUserQuestion to clarify user preferences when it helps you write a better plan.",
6817
+ "If the plan has multiple approaches, pass options to ExitPlanMode so the user can choose.",
6818
+ "End turns with AskUserQuestion (for clarifications) or ExitPlanMode (for approval)."
6819
+ ].join(" ");
6820
+ }
6821
+ function inlineReentryReminder() {
6822
+ return [
6823
+ "Plan mode is active. You MUST NOT make any edits, run non-readonly tools, or otherwise make changes to the system. This supersedes any other instructions you have received.",
6824
+ "",
6825
+ "## Re-entering Plan Mode",
6826
+ "No plan file path is available in this host.",
6827
+ "Before proceeding:",
6828
+ " 1. Re-evaluate the user request and any existing conversation context.",
6829
+ " 2. Use AskUserQuestion to clarify missing requirements or user preferences that affect the plan.",
6830
+ " 3. Call ExitPlanMode with the complete revised plan text in the `plan` parameter.",
6831
+ "",
6832
+ "Your turn must end with either AskUserQuestion (to clarify requirements) or ExitPlanMode (to request plan approval)."
6833
+ ].join("\n");
6834
+ }
5841
6835
  /**
5842
6836
  * Yolo-mode reminder ported from Python `yolo_mode.py`. One-shot per
5843
6837
  * activation: the first turn after `permissionMode` flips to
@@ -18949,6 +19943,9 @@ var TurnManager = class {
18949
19943
  pendingTurnOverrides;
18950
19944
  planMode;
18951
19945
  thinkingLevel;
19946
+ runtime;
19947
+ runtimeSlot;
19948
+ compactionConfig;
18952
19949
  sessionId;
18953
19950
  activeToolExecutionScope;
18954
19951
  /**
@@ -18974,6 +19971,9 @@ var TurnManager = class {
18974
19971
  this.sessionRules = [...deps.sessionRules ?? []];
18975
19972
  this.permissionMode = deps.permissionMode ?? "default";
18976
19973
  this.planMode = deps.planMode ?? false;
19974
+ this.runtime = deps.runtime;
19975
+ this.runtimeSlot = deps.runtimeSlot;
19976
+ this.compactionConfig = deps.compactionConfig;
18977
19977
  this.sessionId = deps.sessionId ?? "unknown";
18978
19978
  }
18979
19979
  setPermissionMode(mode) {
@@ -18998,6 +19998,26 @@ var TurnManager = class {
18998
19998
  getThinkingLevel() {
18999
19999
  return this.thinkingLevel;
19000
20000
  }
20001
+ setCompactionConfig(config) {
20002
+ if (this.runtimeSlot !== void 0) {
20003
+ const current = this.runtimeSlot.current();
20004
+ this.runtimeSlot.replace({
20005
+ ...current,
20006
+ ...config !== void 0 ? { compactionConfig: config } : { compactionConfig: void 0 }
20007
+ });
20008
+ }
20009
+ this.compactionConfig = config;
20010
+ }
20011
+ setRuntime(runtime) {
20012
+ if (this.runtimeSlot !== void 0) {
20013
+ const current = this.runtimeSlot.current();
20014
+ this.runtimeSlot.replace({
20015
+ ...current,
20016
+ runtime
20017
+ });
20018
+ }
20019
+ this.runtime = runtime;
20020
+ }
19001
20021
  getAgentId() {
19002
20022
  return this.agentId;
19003
20023
  }
@@ -19145,7 +20165,7 @@ var TurnManager = class {
19145
20165
  async handlePrompt(req) {
19146
20166
  if (!this.deps.lifecycleStateMachine.isIdle() || this.getCurrentTurnId() !== void 0) return { error: "agent_busy" };
19147
20167
  const input = req.data.input;
19148
- const capability = this.deps.runtime.kosong.getCapability?.(this.deps.contextState.model);
20168
+ const capability = ((this.runtimeSlot?.current())?.runtime ?? this.runtime).kosong.getCapability?.(this.deps.contextState.model);
19149
20169
  if (capability !== void 0 && capability !== UNKNOWN_CAPABILITY$1) {
19150
20170
  let inputContainsImage = false;
19151
20171
  let inputContainsVideo = false;
@@ -19212,16 +20232,28 @@ var TurnManager = class {
19212
20232
  this.deps.contextState.pushSteer(req.data.input);
19213
20233
  return { ok: true };
19214
20234
  }
19215
- drainDynamicInjectionsIntoContext(turnId) {
20235
+ async drainDynamicInjectionsIntoContext(turnId) {
19216
20236
  const manager = this.deps.dynamicInjectionManager;
19217
20237
  if (manager === void 0) return;
20238
+ const planFilePath = this.deps.planFilePathProvider?.();
19218
20239
  const ctx = {
19219
20240
  planMode: this.planMode,
19220
20241
  permissionMode: this.permissionMode,
19221
20242
  turnNumber: this.extractTurnNumber(turnId),
19222
- history: this.deps.contextState.getHistory()
19223
- };
19224
- manager.computeInjections(ctx, this.deps.contextState);
20243
+ history: this.deps.contextState.getHistory(),
20244
+ ...planFilePath !== void 0 ? { planFilePath } : {}
20245
+ };
20246
+ const contextBackedWrites = [];
20247
+ const injections = manager.computeInjections(ctx, { appendSystemReminder: (data) => {
20248
+ const write = this.deps.contextState.appendSystemReminder(data);
20249
+ contextBackedWrites.push(write);
20250
+ return write;
20251
+ } });
20252
+ await Promise.all(contextBackedWrites);
20253
+ for (const injection of injections) {
20254
+ if (injection.kind !== "system_reminder") continue;
20255
+ await this.deps.contextState.appendSystemReminder({ content: typeof injection.content === "string" ? injection.content : JSON.stringify(injection.content) });
20256
+ }
19225
20257
  }
19226
20258
  extractTurnNumber(turnId) {
19227
20259
  const match = /^turn_(\d+)$/.exec(turnId);
@@ -19259,7 +20291,9 @@ var TurnManager = class {
19259
20291
  kind: this.agentType === "sub" ? "subagent" : this.agentType === "independent" ? "independent" : "main",
19260
20292
  ...this.subagentType !== void 0 ? { subagentType: this.subagentType } : {}
19261
20293
  });
19262
- const runtimeForTurn = scope?.wrapRuntime(this.deps.runtime) ?? this.deps.runtime;
20294
+ const runtimeBundle = this.runtimeSlot?.current();
20295
+ const baseRuntime = runtimeBundle?.runtime ?? this.runtime;
20296
+ const runtimeForTurn = scope?.wrapRuntime(baseRuntime) ?? baseRuntime;
19263
20297
  if (scope !== void 0) {
19264
20298
  this.activeToolExecutionScope = scope;
19265
20299
  const scoped = scope.buildSoulConfig({
@@ -19275,10 +20309,11 @@ var TurnManager = class {
19275
20309
  ...scoped.afterToolCall !== void 0 ? { afterToolCall: scoped.afterToolCall } : {}
19276
20310
  };
19277
20311
  } else soulConfig = { tools: [...this.deps.tools] };
19278
- if (this.deps.compactionConfig !== void 0) soulConfig = {
20312
+ const compactionConfig = this.runtimeSlot !== void 0 ? runtimeBundle?.compactionConfig : this.compactionConfig;
20313
+ if (compactionConfig !== void 0) soulConfig = {
19279
20314
  ...soulConfig,
19280
- compactionConfig: this.deps.compactionConfig,
19281
- contextWindow: this.deps.compactionConfig.maxContextSize
20315
+ compactionConfig,
20316
+ contextWindow: compactionConfig.maxContextSize
19282
20317
  };
19283
20318
  const input = trigger.input;
19284
20319
  const runPromise = this.runTurn(turnId, input, soulConfig, runtimeForTurn, controller.signal);
@@ -19287,8 +20322,7 @@ var TurnManager = class {
19287
20322
  return turnId;
19288
20323
  }
19289
20324
  async runTurn(turnId, input, soulConfig, runtime, signal) {
19290
- this.drainDynamicInjectionsIntoContext(turnId);
19291
- await Promise.resolve();
20325
+ await this.drainDynamicInjectionsIntoContext(turnId);
19292
20326
  let result;
19293
20327
  let reason;
19294
20328
  try {
@@ -19433,7 +20467,7 @@ var TurnManager = class {
19433
20467
  */
19434
20468
  emitStatusUpdate(tokenUsage) {
19435
20469
  const used = this.deps.contextState.tokenCountWithPending;
19436
- const total = this.deps.compactionConfig?.maxContextSize ?? DEFAULT_CONTEXT_WINDOW;
20470
+ const total = (this.runtimeSlot !== void 0 ? this.runtimeSlot.current().compactionConfig : this.compactionConfig)?.maxContextSize ?? DEFAULT_CONTEXT_WINDOW;
19437
20471
  const data = {
19438
20472
  context_usage: {
19439
20473
  used,
@@ -19477,6 +20511,101 @@ var WakeQueueScheduler = class {
19477
20511
  };
19478
20512
  //#endregion
19479
20513
  //#region ../../packages/kimi-core/src/soul-plus/soul-plus.ts
20514
+ const PLAN_APPROVAL_APPROVE = "Approve";
20515
+ const PLAN_APPROVAL_REJECT = "Reject";
20516
+ const PLAN_APPROVAL_REJECT_AND_EXIT = "Reject and Exit";
20517
+ const PLAN_APPROVAL_REVISE = "Revise";
20518
+ function isRecord$7(value) {
20519
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20520
+ }
20521
+ function parseQuestionAnswerLabels(raw) {
20522
+ const trimmed = raw.trim();
20523
+ if (trimmed.length === 0) return [];
20524
+ try {
20525
+ const parsed = JSON.parse(trimmed);
20526
+ const answers = isRecord$7(parsed) && isRecord$7(parsed["answers"]) ? parsed["answers"] : void 0;
20527
+ if (answers !== void 0) return Object.values(answers).filter((value) => typeof value === "string" && value.length > 0);
20528
+ } catch {}
20529
+ return [trimmed];
20530
+ }
20531
+ function pushPlanApprovalOption(options, option) {
20532
+ if (options.some((item) => item.label === option.label)) return;
20533
+ options.push(option);
20534
+ }
20535
+ function planApprovalQuestionOptions(args) {
20536
+ const options = args.options !== void 0 && args.options.length >= 2 ? [...args.options] : [{
20537
+ label: PLAN_APPROVAL_APPROVE,
20538
+ description: "Exit plan mode and start execution"
20539
+ }];
20540
+ pushPlanApprovalOption(options, {
20541
+ label: PLAN_APPROVAL_REJECT,
20542
+ description: "Reject and stay in plan mode"
20543
+ });
20544
+ pushPlanApprovalOption(options, {
20545
+ label: PLAN_APPROVAL_REJECT_AND_EXIT,
20546
+ description: "Reject and exit plan mode"
20547
+ });
20548
+ return options;
20549
+ }
20550
+ async function requestEnterPlanModeQuestion(questionRuntime, toolCallId, args, signal) {
20551
+ const reasonSuffix = args.reason ? `\n\nReason: ${args.reason}` : "";
20552
+ return { approved: parseQuestionAnswerLabels((await questionRuntime.askQuestion({
20553
+ toolCallId,
20554
+ questions: [{
20555
+ header: "Plan Mode",
20556
+ question: `Enter plan mode? In plan mode I'll investigate and design a plan before making changes.${reasonSuffix}`,
20557
+ options: [{
20558
+ label: "Yes",
20559
+ description: "Enter plan mode"
20560
+ }, {
20561
+ label: "No",
20562
+ description: "Proceed without planning"
20563
+ }]
20564
+ }],
20565
+ signal
20566
+ })).answer)[0] === "Yes" };
20567
+ }
20568
+ async function requestPlanApprovalQuestion(questionRuntime, args, signal) {
20569
+ const labels = new Set((args.options ?? []).map((option) => option.label));
20570
+ const choice = parseQuestionAnswerLabels((await questionRuntime.askQuestion({
20571
+ toolCallId: `exit_plan_approval_${Date.now().toString(36)}`,
20572
+ questions: [{
20573
+ header: "Plan",
20574
+ question: "Approve this plan?",
20575
+ body: args.plan,
20576
+ options: planApprovalQuestionOptions(args),
20577
+ otherLabel: PLAN_APPROVAL_REVISE,
20578
+ otherDescription: "Stay in plan mode and provide feedback"
20579
+ }],
20580
+ signal
20581
+ })).answer)[0];
20582
+ if (choice === void 0) return {
20583
+ approved: false,
20584
+ chosenLabel: PLAN_APPROVAL_REVISE,
20585
+ feedback: "User dismissed without choosing. Plan mode remains active."
20586
+ };
20587
+ if (choice === PLAN_APPROVAL_REJECT_AND_EXIT) return { approved: true };
20588
+ if (choice === PLAN_APPROVAL_REJECT) return {
20589
+ approved: false,
20590
+ chosenLabel: PLAN_APPROVAL_REVISE,
20591
+ feedback: "Plan rejected by user. Stay in plan mode and wait for the user before revising."
20592
+ };
20593
+ if (choice === PLAN_APPROVAL_REVISE) return {
20594
+ approved: false,
20595
+ chosenLabel: PLAN_APPROVAL_REVISE,
20596
+ feedback: "User requested a plan revision."
20597
+ };
20598
+ if (choice === PLAN_APPROVAL_APPROVE) return { approved: true };
20599
+ if (labels.has(choice)) return {
20600
+ approved: true,
20601
+ chosenLabel: choice
20602
+ };
20603
+ return {
20604
+ approved: false,
20605
+ chosenLabel: PLAN_APPROVAL_REVISE,
20606
+ feedback: `User selected "${choice}". Stay in plan mode and revise before proceeding.`
20607
+ };
20608
+ }
19480
20609
  async function requestPlanApproval(approvalRuntime, turnManager, toolCallId, args, signal) {
19481
20610
  const response = await approvalRuntime.request({
19482
20611
  toolCallId,
@@ -19542,6 +20671,8 @@ var SoulPlus = class {
19542
20671
  components;
19543
20672
  infra;
19544
20673
  hostToolNames;
20674
+ planController;
20675
+ setPlanModeImpl;
19545
20676
  /**
19546
20677
  * Phase 18 A.3–A.6 — lazy `SessionControlHandler` owned by the
19547
20678
  * facade. External hosts (SessionManager) still construct their own
@@ -19565,14 +20696,33 @@ var SoulPlus = class {
19565
20696
  const sessionJournal = deps.sessionJournal;
19566
20697
  const journalWriter = contextState.journalWriter;
19567
20698
  const journalCapability = deps.journalCapability;
19568
- const rawKosong = deps.runtime.kosong;
19569
- const runtime = { kosong: rawKosong };
20699
+ const runtimeSlot = deps.runtimeSlot ?? createSessionRuntimeSlot({
20700
+ model: contextState.model,
20701
+ runtime: deps.runtime,
20702
+ compactionProvider: deps.compactionProvider,
20703
+ ...deps.compactionConfig !== void 0 ? { compactionConfig: deps.compactionConfig } : {}
20704
+ });
20705
+ const runtime = runtimeSlot.current().runtime;
19570
20706
  const approvalRuntime = deps.approvalRuntime;
20707
+ const sessionMeta = deps.stateCache !== void 0 && deps.initialMeta !== void 0 ? new SessionMetaService({
20708
+ sessionId: deps.sessionId,
20709
+ sessionJournal,
20710
+ eventBus,
20711
+ stateCache: deps.stateCache,
20712
+ initialMeta: deps.initialMeta
20713
+ }) : void 0;
20714
+ const planController = new PlanSessionController({
20715
+ sessionId: deps.sessionId,
20716
+ ...deps.pathConfig !== void 0 ? { paths: deps.pathConfig } : {},
20717
+ ...sessionMeta !== void 0 ? { sessionMeta } : {}
20718
+ });
20719
+ this.planController = planController;
19571
20720
  const compactionProvider = deps.compactionProvider;
19572
20721
  let turnManagerRef;
19573
20722
  const compaction = new CompactionOrchestrator({
19574
20723
  contextState,
19575
20724
  compactionProvider,
20725
+ runtimeSlot,
19576
20726
  lifecycleStateMachine: stateMachine,
19577
20727
  journalCapability,
19578
20728
  sink: eventBus,
@@ -19592,7 +20742,11 @@ var SoulPlus = class {
19592
20742
  return turnManagerRef.getCurrentTurnId();
19593
20743
  }
19594
20744
  });
19595
- const toolExecutionScopeFactory = deps.toolExecutionScopeFactory ?? (approvalRuntime !== void 0 && deps.hookEngine !== void 0 ? new DefaultToolExecutionScopeFactory({
20745
+ const effectiveHookEngine = deps.hookEngine ?? (approvalRuntime !== void 0 ? new HookEngine({
20746
+ executors: /* @__PURE__ */ new Map(),
20747
+ sink: eventBus
20748
+ }) : void 0);
20749
+ const toolExecutionScopeFactory = deps.toolExecutionScopeFactory ?? (approvalRuntime !== void 0 && effectiveHookEngine !== void 0 ? new DefaultToolExecutionScopeFactory({
19596
20750
  policy: {
19597
20751
  getPermissionMode: () => {
19598
20752
  if (turnManagerRef === void 0) return "default";
@@ -19607,10 +20761,17 @@ var SoulPlus = class {
19607
20761
  turnManagerRef.addSessionRule(rule);
19608
20762
  },
19609
20763
  approvalRuntime,
19610
- hookEngine: deps.hookEngine
20764
+ hookEngine: effectiveHookEngine
19611
20765
  },
19612
- resultPolicy: new ArchiveToolResultPolicy(deps.pathConfig !== void 0 ? { pathConfig: deps.pathConfig } : {})
19613
- }) : void 0);
20766
+ resultPolicy: new ArchiveToolResultPolicy(deps.pathConfig !== void 0 ? { pathConfig: deps.pathConfig } : {}),
20767
+ planMode: {
20768
+ isActive: () => turnManagerRef?.getPlanMode() ?? false,
20769
+ controller: planController
20770
+ }
20771
+ }) : planController.isAvailable() ? createPlanModeOnlyToolExecutionScopeFactory({ planMode: {
20772
+ isActive: () => turnManagerRef?.getPlanMode() ?? false,
20773
+ controller: planController
20774
+ } }) : void 0);
19614
20775
  const hasSubagentInfra = deps.subagentStore !== void 0 && deps.agentTypeRegistry !== void 0;
19615
20776
  const soulRegistry = new SoulRegistry({
19616
20777
  createHandle: (key, agentDepth) => ({
@@ -19625,7 +20786,8 @@ var SoulPlus = class {
19625
20786
  store: deps.subagentStore,
19626
20787
  typeRegistry: deps.agentTypeRegistry,
19627
20788
  parentTools: deps.tools,
19628
- parentRuntime: { kosong: rawKosong },
20789
+ runtimeSlot,
20790
+ parentRuntime: runtimeSlot.current().runtime,
19629
20791
  parentEventBus: eventBus,
19630
20792
  sessionDir: deps.sessionDir ?? "",
19631
20793
  parentModel: contextState.model,
@@ -19657,6 +20819,7 @@ var SoulPlus = class {
19657
20819
  contextState,
19658
20820
  sessionJournal,
19659
20821
  runtime,
20822
+ runtimeSlot,
19660
20823
  sink: eventBus,
19661
20824
  lifecycleStateMachine: stateMachine,
19662
20825
  soulRegistry,
@@ -19666,6 +20829,7 @@ var SoulPlus = class {
19666
20829
  wakeScheduler,
19667
20830
  dynamicInjectionManager,
19668
20831
  sessionId: deps.sessionId,
20832
+ planFilePathProvider: () => planController.getPlanFilePath() ?? void 0,
19669
20833
  ...deps.sessionRules !== void 0 ? { sessionRules: deps.sessionRules } : {},
19670
20834
  ...deps.permissionMode !== void 0 ? { permissionMode: deps.permissionMode } : {},
19671
20835
  ...toolExecutionScopeFactory !== void 0 ? { toolExecutionScopeFactory } : {},
@@ -19674,25 +20838,41 @@ var SoulPlus = class {
19674
20838
  ...deps.compactionConfig !== void 0 ? { compactionConfig: deps.compactionConfig } : {}
19675
20839
  });
19676
20840
  turnManagerRef = turnManager;
19677
- if (deps.questionRuntime !== void 0) {
19678
- const questionRuntime = deps.questionRuntime;
19679
- const setPlanMode = async (enabled) => {
20841
+ const setPlanMode = async (enabled) => {
20842
+ if (enabled) {
20843
+ await planController.ensurePlanFilePath();
19680
20844
  await contextState.applyConfigChange({
19681
20845
  type: "plan_mode_changed",
19682
20846
  enabled
19683
20847
  });
19684
20848
  turnManager.setPlanMode(enabled);
19685
- };
20849
+ return;
20850
+ }
20851
+ await contextState.applyConfigChange({
20852
+ type: "plan_mode_changed",
20853
+ enabled
20854
+ });
20855
+ turnManager.setPlanMode(enabled);
20856
+ await planController.deactivatePlanMode();
20857
+ };
20858
+ this.setPlanModeImpl = setPlanMode;
20859
+ if (deps.questionRuntime !== void 0) {
20860
+ const questionRuntime = deps.questionRuntime;
20861
+ const planApprovalRuntime = approvalRuntime !== void 0 && !(approvalRuntime instanceof AlwaysAllowApprovalRuntime) ? approvalRuntime : void 0;
20862
+ const enterPlanRequestApproval = planApprovalRuntime !== void 0 ? (toolCallId, args, signal) => requestEnterPlanModeApproval(planApprovalRuntime, turnManager, toolCallId, args, signal) : (toolCallId, args, signal) => requestEnterPlanModeQuestion(questionRuntime, toolCallId, args, signal);
20863
+ const exitPlanRequestApproval = planApprovalRuntime !== void 0 ? (toolCallId, args, signal) => requestPlanApproval(planApprovalRuntime, turnManager, toolCallId, args, signal) : (_toolCallId, args, signal) => requestPlanApprovalQuestion(questionRuntime, args, signal);
19686
20864
  if (isToolEnabled("EnterPlanMode")) toolRegistry.push(new EnterPlanModeTool({
19687
20865
  isPlanModeActive: () => turnManager.getPlanMode(),
19688
20866
  setPlanMode,
19689
20867
  isYoloMode: () => turnManager.getPermissionMode() === "bypassPermissions",
19690
- ...approvalRuntime !== void 0 ? { requestApproval: (toolCallId, args, signal) => requestEnterPlanModeApproval(approvalRuntime, turnManager, toolCallId, args, signal) } : {}
20868
+ requestApproval: enterPlanRequestApproval,
20869
+ getPlanFilePath: () => planController.getPlanFilePath()
19691
20870
  }));
19692
20871
  if (isToolEnabled("ExitPlanMode")) toolRegistry.push(new ExitPlanModeTool({
19693
20872
  isPlanModeActive: () => turnManager.getPlanMode(),
19694
20873
  setPlanMode,
19695
- ...approvalRuntime !== void 0 ? { requestApproval: (toolCallId, args, signal) => requestPlanApproval(approvalRuntime, turnManager, toolCallId, args, signal) } : {}
20874
+ requestApproval: exitPlanRequestApproval,
20875
+ ...planController.isAvailable() ? { readPlan: () => planController.readCurrentPlan() } : {}
19696
20876
  }));
19697
20877
  if (isToolEnabled("AskUserQuestion")) toolRegistry.push(new AskUserQuestionTool(questionRuntime, () => turnManager.getPermissionMode()));
19698
20878
  }
@@ -19703,13 +20883,6 @@ var SoulPlus = class {
19703
20883
  ...deps.onShellDeliver !== void 0 ? { onShellDeliver: deps.onShellDeliver } : {},
19704
20884
  ...deps.logger !== void 0 ? { logger: deps.logger } : {}
19705
20885
  });
19706
- const sessionMeta = deps.stateCache !== void 0 && deps.initialMeta !== void 0 ? new SessionMetaService({
19707
- sessionId: deps.sessionId,
19708
- sessionJournal,
19709
- eventBus,
19710
- stateCache: deps.stateCache,
19711
- initialMeta: deps.initialMeta
19712
- }) : void 0;
19713
20886
  this.lifecycle = {
19714
20887
  stateMachine,
19715
20888
  gate
@@ -19737,7 +20910,7 @@ var SoulPlus = class {
19737
20910
  eventBus,
19738
20911
  toolRegistry,
19739
20912
  permissionRules,
19740
- hookEngine: deps.hookEngine
20913
+ hookEngine: effectiveHookEngine
19741
20914
  };
19742
20915
  if (deps.approvalStateStore !== void 0) deps.approvalStateStore.onChanged((snapshot) => {
19743
20916
  turnManager.setPermissionMode(snapshot.yolo ? "bypassPermissions" : "default");
@@ -19881,6 +21054,21 @@ var SoulPlus = class {
19881
21054
  tryGetSessionMeta() {
19882
21055
  return this.services.sessionMeta;
19883
21056
  }
21057
+ async setPlanMode(enabled) {
21058
+ await this.setPlanModeImpl(enabled);
21059
+ }
21060
+ getPlanFilePath() {
21061
+ return this.planController.getPlanFilePath();
21062
+ }
21063
+ ensurePlanFilePath() {
21064
+ return this.planController.ensurePlanFilePath();
21065
+ }
21066
+ readCurrentPlan() {
21067
+ return this.planController.readCurrentPlan();
21068
+ }
21069
+ clearCurrentPlan() {
21070
+ return this.planController.clearCurrentPlan();
21071
+ }
19884
21072
  /**
19885
21073
  * Activate a skill by name (Slice 2.5 inline mode). Reads the
19886
21074
  * SKILL.md body from the registered `SkillDefinition`, interpolates
@@ -19912,7 +21100,8 @@ var SoulPlus = class {
19912
21100
  this.sessionControlInstance = new DefaultSessionControl({
19913
21101
  turnManager: this.components.turnManager,
19914
21102
  contextState: this.journal.contextState,
19915
- sessionJournal: this.journal.sessionJournal
21103
+ sessionJournal: this.journal.sessionJournal,
21104
+ setPlanModeOverride: (enabled) => this.setPlanMode(enabled)
19916
21105
  });
19917
21106
  return this.sessionControlInstance;
19918
21107
  }
@@ -20098,7 +21287,7 @@ var KosongAdapter = class {
20098
21287
  const kosongTools = params.tools.map((t) => ({
20099
21288
  name: t.name,
20100
21289
  description: t.description,
20101
- parameters: isRecord$5(t.input_schema) ? t.input_schema : { type: "object" }
21290
+ parameters: isRecord$6(t.input_schema) ? t.input_schema : { type: "object" }
20102
21291
  }));
20103
21292
  const onDelta = params.onDelta;
20104
21293
  const onThinkDelta = params.onThinkDelta;
@@ -20196,7 +21385,7 @@ function extractMessage(err) {
20196
21385
  function createKosongAdapter(options) {
20197
21386
  return new KosongAdapter(options);
20198
21387
  }
20199
- function isRecord$5(value) {
21388
+ function isRecord$6(value) {
20200
21389
  return typeof value === "object" && value !== null && !Array.isArray(value);
20201
21390
  }
20202
21391
  function parseToolArgs(raw) {
@@ -20483,44 +21672,6 @@ function safeDispatch(fn, arg, onError) {
20483
21672
  });
20484
21673
  }
20485
21674
  //#endregion
20486
- //#region ../../packages/kimi-core/src/soul-plus/approval/approval-runtime.ts
20487
- /**
20488
- * Thrown when a cross-process / team forwarding hook is invoked before
20489
- * the TeamDaemon wiring lands (Slice 2.6+). Callers should treat this as
20490
- * an unrecoverable misconfiguration in Slice 2.3.
20491
- */
20492
- var NotImplementedError = class extends Error {
20493
- constructor(feature) {
20494
- super(`${feature} not implemented in Slice 2.3 (deferred to TeamDaemon)`);
20495
- this.name = "NotImplementedError";
20496
- }
20497
- };
20498
- /**
20499
- * Default stub used when no wire/UI integration is required (e.g. unit
20500
- * tests of unrelated layers, or embedder harnesses that bypass approvals
20501
- * entirely via their own `beforeToolCall`). Every request is immediately
20502
- * approved and no records are written.
20503
- *
20504
- * For the real implementation see `./wired-approval-runtime.ts`.
20505
- */
20506
- var AlwaysAllowApprovalRuntime = class {
20507
- async request(_req, _signal) {
20508
- return {
20509
- approved: true,
20510
- response: "approved"
20511
- };
20512
- }
20513
- async recoverPendingOnStartup() {}
20514
- resolve(_requestId, _response) {}
20515
- cancelBySource(_source) {}
20516
- async ingestRemoteRequest(_data) {
20517
- throw new NotImplementedError("ApprovalRuntime.ingestRemoteRequest");
20518
- }
20519
- resolveRemote(_data) {
20520
- throw new NotImplementedError("ApprovalRuntime.resolveRemote");
20521
- }
20522
- };
20523
- //#endregion
20524
21675
  //#region ../../packages/kimi-core/src/soul-plus/approval/wired-approval-runtime.ts
20525
21676
  /**
20526
21677
  * WiredApprovalRuntime — the production `ApprovalRuntime` (v2 §9-G).
@@ -22798,122 +23949,6 @@ var ReadTool = class {
22798
23949
  }
22799
23950
  };
22800
23951
  //#endregion
22801
- //#region ../../packages/kimi-core/src/soul-plus/plan/plan-mode-checker.ts
22802
- /**
22803
- * Shared error message surface used by Write / Edit so the LLM always
22804
- * sees a consistent "here's the exit" hint regardless of which tool
22805
- * tripped the block.
22806
- */
22807
- function planModeWriteBlockMessage(planPath) {
22808
- return `Plan mode is active. You may only write to the current plan file: ${planPath ?? "(no plan file selected yet)"}. Call ExitPlanMode to exit plan mode before editing other files.`;
22809
- }
22810
- /**
22811
- * Shared error message surface for Bash mutation blocks.
22812
- */
22813
- function planModeBashBlockMessage() {
22814
- return "Plan mode is active. This command would mutate the filesystem; plan mode forbids that. Call ExitPlanMode to exit plan mode before running mutation commands.";
22815
- }
22816
- /**
22817
- * Classify a shell command as "mutation" vs read-only. Used exclusively
22818
- * by the plan-mode hard block — the detector is conservative: the tests
22819
- * pin seven concrete cases (rm, redirect `>`, `>>`, sed -i, git commit,
22820
- * plus `ls` / `cat` that must NOT trip the gate). The detector leans
22821
- * toward under-blocking (unknown-first-word = allow) so plan-mode
22822
- * doesn't interfere with legitimate read-only explorations.
22823
- *
22824
- * This deliberately does NOT attempt to be a full shell parser. Known
22825
- * and accepted false positives:
22826
- * - Quoted strings containing `>` are blocked (e.g.
22827
- * `echo "hello > world"`) because the regex does not track quote
22828
- * state. Plan-mode callers can work around this by avoiding
22829
- * redirect-like substrings in quoted output.
22830
- * - `&>` / `&>>` (bash stderr+stdout combined redirect) is *not*
22831
- * detected; those are legitimate file writes but the policy's
22832
- * 7-case test pin does not cover them.
22833
- *
22834
- * Stricter parsing is out of scope for Phase 18; hosts that need
22835
- * tighter policy should wrap or replace this helper.
22836
- */
22837
- function isMutatingBashCommand(command) {
22838
- const trimmed = command.trim();
22839
- if (trimmed.length === 0) return false;
22840
- if (/(^|[^&0-9])>{1,2}\s*(?!&)\S/.test(trimmed)) return true;
22841
- const tokens = trimmed.split(/\s+/);
22842
- const firstToken = tokens[0] ?? "";
22843
- if (new Set([
22844
- "rm",
22845
- "rmdir",
22846
- "mv",
22847
- "cp",
22848
- "mkdir",
22849
- "touch",
22850
- "chmod",
22851
- "chown",
22852
- "ln",
22853
- "tee",
22854
- "dd",
22855
- "truncate",
22856
- "shred",
22857
- "patch"
22858
- ]).has(firstToken)) return true;
22859
- if (firstToken === "sed" && (/\s-i(\b|\s|=)/.test(` ${trimmed}`) || trimmed.includes("--in-place"))) return true;
22860
- if (firstToken === "git") {
22861
- const gitMutating = new Set([
22862
- "commit",
22863
- "push",
22864
- "reset",
22865
- "checkout",
22866
- "rebase",
22867
- "merge",
22868
- "tag",
22869
- "stash",
22870
- "cherry-pick",
22871
- "revert",
22872
- "pull",
22873
- "add",
22874
- "rm",
22875
- "mv",
22876
- "clone",
22877
- "init",
22878
- "restore",
22879
- "apply",
22880
- "clean",
22881
- "gc",
22882
- "prune"
22883
- ]);
22884
- const sub = tokens[1];
22885
- if (sub !== void 0 && gitMutating.has(sub)) return true;
22886
- if (sub === "branch" && /\s-[dD](\b|\s)/.test(trimmed)) return true;
22887
- }
22888
- if (new Set([
22889
- "npm",
22890
- "yarn",
22891
- "pnpm",
22892
- "pip",
22893
- "pip3",
22894
- "poetry",
22895
- "cargo",
22896
- "gem",
22897
- "uv"
22898
- ]).has(firstToken)) {
22899
- const sub = tokens[1] ?? "";
22900
- if (new Set([
22901
- "install",
22902
- "i",
22903
- "add",
22904
- "remove",
22905
- "rm",
22906
- "uninstall",
22907
- "update",
22908
- "upgrade",
22909
- "publish",
22910
- "link",
22911
- "unlink"
22912
- ]).has(sub)) return true;
22913
- }
22914
- return false;
22915
- }
22916
- //#endregion
22917
23952
  //#region ../../packages/kimi-core/src/tools/builtin/file/write.ts
22918
23953
  /**
22919
23954
  * WriteTool — overwrite-write a file (§9-F / Appendix E.2).
@@ -28384,206 +29419,6 @@ var SetTodoListTool = class {
28384
29419
  }));
28385
29420
  }
28386
29421
  };
28387
- //#endregion
28388
- //#region ../../packages/kimi-core/src/hooks/engine.ts
28389
- /**
28390
- * Synthesises a stable id for `hook.resolved` emissions. Hooks are
28391
- * registered without an intrinsic id; we derive one from
28392
- * `event:type:matcher`, suffixed with the hook's position inside
28393
- * `this.hooks` (its registration order). The `registrationIndex` is
28394
- * stable across multiple `executeHooks` calls — using the per-call
28395
- * `settled[]` index instead would hand the same id to different hooks
28396
- * across different dispatches, breaking client-side correlation.
28397
- */
28398
- function hookId(hook, registrationIndex) {
28399
- return `${hook.event}:${hook.type}:${hook.matcher ?? ""}:${registrationIndex}`;
28400
- }
28401
- var HookEngine = class {
28402
- deps;
28403
- /**
28404
- * Mutable executor registry. Seeded from `deps.executors` at
28405
- * construction so the constructor arg can stay `ReadonlyMap`-typed
28406
- * while `registerExecutor` still lets callers install additional
28407
- * executors (e.g. `WireHookExecutor`) after the engine is wired.
28408
- */
28409
- executors;
28410
- hooks = [];
28411
- /**
28412
- * Phase 18 L3-2 — invalid-regex warn dedupe. Each distinct invalid
28413
- * matcher fires `onInvalidMatcher` once per engine instance so a
28414
- * misconfigured block-action hook doesn't flood logs on every
28415
- * tool call.
28416
- */
28417
- warnedInvalidMatchers = /* @__PURE__ */ new Set();
28418
- constructor(deps) {
28419
- this.deps = deps;
28420
- this.executors = new Map(deps.executors);
28421
- }
28422
- register(hook) {
28423
- this.hooks.push(hook);
28424
- }
28425
- /**
28426
- * Phase 18 L2-4 — install / replace an executor for a given `type`
28427
- * label at runtime. Used by the wire layer to bolt a
28428
- * `WireHookExecutor` onto an engine that was constructed with only
28429
- * the `command` executor. Replacing an existing entry is silent;
28430
- * callers wanting a conflict check can probe `hasExecutor(type)`.
28431
- */
28432
- registerExecutor(type, executor) {
28433
- this.executors.set(type, executor);
28434
- }
28435
- /** Returns true when an executor has been registered under `type`. */
28436
- hasExecutor(type) {
28437
- return this.executors.has(type);
28438
- }
28439
- unregister(hook) {
28440
- const idx = this.hooks.indexOf(hook);
28441
- if (idx !== -1) this.hooks.splice(idx, 1);
28442
- }
28443
- list(event) {
28444
- if (event === void 0) return [...this.hooks];
28445
- return this.hooks.filter((h) => h.event === event);
28446
- }
28447
- /**
28448
- * Pre-filters hooks by both `event` and `matcher` regex against the
28449
- * target value (tool name for tool-scoped events). Exported-ish via
28450
- * `executeHooks` — v2 §9-C.3 requires "getMatchingHooks(event, input)
28451
- * then concurrent execute" ordering.
28452
- */
28453
- getMatchingHooks(event, input) {
28454
- const matcherValue = extractMatcherValue(input);
28455
- return this.hooks.filter((h) => {
28456
- if (h.event !== event) return false;
28457
- return this.matchesTarget(h, matcherValue);
28458
- });
28459
- }
28460
- matchesTarget(hook, value) {
28461
- const matcher = hook.matcher;
28462
- if (matcher === void 0 || matcher === "") return true;
28463
- let re;
28464
- try {
28465
- re = new RegExp(matcher);
28466
- } catch {
28467
- if (!this.warnedInvalidMatchers.has(matcher)) {
28468
- this.warnedInvalidMatchers.add(matcher);
28469
- this.deps.onInvalidMatcher?.(hook, matcher);
28470
- }
28471
- return false;
28472
- }
28473
- return re.test(value);
28474
- }
28475
- async executeHooks(event, input, signal) {
28476
- const deduped = dedupeByCommand(this.getMatchingHooks(event, input));
28477
- if (deduped.length === 0) {
28478
- this.deps.sink?.emit({
28479
- type: "hook.triggered",
28480
- event,
28481
- matchers: [],
28482
- matched_count: 0
28483
- });
28484
- return {
28485
- blockAction: false,
28486
- additionalContext: []
28487
- };
28488
- }
28489
- this.deps.sink?.emit({
28490
- type: "hook.triggered",
28491
- event,
28492
- matchers: deduped.map((h) => h.matcher ?? ""),
28493
- matched_count: deduped.length
28494
- });
28495
- const settled = await Promise.allSettled(deduped.map((hook) => {
28496
- const executor = this.executors.get(hook.type);
28497
- if (executor === void 0) return Promise.resolve(void 0);
28498
- return executor.execute(hook, input, signal);
28499
- }));
28500
- let blockAction = false;
28501
- let reason;
28502
- const additionalContext = [];
28503
- for (const [i, result] of settled.entries()) {
28504
- const hook = deduped[i];
28505
- const registrationIndex = hook !== void 0 ? this.hooks.indexOf(hook) : -1;
28506
- if (result.status === "rejected") {
28507
- if (hook !== void 0) {
28508
- this.deps.onExecutorError?.(hook, result.reason instanceof Error ? result.reason : new Error(String(result.reason)));
28509
- this.deps.sink?.emit({
28510
- type: "hook.resolved",
28511
- hook_id: hookId(hook, registrationIndex),
28512
- outcome: "error"
28513
- });
28514
- }
28515
- continue;
28516
- }
28517
- const value = result.value;
28518
- if (hook !== void 0) {
28519
- const outcome = value?.ok === false ? "error" : value?.blockAction === true ? "blocked" : "ok";
28520
- this.deps.sink?.emit({
28521
- type: "hook.resolved",
28522
- hook_id: hookId(hook, registrationIndex),
28523
- outcome
28524
- });
28525
- }
28526
- if (value === void 0) continue;
28527
- if (value.blockAction) {
28528
- blockAction = true;
28529
- if (value.reason !== void 0) reason = value.reason;
28530
- }
28531
- if (value.additionalContext !== void 0) additionalContext.push(value.additionalContext);
28532
- }
28533
- return {
28534
- blockAction,
28535
- reason,
28536
- additionalContext
28537
- };
28538
- }
28539
- };
28540
- /**
28541
- * Collapse hook configs that share the same `command` string. `wire` hooks
28542
- * (no `command` field) key on their subscription id so different wire
28543
- * subscriptions still run in parallel; identical subscriptions collapse
28544
- * like identical commands.
28545
- */
28546
- function dedupeByCommand(hooks) {
28547
- const seen = /* @__PURE__ */ new Map();
28548
- for (const hook of hooks) {
28549
- const key = hookDedupeKey(hook);
28550
- if (!seen.has(key)) seen.set(key, hook);
28551
- }
28552
- return [...seen.values()];
28553
- }
28554
- function hookDedupeKey(hook) {
28555
- if (hook.type === "command") return `command:${hook.command}`;
28556
- if (hook.type === "wire") return `wire:${hook.subscriptionId}`;
28557
- return `unknown:${JSON.stringify(hook)}`;
28558
- }
28559
- /**
28560
- * Extracts the string fed to a hook's matcher regex. Event-dependent:
28561
- *
28562
- * - `PreToolUse` / `PostToolUse` / `OnToolFailure` — tool name
28563
- * (mirrors Python's `matcher_value=toolCall.name` contract).
28564
- * - `UserPromptSubmit` — the prompt text itself (Python parity).
28565
- * - `Stop` — the turn reason (`done` / `cancelled` / `error`), so
28566
- * hooks can filter e.g. `/^error$/`.
28567
- * - `Notification` — the notification type string, so a single hook
28568
- * can subscribe to an entire notification class via regex.
28569
- */
28570
- function extractMatcherValue(input) {
28571
- switch (input.event) {
28572
- case "PreToolUse":
28573
- case "PostToolUse":
28574
- case "OnToolFailure": return input.toolCall.name;
28575
- case "UserPromptSubmit": return input.prompt;
28576
- case "Stop": return input.reason;
28577
- case "Notification": return input.notificationType;
28578
- case "StopFailure": return input.error;
28579
- case "SubagentStart":
28580
- case "SubagentStop": return input.agentName;
28581
- case "SessionStart":
28582
- case "SessionEnd":
28583
- case "PreCompact":
28584
- case "PostCompact": return "";
28585
- }
28586
- }
28587
29422
  const _rawWireErrorSchema = z.object({
28588
29423
  code: z.number(),
28589
29424
  message: z.string(),
@@ -29388,6 +30223,13 @@ function errorMessage(error) {
29388
30223
  }
29389
30224
  //#endregion
29390
30225
  //#region ../../packages/kimi-core/src/session/session-application-service.ts
30226
+ function withCompactionConfigFallback(bundle, fallback) {
30227
+ if (bundle.compactionConfig !== void 0 || fallback === void 0) return bundle;
30228
+ return {
30229
+ ...bundle,
30230
+ compactionConfig: fallback
30231
+ };
30232
+ }
29391
30233
  var DefaultSessionApplicationService = class {
29392
30234
  constructor(deps) {
29393
30235
  this.deps = deps;
@@ -29404,18 +30246,33 @@ var DefaultSessionApplicationService = class {
29404
30246
  eventBus,
29405
30247
  ...hookEngine !== void 0 ? { hookEngine } : {}
29406
30248
  });
29407
- const systemPrompt = input.systemPrompt !== void 0 ? input.systemPrompt : this.deps.defaultSystemPromptProvider?.();
30249
+ const systemPrompt = input.systemPrompt ?? this.deps.defaultSystemPromptProvider?.();
29408
30250
  const enabledToolNames = this.deps.enabledToolNamesProvider?.();
30251
+ const model = input.model ?? this.deps.defaultModelProvider();
30252
+ const runtimeBundle = await this.deps.runtimeBundleProvider?.({
30253
+ sessionId,
30254
+ model
30255
+ });
30256
+ const compactionConfig = runtimeBundle?.compactionConfig ?? this.deps.compactionConfigProvider?.({
30257
+ sessionId,
30258
+ model
30259
+ });
30260
+ const runtimeSlotBundle = runtimeBundle !== void 0 ? withCompactionConfigFallback(runtimeBundle, compactionConfig) : void 0;
30261
+ const runtime = runtimeSlotBundle?.runtime ?? this.deps.runtimeProvider();
30262
+ const compactionProvider = runtimeSlotBundle?.compactionProvider ?? this.deps.compactionProviderProvider();
30263
+ const runtimeSlot = runtimeSlotBundle !== void 0 ? createSessionRuntimeSlot(runtimeSlotBundle) : void 0;
29409
30264
  const managed = await this.deps.sessionManager.createSession({
29410
30265
  sessionId,
29411
- runtime: this.deps.runtimeProvider(),
30266
+ runtime,
30267
+ ...runtimeSlot !== void 0 ? { runtimeSlot } : {},
29412
30268
  tools: this.deps.toolsProvider(),
29413
30269
  ...enabledToolNames !== void 0 ? { enabledToolNames } : {},
29414
- model: input.model ?? this.deps.defaultModelProvider(),
30270
+ model,
29415
30271
  ...systemPrompt !== void 0 ? { systemPrompt } : {},
29416
30272
  eventBus,
29417
30273
  workspaceDir: this.deps.workspaceDir,
29418
- compactionProvider: this.deps.compactionProviderProvider(),
30274
+ compactionProvider,
30275
+ ...compactionConfig !== void 0 ? { compactionConfig } : {},
29419
30276
  ...this.deps.approvalRuntime !== void 0 ? { approvalRuntime: this.deps.approvalRuntime } : {},
29420
30277
  ...this.deps.approvalRuntimeFactory !== void 0 ? { approvalRuntimeFactory: this.deps.approvalRuntimeFactory } : {},
29421
30278
  ...hookEngine !== void 0 ? { hookEngine } : {},
@@ -29441,11 +30298,26 @@ var DefaultSessionApplicationService = class {
29441
30298
  eventBus,
29442
30299
  ...hookEngine !== void 0 ? { hookEngine } : {}
29443
30300
  });
30301
+ const initialModel = this.deps.defaultModelProvider();
30302
+ const runtimeBundle = await this.deps.runtimeBundleProvider?.({
30303
+ sessionId,
30304
+ model: initialModel
30305
+ });
30306
+ const compactionConfig = runtimeBundle?.compactionConfig ?? this.deps.compactionConfigProvider?.({
30307
+ sessionId,
30308
+ model: initialModel
30309
+ });
30310
+ const runtimeSlotBundle = runtimeBundle !== void 0 ? withCompactionConfigFallback(runtimeBundle, compactionConfig) : void 0;
30311
+ const runtime = runtimeSlotBundle?.runtime ?? this.deps.runtimeProvider();
30312
+ const compactionProvider = runtimeSlotBundle?.compactionProvider ?? this.deps.compactionProviderProvider();
30313
+ const runtimeSlot = runtimeSlotBundle !== void 0 ? createSessionRuntimeSlot(runtimeSlotBundle) : void 0;
29444
30314
  const managed = await this.deps.sessionManager.resumeSession(sessionId, {
29445
- runtime: this.deps.runtimeProvider(),
30315
+ runtime,
30316
+ ...runtimeSlot !== void 0 ? { runtimeSlot } : {},
29446
30317
  tools: this.deps.toolsProvider(),
29447
30318
  eventBus,
29448
- compactionProvider: this.deps.compactionProviderProvider(),
30319
+ compactionProvider,
30320
+ ...compactionConfig !== void 0 ? { compactionConfig } : {},
29449
30321
  ...this.deps.approvalRuntime !== void 0 ? { approvalRuntime: this.deps.approvalRuntime } : {},
29450
30322
  ...this.deps.approvalRuntimeFactory !== void 0 ? { approvalRuntimeFactory: this.deps.approvalRuntimeFactory } : {},
29451
30323
  ...hookEngine !== void 0 ? { hookEngine } : {},
@@ -29454,6 +30326,21 @@ var DefaultSessionApplicationService = class {
29454
30326
  ...this.deps.questionRuntimeProvider !== void 0 ? { questionRuntime: this.deps.questionRuntimeProvider({ sessionId }) } : {},
29455
30327
  ...this.deps.logger !== void 0 ? { logger: this.deps.logger } : {}
29456
30328
  });
30329
+ const runtimeBundleProvider = this.deps.runtimeBundleProvider;
30330
+ if (runtimeSlot !== void 0 && runtimeBundleProvider !== void 0 && managed.contextState.model !== runtimeSlot.current().model) {
30331
+ const next = await runtimeBundleProvider({
30332
+ sessionId,
30333
+ model: managed.contextState.model
30334
+ });
30335
+ const nextConfig = next.compactionConfig ?? this.deps.compactionConfigProvider?.({
30336
+ sessionId,
30337
+ model: managed.contextState.model
30338
+ });
30339
+ runtimeSlot.replace(withCompactionConfigFallback(next, nextConfig));
30340
+ } else if (runtimeSlot === void 0) managed.soulPlus.getTurnManager().setCompactionConfig(this.deps.compactionConfigProvider?.({
30341
+ sessionId,
30342
+ model: managed.contextState.model
30343
+ }));
29457
30344
  this.deps.sessionLifecycle?.onSessionCreated?.(managed);
29458
30345
  return {
29459
30346
  sessionId: managed.sessionId,
@@ -29557,6 +30444,15 @@ var DefaultSessionApplicationService = class {
29557
30444
  async setPlanMode(sessionId, enabled) {
29558
30445
  await this.getManaged(sessionId).sessionControl.setPlanMode(enabled);
29559
30446
  }
30447
+ getPlanFilePath(sessionId) {
30448
+ return this.getManaged(sessionId).soulPlus.getPlanFilePath();
30449
+ }
30450
+ readCurrentPlan(sessionId) {
30451
+ return this.getManaged(sessionId).soulPlus.readCurrentPlan();
30452
+ }
30453
+ clearCurrentPlan(sessionId) {
30454
+ return this.getManaged(sessionId).soulPlus.clearCurrentPlan();
30455
+ }
29560
30456
  async setYolo(sessionId, enabled) {
29561
30457
  const managed = this.getManaged(sessionId);
29562
30458
  await managed.sessionControl.setYolo(enabled);
@@ -29568,9 +30464,31 @@ var DefaultSessionApplicationService = class {
29568
30464
  }
29569
30465
  async setModel(sessionId, model) {
29570
30466
  const managed = this.getManaged(sessionId);
29571
- if (this.deps.rebuildRuntimeForModel !== void 0) {
30467
+ const compactionConfig = this.deps.compactionConfigProvider?.({
30468
+ sessionId,
30469
+ model
30470
+ });
30471
+ if (this.deps.runtimeBundleProvider !== void 0) {
30472
+ const next = await this.deps.runtimeBundleProvider({
30473
+ sessionId,
30474
+ model
30475
+ });
30476
+ managed.runtimeSlot.replace(withCompactionConfigFallback(next, compactionConfig));
30477
+ } else if (this.deps.rebuildRuntimeForModel !== void 0) {
29572
30478
  await this.deps.rebuildRuntimeForModel(sessionId, model);
29573
- return;
30479
+ managed.runtimeSlot.replace({
30480
+ model,
30481
+ runtime: this.deps.runtimeProvider(),
30482
+ compactionProvider: this.deps.compactionProviderProvider(),
30483
+ compactionConfig
30484
+ });
30485
+ } else {
30486
+ const current = managed.runtimeSlot.current();
30487
+ managed.runtimeSlot.replace({
30488
+ ...current,
30489
+ model,
30490
+ compactionConfig
30491
+ });
29574
30492
  }
29575
30493
  await managed.soulPlus.setModel(model);
29576
30494
  }
@@ -30012,6 +30930,8 @@ const SUPPORTED_WIRE_METHODS = [
30012
30930
  "session.listSkills",
30013
30931
  "session.activateSkill",
30014
30932
  "session.setPlanMode",
30933
+ "session.getPlan",
30934
+ "session.clearPlan",
30015
30935
  "session.setYolo",
30016
30936
  "session.setModel",
30017
30937
  "session.setThinking",
@@ -30132,6 +31052,8 @@ function registerDefaultWireHandlers(deps) {
30132
31052
  defaultModelProvider: () => deps.defaultModelProvider?.() ?? defaultModel,
30133
31053
  ...deps.defaultSystemPromptProvider !== void 0 ? { defaultSystemPromptProvider: deps.defaultSystemPromptProvider } : {},
30134
31054
  compactionProviderProvider: () => deps.compactionProviderProvider?.() ?? compactionProvider,
31055
+ ...deps.compactionConfigProvider !== void 0 ? { compactionConfigProvider: deps.compactionConfigProvider } : {},
31056
+ ...deps.runtimeBundleProvider !== void 0 ? { runtimeBundleProvider: deps.runtimeBundleProvider } : {},
30135
31057
  eventBusProvider: () => eventBusProvider?.() ?? eventBus,
30136
31058
  ...approvalRuntime !== void 0 ? { approvalRuntime } : {},
30137
31059
  ...approvalRuntimeFactory !== void 0 ? { approvalRuntimeFactory } : {},
@@ -30288,8 +31210,16 @@ function registerDefaultWireHandlers(deps) {
30288
31210
  data: { ok: true }
30289
31211
  });
30290
31212
  });
30291
- router.registerMethod("session.prompt", "conversation", async (msg) => {
31213
+ router.registerMethod("session.prompt", "conversation", async (msg, _transport, session) => {
30292
31214
  const payload = SessionPromptSchema.parse(msg.data ?? {});
31215
+ if (session?.contextState.model === "") return createWireResponse({
31216
+ requestId: msg.id,
31217
+ sessionId: msg.session_id,
31218
+ error: {
31219
+ code: -32001,
31220
+ message: "No LLM configured. Run /login to authenticate."
31221
+ }
31222
+ });
30293
31223
  let inputText;
30294
31224
  let inputParts;
30295
31225
  if (typeof payload.input === "string") {
@@ -30527,10 +31457,39 @@ function registerDefaultWireHandlers(deps) {
30527
31457
  router.registerMethod("session.setPlanMode", "config", async (msg, _t, session) => {
30528
31458
  const payload = z.object({ enabled: z.boolean() }).parse(msg.data ?? {});
30529
31459
  await sessionApplication.setPlanMode(msg.session_id, payload.enabled);
31460
+ const planPath = sessionApplication.getPlanFilePath(msg.session_id);
30530
31461
  return createWireResponse({
30531
31462
  requestId: msg.id,
30532
31463
  sessionId: msg.session_id,
30533
- data: { ok: true }
31464
+ data: {
31465
+ ok: true,
31466
+ ...planPath !== null ? { plan_path: planPath } : {}
31467
+ }
31468
+ });
31469
+ });
31470
+ router.registerMethod("session.getPlan", "config", async (msg) => {
31471
+ const current = await sessionApplication.readCurrentPlan(msg.session_id);
31472
+ return createWireResponse({
31473
+ requestId: msg.id,
31474
+ sessionId: msg.session_id,
31475
+ data: {
31476
+ ok: true,
31477
+ ...current !== null ? {
31478
+ plan: current.plan,
31479
+ path: current.path
31480
+ } : {}
31481
+ }
31482
+ });
31483
+ });
31484
+ router.registerMethod("session.clearPlan", "config", async (msg) => {
31485
+ const path = await sessionApplication.clearCurrentPlan(msg.session_id);
31486
+ return createWireResponse({
31487
+ requestId: msg.id,
31488
+ sessionId: msg.session_id,
31489
+ data: {
31490
+ ok: true,
31491
+ ...path !== null ? { path } : {}
31492
+ }
30534
31493
  });
30535
31494
  });
30536
31495
  router.registerMethod("session.setYolo", "config", async (msg, _t, session) => {
@@ -30740,7 +31699,10 @@ function createWireQuestionRuntime(reverse, sessionId) {
30740
31699
  questions: req.questions.map((q) => ({
30741
31700
  question: q.question,
30742
31701
  ...q.header !== void 0 ? { header: q.header } : {},
31702
+ ...q.body !== void 0 ? { body: q.body } : {},
30743
31703
  multi_select: q.multiSelect ?? false,
31704
+ ...q.otherLabel !== void 0 ? { other_label: q.otherLabel } : {},
31705
+ ...q.otherDescription !== void 0 ? { other_description: q.otherDescription } : {},
30744
31706
  options: q.options.map((o) => ({
30745
31707
  label: o.label,
30746
31708
  ...o.description !== void 0 ? { description: o.description } : {}
@@ -31421,7 +32383,7 @@ function projectReplayState(records, sessionInitialized) {
31421
32383
  case "step_end":
31422
32384
  if (openStep !== null && openStep.stepUuid === r.uuid) {
31423
32385
  openStep.hasStepEnd = true;
31424
- if (r.usage !== void 0) tokenCount += r.usage.input_tokens + r.usage.output_tokens;
32386
+ if (r.usage !== void 0) tokenCount = r.usage.input_tokens + r.usage.output_tokens;
31425
32387
  }
31426
32388
  flushOpenStep();
31427
32389
  break;
@@ -31688,6 +32650,15 @@ function bindTodoStore(tools, todoStore) {
31688
32650
  return tool;
31689
32651
  });
31690
32652
  }
32653
+ function normalizeRuntimeSlot(slot, fallback) {
32654
+ const runtimeSlot = slot ?? createSessionRuntimeSlot(fallback);
32655
+ const current = runtimeSlot.current();
32656
+ if (current.compactionConfig === void 0 && fallback.compactionConfig !== void 0) runtimeSlot.replace({
32657
+ ...current,
32658
+ compactionConfig: fallback.compactionConfig
32659
+ });
32660
+ return runtimeSlot;
32661
+ }
31691
32662
  var SessionManager = class {
31692
32663
  paths;
31693
32664
  producer;
@@ -31746,6 +32717,13 @@ var SessionManager = class {
31746
32717
  const stateCache = new StateCache(this.paths.statePath(sessionId));
31747
32718
  const todoStore = new SessionTodoStore(stateCache, []);
31748
32719
  const sessionTools = bindTodoStore(options.tools, todoStore);
32720
+ const runtimeSlot = normalizeRuntimeSlot(options.runtimeSlot, {
32721
+ model: options.model,
32722
+ runtime: options.runtime,
32723
+ compactionProvider: options.compactionProvider,
32724
+ ...options.compactionConfig !== void 0 ? { compactionConfig: options.compactionConfig } : {}
32725
+ });
32726
+ const runtimeBundle = runtimeSlot.current();
31749
32727
  const mainInitInput = {
31750
32728
  type: "session_initialized",
31751
32729
  agent_type: "main",
@@ -31796,7 +32774,8 @@ var SessionManager = class {
31796
32774
  sessionId,
31797
32775
  contextState,
31798
32776
  sessionJournal,
31799
- runtime: options.runtime,
32777
+ runtimeSlot,
32778
+ runtime: runtimeBundle.runtime,
31800
32779
  eventBus,
31801
32780
  tools: sessionTools,
31802
32781
  ...options.enabledToolNames !== void 0 ? { enabledToolNames: options.enabledToolNames } : {},
@@ -31805,8 +32784,8 @@ var SessionManager = class {
31805
32784
  ...options.onShellDeliver !== void 0 ? { onShellDeliver: options.onShellDeliver } : {},
31806
32785
  ...options.skillManager !== void 0 ? { skillManager: options.skillManager } : {},
31807
32786
  ...options.questionRuntime !== void 0 ? { questionRuntime: options.questionRuntime } : {},
31808
- ...options.compactionConfig !== void 0 ? { compactionConfig: options.compactionConfig } : {},
31809
- compactionProvider: options.compactionProvider,
32787
+ ...runtimeBundle.compactionConfig !== void 0 ? { compactionConfig: runtimeBundle.compactionConfig } : {},
32788
+ compactionProvider: runtimeBundle.compactionProvider,
31810
32789
  journalCapability: options.journalCapability ?? createWiredJournalCapability({
31811
32790
  sessionDir,
31812
32791
  journalWriter,
@@ -31838,13 +32817,15 @@ var SessionManager = class {
31838
32817
  sessionControl: new DefaultSessionControl({
31839
32818
  turnManager: turnManagerRef,
31840
32819
  contextState,
31841
- sessionJournal
32820
+ sessionJournal,
32821
+ setPlanModeOverride: (enabled) => soulPlus.setPlanMode(enabled)
31842
32822
  }),
31843
32823
  contextState,
31844
32824
  eventBus,
31845
32825
  stateCache,
31846
32826
  sessionJournal,
31847
32827
  journalWriter,
32828
+ runtimeSlot,
31848
32829
  lifecycleStateMachine,
31849
32830
  todoStore
31850
32831
  };
@@ -31909,6 +32890,13 @@ var SessionManager = class {
31909
32890
  const initialTodos = isClean ? cloneTodos(stateData?.todos ?? projected.todos) : cloneTodos(projected.todos);
31910
32891
  const todoStore = new SessionTodoStore(stateCache, initialTodos);
31911
32892
  const sessionTools = bindTodoStore(options.tools, todoStore);
32893
+ const runtimeSlot = normalizeRuntimeSlot(options.runtimeSlot, {
32894
+ model: projected.model,
32895
+ runtime: options.runtime,
32896
+ compactionProvider: options.compactionProvider,
32897
+ ...options.compactionConfig !== void 0 ? { compactionConfig: options.compactionConfig } : {}
32898
+ });
32899
+ const runtimeBundle = runtimeSlot.current();
31912
32900
  if (stateData !== null) await stateCache.write({
31913
32901
  ...stateData,
31914
32902
  status: "active",
@@ -31923,6 +32911,7 @@ var SessionManager = class {
31923
32911
  const pickDescription = isClean ? stateData?.description ?? replayedMeta.description : replayedMeta.description ?? stateData?.description;
31924
32912
  const pickArchived = isClean ? stateData?.archived ?? replayedMeta.archived : replayedMeta.archived ?? stateData?.archived;
31925
32913
  const pickLastModel = isClean ? stateData?.model ?? replayedMeta.last_model : replayedMeta.last_model ?? stateData?.model;
32914
+ const pickPlanSlug = isClean ? stateData?.plan_slug ?? replayedMeta.plan_slug : replayedMeta.plan_slug ?? stateData?.plan_slug;
31926
32915
  const initialMeta = {
31927
32916
  session_id: sessionId,
31928
32917
  created_at: stateData?.created_at ?? now,
@@ -31933,6 +32922,7 @@ var SessionManager = class {
31933
32922
  ...pickDescription !== void 0 ? { description: pickDescription } : {},
31934
32923
  ...pickArchived !== void 0 ? { archived: pickArchived } : {},
31935
32924
  ...pickLastModel !== void 0 ? { last_model: pickLastModel } : {},
32925
+ ...pickPlanSlug !== void 0 ? { plan_slug: pickPlanSlug } : {},
31936
32926
  producer: replayResult.producer,
31937
32927
  last_exit_code: "dirty"
31938
32928
  };
@@ -31951,7 +32941,8 @@ var SessionManager = class {
31951
32941
  sessionId,
31952
32942
  contextState,
31953
32943
  sessionJournal,
31954
- runtime: options.runtime,
32944
+ runtimeSlot,
32945
+ runtime: runtimeBundle.runtime,
31955
32946
  eventBus,
31956
32947
  tools: sessionTools,
31957
32948
  ...options.enabledToolNames !== void 0 ? { enabledToolNames: options.enabledToolNames } : {},
@@ -31960,8 +32951,8 @@ var SessionManager = class {
31960
32951
  ...options.onShellDeliver !== void 0 ? { onShellDeliver: options.onShellDeliver } : {},
31961
32952
  ...options.skillManager !== void 0 ? { skillManager: options.skillManager } : {},
31962
32953
  ...options.questionRuntime !== void 0 ? { questionRuntime: options.questionRuntime } : {},
31963
- ...options.compactionConfig !== void 0 ? { compactionConfig: options.compactionConfig } : {},
31964
- compactionProvider: options.compactionProvider,
32954
+ ...runtimeBundle.compactionConfig !== void 0 ? { compactionConfig: runtimeBundle.compactionConfig } : {},
32955
+ compactionProvider: runtimeBundle.compactionProvider,
31965
32956
  journalCapability: options.journalCapability ?? createWiredJournalCapability({
31966
32957
  sessionDir,
31967
32958
  journalWriter,
@@ -31988,7 +32979,10 @@ var SessionManager = class {
31988
32979
  await soulPlus.init();
31989
32980
  turnManagerRef.setPermissionMode(effectivePermissionMode);
31990
32981
  if (projected.thinkingLevel !== void 0) turnManagerRef.setThinkingLevel(projected.thinkingLevel);
31991
- if (projected.planMode) turnManagerRef.setPlanMode(true);
32982
+ if (projected.planMode) {
32983
+ await soulPlus.ensurePlanFilePath();
32984
+ turnManagerRef.setPlanMode(true);
32985
+ }
31992
32986
  const notificationRecords = replayResult.records.filter((r) => r.type === "notification");
31993
32987
  if (notificationRecords.length > 0) soulPlus.getNotificationManager().primeDedupeIndex(notificationRecords);
31994
32988
  for (const agentId of lostSubagentIds) await soulPlus.emitNotification({
@@ -32007,13 +33001,15 @@ var SessionManager = class {
32007
33001
  sessionControl: new DefaultSessionControl({
32008
33002
  turnManager: turnManagerRef,
32009
33003
  contextState,
32010
- sessionJournal
33004
+ sessionJournal,
33005
+ setPlanModeOverride: (enabled) => soulPlus.setPlanMode(enabled)
32011
33006
  }),
32012
33007
  contextState,
32013
33008
  eventBus,
32014
33009
  stateCache,
32015
33010
  sessionJournal,
32016
33011
  journalWriter,
33012
+ runtimeSlot,
32017
33013
  lifecycleStateMachine,
32018
33014
  todoStore
32019
33015
  };
@@ -32263,6 +33259,7 @@ var SessionManager = class {
32263
33259
  ...meta?.description !== void 0 ? { description: meta.description } : {},
32264
33260
  ...meta?.archived !== void 0 ? { archived: meta.archived } : {},
32265
33261
  ...meta?.last_model !== void 0 ? { model: meta.last_model } : {},
33262
+ ...meta?.plan_slug !== void 0 ? { plan_slug: meta.plan_slug } : {},
32266
33263
  ...currentPlanMode ? { plan_mode: true } : { plan_mode: false },
32267
33264
  todos: cloneTodos(currentTodos),
32268
33265
  last_exit_code: "clean"
@@ -39213,7 +40210,7 @@ async function loadAgentBlock(filePath, visited = /* @__PURE__ */ new Set()) {
39213
40210
  }
39214
40211
  async function readAgentBlock(absPath) {
39215
40212
  const record = asRecord$1(load$2(await readFile(absPath, "utf-8")), `agent yaml root (${absPath})`);
39216
- if (isRecord$4(record["agent"])) throw new Error(`Invalid agent YAML: ${absPath} uses an unsupported 'agent:' wrapper. Use TS-native root fields.`);
40213
+ if (isRecord$5(record["agent"])) throw new Error(`Invalid agent YAML: ${absPath} uses an unsupported 'agent:' wrapper. Use TS-native root fields.`);
39217
40214
  return normalizeNativeAgent(record, absPath);
39218
40215
  }
39219
40216
  function normalizeNativeAgent(raw, source) {
@@ -39348,11 +40345,11 @@ async function getBundledAgentYamlPath() {
39348
40345
  } catch {}
39349
40346
  throw new Error(`getBundledAgentYamlPath: unable to locate bundled agent.yaml. Checked: ${candidates.join(", ")}`);
39350
40347
  }
39351
- function isRecord$4(value) {
40348
+ function isRecord$5(value) {
39352
40349
  return typeof value === "object" && value !== null && !Array.isArray(value);
39353
40350
  }
39354
40351
  function asRecord$1(value, label) {
39355
- if (!isRecord$4(value)) throw new Error(`Invalid agent YAML: ${label} must be a mapping`);
40352
+ if (!isRecord$5(value)) throw new Error(`Invalid agent YAML: ${label} must be a mapping`);
39356
40353
  return value;
39357
40354
  }
39358
40355
  function asOptionalRecord(value, label) {
@@ -40653,6 +41650,10 @@ const ThinkingConfigSchema = z.object({
40653
41650
  ]).optional(),
40654
41651
  effort: z.string().optional()
40655
41652
  });
41653
+ const LoopControlSchema = z.object({
41654
+ reservedContextSize: z.number().int().min(0).optional(),
41655
+ compactionTriggerRatio: z.number().min(.5).max(.99).optional()
41656
+ });
40656
41657
  const MoonshotServiceConfigSchema = z.object({
40657
41658
  baseUrl: z.string().optional(),
40658
41659
  apiKey: z.string().optional(),
@@ -40680,6 +41681,7 @@ const KimiConfigSchema = z.object({
40680
41681
  services: ServicesConfigSchema.optional(),
40681
41682
  mergeAllAvailableSkills: z.boolean().optional(),
40682
41683
  showThinkingStream: z.boolean().optional(),
41684
+ loopControl: LoopControlSchema.optional(),
40683
41685
  raw: z.record(z.string(), z.unknown()).optional()
40684
41686
  });
40685
41687
  //#endregion
@@ -40861,6 +41863,12 @@ function serviceToToml(service, rawService) {
40861
41863
  if (service?.customHeaders !== void 0) out["custom_headers"] = service.customHeaders;
40862
41864
  return out;
40863
41865
  }
41866
+ function loopControlToToml(loopControl, rawLoopControl) {
41867
+ const out = cloneRecord(rawLoopControl);
41868
+ setDefined(out, "reserved_context_size", loopControl.reservedContextSize);
41869
+ setDefined(out, "compaction_trigger_ratio", loopControl.compactionTriggerRatio);
41870
+ return out;
41871
+ }
40864
41872
  function configToTomlData(config) {
40865
41873
  const out = cloneRecord(config.raw);
40866
41874
  setDefined(out, "default_provider", config.defaultProvider);
@@ -40885,6 +41893,7 @@ function configToTomlData(config) {
40885
41893
  if (config.services?.moonshotSearch !== void 0) services["moonshot_search"] = serviceToToml(config.services.moonshotSearch, services["moonshot_search"]);
40886
41894
  if (config.services?.moonshotFetch !== void 0) services["moonshot_fetch"] = serviceToToml(config.services.moonshotFetch, services["moonshot_fetch"]);
40887
41895
  if (Object.keys(services).length > 0) out["services"] = services;
41896
+ if (config.loopControl !== void 0) out["loop_control"] = loopControlToToml(config.loopControl, out["loop_control"]);
40888
41897
  return out;
40889
41898
  }
40890
41899
  //#endregion
@@ -77282,7 +78291,7 @@ function createVideoUploader(provider) {
77282
78291
  }
77283
78292
  function resolveDefaultSoulPlusDefaultModel(options) {
77284
78293
  const defaultModel = options.defaultModel ?? options.modelAlias ?? options.kimiConfig?.defaultModel;
77285
- if (defaultModel === void 0 || defaultModel.length === 0) throw new Error("createDefaultSoulPlusWireClient: defaultModel/modelAlias is required when config has no defaultModel");
78294
+ if (defaultModel === void 0 || defaultModel.length === 0) return "";
77286
78295
  return defaultModel;
77287
78296
  }
77288
78297
  function getDefaultSoulPlusMaxContextSize(options, defaultModel) {
@@ -77298,6 +78307,12 @@ async function createDefaultSoulPlusRuntimeBundle(options) {
77298
78307
  maxContextSize
77299
78308
  };
77300
78309
  if (options.runtime !== void 0 || options.compactionProvider !== void 0) throw new Error("createDefaultSoulPlusWireClient: runtime and compactionProvider must be provided together");
78310
+ if (defaultModel.length === 0) return {
78311
+ runtime: createRuntime({ kosong: new UnconfiguredKosongAdapter() }),
78312
+ compactionProvider: new UnconfiguredCompactionProvider(),
78313
+ defaultModel,
78314
+ maxContextSize
78315
+ };
77301
78316
  if (options.kimiConfig === void 0) throw new Error("createDefaultSoulPlusWireClient: provide either runtime+compactionProvider or kimiConfig");
77302
78317
  const provider = await createProviderFromConfig(options.kimiConfig, defaultModel, {
77303
78318
  ...options.oauthResolver !== void 0 ? { oauthResolver: options.oauthResolver } : {},
@@ -77316,6 +78331,17 @@ async function createDefaultSoulPlusRuntimeBundle(options) {
77316
78331
  ...videoUploader !== void 0 ? { videoUploader } : {}
77317
78332
  };
77318
78333
  }
78334
+ const LLM_NOT_CONFIGURED_MESSAGE = "No LLM configured. Run /login to authenticate.";
78335
+ var UnconfiguredKosongAdapter = class {
78336
+ async chat(_params) {
78337
+ throw new LLMNotSetError(LLM_NOT_CONFIGURED_MESSAGE);
78338
+ }
78339
+ };
78340
+ var UnconfiguredCompactionProvider = class {
78341
+ async run() {
78342
+ throw new LLMNotSetError(LLM_NOT_CONFIGURED_MESSAGE);
78343
+ }
78344
+ };
77319
78345
  //#endregion
77320
78346
  //#region ../../packages/kaos/src/errors.ts
77321
78347
  /**
@@ -90347,7 +91373,7 @@ function tokenFromWire(wire) {
90347
91373
  * Load semantics: missing file → undefined. Corrupt JSON / wrong shape →
90348
91374
  * undefined (never throws). Callers treat undefined as "no token stored".
90349
91375
  */
90350
- function isRecord$3(value) {
91376
+ function isRecord$4(value) {
90351
91377
  return typeof value === "object" && value !== null && !Array.isArray(value);
90352
91378
  }
90353
91379
  var FileTokenStorage = class {
@@ -90383,7 +91409,7 @@ var FileTokenStorage = class {
90383
91409
  } catch {
90384
91410
  return;
90385
91411
  }
90386
- if (!isRecord$3(parsed)) return void 0;
91412
+ if (!isRecord$4(parsed)) return void 0;
90387
91413
  return tokenFromWire(parsed);
90388
91414
  }
90389
91415
  async save(name, token) {
@@ -90522,7 +91548,7 @@ const RETRYABLE_STATUSES = new Set([
90522
91548
  503,
90523
91549
  504
90524
91550
  ]);
90525
- function isRecord$2(value) {
91551
+ function isRecord$3(value) {
90526
91552
  return typeof value === "object" && value !== null && !Array.isArray(value);
90527
91553
  }
90528
91554
  function pickErrorDetail(data) {
@@ -90576,7 +91602,7 @@ async function postForm(url, params, deviceHeaders, options) {
90576
91602
  const text = await response.text();
90577
91603
  if (text.length > 0) {
90578
91604
  const parsed = JSON.parse(text);
90579
- if (isRecord$2(parsed)) data = parsed;
91605
+ if (isRecord$3(parsed)) data = parsed;
90580
91606
  }
90581
91607
  } catch {}
90582
91608
  return {
@@ -92211,9 +93237,9 @@ function parseManagedUsagePayload(payload) {
92211
93237
  const item = rawLimits[idx];
92212
93238
  if (!item || typeof item !== "object") continue;
92213
93239
  const detailRaw = item["detail"];
92214
- const detail = isRecord$1(detailRaw) ? detailRaw : item;
93240
+ const detail = isRecord$2(detailRaw) ? detailRaw : item;
92215
93241
  const windowRaw = item["window"];
92216
- const row = toUsageRow(detail, limitLabel(item, detail, isRecord$1(windowRaw) ? windowRaw : {}, idx));
93242
+ const row = toUsageRow(detail, limitLabel(item, detail, isRecord$2(windowRaw) ? windowRaw : {}, idx));
92217
93243
  if (row !== null) limits.push(row);
92218
93244
  }
92219
93245
  return {
@@ -92222,7 +93248,7 @@ function parseManagedUsagePayload(payload) {
92222
93248
  };
92223
93249
  }
92224
93250
  function toUsageRow(raw, defaultLabel) {
92225
- if (!isRecord$1(raw)) return null;
93251
+ if (!isRecord$2(raw)) return null;
92226
93252
  const limit = toInt(raw["limit"]);
92227
93253
  let used = toInt(raw["used"]);
92228
93254
  if (used === null) {
@@ -92316,7 +93342,7 @@ function toInt(value) {
92316
93342
  }
92317
93343
  return null;
92318
93344
  }
92319
- function isRecord$1(v) {
93345
+ function isRecord$2(v) {
92320
93346
  return typeof v === "object" && v !== null && !Array.isArray(v);
92321
93347
  }
92322
93348
  async function fetchManagedUsage(url, accessToken, opts = {}) {
@@ -92382,11 +93408,11 @@ function capabilitiesForModel(model) {
92382
93408
  function defaultBaseUrl(baseUrl) {
92383
93409
  return (baseUrl ?? kimiCodeBaseUrl()).replace(/\/+$/, "");
92384
93410
  }
92385
- function isRecord(value) {
93411
+ function isRecord$1(value) {
92386
93412
  return typeof value === "object" && value !== null && !Array.isArray(value);
92387
93413
  }
92388
93414
  function toModelInfo(item) {
92389
- if (!isRecord(item) || typeof item["id"] !== "string" || item["id"].length === 0) return;
93415
+ if (!isRecord$1(item) || typeof item["id"] !== "string" || item["id"].length === 0) return;
92390
93416
  const displayName = item["display_name"];
92391
93417
  return {
92392
93418
  id: item["id"],
@@ -92406,7 +93432,7 @@ async function fetchManagedKimiCodeModels(options) {
92406
93432
  } });
92407
93433
  if (!response.ok) throw new Error(`Failed to list Kimi Code models (HTTP ${response.status}).`);
92408
93434
  const payload = await response.json();
92409
- if (!isRecord(payload) || !Array.isArray(payload["data"])) throw new Error(`Unexpected models response for ${baseUrl}.`);
93435
+ if (!isRecord$1(payload) || !Array.isArray(payload["data"])) throw new Error(`Unexpected models response for ${baseUrl}.`);
92410
93436
  return payload["data"].map((item) => toModelInfo(item)).filter((item) => item !== void 0);
92411
93437
  }
92412
93438
  function applyManagedKimiCodeConfig(config, options) {
@@ -92730,6 +93756,7 @@ var DefaultSoulPlusOAuthServiceImpl = class {
92730
93756
  this.oauthResolver = async (providerName) => this.managerFor(providerName).ensureFresh();
92731
93757
  }
92732
93758
  prepareRequiredManagers() {
93759
+ if (this.options.forceManagedKimiCodeOAuth === true) this.managerFor(KIMI_CODE_PROVIDER_NAME);
92733
93760
  const selected = this.resolveSelectedProvider();
92734
93761
  if (selected.selectedProviderNeedsOAuth && selected.providerName !== "managed:kimi-code") throw new Error(`OAuth provider "${selected.providerName}" is not yet supported. Only managed:kimi-code device-code OAuth is wired for the default SoulPlus host.`);
92735
93762
  if (selected.providerName === "managed:kimi-code" && selected.selectedProviderNeedsOAuth) this.managerFor(KIMI_CODE_PROVIDER_NAME);
@@ -92757,6 +93784,7 @@ var DefaultSoulPlusOAuthServiceImpl = class {
92757
93784
  pathConfig: this.options.pathConfig,
92758
93785
  accessToken
92759
93786
  });
93787
+ await this.options.onConfigProvisioned?.(loadConfig({ pathConfig: this.options.pathConfig }));
92760
93788
  return {
92761
93789
  provider_name: name,
92762
93790
  ok: true
@@ -93330,6 +94358,12 @@ var KimiWireClient = class {
93330
94358
  setPlanMode(sessionId, enabled) {
93331
94359
  return this.request("session.setPlanMode", { enabled }, { sessionId });
93332
94360
  }
94361
+ getPlan(sessionId) {
94362
+ return this.request("session.getPlan", {}, { sessionId });
94363
+ }
94364
+ clearPlan(sessionId) {
94365
+ return this.request("session.clearPlan", {}, { sessionId });
94366
+ }
93333
94367
  setYolo(sessionId, enabled) {
93334
94368
  return this.request("session.setYolo", { enabled }, { sessionId });
93335
94369
  }
@@ -93547,6 +94581,8 @@ async function startCoreWireServer(options) {
93547
94581
  ...options.runtimeProvider !== void 0 ? { runtimeProvider: options.runtimeProvider } : {},
93548
94582
  ...options.defaultModelProvider !== void 0 ? { defaultModelProvider: options.defaultModelProvider } : {},
93549
94583
  ...options.compactionProviderProvider !== void 0 ? { compactionProviderProvider: options.compactionProviderProvider } : {},
94584
+ ...options.compactionConfigProvider !== void 0 ? { compactionConfigProvider: options.compactionConfigProvider } : {},
94585
+ ...options.runtimeBundleProvider !== void 0 ? { runtimeBundleProvider: options.runtimeBundleProvider } : {},
93550
94586
  ...options.rebuildRuntimeForModel !== void 0 ? { rebuildRuntimeForModel: options.rebuildRuntimeForModel } : {},
93551
94587
  ...options.defaultSystemPromptProvider !== void 0 ? { defaultSystemPromptProvider: options.defaultSystemPromptProvider } : {},
93552
94588
  pathConfig: options.pathConfig,
@@ -93630,38 +94666,71 @@ async function createInProcessWireClient(options) {
93630
94666
  }
93631
94667
  //#endregion
93632
94668
  //#region ../../packages/kimi-sdk/src/default-soul-plus-wire-client.ts
94669
+ const DEFAULT_RESERVED_CONTEXT_SIZE = 5e4;
94670
+ const DEFAULT_RESERVED_CONTEXT_FRACTION = .25;
93633
94671
  async function createDefaultSoulPlusWireClient(options) {
93634
94672
  const pathConfig = options.pathConfig ?? new PathConfig();
93635
- const kimiConfig = options.kimiConfig ?? loadConfig({
94673
+ const loadHostConfig = () => options.kimiConfig ?? loadConfig({
93636
94674
  pathConfig,
93637
94675
  workspaceDir: options.workspaceDir
93638
94676
  });
93639
- const requestedDefaultModel = resolveDefaultSoulPlusDefaultModel({
93640
- ...options,
93641
- kimiConfig
93642
- });
93643
- const effectiveConfig = resolveEffectiveConfig(kimiConfig, { requestedModel: requestedDefaultModel });
93644
- const defaultModel = resolveDefaultSoulPlusDefaultModel({
93645
- ...options,
93646
- kimiConfig: effectiveConfig,
93647
- defaultModel: requestedDefaultModel
93648
- });
93649
- const authService = createDefaultSoulPlusOAuthService({
94677
+ const resolveHostConfig = (config) => {
94678
+ const requestedDefaultModel = resolveDefaultSoulPlusDefaultModel({
94679
+ ...options,
94680
+ kimiConfig: config
94681
+ });
94682
+ const effective = resolveEffectiveConfig(config, { requestedModel: requestedDefaultModel });
94683
+ return {
94684
+ effectiveConfig: effective,
94685
+ defaultModel: resolveDefaultSoulPlusDefaultModel({
94686
+ ...options,
94687
+ kimiConfig: effective,
94688
+ defaultModel: requestedDefaultModel
94689
+ })
94690
+ };
94691
+ };
94692
+ let { effectiveConfig, defaultModel } = resolveHostConfig(loadHostConfig());
94693
+ let authService;
94694
+ let activeVideoUploader;
94695
+ let activeDefaultModel = defaultModel;
94696
+ let activeMaxContextSize = 2e5;
94697
+ const buildRuntimeBundle = async (model) => {
94698
+ const next = await createDefaultSoulPlusRuntimeBundle({
94699
+ ...options,
94700
+ kimiConfig: effectiveConfig,
94701
+ defaultModel: model,
94702
+ oauthResolver: authService.oauthResolver,
94703
+ deferOAuth: true,
94704
+ tokenRefresher: authService
94705
+ });
94706
+ activeVideoUploader = next.videoUploader;
94707
+ return {
94708
+ model: next.defaultModel,
94709
+ runtime: next.runtime,
94710
+ compactionProvider: next.compactionProvider,
94711
+ compactionConfig: buildCompactionConfig(effectiveConfig, next.defaultModel, next.maxContextSize)
94712
+ };
94713
+ };
94714
+ const reloadConfigFromDisk = async () => {
94715
+ if (options.kimiConfig !== void 0) return;
94716
+ const resolved = resolveHostConfig(loadHostConfig());
94717
+ effectiveConfig = resolved.effectiveConfig;
94718
+ defaultModel = resolved.defaultModel;
94719
+ activeDefaultModel = defaultModel;
94720
+ activeMaxContextSize = getDefaultSoulPlusMaxContextSize({
94721
+ kimiConfig: effectiveConfig,
94722
+ defaultModel
94723
+ }, defaultModel);
94724
+ };
94725
+ authService = createDefaultSoulPlusOAuthService({
93650
94726
  kimiConfig: effectiveConfig,
93651
94727
  modelAlias: defaultModel,
93652
- pathConfig
93653
- });
93654
- const initialRuntimeBundle = await createDefaultSoulPlusRuntimeBundle({
93655
- ...options,
93656
- kimiConfig: effectiveConfig,
93657
- defaultModel,
93658
- oauthResolver: authService.oauthResolver,
93659
- deferOAuth: true,
93660
- tokenRefresher: authService
94728
+ pathConfig,
94729
+ forceManagedKimiCodeOAuth: defaultModel.length === 0,
94730
+ onConfigProvisioned: reloadConfigFromDisk
93661
94731
  });
93662
- let activeRuntime = initialRuntimeBundle.runtime;
93663
- let activeCompactionProvider = initialRuntimeBundle.compactionProvider;
93664
- let activeDefaultModel = defaultModel;
94732
+ const initialRuntimeBundle = await buildRuntimeBundle(defaultModel);
94733
+ activeMaxContextSize = initialRuntimeBundle.compactionConfig?.maxContextSize ?? 2e5;
93665
94734
  const defaults = await prepareDefaultSoulPlusSessionDefaults({
93666
94735
  workspaceDir: options.workspaceDir,
93667
94736
  pathConfig
@@ -93679,41 +94748,27 @@ async function createDefaultSoulPlusWireClient(options) {
93679
94748
  enabledToolNames: defaults.enabledToolNames,
93680
94749
  webSearchProvider: webProviders.webSearchProvider,
93681
94750
  urlFetcher: webProviders.urlFetcher,
93682
- ...initialRuntimeBundle.videoUploader !== void 0 ? { videoUploader: initialRuntimeBundle.videoUploader } : {}
94751
+ ...activeVideoUploader !== void 0 ? { videoUploader: activeVideoUploader } : {}
93683
94752
  });
93684
94753
  const backgroundManager = builtin.backgroundManager;
93685
94754
  const handle = await createInProcessWireClient({
93686
94755
  pathConfig,
93687
- runtime: activeRuntime,
93688
- runtimeProvider: () => activeRuntime,
93689
- compactionProvider: activeCompactionProvider,
93690
- compactionProviderProvider: () => activeCompactionProvider,
94756
+ runtime: initialRuntimeBundle.runtime,
94757
+ compactionProvider: initialRuntimeBundle.compactionProvider,
94758
+ runtimeBundleProvider: ({ model }) => buildRuntimeBundle(model),
93691
94759
  workspaceDir: options.workspaceDir,
93692
94760
  defaultModel: activeDefaultModel,
93693
94761
  defaultModelProvider: () => activeDefaultModel,
93694
94762
  enabledToolNames: defaults.enabledToolNames,
93695
94763
  enableWiredApprovals: true,
93696
- rebuildRuntimeForModel: async (_sessionId, model) => {
93697
- const next = await createDefaultSoulPlusRuntimeBundle({
93698
- ...options,
93699
- kimiConfig: effectiveConfig,
93700
- defaultModel: model,
93701
- oauthResolver: authService.oauthResolver,
93702
- deferOAuth: true,
93703
- tokenRefresher: authService
93704
- });
93705
- activeRuntime = next.runtime;
93706
- activeCompactionProvider = next.compactionProvider;
93707
- activeDefaultModel = next.defaultModel;
93708
- },
93709
94764
  defaultSystemPromptProvider: () => defaults.systemPrompt,
93710
94765
  tools: builtin.tools,
93711
94766
  skillManager: defaults.skillManager,
93712
94767
  ...defaults.agentTypeRegistry !== void 0 ? { agentTypeRegistry: defaults.agentTypeRegistry } : {},
93713
94768
  authService,
93714
94769
  modelsProvider: () => ({
93715
- models: Object.keys(kimiConfig.models ?? {}),
93716
- default_model: effectiveConfig.defaultModel
94770
+ models: Object.keys(effectiveConfig.models ?? {}),
94771
+ ...activeDefaultModel.length > 0 ? { default_model: activeDefaultModel } : {}
93717
94772
  }),
93718
94773
  configProvider: () => effectiveConfig,
93719
94774
  ...options.logger !== void 0 ? { logger: options.logger } : {},
@@ -93726,14 +94781,30 @@ async function createDefaultSoulPlusWireClient(options) {
93726
94781
  await handle.dispose();
93727
94782
  },
93728
94783
  workspaceDir: options.workspaceDir,
93729
- defaultModel,
93730
- maxContextSize: initialRuntimeBundle.maxContextSize,
93731
- defaultThinking: effectiveConfig.defaultThinking ?? false,
93732
- defaultYolo: effectiveConfig.yolo ?? effectiveConfig.defaultYolo ?? false,
93733
- defaultPlanMode: effectiveConfig.planMode ?? effectiveConfig.defaultPlanMode ?? false,
93734
- theme: effectiveConfig.theme === "light" ? "light" : "dark",
93735
- defaultEditor: effectiveConfig.defaultEditor ?? "",
93736
- availableModels: effectiveConfig.models ?? {},
94784
+ get defaultModel() {
94785
+ return activeDefaultModel;
94786
+ },
94787
+ get maxContextSize() {
94788
+ return activeMaxContextSize;
94789
+ },
94790
+ get defaultThinking() {
94791
+ return effectiveConfig.defaultThinking ?? false;
94792
+ },
94793
+ get defaultYolo() {
94794
+ return effectiveConfig.yolo ?? effectiveConfig.defaultYolo ?? false;
94795
+ },
94796
+ get defaultPlanMode() {
94797
+ return effectiveConfig.planMode ?? effectiveConfig.defaultPlanMode ?? false;
94798
+ },
94799
+ get theme() {
94800
+ return effectiveConfig.theme === "light" ? "light" : "dark";
94801
+ },
94802
+ get defaultEditor() {
94803
+ return effectiveConfig.defaultEditor ?? "";
94804
+ },
94805
+ get availableModels() {
94806
+ return effectiveConfig.models ?? {};
94807
+ },
93737
94808
  async syncSessionRuntime(sessionId, syncOptions = {}) {
93738
94809
  if (syncOptions.plan === true) await handle.client.setPlanMode(sessionId, true);
93739
94810
  if (syncOptions.yolo === true) await handle.client.setYolo(sessionId, true);
@@ -93745,6 +94816,21 @@ async function createDefaultSoulPlusWireClient(options) {
93745
94816
  }
93746
94817
  };
93747
94818
  }
94819
+ function buildCompactionConfig(config, model, maxContextSize) {
94820
+ const loop = config.loopControl;
94821
+ const resolvedMaxContextSize = config.models?.[model] !== void 0 ? getDefaultSoulPlusMaxContextSize({
94822
+ kimiConfig: config,
94823
+ defaultModel: model
94824
+ }, model) : maxContextSize;
94825
+ return {
94826
+ maxContextSize: resolvedMaxContextSize,
94827
+ ...loop?.compactionTriggerRatio !== void 0 ? { triggerRatio: loop.compactionTriggerRatio } : {},
94828
+ reservedContextSize: loop?.reservedContextSize ?? defaultReservedContextSize(resolvedMaxContextSize)
94829
+ };
94830
+ }
94831
+ function defaultReservedContextSize(maxContextSize) {
94832
+ return Math.max(0, Math.min(DEFAULT_RESERVED_CONTEXT_SIZE, Math.floor(maxContextSize * DEFAULT_RESERVED_CONTEXT_FRACTION)));
94833
+ }
93748
94834
  function inferUserAgent(defaultHeaders) {
93749
94835
  return defaultHeaders?.["User-Agent"] ?? "KimiCLI/unknown";
93750
94836
  }
@@ -94504,6 +95590,49 @@ async function createKimiAgent() {
94504
95590
  };
94505
95591
  }
94506
95592
  //#endregion
95593
+ //#region src/utils/history/input-history.ts
95594
+ /**
95595
+ * User input history persistence — JSONL file with `{"content": "..."}` per line.
95596
+ *
95597
+ * Mirrors the Python implementation in `kimi_cli/ui/shell/prompt.py`:
95598
+ * - One JSON object per line (`_HistoryEntry { content }`)
95599
+ * - Append-only writes
95600
+ * - Skip empty entries
95601
+ * - Skip when same as last entry (consecutive deduplication)
95602
+ * - Tolerate corrupt lines: log + skip, do not abort load
95603
+ */
95604
+ async function loadInputHistory(file) {
95605
+ let raw;
95606
+ try {
95607
+ raw = await readFile(file, "utf-8");
95608
+ } catch (err) {
95609
+ if (err.code === "ENOENT") return [];
95610
+ throw err;
95611
+ }
95612
+ const entries = [];
95613
+ for (const line of raw.split("\n")) {
95614
+ const trimmed = line.trim();
95615
+ if (trimmed.length === 0) continue;
95616
+ try {
95617
+ const parsed = JSON.parse(trimmed);
95618
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.content === "string") entries.push({ content: parsed.content });
95619
+ } catch {}
95620
+ }
95621
+ return entries;
95622
+ }
95623
+ /**
95624
+ * Append an entry to the history file. Returns true if written, false if
95625
+ * skipped (empty or equal to `lastContent`).
95626
+ */
95627
+ async function appendInputHistory(file, text, lastContent) {
95628
+ const content = text.trim();
95629
+ if (content.length === 0) return false;
95630
+ if (content === lastContent) return false;
95631
+ await mkdir(dirname(file), { recursive: true });
95632
+ await appendFile(file, `${JSON.stringify({ content })}\n`, "utf-8");
95633
+ return true;
95634
+ }
95635
+ //#endregion
94507
95636
  //#region src/utils/process/external-editor.ts
94508
95637
  /**
94509
95638
  * External-editor helper — spawn $VISUAL / $EDITOR (or a configured
@@ -94552,99 +95681,6 @@ function shellQuote(path) {
94552
95681
  return `'${path.replace(/'/g, "'\\''")}'`;
94553
95682
  }
94554
95683
  //#endregion
94555
- //#region src/utils/history/input-history.ts
94556
- /**
94557
- * User input history persistence — JSONL file with `{"content": "..."}` per line.
94558
- *
94559
- * Mirrors the Python implementation in `kimi_cli/ui/shell/prompt.py`:
94560
- * - One JSON object per line (`_HistoryEntry { content }`)
94561
- * - Append-only writes
94562
- * - Skip empty entries
94563
- * - Skip when same as last entry (consecutive deduplication)
94564
- * - Tolerate corrupt lines: log + skip, do not abort load
94565
- */
94566
- async function loadInputHistory(file) {
94567
- let raw;
94568
- try {
94569
- raw = await readFile(file, "utf-8");
94570
- } catch (err) {
94571
- if (err.code === "ENOENT") return [];
94572
- throw err;
94573
- }
94574
- const entries = [];
94575
- for (const line of raw.split("\n")) {
94576
- const trimmed = line.trim();
94577
- if (trimmed.length === 0) continue;
94578
- try {
94579
- const parsed = JSON.parse(trimmed);
94580
- if (typeof parsed === "object" && parsed !== null && typeof parsed.content === "string") entries.push({ content: parsed.content });
94581
- } catch {}
94582
- }
94583
- return entries;
94584
- }
94585
- /**
94586
- * Append an entry to the history file. Returns true if written, false if
94587
- * skipped (empty or equal to `lastContent`).
94588
- */
94589
- async function appendInputHistory(file, text, lastContent) {
94590
- const content = text.trim();
94591
- if (content.length === 0) return false;
94592
- if (content === lastContent) return false;
94593
- await mkdir(dirname(file), { recursive: true });
94594
- await appendFile(file, `${JSON.stringify({ content })}\n`, "utf-8");
94595
- return true;
94596
- }
94597
- //#endregion
94598
- //#region src/tui/input/image-placeholder.ts
94599
- const PLACEHOLDER_REGEX = /\[image #(\d+) \((\d+)×(\d+)\)\]/g;
94600
- function extractImageAttachments(text, store) {
94601
- const parts = [];
94602
- const cleanedSegments = [];
94603
- const attachmentIds = [];
94604
- let cursor = 0;
94605
- let hasImages = false;
94606
- PLACEHOLDER_REGEX.lastIndex = 0;
94607
- let match;
94608
- while ((match = PLACEHOLDER_REGEX.exec(text)) !== null) {
94609
- const [literal, idStr] = match;
94610
- if (idStr === void 0) continue;
94611
- const id = Number.parseInt(idStr, 10);
94612
- const attachment = store.get(id);
94613
- if (attachment === void 0) continue;
94614
- const before = text.slice(cursor, match.index);
94615
- pushText(parts, before);
94616
- cleanedSegments.push(before);
94617
- parts.push(buildImagePart(attachment));
94618
- attachmentIds.push(id);
94619
- hasImages = true;
94620
- cursor = match.index + literal.length;
94621
- }
94622
- const tail = text.slice(cursor);
94623
- pushText(parts, tail);
94624
- cleanedSegments.push(tail);
94625
- return {
94626
- parts: hasImages ? parts : [],
94627
- hasImages,
94628
- cleanedText: cleanedSegments.join("").replaceAll(/\s+/g, " ").trim(),
94629
- attachmentIds
94630
- };
94631
- }
94632
- function pushText(parts, segment) {
94633
- if (segment.length === 0) return;
94634
- if (segment.trim().length === 0) return;
94635
- parts.push({
94636
- type: "text",
94637
- text: segment
94638
- });
94639
- }
94640
- function buildImagePart(att) {
94641
- const base64 = Buffer.from(att.bytes).toString("base64");
94642
- return {
94643
- type: "image_url",
94644
- image_url: { url: `data:${att.mime};base64,${base64}` }
94645
- };
94646
- }
94647
- //#endregion
94648
95684
  //#region src/tui/components/messages/assistant-message.ts
94649
95685
  const BULLET$1 = "● ";
94650
95686
  const INDENT$2 = " ";
@@ -95483,6 +96519,56 @@ function emitPrimary(state, message) {
95483
96519
  emitStatus(state, message, state.colors.primary);
95484
96520
  }
95485
96521
  //#endregion
96522
+ //#region src/tui/input/image-placeholder.ts
96523
+ const PLACEHOLDER_REGEX = /\[image #(\d+) \((\d+)×(\d+)\)\]/g;
96524
+ function extractImageAttachments(text, store) {
96525
+ const parts = [];
96526
+ const cleanedSegments = [];
96527
+ const attachmentIds = [];
96528
+ let cursor = 0;
96529
+ let hasImages = false;
96530
+ PLACEHOLDER_REGEX.lastIndex = 0;
96531
+ let match;
96532
+ while ((match = PLACEHOLDER_REGEX.exec(text)) !== null) {
96533
+ const [literal, idStr] = match;
96534
+ if (idStr === void 0) continue;
96535
+ const id = Number.parseInt(idStr, 10);
96536
+ const attachment = store.get(id);
96537
+ if (attachment === void 0) continue;
96538
+ const before = text.slice(cursor, match.index);
96539
+ pushText(parts, before);
96540
+ cleanedSegments.push(before);
96541
+ parts.push(buildImagePart(attachment));
96542
+ attachmentIds.push(id);
96543
+ hasImages = true;
96544
+ cursor = match.index + literal.length;
96545
+ }
96546
+ const tail = text.slice(cursor);
96547
+ pushText(parts, tail);
96548
+ cleanedSegments.push(tail);
96549
+ return {
96550
+ parts: hasImages ? parts : [],
96551
+ hasImages,
96552
+ cleanedText: cleanedSegments.join("").replaceAll(/\s+/g, " ").trim(),
96553
+ attachmentIds
96554
+ };
96555
+ }
96556
+ function pushText(parts, segment) {
96557
+ if (segment.length === 0) return;
96558
+ if (segment.trim().length === 0) return;
96559
+ parts.push({
96560
+ type: "text",
96561
+ text: segment
96562
+ });
96563
+ }
96564
+ function buildImagePart(att) {
96565
+ const base64 = Buffer.from(att.bytes).toString("base64");
96566
+ return {
96567
+ type: "image_url",
96568
+ image_url: { url: `data:${att.mime};base64,${base64}` }
96569
+ };
96570
+ }
96571
+ //#endregion
95486
96572
  //#region src/utils/git/git-ls-files.ts
95487
96573
  /**
95488
96574
  * Git-aware file listing + relevance signals with a short-TTL cache.
@@ -95679,6 +96765,38 @@ function openUrl(url) {
95679
96765
  ]] : ["xdg-open", [url]];
95680
96766
  execFile(args[0], args[1], () => {});
95681
96767
  }
96768
+ function isRecord(value) {
96769
+ return typeof value === "object" && value !== null && !Array.isArray(value);
96770
+ }
96771
+ function readAvailableModels(config) {
96772
+ if (!isRecord(config) || !isRecord(config["models"])) return {};
96773
+ const out = {};
96774
+ for (const [alias, raw] of Object.entries(config["models"])) {
96775
+ if (!isRecord(raw) || typeof raw["provider"] !== "string" || typeof raw["model"] !== "string") continue;
96776
+ out[alias] = {
96777
+ provider: raw["provider"],
96778
+ model: raw["model"],
96779
+ ...typeof raw["maxContextSize"] === "number" ? { maxContextSize: raw["maxContextSize"] } : {},
96780
+ ...Array.isArray(raw["capabilities"]) ? { capabilities: raw["capabilities"].filter((v) => typeof v === "string") } : {}
96781
+ };
96782
+ }
96783
+ return out;
96784
+ }
96785
+ async function refreshConfigAfterLogin(client, ctx) {
96786
+ const [modelsResponse, configResponse] = await Promise.all([client.getModels(), client.getConfig()]);
96787
+ const config = isRecord(configResponse.config) ? configResponse.config : {};
96788
+ const availableModels = readAvailableModels(config);
96789
+ const defaultModel = modelsResponse.default_model ?? (typeof config["defaultModel"] === "string" ? config["defaultModel"] : void 0);
96790
+ const selected = defaultModel !== void 0 ? availableModels[defaultModel] : void 0;
96791
+ const patch = {
96792
+ availableModels,
96793
+ ...defaultModel !== void 0 ? { model: defaultModel } : {},
96794
+ ...selected?.maxContextSize !== void 0 ? { maxContextTokens: selected.maxContextSize } : {},
96795
+ ...typeof config["defaultThinking"] === "boolean" ? { thinking: config["defaultThinking"] } : {}
96796
+ };
96797
+ if (defaultModel !== void 0 && typeof ctx.appState.sessionId === "string" && ctx.appState.sessionId.length > 0) await client.setModel(ctx.appState.sessionId, defaultModel);
96798
+ ctx.setAppState(patch);
96799
+ }
95682
96800
  function createAuthCommands(deps = {}) {
95683
96801
  const providerName = deps.defaultProviderName ?? "managed:kimi-code";
95684
96802
  return [{
@@ -95708,6 +96826,11 @@ function createAuthCommands(deps = {}) {
95708
96826
  });
95709
96827
  try {
95710
96828
  await deps.client.authLogin(providerName);
96829
+ try {
96830
+ await refreshConfigAfterLogin(deps.client, ctx);
96831
+ } catch (refreshError) {
96832
+ return ok$2(`Login successful, but failed to refresh config: ${refreshError instanceof Error ? refreshError.message : String(refreshError)}`);
96833
+ }
95711
96834
  return {
95712
96835
  type: "ok",
95713
96836
  message: "✓ Login successful!",
@@ -95814,8 +96937,8 @@ const shellCommands = [
95814
96937
  const newTitle = trimmed.slice(0, 200);
95815
96938
  try {
95816
96939
  await ctx.client.rename(ctx.appState.sessionId, newTitle);
95817
- } catch (err) {
95818
- return ok$1(`Failed to set title: ${err instanceof Error ? err.message : String(err)}`);
96940
+ } catch (error) {
96941
+ return ok$1(`Failed to set title: ${error instanceof Error ? error.message : String(error)}`);
95819
96942
  }
95820
96943
  return ok$1(`Session title set to: ${newTitle}`);
95821
96944
  }
@@ -95841,12 +96964,23 @@ const shellCommands = [
95841
96964
  description: "Toggle plan mode",
95842
96965
  mode: "both",
95843
96966
  async execute(args, ctx) {
96967
+ const subcmd = args.trim().toLowerCase();
96968
+ if (subcmd === "view") {
96969
+ const res = await ctx.client.getPlan(ctx.appState.sessionId);
96970
+ if (res.plan !== void 0 && res.plan.length > 0) return ok$1(res.plan);
96971
+ return ok$1("No plan file found for this session.");
96972
+ }
96973
+ if (subcmd === "clear") {
96974
+ await ctx.client.clearPlan(ctx.appState.sessionId);
96975
+ return ok$1("Plan cleared.");
96976
+ }
95844
96977
  let enabled;
95845
- if (args === "on") enabled = true;
95846
- else if (args === "off") enabled = false;
96978
+ if (subcmd === "on") enabled = true;
96979
+ else if (subcmd === "off") enabled = false;
95847
96980
  else enabled = !ctx.appState.planMode;
95848
- await ctx.client.setPlanMode(ctx.appState.sessionId, enabled);
96981
+ const res = await ctx.client.setPlanMode(ctx.appState.sessionId, enabled);
95849
96982
  ctx.setAppState({ planMode: enabled });
96983
+ if (enabled && res.plan_path !== void 0) return ok$1(`Plan mode ON. Write your plan to: ${res.plan_path}`);
95850
96984
  return ok$1(`Plan mode: ${enabled ? "on" : "off"}`);
95851
96985
  }
95852
96986
  },
@@ -96008,6 +97142,40 @@ function createDefaultRegistry(authDeps) {
96008
97142
  * Custom editor extending pi-tui Editor with app-level keybindings.
96009
97143
  */
96010
97144
  const ANSI_SGR = /\x1b\[[0-9;]*m/g;
97145
+ const KITTY_CSI_U = /^\x1b\[(\d+);(\d+)((?::\d+)*)u$/;
97146
+ const CAPS_LOCK_BIT = 64;
97147
+ const CTRL_BIT = 4;
97148
+ const SHIFT_BIT = 1;
97149
+ /**
97150
+ * Workaround for a pi-tui bug that surfaces when Kitty keyboard protocol
97151
+ * is active AND caps_lock is on. In that state terminals emit, e.g.,
97152
+ * `ESC[68;69u` for ctrl+d (codepoint=68=`D`, modifier=ctrl|caps_lock).
97153
+ * pi-tui's `matchesKittySequence` masks `caps_lock` out of the *modifier*
97154
+ * but leaves the *codepoint* capitalised, so `matchesKey(data, "ctrl+d")`
97155
+ * (which expects codepoint=100=`d`) fails and every ctrl-shortcut is
97156
+ * silently dropped.
97157
+ *
97158
+ * We rewrite the sequence back to its unlocked form before dispatching,
97159
+ * but only when ctrl is held and shift is not — i.e. exactly the
97160
+ * `ctrl+<letter>` case. Plain uppercase (caps_lock only, no ctrl) and
97161
+ * explicit ctrl+shift+<letter> are left alone.
97162
+ */
97163
+ function normalizeCapsLockedCtrl(data) {
97164
+ const m = data.match(KITTY_CSI_U);
97165
+ if (m === null) return data;
97166
+ const codepoint = Number(m[1]);
97167
+ const modifierPlus1 = Number(m[2]);
97168
+ const tail = m[3] ?? "";
97169
+ if (!Number.isFinite(codepoint) || !Number.isFinite(modifierPlus1)) return data;
97170
+ const modifier = modifierPlus1 - 1;
97171
+ if ((modifier & CAPS_LOCK_BIT) === 0) return data;
97172
+ if ((modifier & CTRL_BIT) === 0) return data;
97173
+ if ((modifier & SHIFT_BIT) !== 0) return data;
97174
+ if (codepoint < 65 || codepoint > 90) return data;
97175
+ const loweredCodepoint = codepoint + 32;
97176
+ const strippedModifier = (modifier & ~CAPS_LOCK_BIT) + 1;
97177
+ return `\x1b[${String(loweredCodepoint)};${String(strippedModifier)}${tail}u`;
97178
+ }
96011
97179
  /** Convert a visible-char index (ANSI-stripped) back to an index into the raw ANSI-bearing string. */
96012
97180
  function mapVisibleIdxToRaw(line, visibleIdx) {
96013
97181
  let visibleCount = 0;
@@ -96073,51 +97241,52 @@ var CustomEditor = class extends Editor {
96073
97241
  return lines;
96074
97242
  }
96075
97243
  handleInput(data) {
96076
- if (matchesKey(data, process.platform === "win32" ? "alt+v" : Key.ctrl("v")) && this.onPasteImage !== void 0) {
97244
+ const normalized = normalizeCapsLockedCtrl(data);
97245
+ if (matchesKey(normalized, process.platform === "win32" ? "alt+v" : Key.ctrl("v")) && this.onPasteImage !== void 0) {
96077
97246
  const handler = this.onPasteImage;
96078
97247
  handler().then((handled) => {
96079
- if (!handled) super.handleInput.call(this, data);
97248
+ if (!handled) super.handleInput.call(this, normalized);
96080
97249
  });
96081
97250
  return;
96082
97251
  }
96083
- if (matchesKey(data, Key.ctrl("d"))) {
97252
+ if (matchesKey(normalized, Key.ctrl("d"))) {
96084
97253
  if (this.getText().length === 0) {
96085
97254
  this.onCtrlD?.();
96086
97255
  return;
96087
97256
  }
96088
97257
  }
96089
- if (matchesKey(data, Key.ctrl("c"))) {
97258
+ if (matchesKey(normalized, Key.ctrl("c"))) {
96090
97259
  this.onCtrlC?.();
96091
97260
  return;
96092
97261
  }
96093
- if (matchesKey(data, Key.ctrl("g"))) {
97262
+ if (matchesKey(normalized, Key.ctrl("g"))) {
96094
97263
  this.onOpenExternalEditor?.();
96095
97264
  return;
96096
97265
  }
96097
- if (matchesKey(data, Key.ctrl("o"))) {
97266
+ if (matchesKey(normalized, Key.ctrl("o"))) {
96098
97267
  this.onToggleToolExpand?.();
96099
97268
  return;
96100
97269
  }
96101
- if (matchesKey(data, Key.ctrl("s"))) {
97270
+ if (matchesKey(normalized, Key.ctrl("s"))) {
96102
97271
  this.onCtrlS?.();
96103
97272
  return;
96104
97273
  }
96105
- if (matchesKey(data, "shift+tab")) {
97274
+ if (matchesKey(normalized, "shift+tab")) {
96106
97275
  this.onShiftTab?.();
96107
97276
  return;
96108
97277
  }
96109
- if (matchesKey(data, Key.up)) {
97278
+ if (matchesKey(normalized, Key.up)) {
96110
97279
  if (this.getText().length === 0 && this.onUpArrowEmpty) {
96111
97280
  if (this.onUpArrowEmpty()) return;
96112
97281
  }
96113
97282
  }
96114
- if (matchesKey(data, Key.escape)) {
97283
+ if (matchesKey(normalized, Key.escape)) {
96115
97284
  if (!this.isShowingAutocomplete()) {
96116
97285
  this.onEscape?.();
96117
97286
  return;
96118
97287
  }
96119
97288
  }
96120
- super.handleInput(data);
97289
+ super.handleInput(normalized);
96121
97290
  }
96122
97291
  };
96123
97292
  /**
@@ -96972,6 +98141,10 @@ function cancelStream(state) {
96972
98141
  }
96973
98142
  //#endregion
96974
98143
  //#region src/tui/actions/input-ops.ts
98144
+ /**
98145
+ * User input orchestration: submission / history / external editor /
98146
+ * plan-mode toggle / reload.
98147
+ */
96975
98148
  function handleUserInput(state, text, hooks, onSlash) {
96976
98149
  if (text.trim().length === 0) return;
96977
98150
  if (state.appState.isReplaying) {
@@ -97017,9 +98190,9 @@ async function openExternalEditor(state) {
97017
98190
  await new Promise((resolve) => setImmediate(resolve));
97018
98191
  try {
97019
98192
  const result = await editInExternalEditor(seed, cmd);
97020
- if (result !== void 0) state.editor.setText(result.replace(/\r\n/g, "\n").replace(/\n$/, ""));
97021
- } catch (err) {
97022
- emitError(state, `External editor failed: ${err instanceof Error ? err.message : String(err)}`);
98193
+ if (result !== void 0) state.editor.setText(result.replaceAll("\r\n", "\n").replace(/\n$/, ""));
98194
+ } catch (error) {
98195
+ emitError(state, `External editor failed: ${error instanceof Error ? error.message : String(error)}`);
97023
98196
  } finally {
97024
98197
  if (typeof process.stdin.pause === "function") process.stdin.pause();
97025
98198
  state.ui.start();
@@ -97030,13 +98203,18 @@ async function openExternalEditor(state) {
97030
98203
  }
97031
98204
  async function togglePlanMode(state, hooks) {
97032
98205
  const enabled = !state.appState.planMode;
98206
+ let planPath;
97033
98207
  try {
97034
- await state.client.setPlanMode(state.appState.sessionId, enabled);
97035
- } catch (err) {
97036
- emitError(state, `Failed to toggle plan mode: ${err instanceof Error ? err.message : String(err)}`);
98208
+ planPath = (await state.client.setPlanMode(state.appState.sessionId, enabled)).plan_path;
98209
+ } catch (error) {
98210
+ emitError(state, `Failed to toggle plan mode: ${error instanceof Error ? error.message : String(error)}`);
97037
98211
  return;
97038
98212
  }
97039
98213
  setState(state, { planMode: enabled }, hooks);
98214
+ if (enabled && planPath !== void 0) {
98215
+ emitPrimary(state, `Plan mode: ON. Write your plan to: ${planPath}`);
98216
+ return;
98217
+ }
97040
98218
  emitPrimary(state, `Plan mode: ${enabled ? "ON" : "OFF"}`);
97041
98219
  }
97042
98220
  async function performReload(state, action, hooks) {
@@ -97825,6 +99003,20 @@ function handleContentDelta(ectx, data) {
97825
99003
  streamingPhase: "composing",
97826
99004
  streamingStartTime: Date.now()
97827
99005
  });
99006
+ return;
99007
+ }
99008
+ if (data.type === "tool_call_part") {
99009
+ if (ectx.state.thinkingDraft.length > 0) flushThinkingToTranscript(ectx, "idle");
99010
+ ectx.patchLivePane({
99011
+ mode: "idle",
99012
+ pendingToolCall: null,
99013
+ pendingApproval: null,
99014
+ pendingQuestion: null
99015
+ });
99016
+ ectx.setAppState({
99017
+ streamingPhase: "composing",
99018
+ streamingStartTime: Date.now()
99019
+ });
97828
99020
  }
97829
99021
  }
97830
99022
  //#endregion
@@ -97874,7 +99066,7 @@ function handleToolResult(ectx, data) {
97874
99066
  }
97875
99067
  ectx.state.activeToolCalls.delete(data.tool_call_id);
97876
99068
  ectx.patchLivePane({
97877
- mode: "idle",
99069
+ mode: "waiting",
97878
99070
  pendingToolCall: null
97879
99071
  });
97880
99072
  }
@@ -98299,6 +99491,12 @@ var ApprovalPanelComponent = class extends Container {
98299
99491
  this.rebuildOptions();
98300
99492
  return;
98301
99493
  }
99494
+ const printable = decodeKittyPrintable(data);
99495
+ if (printable !== void 0) {
99496
+ this.feedbackText += printable;
99497
+ this.rebuildOptions();
99498
+ return;
99499
+ }
98302
99500
  if (data.length > 0 && !data.startsWith("\x1B")) {
98303
99501
  this.feedbackText += data;
98304
99502
  this.rebuildOptions();
@@ -98320,7 +99518,8 @@ var ApprovalPanelComponent = class extends Container {
98320
99518
  this.selectAndSubmit(this.selectedIndex);
98321
99519
  return;
98322
99520
  }
98323
- const numericIndex = Number(data) - 1;
99521
+ const printable = decodeKittyPrintable(data) ?? data;
99522
+ const numericIndex = Number(printable) - 1;
98324
99523
  if (Number.isInteger(numericIndex) && numericIndex >= 0 && numericIndex < this.choiceCount()) this.selectAndSubmit(numericIndex);
98325
99524
  }
98326
99525
  render(width) {
@@ -98530,13 +99729,14 @@ var QuestionDialogComponent = class extends Container {
98530
99729
  this.activateQuestionOption(this.currentCursor());
98531
99730
  return;
98532
99731
  }
98533
- const numIdx = NUMBER_KEYS.indexOf(data);
99732
+ const printable = decodeKittyPrintable(data) ?? data;
99733
+ const numIdx = NUMBER_KEYS.indexOf(printable);
98534
99734
  if (numIdx >= 0 && numIdx < optionCount) {
98535
99735
  this.cursors[questionIdx] = numIdx;
98536
99736
  this.activateQuestionOption(numIdx);
98537
99737
  return;
98538
99738
  }
98539
- if (data === " " || matchesKey(data, Key.space)) {
99739
+ if (printable === " " || matchesKey(data, Key.space)) {
98540
99740
  if (question.multi_select) this.activateQuestionOption(this.currentCursor());
98541
99741
  return;
98542
99742
  }
@@ -98573,6 +99773,12 @@ var QuestionDialogComponent = class extends Container {
98573
99773
  this.reviewMessage = void 0;
98574
99774
  return;
98575
99775
  }
99776
+ const printable = decodeKittyPrintable(data);
99777
+ if (printable !== void 0) {
99778
+ this.otherDrafts[questionIdx] = (this.otherDrafts[questionIdx] ?? "") + printable;
99779
+ this.reviewMessage = void 0;
99780
+ return;
99781
+ }
98576
99782
  if (data === " " || matchesKey(data, Key.space)) {
98577
99783
  this.otherDrafts[questionIdx] = (this.otherDrafts[questionIdx] ?? "") + " ";
98578
99784
  this.reviewMessage = void 0;
@@ -98606,12 +99812,13 @@ var QuestionDialogComponent = class extends Container {
98606
99812
  this.executeSubmitAction(this.submitActionIdx);
98607
99813
  return;
98608
99814
  }
98609
- if (data === "1") {
99815
+ const printable = decodeKittyPrintable(data) ?? data;
99816
+ if (printable === "1") {
98610
99817
  this.submitActionIdx = 0;
98611
99818
  this.executeSubmitAction(0);
98612
99819
  return;
98613
99820
  }
98614
- if (data === "2") {
99821
+ if (printable === "2") {
98615
99822
  this.submitActionIdx = 1;
98616
99823
  this.executeSubmitAction(1);
98617
99824
  return;
@@ -99155,7 +100362,8 @@ var HelpPanelComponent = class extends Container {
99155
100362
  this.opts = opts;
99156
100363
  }
99157
100364
  handleInput(data) {
99158
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.enter) || data === "q" || data === "Q") {
100365
+ const printable = decodeKittyPrintable(data) ?? data;
100366
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.enter) || printable === "q" || printable === "Q") {
99159
100367
  this.opts.onClose();
99160
100368
  return;
99161
100369
  }
@@ -99714,6 +100922,14 @@ var MoonLoader = class extends Text {
99714
100922
  this.intervalId = null;
99715
100923
  }
99716
100924
  }
100925
+ setLabel(label) {
100926
+ this.label = label;
100927
+ this.updateDisplay();
100928
+ }
100929
+ setColorFn(colorFn) {
100930
+ this.colorFn = colorFn;
100931
+ this.updateDisplay();
100932
+ }
99717
100933
  updateDisplay() {
99718
100934
  const frame = this.frames[this.currentFrame];
99719
100935
  const coloredFrame = this.colorFn ? this.colorFn(frame) : frame;
@@ -99768,6 +100984,10 @@ function updateActivityPane(state) {
99768
100984
  case "composing":
99769
100985
  stopLoader(state);
99770
100986
  if (!state.phaseSpinner) state.phaseSpinner = new MoonLoader(state.ui, "braille", (s) => chalk.hex(state.colors.primary)(s), "working...");
100987
+ else {
100988
+ state.phaseSpinner.setLabel("working...");
100989
+ state.phaseSpinner.setColorFn((s) => chalk.hex(state.colors.primary)(s));
100990
+ }
99771
100991
  state.activityContainer.addChild(state.phaseSpinner);
99772
100992
  break;
99773
100993
  case "tool":
@@ -100710,6 +101930,9 @@ var KimiTUI = class {
100710
101930
  getCurrentSessionId() {
100711
101931
  return this.state.appState.sessionId;
100712
101932
  }
101933
+ hasSessionContent() {
101934
+ return this.state.transcriptEntries.length > 0;
101935
+ }
100713
101936
  startWireSubscription() {
100714
101937
  const ectx = buildEventCtx(this.state, this.stateHooks, this.streamCallbacks, (entry) => this.addEntry(entry));
100715
101938
  const sendQueued = (text) => {
@@ -100792,9 +102015,6 @@ var KimiTUI = class {
100792
102015
  //#region src/cli/run-shell.ts
100793
102016
  /**
100794
102017
  * Shell runner —— 把 `AgentContext` 接进 `KimiTUI`。
100795
- *
100796
- * 新 wire 架构下不再有 "resume this session: kimi -r" 的提示(方案明确
100797
- * 不支持 session 重新打开)。
100798
102018
  */
100799
102019
  function toInitialSessionIntent(opts) {
100800
102020
  if (opts.session === "") return { kind: "picker" };
@@ -100836,7 +102056,11 @@ async function runShell(opts, version) {
100836
102056
  syncSessionRuntime: ctx.syncSessionRuntime
100837
102057
  } });
100838
102058
  tui.onExit = async () => {
102059
+ const sessionId = tui.getCurrentSessionId();
102060
+ const hasContent = tui.hasSessionContent();
100839
102061
  await ctx.dispose();
102062
+ process.stdout.write("Bye!\n");
102063
+ if (sessionId !== "" && hasContent) process.stderr.write(`\nTo resume this session: kimi -r ${sessionId}\n`);
100840
102064
  process.exit(0);
100841
102065
  };
100842
102066
  try {