@oh-my-pi/pi-coding-agent 15.7.3 → 15.7.5

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 (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/config/settings-schema.d.ts +3 -22
  3. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  4. package/dist/types/extensibility/shared-events.d.ts +2 -2
  5. package/dist/types/internal-urls/local-protocol.d.ts +19 -9
  6. package/dist/types/internal-urls/types.d.ts +14 -0
  7. package/dist/types/lsp/client.d.ts +3 -0
  8. package/dist/types/mcp/manager.d.ts +14 -5
  9. package/dist/types/modes/controllers/command-controller.d.ts +2 -3
  10. package/dist/types/session/agent-session.d.ts +2 -6
  11. package/dist/types/session/shake-types.d.ts +3 -3
  12. package/dist/types/task/repair-args.d.ts +52 -0
  13. package/dist/types/tiny/models.d.ts +0 -14
  14. package/dist/types/tiny/title-client.d.ts +28 -2
  15. package/dist/types/tiny/title-protocol.d.ts +8 -9
  16. package/dist/types/tools/find.d.ts +1 -1
  17. package/dist/types/tools/path-utils.d.ts +7 -0
  18. package/dist/types/tui/output-block.d.ts +7 -7
  19. package/package.json +9 -9
  20. package/scripts/build-binary.ts +0 -1
  21. package/src/cli.ts +59 -0
  22. package/src/config/settings-schema.ts +3 -24
  23. package/src/config/settings.ts +10 -0
  24. package/src/extensibility/custom-tools/types.ts +2 -2
  25. package/src/extensibility/shared-events.ts +2 -2
  26. package/src/internal-urls/docs-index.generated.ts +2 -2
  27. package/src/internal-urls/local-protocol.ts +23 -11
  28. package/src/internal-urls/types.ts +15 -0
  29. package/src/lsp/client.ts +28 -5
  30. package/src/mcp/manager.ts +87 -4
  31. package/src/modes/controllers/command-controller.ts +7 -39
  32. package/src/modes/controllers/event-controller.ts +33 -26
  33. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  34. package/src/prompts/system/project-prompt.md +3 -2
  35. package/src/prompts/system/subagent-system-prompt.md +12 -8
  36. package/src/prompts/system/system-prompt.md +8 -6
  37. package/src/session/agent-session.ts +11 -93
  38. package/src/session/shake-types.ts +4 -5
  39. package/src/slash-commands/builtin-registry.ts +2 -4
  40. package/src/task/executor.ts +14 -4
  41. package/src/task/index.ts +3 -2
  42. package/src/task/repair-args.ts +117 -0
  43. package/src/tiny/models.ts +0 -28
  44. package/src/tiny/title-client.ts +133 -43
  45. package/src/tiny/title-protocol.ts +11 -16
  46. package/src/tiny/worker.ts +6 -61
  47. package/src/tools/ast-edit.ts +3 -0
  48. package/src/tools/ast-grep.ts +3 -0
  49. package/src/tools/find.ts +20 -6
  50. package/src/tools/gh.ts +1 -0
  51. package/src/tools/path-utils.ts +13 -2
  52. package/src/tools/read.ts +1 -0
  53. package/src/tools/search.ts +12 -1
  54. package/src/tui/output-block.ts +37 -75
  55. package/src/utils/git.ts +9 -3
@@ -51,11 +51,8 @@ import {
51
51
  prepareCompaction,
52
52
  type ShakeConfig,
53
53
  type ShakeRegion,
54
- type ShakeSummaryComplete,
55
- type ShakeSummaryItem,
56
54
  type SummaryOptions,
57
55
  shouldCompact,
58
- summarizeShakeRegions,
59
56
  } from "@oh-my-pi/pi-agent-core/compaction";
60
57
  import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "@oh-my-pi/pi-agent-core/compaction/pruning";
61
58
  import type {
@@ -185,8 +182,7 @@ import {
185
182
  resolveThinkingLevelForModel,
186
183
  toReasoningEffort,
187
184
  } from "../thinking";
188
- import { isTinyMemoryLocalModelKey } from "../tiny/models";
189
- import { shutdownTinyTitleClient, tinyModelClient } from "../tiny/title-client";
185
+ import { shutdownTinyTitleClient } from "../tiny/title-client";
190
186
  import {
191
187
  buildDiscoverableToolSearchIndex,
192
188
  collectDiscoverableTools,
@@ -241,11 +237,11 @@ export type AgentSessionEvent =
241
237
  | {
242
238
  type: "auto_compaction_start";
243
239
  reason: "threshold" | "overflow" | "idle" | "incomplete";
244
- action: "context-full" | "handoff" | "shake" | "shake-summary";
240
+ action: "context-full" | "handoff" | "shake";
245
241
  }
246
242
  | {
247
243
  type: "auto_compaction_end";
248
- action: "context-full" | "handoff" | "shake" | "shake-summary";
244
+ action: "context-full" | "handoff" | "shake";
249
245
  result: CompactionResult | undefined;
250
246
  aborted: boolean;
251
247
  willRetry: boolean;
@@ -5659,10 +5655,6 @@ export class AgentSession {
5659
5655
  * - `images` delegates to {@link dropImages}.
5660
5656
  * - `elide` replaces whole tool-call results and large fenced/XML blocks
5661
5657
  * with short placeholders that embed an `artifact://` recovery link.
5662
- * - `summary` extractively compresses the same regions with the configured
5663
- * local on-device model (`providers.shakeSummaryModel`), falling back to
5664
- * the elide placeholder per region (or wholesale when the local model is
5665
- * unavailable). Never calls a remote/cloud LLM.
5666
5658
  *
5667
5659
  * Mutates the branch in place, persists via `rewriteEntries`, replays the
5668
5660
  * rebuilt context through the agent, and tears down provider sessions that
@@ -5683,29 +5675,7 @@ export class AgentSession {
5683
5675
  }
5684
5676
 
5685
5677
  const artifactId = await this.#saveShakeArtifact(regions);
5686
- let replacements: string[];
5687
- if (mode === "summary") {
5688
- // Manual `/shake summary` installs the compaction controller so Esc /
5689
- // `abortCompaction()` can cancel the local-model pass; the auto-shake path
5690
- // passes its own signal and manages `#autoCompactionAbortController`.
5691
- let controller: AbortController | undefined;
5692
- let signal = opts.signal;
5693
- if (!signal) {
5694
- if (this.#compactionAbortController) throw new Error("Compaction already in progress");
5695
- controller = new AbortController();
5696
- this.#compactionAbortController = controller;
5697
- signal = controller.signal;
5698
- }
5699
- try {
5700
- replacements = await this.#buildShakeSummaryReplacements(regions, artifactId, signal);
5701
- } finally {
5702
- if (controller && this.#compactionAbortController === controller) {
5703
- this.#compactionAbortController = undefined;
5704
- }
5705
- }
5706
- } else {
5707
- replacements = regions.map((region, index) => this.#shakeElidePlaceholder(region, index, artifactId));
5708
- }
5678
+ const replacements = regions.map((region, index) => this.#shakeElidePlaceholder(region, index, artifactId));
5709
5679
 
5710
5680
  let toolResultsDropped = 0;
5711
5681
  let blocksDropped = 0;
@@ -5762,56 +5732,6 @@ export class AgentSession {
5762
5732
  }
5763
5733
  }
5764
5734
 
5765
- /**
5766
- * Build per-region replacements for summary mode using the configured local
5767
- * on-device model (`providers.shakeSummaryModel`) via {@link tinyModelClient}.
5768
- * Shake summary never calls a remote/cloud LLM. When the configured model is
5769
- * not a known local key, every region falls back to the elide placeholder.
5770
- * Otherwise compresses via {@link summarizeShakeRegions}; per region, uses
5771
- * the parsed summary (with a recovery footer) or the elide placeholder when
5772
- * the local model omitted it / was unavailable. Any thrown failure degrades
5773
- * the whole batch to elide so the reduction still happens.
5774
- */
5775
- async #buildShakeSummaryReplacements(
5776
- regions: ShakeRegion[],
5777
- artifactId: string | undefined,
5778
- signal: AbortSignal | undefined,
5779
- ): Promise<string[]> {
5780
- const elide = (): string[] =>
5781
- regions.map((region, index) => this.#shakeElidePlaceholder(region, index, artifactId));
5782
-
5783
- const modelKey = this.settings.get("providers.shakeSummaryModel");
5784
- if (!isTinyMemoryLocalModelKey(modelKey)) return elide();
5785
-
5786
- const items: ShakeSummaryItem[] = regions.map((region, index) => ({
5787
- index,
5788
- label: region.label,
5789
- text: region.originalText,
5790
- }));
5791
-
5792
- const complete: ShakeSummaryComplete = (promptText, opts) =>
5793
- tinyModelClient.complete(modelKey, promptText, { maxTokens: opts.maxTokens, signal: opts.signal });
5794
-
5795
- let summaries: Map<number, string>;
5796
- try {
5797
- summaries = await summarizeShakeRegions(items, complete, { signal });
5798
- } catch (error) {
5799
- logger.warn("Shake summary compression failed; falling back to elide", {
5800
- error: error instanceof Error ? error.message : String(error),
5801
- });
5802
- return elide();
5803
- }
5804
-
5805
- return regions.map((region, index) => {
5806
- const summary = summaries.get(index);
5807
- if (!summary) return this.#shakeElidePlaceholder(region, index, artifactId);
5808
- if (artifactId) {
5809
- return `${summary}\n\n[recover full: artifact://${artifactId} (region ${index + 1})]`;
5810
- }
5811
- return summary;
5812
- });
5813
- }
5814
-
5815
5735
  /**
5816
5736
  * Manually compact the session context.
5817
5737
  * Aborts current agent operation first.
@@ -7025,11 +6945,11 @@ export class AgentSession {
7025
6945
  if (reason !== "idle" && !compactionSettings.enabled) return false;
7026
6946
  const generation = this.#promptGeneration;
7027
6947
 
7028
- // Shake strategies run inline (cheap, no remote LLM). On overflow recovery,
7029
- // if shake reclaims nothing we fall through to the summary-compaction body
7030
- // below so the oversized input still gets resolved.
7031
- if (compactionSettings.strategy === "shake" || compactionSettings.strategy === "shake-summary") {
7032
- const outcome = await this.#runAutoShake(reason, compactionSettings.strategy, willRetry, generation);
6948
+ // Shake runs inline (cheap, no remote LLM). On overflow recovery, if shake
6949
+ // reclaims nothing we fall through to the summary-compaction body below so
6950
+ // the oversized input still gets resolved.
6951
+ if (compactionSettings.strategy === "shake") {
6952
+ const outcome = await this.#runAutoShake(reason, willRetry, generation);
7033
6953
  if (outcome !== "fallback") return false;
7034
6954
  }
7035
6955
  // "overflow" and "incomplete" force inline execution because they are recovery
@@ -7426,12 +7346,10 @@ export class AgentSession {
7426
7346
  */
7427
7347
  async #runAutoShake(
7428
7348
  reason: "overflow" | "threshold" | "idle" | "incomplete",
7429
- strategy: "shake" | "shake-summary",
7430
7349
  willRetry: boolean,
7431
7350
  generation: number,
7432
7351
  ): Promise<"handled" | "fallback"> {
7433
- const action = strategy === "shake-summary" ? "shake-summary" : "shake";
7434
- const mode = strategy === "shake-summary" ? "summary" : "elide";
7352
+ const action = "shake";
7435
7353
  await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
7436
7354
  this.#autoCompactionAbortController?.abort();
7437
7355
  const controller = new AbortController();
@@ -7439,7 +7357,7 @@ export class AgentSession {
7439
7357
  const signal = controller.signal;
7440
7358
  const compactionSettings = this.settings.getGroup("compaction");
7441
7359
  try {
7442
- const result = await this.shake(mode, { config: DEFAULT_SHAKE_CONFIG, signal });
7360
+ const result = await this.shake("elide", { config: DEFAULT_SHAKE_CONFIG, signal });
7443
7361
  if (signal.aborted) {
7444
7362
  await this.#emitSessionEvent({
7445
7363
  type: "auto_compaction_end",
@@ -6,14 +6,14 @@
6
6
  */
7
7
 
8
8
  /** Mode selector for `AgentSession.shake`. */
9
- export type ShakeMode = "elide" | "summary" | "images";
9
+ export type ShakeMode = "elide" | "images";
10
10
 
11
11
  /** Outcome of an `AgentSession.shake` run. */
12
12
  export interface ShakeResult {
13
13
  mode: ShakeMode;
14
- /** Whole tool-call results dropped/compressed. */
14
+ /** Whole tool-call results dropped. */
15
15
  toolResultsDropped: number;
16
- /** Large fenced/XML blocks dropped/compressed. */
16
+ /** Large fenced/XML blocks dropped. */
17
17
  blocksDropped: number;
18
18
  /** Image blocks removed (images mode only). */
19
19
  imagesDropped?: number;
@@ -39,6 +39,5 @@ export function formatShakeSummary(result: ShakeResult): string {
39
39
  parts.push(`${result.blocksDropped} block${result.blocksDropped === 1 ? "" : "s"}`);
40
40
  }
41
41
  if (parts.length === 0) return "Nothing to shake.";
42
- const verb = result.mode === "summary" ? "Compressed" : "Shook";
43
- return `${verb} ${parts.join(" + ")} (~${result.tokensFreed} tokens freed).`;
42
+ return `Shook ${parts.join(" + ")} (~${result.tokensFreed} tokens freed).`;
44
43
  }
@@ -62,9 +62,8 @@ const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashComma
62
62
  function parseShakeMode(args: string): ShakeMode | { error: string } {
63
63
  const verb = args.trim().toLowerCase();
64
64
  if (verb === "" || verb === "elide") return "elide";
65
- if (verb === "summary") return "summary";
66
65
  if (verb === "images") return "images";
67
- return { error: `Unknown /shake mode "${verb}". Use elide, summary, or images.` };
66
+ return { error: `Unknown /shake mode "${verb}". Use elide or images.` };
68
67
  }
69
68
 
70
69
  const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
@@ -826,10 +825,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
826
825
  acpDescription: "Shake heavy content out of the conversation context",
827
826
  subcommands: [
828
827
  { name: "elide", description: "Strip tool results + large blocks (default)" },
829
- { name: "summary", description: "Compress heavy regions with a local on-device model" },
830
828
  { name: "images", description: "Strip image blocks" },
831
829
  ],
832
- acpInputHint: "[elide|summary|images]",
830
+ acpInputHint: "[elide|images]",
833
831
  allowArgs: true,
834
832
  handle: async (command, runtime) => {
835
833
  const mode = parseShakeMode(command.args);
@@ -633,7 +633,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
633
633
  if (atMaxDepth && toolNames?.includes("task")) {
634
634
  toolNames = toolNames.filter(name => name !== "task");
635
635
  }
636
- // IRC is always available; the [COOP] prompt advertises it, so a restricted
636
+ // IRC is always available; the COOP prompt section advertises it, so a restricted
637
637
  // whitelist must still carry `irc` for the subagent to actually use it.
638
638
  if (toolNames && !toolNames.includes("irc")) {
639
639
  toolNames = [...toolNames, "irc"];
@@ -1446,9 +1446,19 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1446
1446
  );
1447
1447
  await awaitAbortable(session.waitForIdle());
1448
1448
  } catch (err) {
1449
- logger.error("Subagent prompt failed", {
1450
- error: err instanceof Error ? err.message : String(err),
1451
- });
1449
+ if (abortSignal.aborted || err instanceof ToolAbortError) {
1450
+ // Benign control-flow exit user cancel (^C) or compaction aborting
1451
+ // pending operations both surface here as ToolAbortError. The outer
1452
+ // catch and finally already mark the run aborted; logging at ERROR
1453
+ // would spam operator dashboards with non-failures.
1454
+ logger.debug("Subagent prompt aborted", {
1455
+ reason: abortReason ?? "signal",
1456
+ });
1457
+ } else {
1458
+ logger.error("Subagent prompt failed", {
1459
+ error: err instanceof Error ? err.message : String(err),
1460
+ });
1461
+ }
1452
1462
  }
1453
1463
  }
1454
1464
 
package/src/task/index.ts CHANGED
@@ -48,6 +48,7 @@ import { runSubprocess } from "./executor";
48
48
  import { AgentOutputManager } from "./output-manager";
49
49
  import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
50
50
  import { renderResult, renderCall as renderTaskCall } from "./render";
51
+ import { repairTaskParams } from "./repair-args";
51
52
  import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
52
53
  import {
53
54
  applyNestedPatches,
@@ -247,7 +248,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
247
248
  }
248
249
 
249
250
  renderCall(args: unknown, options: Parameters<typeof renderTaskCall>[1], theme: Theme) {
250
- return renderTaskCall(args as TaskParams, options, theme);
251
+ return renderTaskCall(repairTaskParams(args as TaskParams), options, theme);
251
252
  }
252
253
 
253
254
  /** Dynamic description that reflects current disabled-agent settings */
@@ -292,7 +293,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
292
293
  signal?: AbortSignal,
293
294
  onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
294
295
  ): Promise<AgentToolResult<TaskToolDetails>> {
295
- const params = rawParams as TaskParams;
296
+ const params = repairTaskParams(rawParams as TaskParams);
296
297
  const simpleMode = this.#getTaskSimpleMode();
297
298
  const validationError = validateTaskModeParams(simpleMode, params);
298
299
  if (validationError) {
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Repair double-encoded JSON string arguments for the task tool.
3
+ *
4
+ * Models occasionally JSON-escape a string value twice when emitting a
5
+ * `task` tool call, so a `context`/`assignment` that should read
6
+ *
7
+ * # Role
8
+ * You are a judge … "describe this" … return —
9
+ *
10
+ * arrives — after the one JSON decode the provider already applied — as the
11
+ * literal text
12
+ *
13
+ * # Role\nYou are a judge … \"describe this\" … return \u2014
14
+ *
15
+ * i.e. every newline, quote, and unicode character is still backslash-escaped.
16
+ * The subagent then receives that garbled prompt, and the call preview renders
17
+ * one long blob with visible `\n` / `\"` / `\uXXXX`.
18
+ *
19
+ * The *whole-arguments* form of this quirk (the entire `arguments` blob is a
20
+ * JSON string) is already auto-corrected by the validator's JSON-string
21
+ * coercion. This module handles the *per-field* form, where the object parses
22
+ * fine but an individual string value is double-encoded — the validator never
23
+ * fires there because a double-encoded string is still a structurally valid
24
+ * string.
25
+ *
26
+ * This is deliberately scoped to the task tool's natural-language fields
27
+ * (`context`, `assignment`, `description`). It is NOT applied to code-bearing
28
+ * tools (write/edit/bash/search), where a backslash or quote is load-bearing
29
+ * and a false-positive unescape would silently corrupt a file or command.
30
+ */
31
+ import type { TaskItem, TaskParams } from "./types";
32
+
33
+ /** A backslash that escapes a structural char — `\"`, `\\`, `\/`, or `\uXXXX`. */
34
+ const STRUCTURAL_ESCAPE = /\\(?:["\\/]|u[0-9a-fA-F]{4})/;
35
+
36
+ /**
37
+ * Whether `value` carries the signature of whole-string double-encoding rather
38
+ * than an incidental escape mention. A lone `\n`/`\t` in an instruction (e.g.
39
+ * "split lines on \n") is far more likely a literal mention than a
40
+ * double-encoded document, so it is left alone; a structural escape (`\"`,
41
+ * `\\`, `\uXXXX`) or two-plus escape sequences indicates a re-escaped payload.
42
+ */
43
+ function hasDoubleEncodeSignature(value: string): boolean {
44
+ if (STRUCTURAL_ESCAPE.test(value)) return true;
45
+ let count = 0;
46
+ for (let i = 0; i < value.length; i++) {
47
+ if (value.charCodeAt(i) === 0x5c /* \ */) {
48
+ count += 1;
49
+ if (count >= 2) return true;
50
+ i += 1; // skip the escaped char so `\\` counts once
51
+ }
52
+ }
53
+ return false;
54
+ }
55
+
56
+ /**
57
+ * Return the once-unescaped string when `value` is uniformly double-encoded
58
+ * JSON (a well-formed JSON string body that decodes to a different string);
59
+ * otherwise return `value` unchanged.
60
+ *
61
+ * The `JSON.parse(\`"${value}"\`)` round-trip is the safety net: it only
62
+ * succeeds when *every* backslash begins a valid JSON escape and no bare
63
+ * double-quote exists — exactly the signature of double-encoding. Genuine
64
+ * prose with a Windows path (`C:\Users`), a regex (`\d+`), an embedded quote,
65
+ * or a real (already-decoded) newline makes the parse throw, so the value is
66
+ * returned untouched.
67
+ */
68
+ export function repairDoubleEncodedJsonString(value: string): string {
69
+ // Fast path: no backslash → nothing was escaped → the parse can never differ.
70
+ if (!value.includes("\\")) return value;
71
+ if (!hasDoubleEncodeSignature(value)) return value;
72
+ let decoded: unknown;
73
+ try {
74
+ decoded = JSON.parse(`"${value}"`);
75
+ } catch {
76
+ return value;
77
+ }
78
+ return typeof decoded === "string" && decoded !== value ? decoded : value;
79
+ }
80
+
81
+ /** Repair a single (possibly partial) task item's prose fields. */
82
+ function repairTaskItem(task: TaskItem): TaskItem {
83
+ if (task === null || typeof task !== "object") return task;
84
+ const assignment =
85
+ typeof task.assignment === "string" ? repairDoubleEncodedJsonString(task.assignment) : task.assignment;
86
+ const description =
87
+ typeof task.description === "string" ? repairDoubleEncodedJsonString(task.description) : task.description;
88
+ if (assignment === task.assignment && description === task.description) return task;
89
+ return { ...task, assignment, description };
90
+ }
91
+
92
+ /**
93
+ * Repair double-encoded prose in task-tool params (`context` and each task's
94
+ * `assignment`/`description`). Returns the same reference when nothing changed
95
+ * so callers can cheaply skip work. Defensive against partially-streamed args
96
+ * (missing/undefined fields, partial task arrays) so it is safe on the render
97
+ * path as well as on execution.
98
+ */
99
+ export function repairTaskParams(params: TaskParams): TaskParams {
100
+ if (params === null || typeof params !== "object") return params;
101
+
102
+ const context = typeof params.context === "string" ? repairDoubleEncodedJsonString(params.context) : params.context;
103
+
104
+ let tasks = params.tasks;
105
+ if (Array.isArray(params.tasks)) {
106
+ let changed = false;
107
+ const repaired = params.tasks.map(task => {
108
+ const next = repairTaskItem(task);
109
+ if (next !== task) changed = true;
110
+ return next;
111
+ });
112
+ if (changed) tasks = repaired;
113
+ }
114
+
115
+ if (context === params.context && tasks === params.tasks) return params;
116
+ return { ...params, context, tasks };
117
+ }
@@ -196,34 +196,6 @@ export function getTinyMemoryModelSpec(key: TinyMemoryLocalModelKey): (typeof TI
196
196
  return spec;
197
197
  }
198
198
 
199
- /**
200
- * Shake-summary models. Shake's `summary` mode (and the `shake-summary`
201
- * compaction strategy) compress heavy regions strictly on-device — there is no
202
- * online/remote option, so this registry reuses the local memory models only.
203
- */
204
- export const SHAKE_SUMMARY_MODEL_VALUES = [
205
- "qwen3-1.7b",
206
- "gemma-3-1b",
207
- "qwen2.5-1.5b",
208
- "lfm2-1.2b",
209
- ] as const satisfies readonly TinyMemoryLocalModelKey[];
210
-
211
- export type ShakeSummaryModelKey = (typeof SHAKE_SUMMARY_MODEL_VALUES)[number];
212
-
213
- // Guard: every local memory model is offered for shake summary (catches drift).
214
- type MissingShakeSummaryValue = Exclude<TinyMemoryLocalModelKey, ShakeSummaryModelKey>;
215
- const SHAKE_SUMMARY_MODEL_VALUES_MATCH_REGISTRY: MissingShakeSummaryValue extends never ? true : never = true;
216
- void SHAKE_SUMMARY_MODEL_VALUES_MATCH_REGISTRY;
217
-
218
- export const SHAKE_SUMMARY_MODEL_OPTIONS = TINY_MEMORY_LOCAL_MODELS.map(model => ({
219
- value: model.key,
220
- label: model.label,
221
- description: model.description,
222
- })) satisfies ReadonlyArray<{ value: ShakeSummaryModelKey; label: string; description: string }>;
223
-
224
- /** Default shake-summary local model when none is named. */
225
- export const DEFAULT_SHAKE_SUMMARY_MODEL_KEY: ShakeSummaryModelKey = DEFAULT_MEMORY_LOCAL_MODEL_KEY;
226
-
227
199
  /** Any local model key (title or memory), used by the shared inference worker. */
228
200
  export type TinyLocalModelKey = TinyTitleLocalModelKey | TinyMemoryLocalModelKey;
229
201