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

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 (177) hide show
  1. package/CHANGELOG.md +95 -6
  2. package/dist/types/auto-thinking/classifier.d.ts +35 -0
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/cli/extension-flags.d.ts +36 -0
  5. package/dist/types/config/config-file.d.ts +4 -0
  6. package/dist/types/config/file-lock.d.ts +23 -0
  7. package/dist/types/config/keybindings.d.ts +2 -1
  8. package/dist/types/config/model-registry.d.ts +6 -0
  9. package/dist/types/config/settings-schema.d.ts +112 -69
  10. package/dist/types/edit/hashline/diff.d.ts +9 -3
  11. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
  12. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
  13. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
  14. package/dist/types/eval/agent-bridge.d.ts +25 -0
  15. package/dist/types/eval/backend.d.ts +17 -2
  16. package/dist/types/eval/budget-bridge.d.ts +29 -0
  17. package/dist/types/eval/idle-timeout.d.ts +28 -0
  18. package/dist/types/eval/js/executor.d.ts +8 -0
  19. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  20. package/dist/types/eval/py/executor.d.ts +13 -0
  21. package/dist/types/exec/bash-executor.d.ts +1 -0
  22. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  23. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  24. package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
  25. package/dist/types/extensibility/plugins/manager.d.ts +12 -1
  26. package/dist/types/extensibility/shared-events.d.ts +2 -2
  27. package/dist/types/memory-backend/index.d.ts +1 -1
  28. package/dist/types/memory-backend/resolve.d.ts +1 -1
  29. package/dist/types/memory-backend/types.d.ts +3 -3
  30. package/dist/types/mnemopi/backend.d.ts +4 -0
  31. package/dist/types/mnemopi/config.d.ts +29 -0
  32. package/dist/types/mnemopi/state.d.ts +72 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -2
  34. package/dist/types/modes/components/model-selector.d.ts +3 -2
  35. package/dist/types/modes/components/omfg-panel.d.ts +19 -0
  36. package/dist/types/modes/controllers/command-controller.d.ts +7 -0
  37. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  38. package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
  39. package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
  40. package/dist/types/modes/gradient-highlight.d.ts +5 -1
  41. package/dist/types/modes/interactive-mode.d.ts +7 -3
  42. package/dist/types/modes/magic-keywords.d.ts +14 -0
  43. package/dist/types/modes/markdown-prose.d.ts +27 -0
  44. package/dist/types/modes/orchestrate.d.ts +7 -2
  45. package/dist/types/modes/shared.d.ts +1 -1
  46. package/dist/types/modes/theme/theme.d.ts +2 -1
  47. package/dist/types/modes/turn-budget.d.ts +18 -0
  48. package/dist/types/modes/types.d.ts +7 -3
  49. package/dist/types/modes/ultrathink.d.ts +7 -2
  50. package/dist/types/modes/workflow.d.ts +15 -0
  51. package/dist/types/sdk.d.ts +15 -4
  52. package/dist/types/session/agent-session.d.ts +59 -23
  53. package/dist/types/session/session-manager.d.ts +18 -0
  54. package/dist/types/session/session-storage.d.ts +6 -0
  55. package/dist/types/session/shake-types.d.ts +24 -0
  56. package/dist/types/task/executor.d.ts +2 -2
  57. package/dist/types/thinking.d.ts +39 -1
  58. package/dist/types/tiny/device.d.ts +3 -3
  59. package/dist/types/tiny/models.d.ts +34 -1
  60. package/dist/types/tiny/title-protocol.d.ts +4 -0
  61. package/dist/types/tools/index.d.ts +19 -3
  62. package/dist/types/tools/memory-edit.d.ts +1 -1
  63. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  64. package/package.json +10 -10
  65. package/src/auto-thinking/classifier.ts +180 -0
  66. package/src/autoresearch/tools/run-experiment.ts +45 -113
  67. package/src/cli/args.ts +39 -16
  68. package/src/cli/extension-flags.ts +48 -0
  69. package/src/cli/plugin-cli.ts +11 -2
  70. package/src/config/config-file.ts +98 -13
  71. package/src/config/file-lock.ts +60 -17
  72. package/src/config/keybindings.ts +78 -27
  73. package/src/config/model-registry.ts +7 -1
  74. package/src/config/settings-schema.ts +118 -71
  75. package/src/config/settings.ts +12 -0
  76. package/src/edit/hashline/diff.ts +87 -22
  77. package/src/edit/renderer.ts +16 -12
  78. package/src/edit/streaming.ts +17 -6
  79. package/src/eval/__tests__/agent-bridge.test.ts +433 -0
  80. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  81. package/src/eval/__tests__/idle-timeout.test.ts +66 -0
  82. package/src/eval/__tests__/shared-executors.test.ts +53 -0
  83. package/src/eval/agent-bridge.ts +295 -0
  84. package/src/eval/backend.ts +17 -2
  85. package/src/eval/budget-bridge.ts +48 -0
  86. package/src/eval/idle-timeout.ts +80 -0
  87. package/src/eval/js/executor.ts +35 -7
  88. package/src/eval/js/index.ts +2 -1
  89. package/src/eval/js/shared/local-module-loader.ts +75 -10
  90. package/src/eval/js/shared/prelude.txt +85 -1
  91. package/src/eval/js/tool-bridge.ts +9 -0
  92. package/src/eval/py/executor.ts +41 -14
  93. package/src/eval/py/index.ts +2 -1
  94. package/src/eval/py/prelude.py +132 -1
  95. package/src/exec/bash-executor.ts +2 -3
  96. package/src/extensibility/custom-tools/types.ts +2 -2
  97. package/src/extensibility/extensions/runner.ts +12 -2
  98. package/src/extensibility/plugins/git-url.ts +90 -4
  99. package/src/extensibility/plugins/manager.ts +103 -7
  100. package/src/extensibility/shared-events.ts +2 -2
  101. package/src/internal-urls/docs-index.generated.ts +88 -88
  102. package/src/main.ts +50 -56
  103. package/src/memory-backend/index.ts +1 -1
  104. package/src/memory-backend/resolve.ts +3 -3
  105. package/src/memory-backend/types.ts +3 -3
  106. package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
  107. package/src/{mnemosyne → mnemopi}/config.ts +36 -36
  108. package/src/{mnemosyne → mnemopi}/state.ts +67 -67
  109. package/src/modes/acp/acp-agent.ts +13 -3
  110. package/src/modes/components/agent-dashboard.ts +6 -6
  111. package/src/modes/components/custom-editor.ts +4 -11
  112. package/src/modes/components/extensions/state-manager.ts +3 -4
  113. package/src/modes/components/footer.ts +18 -12
  114. package/src/modes/components/hook-selector.ts +86 -20
  115. package/src/modes/components/model-selector.ts +20 -11
  116. package/src/modes/components/oauth-selector.ts +93 -21
  117. package/src/modes/components/omfg-panel.ts +141 -0
  118. package/src/modes/components/settings-defs.ts +9 -2
  119. package/src/modes/components/settings-selector.ts +4 -1
  120. package/src/modes/components/status-line/segments.ts +13 -5
  121. package/src/modes/components/tips.txt +2 -1
  122. package/src/modes/components/tool-execution.ts +38 -19
  123. package/src/modes/components/tree-selector.ts +4 -3
  124. package/src/modes/components/user-message-selector.ts +94 -19
  125. package/src/modes/components/user-message.ts +8 -1
  126. package/src/modes/controllers/command-controller.ts +57 -0
  127. package/src/modes/controllers/event-controller.ts +65 -3
  128. package/src/modes/controllers/input-controller.ts +14 -11
  129. package/src/modes/controllers/omfg-controller.ts +283 -0
  130. package/src/modes/controllers/omfg-rule.ts +647 -0
  131. package/src/modes/controllers/selector-controller.ts +21 -6
  132. package/src/modes/gradient-highlight.ts +23 -6
  133. package/src/modes/interactive-mode.ts +41 -7
  134. package/src/modes/magic-keywords.ts +20 -0
  135. package/src/modes/markdown-prose.ts +247 -0
  136. package/src/modes/orchestrate.ts +17 -11
  137. package/src/modes/shared.ts +3 -11
  138. package/src/modes/theme/theme.ts +6 -0
  139. package/src/modes/turn-budget.ts +31 -0
  140. package/src/modes/types.ts +7 -1
  141. package/src/modes/ultrathink.ts +16 -10
  142. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  143. package/src/modes/workflow.ts +42 -0
  144. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  145. package/src/prompts/system/auto-thinking-difficulty.md +12 -0
  146. package/src/prompts/system/omfg-user.md +51 -0
  147. package/src/prompts/system/system-prompt.md +1 -0
  148. package/src/prompts/system/workflow-notice.md +70 -0
  149. package/src/prompts/tools/eval.md +13 -1
  150. package/src/prompts/tools/memory-edit.md +1 -1
  151. package/src/sdk.ts +86 -38
  152. package/src/session/agent-session.ts +558 -80
  153. package/src/session/session-manager.ts +32 -0
  154. package/src/session/session-storage.ts +68 -8
  155. package/src/session/shake-types.ts +44 -0
  156. package/src/slash-commands/builtin-registry.ts +41 -16
  157. package/src/task/executor.ts +3 -3
  158. package/src/task/index.ts +6 -6
  159. package/src/thinking.ts +73 -1
  160. package/src/tiny/device.ts +4 -10
  161. package/src/tiny/models.ts +54 -2
  162. package/src/tiny/title-protocol.ts +11 -1
  163. package/src/tiny/worker.ts +19 -7
  164. package/src/tools/eval.ts +202 -26
  165. package/src/tools/grouped-file-output.ts +9 -2
  166. package/src/tools/index.ts +17 -5
  167. package/src/tools/memory-edit.ts +4 -4
  168. package/src/tools/memory-recall.ts +5 -5
  169. package/src/tools/memory-reflect.ts +5 -5
  170. package/src/tools/memory-retain.ts +4 -4
  171. package/src/tools/render-utils.ts +2 -1
  172. package/src/tools/search.ts +480 -76
  173. package/dist/types/mnemosyne/backend.d.ts +0 -4
  174. package/dist/types/mnemosyne/config.d.ts +0 -29
  175. package/dist/types/mnemosyne/state.d.ts +0 -72
  176. /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
  177. /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
@@ -1837,6 +1837,12 @@ export class SessionManager {
1837
1837
  premiumRequests: 0,
1838
1838
  cost: 0,
1839
1839
  } satisfies UsageStatistics;
1840
+ /** Per-turn output-token budget set by a `+Nk` directive (total null when none this turn). */
1841
+ #turnBudget: { total: number | null; hard: boolean } = { total: null, hard: false };
1842
+ /** Cumulative `output` snapshot captured when the current turn budget window opened. */
1843
+ #turnBaselineOutput = 0;
1844
+ /** Output tokens consumed by eval-spawned subagents in the current turn window. */
1845
+ #turnEvalOutput = 0;
1840
1846
  #persistWriter: NdjsonFileWriter | undefined;
1841
1847
  #persistWriterPath: string | undefined;
1842
1848
  #persistChain: Promise<void> = Promise.resolve();
@@ -2397,6 +2403,32 @@ export class SessionManager {
2397
2403
  return this.#usageStatistics;
2398
2404
  }
2399
2405
 
2406
+ /**
2407
+ * Open a new per-turn budget window: snapshot the cumulative output baseline,
2408
+ * reset the eval-subagent counter, and set the (optional) ceiling. Called once
2409
+ * per real user message; `total` is null when no `+Nk` directive was present.
2410
+ */
2411
+ beginTurnBudget(total: number | null, hard: boolean): void {
2412
+ this.#turnBudget = { total, hard };
2413
+ this.#turnBaselineOutput = this.#usageStatistics.output;
2414
+ this.#turnEvalOutput = 0;
2415
+ }
2416
+
2417
+ /** Record output tokens consumed by an eval-spawned subagent in the current turn. */
2418
+ recordEvalSubagentOutput(output: number): void {
2419
+ if (Number.isFinite(output) && output > 0) this.#turnEvalOutput += output;
2420
+ }
2421
+
2422
+ /**
2423
+ * Current turn budget for the eval `budget` helper: the ceiling (null = none),
2424
+ * output tokens spent this turn (main loop + eval-spawned subagents, no
2425
+ * double-count), and whether the ceiling is hard.
2426
+ */
2427
+ getTurnBudget(): { total: number | null; spent: number; hard: boolean } {
2428
+ const mainDelta = Math.max(0, this.#usageStatistics.output - this.#turnBaselineOutput);
2429
+ return { total: this.#turnBudget.total, spent: mainDelta + this.#turnEvalOutput, hard: this.#turnBudget.hard };
2430
+ }
2431
+
2400
2432
  getSessionDir(): string {
2401
2433
  return this.sessionDir;
2402
2434
  }
@@ -272,8 +272,8 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
272
272
  if (this.#closed) throw new Error("Writer closed");
273
273
  if (this.#error) throw this.#error;
274
274
  try {
275
- const existing = this.#storage.existsSync(this.#path) ? this.#storage.readTextSync(this.#path) : "";
276
- this.#storage.writeTextSync(this.#path, `${existing}${line}`);
275
+ // O(1) chunked append see MemorySessionStorage.appendChunkSync.
276
+ this.#storage.appendChunkSync(this.#path, line);
277
277
  } catch (err) {
278
278
  throw this.#recordError(err);
279
279
  }
@@ -302,8 +302,26 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
302
302
  }
303
303
  }
304
304
 
305
+ /**
306
+ * Mirror entry stored per path. Chunks accumulate via O(1) `push` on
307
+ * `appendChunkSync`; readers materialise into a single string lazily.
308
+ * `byteLen` is kept in sync so `statSync` is O(1) (and returns true UTF-8
309
+ * bytes, not character count).
310
+ */
311
+ interface MirrorEntry {
312
+ chunks: string[];
313
+ byteLen: number;
314
+ mtimeMs: number;
315
+ }
316
+
317
+ function materialiseMirror(entry: MirrorEntry): string {
318
+ if (entry.chunks.length === 0) return "";
319
+ if (entry.chunks.length === 1) return entry.chunks[0];
320
+ return entry.chunks.join("");
321
+ }
322
+
305
323
  export class MemorySessionStorage implements SessionStorage {
306
- #files = new Map<string, { content: string; mtimeMs: number }>();
324
+ #files = new Map<string, MirrorEntry>();
307
325
 
308
326
  ensureDirSync(_dir: string): void {
309
327
  // No-op for in-memory storage.
@@ -314,20 +332,40 @@ export class MemorySessionStorage implements SessionStorage {
314
332
  }
315
333
 
316
334
  writeTextSync(path: string, content: string): void {
317
- this.#files.set(path, { content, mtimeMs: Date.now() });
335
+ this.#files.set(path, {
336
+ chunks: content.length === 0 ? [] : [content],
337
+ byteLen: Buffer.byteLength(content, "utf-8"),
338
+ mtimeMs: Date.now(),
339
+ });
340
+ }
341
+
342
+ /**
343
+ * Internal O(1) append used by {@link MemorySessionStorageWriter}. Lazily
344
+ * creates the entry. External callers should go through `openWriter()`
345
+ * rather than touching the mirror directly.
346
+ */
347
+ appendChunkSync(path: string, chunk: string): void {
348
+ let entry = this.#files.get(path);
349
+ if (!entry) {
350
+ entry = { chunks: [], byteLen: 0, mtimeMs: Date.now() };
351
+ this.#files.set(path, entry);
352
+ }
353
+ entry.chunks.push(chunk);
354
+ entry.byteLen += Buffer.byteLength(chunk, "utf-8");
355
+ entry.mtimeMs = Date.now();
318
356
  }
319
357
 
320
358
  readTextSync(path: string): string {
321
359
  const entry = this.#files.get(path);
322
360
  if (!entry) throw new Error(`File not found: ${path}`);
323
- return entry.content;
361
+ return materialiseMirror(entry);
324
362
  }
325
363
 
326
364
  statSync(path: string): SessionStorageStat {
327
365
  const entry = this.#files.get(path);
328
366
  if (!entry) throw new Error(`File not found: ${path}`);
329
367
  return {
330
- size: entry.content.length,
368
+ size: entry.byteLen,
331
369
  mtimeMs: entry.mtimeMs,
332
370
  mtime: new Date(entry.mtimeMs),
333
371
  };
@@ -353,13 +391,35 @@ export class MemorySessionStorage implements SessionStorage {
353
391
  readText(path: string): Promise<string> {
354
392
  const entry = this.#files.get(path);
355
393
  if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
356
- return Promise.resolve(entry.content);
394
+ return Promise.resolve(materialiseMirror(entry));
357
395
  }
358
396
 
359
397
  readTextPrefix(path: string, maxBytes: number): Promise<string> {
360
398
  const entry = this.#files.get(path);
361
399
  if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
362
- return Promise.resolve(entry.content.slice(0, maxBytes));
400
+ if (entry.chunks.length === 0 || maxBytes <= 0) return Promise.resolve("");
401
+
402
+ // Walk chunks until the byte budget is exhausted. Avoids materialising
403
+ // the full mirror just to slice a prefix — bounded work for big files.
404
+ let accumulatedBytes = 0;
405
+ const out: string[] = [];
406
+ for (const chunk of entry.chunks) {
407
+ const chunkBytes = Buffer.byteLength(chunk, "utf-8");
408
+ if (accumulatedBytes + chunkBytes <= maxBytes) {
409
+ out.push(chunk);
410
+ accumulatedBytes += chunkBytes;
411
+ if (accumulatedBytes === maxBytes) break;
412
+ continue;
413
+ }
414
+ // Boundary chunk: slice in byte space and decode. Result MAY be
415
+ // shorter than the budget if a multi-byte codepoint straddles the
416
+ // boundary — matches `peekFile` semantics (partial decode at cap).
417
+ const remainingBytes = maxBytes - accumulatedBytes;
418
+ const utf8 = Buffer.from(chunk, "utf-8");
419
+ out.push(utf8Decoder.decode(utf8.subarray(0, remainingBytes)));
420
+ break;
421
+ }
422
+ return Promise.resolve(out.join(""));
363
423
  }
364
424
 
365
425
  writeText(path: string, content: string): Promise<void> {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Public shape of the `shake` operation, kept in a dependency-free leaf module
3
+ * so slash-command registries and controllers can import `formatShakeSummary`
4
+ * without pulling in the heavy `agent-session` module graph (which would form
5
+ * an import cycle through the slash-command registry).
6
+ */
7
+
8
+ /** Mode selector for `AgentSession.shake`. */
9
+ export type ShakeMode = "elide" | "summary" | "images";
10
+
11
+ /** Outcome of an `AgentSession.shake` run. */
12
+ export interface ShakeResult {
13
+ mode: ShakeMode;
14
+ /** Whole tool-call results dropped/compressed. */
15
+ toolResultsDropped: number;
16
+ /** Large fenced/XML blocks dropped/compressed. */
17
+ blocksDropped: number;
18
+ /** Image blocks removed (images mode only). */
19
+ imagesDropped?: number;
20
+ /** Estimated context tokens reclaimed. */
21
+ tokensFreed: number;
22
+ /** Session artifact holding the dropped originals, when persisted. */
23
+ artifactId?: string;
24
+ }
25
+
26
+ /** One-line operator summary of a {@link ShakeResult} (shared by TUI + ACP). */
27
+ export function formatShakeSummary(result: ShakeResult): string {
28
+ if (result.mode === "images") {
29
+ const n = result.imagesDropped ?? 0;
30
+ return n === 0
31
+ ? "No images found in this session."
32
+ : `Dropped ${n} image${n === 1 ? "" : "s"} from this session.`;
33
+ }
34
+ const parts: string[] = [];
35
+ if (result.toolResultsDropped > 0) {
36
+ parts.push(`${result.toolResultsDropped} tool result${result.toolResultsDropped === 1 ? "" : "s"}`);
37
+ }
38
+ if (result.blocksDropped > 0) {
39
+ parts.push(`${result.blocksDropped} block${result.blocksDropped === 1 ? "" : "s"}`);
40
+ }
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).`;
44
+ }
@@ -21,6 +21,7 @@ import {
21
21
  } from "../extensibility/plugins/marketplace";
22
22
  import { resolveMemoryBackend } from "../memory-backend";
23
23
  import type { InteractiveModeContext } from "../modes/types";
24
+ import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
24
25
  import { getChangelogPath, parseChangelog } from "../utils/changelog";
25
26
  import { buildContextReportText } from "./helpers/context-report";
26
27
  import { formatDuration } from "./helpers/format";
@@ -57,6 +58,15 @@ const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashComma
57
58
  return commandConsumed();
58
59
  };
59
60
 
61
+ /** Parse the `/shake` subcommand into a {@link ShakeMode}; empty defaults to elide. */
62
+ function parseShakeMode(args: string): ShakeMode | { error: string } {
63
+ const verb = args.trim().toLowerCase();
64
+ if (verb === "" || verb === "elide") return "elide";
65
+ if (verb === "summary") return "summary";
66
+ if (verb === "images") return "images";
67
+ return { error: `Unknown /shake mode "${verb}". Use elide, summary, or images.` };
68
+ }
69
+
60
70
  const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
61
71
  {
62
72
  name: "settings",
@@ -811,27 +821,31 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
811
821
  },
812
822
  },
813
823
  {
814
- name: "drop-images",
815
- description: "Strip every image from this session's history",
816
- acpDescription: "Drop all images from the conversation history",
817
- handle: async (_command, runtime) => {
818
- const { removed } = await runtime.session.dropImages();
819
- await runtime.output(
820
- removed === 0
821
- ? "No images found in this session."
822
- : `Dropped ${removed} image${removed === 1 ? "" : "s"} from this session.`,
823
- );
824
+ name: "shake",
825
+ description: "Drop heavy content from context (tool results, large blocks)",
826
+ acpDescription: "Shake heavy content out of the conversation context",
827
+ subcommands: [
828
+ { name: "elide", description: "Strip tool results + large blocks (default)" },
829
+ { name: "summary", description: "Compress heavy regions with a local on-device model" },
830
+ { name: "images", description: "Strip image blocks" },
831
+ ],
832
+ acpInputHint: "[elide|summary|images]",
833
+ allowArgs: true,
834
+ handle: async (command, runtime) => {
835
+ const mode = parseShakeMode(command.args);
836
+ if (typeof mode !== "string") return usage(mode.error, runtime);
837
+ const result = await runtime.session.shake(mode);
838
+ await runtime.output(formatShakeSummary(result));
824
839
  return commandConsumed();
825
840
  },
826
- handleTui: async (_command, runtime) => {
841
+ handleTui: async (command, runtime) => {
827
842
  runtime.ctx.editor.setText("");
828
- const { removed } = await runtime.ctx.session.dropImages();
829
- if (removed === 0) {
830
- runtime.ctx.showStatus("No images found in this session.");
843
+ const mode = parseShakeMode(command.args);
844
+ if (typeof mode !== "string") {
845
+ runtime.ctx.showWarning(mode.error);
831
846
  return;
832
847
  }
833
- runtime.ctx.rebuildChatFromMessages();
834
- runtime.ctx.showStatus(`Dropped ${removed} image${removed === 1 ? "" : "s"} from this session.`);
848
+ await runtime.ctx.handleShakeCommand(mode);
835
849
  },
836
850
  },
837
851
  {
@@ -864,6 +878,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
864
878
  await runtime.ctx.handleBtwCommand(question);
865
879
  },
866
880
  },
881
+ {
882
+ name: "omfg",
883
+ description: "Forge a TTSR rule from a complaint to stop a recurring behavior",
884
+ inlineHint: "<complaint>",
885
+ allowArgs: true,
886
+ handleTui: async (command, runtime) => {
887
+ const complaint = command.text.slice(`/${command.name}`.length).trim();
888
+ runtime.ctx.editor.setText("");
889
+ await runtime.ctx.handleOmfgCommand(complaint);
890
+ },
891
+ },
867
892
  {
868
893
  name: "retry",
869
894
  description: "Retry the last failed agent turn",
@@ -21,7 +21,7 @@ import type { HindsightSessionState } from "../hindsight/state";
21
21
  import type { LocalProtocolOptions } from "../internal-urls";
22
22
  import { callTool } from "../mcp/client";
23
23
  import type { MCPManager } from "../mcp/manager";
24
- import type { MnemosyneSessionState } from "../mnemosyne/state";
24
+ import type { MnemopiSessionState } from "../mnemopi/state";
25
25
  import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
26
26
  import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
27
27
  import { AgentRegistry } from "../registry/agent-registry";
@@ -186,7 +186,7 @@ export interface ExecutorOptions {
186
186
  */
187
187
  parentArtifactManager?: ArtifactManager;
188
188
  parentHindsightSessionState?: HindsightSessionState;
189
- parentMnemosyneSessionState?: MnemosyneSessionState;
189
+ parentMnemopiSessionState?: MnemopiSessionState;
190
190
  /** Parent agent's eval executor session id. Subagents reuse it so eval state is shared. */
191
191
  parentEvalSessionId?: string;
192
192
  /**
@@ -1245,7 +1245,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1245
1245
  spawns: spawnsEnv,
1246
1246
  taskDepth: childDepth,
1247
1247
  parentHindsightSessionState: options.parentHindsightSessionState,
1248
- parentMnemosyneSessionState: options.parentMnemosyneSessionState,
1248
+ parentMnemopiSessionState: options.parentMnemopiSessionState,
1249
1249
  parentTaskPrefix: id,
1250
1250
  agentId: id,
1251
1251
  agentDisplayName: agent.name,
package/src/task/index.ts CHANGED
@@ -762,8 +762,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
762
762
  const tempArtifactsDir = artifactsDir ? null : path.join(os.tmpdir(), `omp-task-${Snowflake.next()}`);
763
763
  const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
764
764
 
765
- // Share the parent session's local:// root with subagents so they read/write the same scratch space
766
- const localProtocolOptions: LocalProtocolOptions = {
765
+ const localProtocolOptions: LocalProtocolOptions = this.session.localProtocolOptions ?? {
767
766
  getArtifactsDir: this.session.getArtifactsDir ?? (() => null),
768
767
  getSessionId: this.session.getSessionId ?? (() => null),
769
768
  };
@@ -864,6 +863,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
864
863
  );
865
864
  const promptTemplates = this.session.promptTemplates;
866
865
  const parentEvalSessionId = this.session.getEvalSessionId?.() ?? undefined;
866
+ const mcpManager = this.session.mcpManager ?? MCPManager.instance();
867
867
 
868
868
  // Initialize progress for all tasks
869
869
  for (let i = 0; i < tasksWithUniqueIds.length; i++) {
@@ -921,7 +921,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
921
921
  authStorage: this.session.authStorage,
922
922
  modelRegistry: this.session.modelRegistry,
923
923
  settings: this.session.settings,
924
- mcpManager: MCPManager.instance(),
924
+ mcpManager,
925
925
  contextFiles,
926
926
  skills: availableSkills,
927
927
  autoloadSkills: resolvedAutoloadSkills,
@@ -930,7 +930,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
930
930
  localProtocolOptions,
931
931
  parentArtifactManager,
932
932
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
933
- parentMnemosyneSessionState: this.session.getMnemosyneSessionState?.(),
933
+ parentMnemopiSessionState: this.session.getMnemopiSessionState?.(),
934
934
  parentTelemetry: this.session.getTelemetry?.(),
935
935
  parentEvalSessionId,
936
936
  });
@@ -978,7 +978,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
978
978
  authStorage: this.session.authStorage,
979
979
  modelRegistry: this.session.modelRegistry,
980
980
  settings: this.session.settings,
981
- mcpManager: MCPManager.instance(),
981
+ mcpManager,
982
982
  contextFiles,
983
983
  skills: availableSkills,
984
984
  autoloadSkills: resolvedAutoloadSkills,
@@ -987,7 +987,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
987
987
  localProtocolOptions,
988
988
  parentArtifactManager,
989
989
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
990
- parentMnemosyneSessionState: this.session.getMnemosyneSessionState?.(),
990
+ parentMnemopiSessionState: this.session.getMnemopiSessionState?.(),
991
991
  parentTelemetry: this.session.getTelemetry?.(),
992
992
  parentEvalSessionId,
993
993
  });
package/src/thinking.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ResolvedThinkingLevel, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import { clampThinkingLevelForModel, type Effort, type Model, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
2
+ import { clampThinkingLevelForModel, Effort, getSupportedEfforts, type Model, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
3
3
 
4
4
  /**
5
5
  * Metadata used to render thinking selector values in the coding-agent UI.
@@ -85,3 +85,75 @@ export function resolveThinkingLevelForModel(
85
85
  }
86
86
  return clampThinkingLevelForModel(model, level);
87
87
  }
88
+
89
+ /**
90
+ * Sentinel selector for the coding-agent "auto" thinking mode. Kept entirely
91
+ * inside the coding-agent layer: it is never an {@link Effort} or
92
+ * {@link ThinkingLevel}, so provider mapping/clamping keeps seeing concrete
93
+ * efforts. The session resolves `auto` to a concrete effort each turn.
94
+ */
95
+ export const AUTO_THINKING = "auto" as const;
96
+
97
+ /** A thinking selector as configured by the user — a concrete level or `auto`. */
98
+ export type ConfiguredThinkingLevel = ThinkingLevel | typeof AUTO_THINKING;
99
+
100
+ /** Metadata used to render the `auto` selector value alongside concrete levels. */
101
+ export interface ConfiguredThinkingLevelMetadata {
102
+ value: ConfiguredThinkingLevel;
103
+ label: string;
104
+ description: string;
105
+ }
106
+
107
+ const AUTO_THINKING_METADATA: ConfiguredThinkingLevelMetadata = {
108
+ value: AUTO_THINKING,
109
+ label: "auto",
110
+ description: "Auto-detect per prompt (low–xhigh)",
111
+ };
112
+
113
+ /**
114
+ * Parses a configured thinking selector, accepting `auto` in addition to every
115
+ * value {@link parseThinkingLevel} accepts. {@link parseThinkingLevel} itself
116
+ * stays strict so model-suffix parsing (`model:high`) keeps rejecting `auto`.
117
+ */
118
+ export function parseConfiguredThinkingLevel(value: string | null | undefined): ConfiguredThinkingLevel | undefined {
119
+ if (value === AUTO_THINKING) return AUTO_THINKING;
120
+ return parseThinkingLevel(value);
121
+ }
122
+
123
+ /** Returns display metadata for a configured selector, including `auto`. */
124
+ export function getConfiguredThinkingLevelMetadata(level: ConfiguredThinkingLevel): ConfiguredThinkingLevelMetadata {
125
+ return level === AUTO_THINKING ? AUTO_THINKING_METADATA : getThinkingLevelMetadata(level);
126
+ }
127
+
128
+ /**
129
+ * Resolves an auto-classified effort against the active model's supported
130
+ * range. Unlike {@link clampThinkingLevelForModel}, `auto` never resolves below
131
+ * {@link Effort.Low}: the eligible pool is the model's supported efforts at or
132
+ * above Low (falling back to the full supported set only when the model maxes
133
+ * out below Low). Within that pool the request snaps to the highest level not
134
+ * exceeding it, or the pool minimum when the request is below the pool.
135
+ */
136
+ export function clampAutoThinkingEffort(model: Model | undefined, effort: Effort): Effort {
137
+ const supported = model ? getSupportedEfforts(model) : THINKING_EFFORTS;
138
+ if (supported.length === 0) return effort;
139
+ const lowIndex = THINKING_EFFORTS.indexOf(Effort.Low);
140
+ const eligible = supported.filter(level => THINKING_EFFORTS.indexOf(level) >= lowIndex);
141
+ const pool = eligible.length > 0 ? eligible : supported;
142
+ const requestedIndex = THINKING_EFFORTS.indexOf(effort);
143
+ let chosen = pool[0];
144
+ for (const candidate of pool) {
145
+ if (THINKING_EFFORTS.indexOf(candidate) > requestedIndex) break;
146
+ chosen = candidate;
147
+ }
148
+ return chosen;
149
+ }
150
+
151
+ /**
152
+ * The provisional concrete level shown while `auto` is configured but before a
153
+ * turn has been classified. Prefers the model's `defaultLevel`, otherwise High,
154
+ * clamped into the auto range. Returns `undefined` for non-reasoning models.
155
+ */
156
+ export function resolveProvisionalAutoLevel(model: Model | undefined): Effort | undefined {
157
+ if (!model?.reasoning) return undefined;
158
+ return clampAutoThinkingEffort(model, model.thinking?.defaultLevel ?? Effort.High);
159
+ }
@@ -27,12 +27,6 @@ const DEVICE_VALUES: Record<TinyModelDevice, true> = {
27
27
  "webnn-cpu": true,
28
28
  };
29
29
 
30
- function defaultTinyModelDevice(): TinyModelDevice {
31
- if (process.platform === "win32") return "dml";
32
- if (process.platform === "linux" && process.arch === "x64") return "cuda";
33
- return CPU_DEVICE;
34
- }
35
-
36
30
  function usesDarwinWorkerWebGpu(device: TinyModelDevice): boolean {
37
31
  return process.platform === "darwin" && (device === "gpu" || device === "webgpu" || device === "auto");
38
32
  }
@@ -51,7 +45,7 @@ export function resolveTinyModelDevicePreference(
51
45
  value: string | undefined = $env.PI_TINY_DEVICE,
52
46
  ): TinyModelDevicePreference {
53
47
  return {
54
- device: normalizeTinyModelDevice(value) ?? defaultTinyModelDevice(),
48
+ device: normalizeTinyModelDevice(value) ?? CPU_DEVICE,
55
49
  raw: value,
56
50
  };
57
51
  }
@@ -62,7 +56,7 @@ export function tinyModelDeviceLoadOrder(preference: TinyModelDevicePreference):
62
56
  return [preference.device, CPU_DEVICE];
63
57
  }
64
58
 
65
- /** Sentinel `providers.tinyModelDevice` value meaning "use the built-in platform default". */
59
+ /** Sentinel `providers.tinyModelDevice` value meaning "use the built-in CPU default". */
66
60
  export const TINY_MODEL_DEVICE_DEFAULT = "default";
67
61
 
68
62
  /** Accepted values for the `providers.tinyModelDevice` setting (validation + UI). */
@@ -85,7 +79,7 @@ export const TINY_MODEL_DEVICE_SETTING_VALUES = [
85
79
 
86
80
  /** Submenu metadata for the `providers.tinyModelDevice` setting. */
87
81
  export const TINY_MODEL_DEVICE_SETTING_OPTIONS = [
88
- { value: "default", label: "Default", description: "DirectML on Windows, CUDA on Linux x64, CPU elsewhere" },
82
+ { value: "default", label: "Default", description: "CPU-only inference" },
89
83
  { value: "gpu", label: "GPU", description: "Accelerated provider (WebGPU/Metal, CUDA, or DirectML)" },
90
84
  { value: "cpu", label: "CPU", description: "CPU-only inference" },
91
85
  { value: "metal", label: "Metal", description: "WebGPU alias for Apple GPUs" },
@@ -108,7 +102,7 @@ export const TINY_MODEL_DEVICE_SETTING_OPTIONS = [
108
102
  /**
109
103
  * Map a `providers.tinyModelDevice` setting value onto a `PI_TINY_DEVICE` env
110
104
  * value for the worker. Returns `undefined` for the default sentinel so the
111
- * worker keeps its built-in platform default; the worker still validates the
105
+ * worker keeps its built-in CPU default; the worker still validates the
112
106
  * forwarded value via {@link normalizeTinyModelDevice}.
113
107
  */
114
108
  export function tinyModelDeviceSettingToEnv(value: string | undefined): string | undefined {
@@ -108,7 +108,7 @@ export const ONLINE_MEMORY_MODEL_KEY = "online";
108
108
  export const DEFAULT_MEMORY_LOCAL_MODEL_KEY = "qwen3-1.7b";
109
109
 
110
110
  /**
111
- * Local models for Mnemosyne memory tasks (fact extraction + consolidation).
111
+ * Local models for Mnemopi memory tasks (fact extraction + consolidation).
112
112
  * These are larger (1B-1.7B) than the title models: structured extraction and
113
113
  * faithful summarization need more capacity than 3-6 word titles. All q4.
114
114
  * Ranking/recipe rationale lives in docs/local-models.md.
@@ -177,7 +177,7 @@ export const TINY_MEMORY_MODEL_OPTIONS = [
177
177
  value: ONLINE_MEMORY_MODEL_KEY,
178
178
  label: "Online (smol/remote)",
179
179
  description:
180
- "Use the configured Mnemosyne LLM mode (smol or remote); no local model download or on-device inference.",
180
+ "Use the configured Mnemopi LLM mode (smol or remote); no local model download or on-device inference.",
181
181
  },
182
182
  ...TINY_MEMORY_LOCAL_MODELS.map(model => ({
183
183
  value: model.key,
@@ -196,6 +196,34 @@ 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
+
199
227
  /** Any local model key (title or memory), used by the shared inference worker. */
200
228
  export type TinyLocalModelKey = TinyTitleLocalModelKey | TinyMemoryLocalModelKey;
201
229
 
@@ -216,3 +244,27 @@ export const TINY_LOCAL_MODELS = [
216
244
  ...TINY_TITLE_LOCAL_MODELS,
217
245
  ...TINY_MEMORY_LOCAL_MODELS,
218
246
  ] as const satisfies readonly TinyTitleLocalModelSpec[];
247
+
248
+ /**
249
+ * Difficulty-classifier model for the `auto` thinking level. Defaults to the
250
+ * online smol path; the local options reuse the memory-model registry because
251
+ * the shared worker's `complete()` only accepts memory local keys, and the
252
+ * 1B+ memory models classify coding difficulty far more reliably than the
253
+ * sub-1B title models.
254
+ */
255
+ export const ONLINE_AUTO_THINKING_MODEL_KEY = ONLINE_MEMORY_MODEL_KEY;
256
+ export const AUTO_THINKING_MODEL_VALUES = TINY_MEMORY_MODEL_VALUES;
257
+ export type AutoThinkingModelKey = TinyMemoryModelKey;
258
+
259
+ export const AUTO_THINKING_MODEL_OPTIONS = [
260
+ {
261
+ value: ONLINE_AUTO_THINKING_MODEL_KEY,
262
+ label: "Online (smol)",
263
+ description: "Classify prompt difficulty with the online smol model; no local download or on-device inference.",
264
+ },
265
+ ...TINY_MEMORY_LOCAL_MODELS.map(model => ({
266
+ value: model.key,
267
+ label: model.label,
268
+ description: model.description,
269
+ })),
270
+ ] satisfies ReadonlyArray<{ value: AutoThinkingModelKey; label: string; description: string }>;
@@ -30,7 +30,17 @@ export interface TinyTitleProgressEvent {
30
30
  export type TinyTitleWorkerInbound =
31
31
  | { type: "ping"; id: string }
32
32
  | { type: "generate"; id: string; modelKey: TinyTitleLocalModelKey; message: string }
33
- | { type: "complete"; id: string; modelKey: TinyLocalModelKey; prompt: string; maxTokens?: number }
33
+ | {
34
+ type: "complete";
35
+ id: string;
36
+ modelKey: TinyLocalModelKey;
37
+ prompt: string;
38
+ maxTokens?: number;
39
+ /** Optional assistant-turn prefix appended after the generation prompt to pin output format. */
40
+ prefill?: string;
41
+ /** Optional literal stop string; generation halts once it appears in the decoded tail. */
42
+ stop?: string;
43
+ }
34
44
  | { type: "download"; id: string; modelKey: TinyLocalModelKey }
35
45
  | { type: "close" };
36
46