@oh-my-pi/pi-coding-agent 15.5.13 → 15.6.0

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 (192) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/types/cli/classify-install-target.d.ts +0 -10
  3. package/dist/types/cli/initial-message.d.ts +1 -1
  4. package/dist/types/cli/tiny-models-cli.d.ts +9 -0
  5. package/dist/types/commands/tiny-models.d.ts +22 -0
  6. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  7. package/dist/types/commit/analysis/summary.d.ts +1 -1
  8. package/dist/types/commit/changelog/generate.d.ts +1 -1
  9. package/dist/types/commit/changelog/index.d.ts +2 -2
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  12. package/dist/types/config/model-id-affixes.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +1 -1
  14. package/dist/types/config/models-config-schema.d.ts +2 -0
  15. package/dist/types/config/settings-schema.d.ts +233 -17
  16. package/dist/types/discovery/helpers.d.ts +1 -1
  17. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  18. package/dist/types/eval/__tests__/llm-bridge.test.d.ts +1 -0
  19. package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
  20. package/dist/types/eval/llm-bridge.d.ts +25 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +15 -0
  23. package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
  24. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
  25. package/dist/types/internal-urls/local-protocol.d.ts +2 -1
  26. package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
  27. package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
  28. package/dist/types/internal-urls/router.d.ts +8 -1
  29. package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
  30. package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
  31. package/dist/types/internal-urls/types.d.ts +26 -0
  32. package/dist/types/memory-backend/index.d.ts +1 -0
  33. package/dist/types/memory-backend/resolve.d.ts +2 -1
  34. package/dist/types/memory-backend/types.d.ts +7 -1
  35. package/dist/types/mnemosyne/backend.d.ts +4 -0
  36. package/dist/types/mnemosyne/config.d.ts +29 -0
  37. package/dist/types/mnemosyne/index.d.ts +3 -0
  38. package/dist/types/mnemosyne/state.d.ts +72 -0
  39. package/dist/types/modes/components/custom-editor.d.ts +2 -3
  40. package/dist/types/modes/components/hook-selector.d.ts +27 -0
  41. package/dist/types/modes/components/index.d.ts +1 -0
  42. package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
  43. package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
  44. package/dist/types/modes/components/welcome.d.ts +1 -0
  45. package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
  46. package/dist/types/modes/gradient-highlight.d.ts +23 -0
  47. package/dist/types/modes/interactive-mode.d.ts +4 -2
  48. package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
  49. package/dist/types/modes/orchestrate.d.ts +10 -0
  50. package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
  51. package/dist/types/modes/theme/theme.d.ts +2 -1
  52. package/dist/types/modes/ultrathink.d.ts +3 -3
  53. package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
  54. package/dist/types/sdk.d.ts +3 -0
  55. package/dist/types/session/agent-session.d.ts +35 -0
  56. package/dist/types/system-prompt.d.ts +2 -0
  57. package/dist/types/task/executor.d.ts +2 -0
  58. package/dist/types/task/render.d.ts +5 -1
  59. package/dist/types/tiny/models.d.ts +185 -0
  60. package/dist/types/tiny/text.d.ts +4 -0
  61. package/dist/types/tiny/title-client.d.ts +24 -0
  62. package/dist/types/tiny/title-protocol.d.ts +74 -0
  63. package/dist/types/tiny/worker.d.ts +2 -0
  64. package/dist/types/tools/bash.d.ts +3 -1
  65. package/dist/types/tools/index.d.ts +7 -4
  66. package/dist/types/tools/memory-edit.d.ts +40 -0
  67. package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
  68. package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
  69. package/dist/types/tools/memory-render.d.ts +60 -0
  70. package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
  71. package/dist/types/tools/todo-write.d.ts +8 -0
  72. package/dist/types/tools/tool-result.d.ts +2 -0
  73. package/dist/types/utils/title-generator.d.ts +3 -0
  74. package/package.json +18 -14
  75. package/scripts/build-binary.ts +1 -0
  76. package/src/cli/tiny-models-cli.ts +127 -0
  77. package/src/cli-commands.ts +1 -0
  78. package/src/cli.ts +8 -8
  79. package/src/commands/tiny-models.ts +36 -0
  80. package/src/config/model-equivalence.ts +43 -2
  81. package/src/config/model-id-affixes.ts +64 -0
  82. package/src/config/model-registry.ts +166 -8
  83. package/src/config/models-config-schema.ts +1 -1
  84. package/src/config/settings-schema.ts +206 -14
  85. package/src/edit/hashline/diff.ts +5 -7
  86. package/src/eval/__tests__/llm-bridge.test.ts +297 -0
  87. package/src/eval/__tests__/shared-executors.test.ts +36 -0
  88. package/src/eval/js/shared/local-module-loader.ts +13 -1
  89. package/src/eval/js/shared/prelude.txt +8 -0
  90. package/src/eval/js/shared/rewrite-imports.ts +31 -26
  91. package/src/eval/js/tool-bridge.ts +4 -0
  92. package/src/eval/llm-bridge.ts +181 -0
  93. package/src/eval/py/prelude.py +52 -31
  94. package/src/export/html/template.generated.ts +1 -1
  95. package/src/export/html/template.js +0 -13
  96. package/src/extensibility/plugins/legacy-pi-compat.ts +60 -23
  97. package/src/internal-urls/agent-protocol.ts +18 -1
  98. package/src/internal-urls/artifact-protocol.ts +19 -1
  99. package/src/internal-urls/docs-index.generated.ts +5 -4
  100. package/src/internal-urls/local-protocol.ts +14 -1
  101. package/src/internal-urls/memory-protocol.ts +6 -1
  102. package/src/internal-urls/omp-protocol.ts +5 -1
  103. package/src/internal-urls/router.ts +20 -1
  104. package/src/internal-urls/rule-protocol.ts +8 -1
  105. package/src/internal-urls/skill-protocol.ts +8 -1
  106. package/src/internal-urls/types.ts +27 -0
  107. package/src/lsp/render.ts +1 -1
  108. package/src/main.ts +4 -0
  109. package/src/mcp/oauth-flow.ts +2 -2
  110. package/src/memory-backend/index.ts +1 -0
  111. package/src/memory-backend/resolve.ts +4 -1
  112. package/src/memory-backend/types.ts +8 -1
  113. package/src/mnemosyne/backend.ts +374 -0
  114. package/src/mnemosyne/config.ts +160 -0
  115. package/src/mnemosyne/index.ts +3 -0
  116. package/src/mnemosyne/state.ts +548 -0
  117. package/src/modes/acp/acp-agent.ts +11 -6
  118. package/src/modes/components/agent-dashboard.ts +4 -4
  119. package/src/modes/components/custom-editor.ts +3 -2
  120. package/src/modes/components/diff.ts +2 -2
  121. package/src/modes/components/extensions/extension-list.ts +3 -2
  122. package/src/modes/components/footer.ts +5 -6
  123. package/src/modes/components/history-search.ts +3 -3
  124. package/src/modes/components/hook-selector.ts +94 -8
  125. package/src/modes/components/index.ts +1 -0
  126. package/src/modes/components/mcp-add-wizard.ts +3 -3
  127. package/src/modes/components/model-selector.ts +124 -26
  128. package/src/modes/components/oauth-selector.ts +3 -3
  129. package/src/modes/components/session-observer-overlay.ts +19 -13
  130. package/src/modes/components/session-selector.ts +3 -3
  131. package/src/modes/components/settings-defs.ts +7 -0
  132. package/src/modes/components/status-line/context-thresholds.ts +11 -0
  133. package/src/modes/components/status-line/presets.ts +1 -0
  134. package/src/modes/components/status-line/segments.ts +25 -2
  135. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  136. package/src/modes/components/tips.txt +12 -0
  137. package/src/modes/components/tool-execution.ts +67 -3
  138. package/src/modes/components/tree-selector.ts +3 -3
  139. package/src/modes/components/user-message-selector.ts +3 -3
  140. package/src/modes/components/welcome.ts +55 -1
  141. package/src/modes/controllers/command-controller.ts +16 -1
  142. package/src/modes/controllers/extension-ui-controller.ts +3 -1
  143. package/src/modes/controllers/input-controller.ts +57 -0
  144. package/src/modes/gradient-highlight.ts +70 -0
  145. package/src/modes/interactive-mode.ts +80 -196
  146. package/src/modes/internal-url-autocomplete.ts +143 -0
  147. package/src/modes/orchestrate.ts +36 -0
  148. package/src/modes/prompt-action-autocomplete.ts +12 -0
  149. package/src/modes/theme/theme.ts +7 -0
  150. package/src/modes/ultrathink.ts +9 -53
  151. package/src/modes/utils/keybinding-matchers.ts +11 -0
  152. package/src/prompts/system/memory-consolidation-system.md +8 -0
  153. package/src/prompts/system/memory-extraction-system.md +26 -0
  154. package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +5 -16
  155. package/src/prompts/system/system-prompt.md +2 -0
  156. package/src/prompts/system/tiny-title-system.md +8 -0
  157. package/src/prompts/tools/eval.md +2 -0
  158. package/src/prompts/tools/memory-edit.md +8 -0
  159. package/src/prompts/tools/task.md +4 -7
  160. package/src/sdk.ts +8 -6
  161. package/src/session/agent-session.ts +147 -44
  162. package/src/session/session-manager.ts +47 -0
  163. package/src/slash-commands/builtin-registry.ts +10 -1
  164. package/src/system-prompt.ts +4 -0
  165. package/src/task/commands.ts +1 -5
  166. package/src/task/executor.ts +8 -0
  167. package/src/task/index.ts +2 -0
  168. package/src/task/render.ts +69 -26
  169. package/src/tiny/models.ts +217 -0
  170. package/src/tiny/text.ts +19 -0
  171. package/src/tiny/title-client.ts +340 -0
  172. package/src/tiny/title-protocol.ts +51 -0
  173. package/src/tiny/worker.ts +523 -0
  174. package/src/tools/bash.ts +58 -16
  175. package/src/tools/browser/tab-worker.ts +1 -1
  176. package/src/tools/eval.ts +24 -48
  177. package/src/tools/index.ts +17 -15
  178. package/src/tools/memory-edit.ts +59 -0
  179. package/src/tools/memory-recall.ts +100 -0
  180. package/src/tools/memory-reflect.ts +88 -0
  181. package/src/tools/memory-render.ts +185 -0
  182. package/src/tools/memory-retain.ts +91 -0
  183. package/src/tools/renderers.ts +4 -2
  184. package/src/tools/todo-write.ts +128 -29
  185. package/src/tools/tool-result.ts +8 -0
  186. package/src/utils/title-generator.ts +115 -13
  187. package/dist/types/tools/calculator.d.ts +0 -77
  188. package/src/prompts/tools/calculator.md +0 -10
  189. package/src/tools/calculator.ts +0 -541
  190. package/src/tools/hindsight-recall.ts +0 -69
  191. package/src/tools/hindsight-reflect.ts +0 -58
  192. package/src/tools/hindsight-retain.ts +0 -57
package/src/tools/eval.ts CHANGED
@@ -13,10 +13,19 @@ import { truncateToVisualLines } from "../modes/components/visual-truncate";
13
13
  import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
14
14
  import evalDescription from "../prompts/tools/eval.md" with { type: "text" };
15
15
  import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary, TailBuffer } from "../session/streaming-output";
16
- import { getTreeBranch, getTreeContinuePrefix, renderCodeCell } from "../tui";
16
+ import { renderCodeCell } from "../tui";
17
17
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
18
18
  import { resolveEvalBackends, type ToolSession } from ".";
19
19
  import { truncateForPrompt } from "./approval";
20
+ import {
21
+ JSON_TREE_MAX_DEPTH_COLLAPSED,
22
+ JSON_TREE_MAX_DEPTH_EXPANDED,
23
+ JSON_TREE_MAX_LINES_COLLAPSED,
24
+ JSON_TREE_MAX_LINES_EXPANDED,
25
+ JSON_TREE_SCALAR_LEN_COLLAPSED,
26
+ JSON_TREE_SCALAR_LEN_EXPANDED,
27
+ renderJsonTreeLines,
28
+ } from "./json-tree";
20
29
  import {
21
30
  formatStyledTruncationWarning,
22
31
  resolveOutputMaxColumns,
@@ -61,15 +70,6 @@ export type EvalToolResult = {
61
70
 
62
71
  export type EvalProxyExecutor = (params: EvalToolParams, signal?: AbortSignal) => Promise<EvalToolResult>;
63
72
 
64
- function formatJsonScalar(value: unknown): string {
65
- if (value === null) return "null";
66
- if (value === undefined) return "undefined";
67
- if (typeof value === "string") return JSON.stringify(value);
68
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
69
- if (typeof value === "function") return "[function]";
70
- return "[object]";
71
- }
72
-
73
73
  /** Cap per `display()` value sent back to the model. */
74
74
  const MAX_DISPLAY_TEXT_BYTES = 8000;
75
75
 
@@ -102,41 +102,6 @@ function formatDisplayOutputsForText(outputs: EvalDisplayOutput[]): string {
102
102
  return chunks.join("\n\n");
103
103
  }
104
104
 
105
- function renderJsonTree(value: unknown, theme: Theme, expanded: boolean, maxDepth = expanded ? 6 : 2): string[] {
106
- const maxItems = expanded ? 20 : 5;
107
-
108
- const renderNode = (node: unknown, prefix: string, depth: number, isLast: boolean, label?: string): string[] => {
109
- const branch = getTreeBranch(isLast, theme);
110
- const displayLabel = label ? `${label}: ` : "";
111
-
112
- if (depth >= maxDepth || node === null || typeof node !== "object") {
113
- return [`${prefix}${branch} ${displayLabel}${formatJsonScalar(node)}`];
114
- }
115
-
116
- const isArray = Array.isArray(node);
117
- const entries = isArray
118
- ? node.map((val, index) => [String(index), val] as const)
119
- : Object.entries(node as object);
120
- const header = `${prefix}${branch} ${displayLabel}${isArray ? `Array(${entries.length})` : `Object(${entries.length})`}`;
121
- const lines = [header];
122
-
123
- const childPrefix = prefix + getTreeContinuePrefix(isLast, theme);
124
- const visible = entries.slice(0, maxItems);
125
- for (let i = 0; i < visible.length; i++) {
126
- const [key, val] = visible[i];
127
- const childLast = i === visible.length - 1 && (expanded || entries.length <= maxItems);
128
- lines.push(...renderNode(val, childPrefix, depth + 1, childLast, isArray ? `[${key}]` : key));
129
- }
130
- if (!expanded && entries.length > maxItems) {
131
- const moreBranch = theme.tree.last;
132
- lines.push(`${childPrefix}${moreBranch} ${entries.length - maxItems} more item(s)`);
133
- }
134
- return lines;
135
- };
136
-
137
- return renderNode(value, "", 0, true);
138
- }
139
-
140
105
  export interface EvalToolDescriptionOptions {
141
106
  py?: boolean;
142
107
  js?: boolean;
@@ -669,6 +634,7 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
669
634
  sh: "icon.package",
670
635
  env: "icon.package",
671
636
  batch: "icon.package",
637
+ llm: "icon.package",
672
638
  };
673
639
 
674
640
  const iconKey = opIcons[op] ?? "icon.file";
@@ -735,6 +701,11 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
735
701
  case "batch":
736
702
  parts.push(`${data.files} file${(data.files as number) !== 1 ? "s" : ""} processed`);
737
703
  break;
704
+ case "llm":
705
+ if (data.model) parts.push(String(data.model));
706
+ if (data.tier && data.tier !== data.model) parts.push(`(${data.tier})`);
707
+ parts.push(`${data.chars ?? 0} chars`);
708
+ break;
738
709
  case "wc":
739
710
  parts.push(`${data.lines}L ${data.words}W ${data.chars}C`);
740
711
  break;
@@ -950,10 +921,15 @@ export const evalToolRenderer = {
950
921
  const output = stripOutputNotice(rawOutput, details?.meta).trimEnd();
951
922
 
952
923
  const jsonOutputs = details?.jsonOutputs ?? [];
924
+ const treeExpanded = options.renderContext?.expanded ?? options.expanded;
925
+ const treeDepth = treeExpanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
926
+ const treeLineCap = treeExpanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
927
+ const treeScalarLen = treeExpanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
928
+ const labelOutputs = jsonOutputs.length > 1;
953
929
  const jsonLines = jsonOutputs.flatMap((value, index) => {
954
- const header = `JSON output ${index + 1}`;
955
- const treeLines = renderJsonTree(value, uiTheme, options.renderContext?.expanded ?? options.expanded);
956
- return [header, ...treeLines];
930
+ const tree = renderJsonTreeLines(value, uiTheme, treeDepth, treeLineCap, treeScalarLen);
931
+ const body = tree.truncated ? [...tree.lines, uiTheme.fg("dim", "…")] : tree.lines;
932
+ return labelOutputs ? [uiTheme.fg("dim", `display[${index + 1}]`), ...body] : body;
957
933
  });
958
934
 
959
935
  const timeoutSeconds = options.renderContext?.timeout;
@@ -11,6 +11,7 @@ import type { GoalModeState, GoalRuntime } from "../goals";
11
11
  import { GoalTool } from "../goals/tools/goal-tool";
12
12
  import type { HindsightSessionState } from "../hindsight/state";
13
13
  import { LspTool } from "../lsp";
14
+ import type { MnemosyneSessionState } from "../mnemosyne/state";
14
15
  import type { PlanModeState } from "../plan-mode/state";
15
16
  import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
16
17
  import type { ArtifactManager } from "../session/artifacts";
@@ -28,18 +29,18 @@ import { AstEditTool } from "./ast-edit";
28
29
  import { AstGrepTool } from "./ast-grep";
29
30
  import { BashTool } from "./bash";
30
31
  import { BrowserTool } from "./browser";
31
- import { CalculatorTool } from "./calculator";
32
32
  import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
33
33
  import { DebugTool } from "./debug";
34
34
  import { EvalTool } from "./eval";
35
35
  import { FindTool } from "./find";
36
36
  import { GithubTool } from "./gh";
37
- import { HindsightRecallTool } from "./hindsight-recall";
38
- import { HindsightReflectTool } from "./hindsight-reflect";
39
- import { HindsightRetainTool } from "./hindsight-retain";
40
37
  import { InspectImageTool } from "./inspect-image";
41
38
  import { IrcTool } from "./irc";
42
39
  import { JobTool } from "./job";
40
+ import { MemoryEditTool } from "./memory-edit";
41
+ import { MemoryRecallTool } from "./memory-recall";
42
+ import { MemoryReflectTool } from "./memory-reflect";
43
+ import { MemoryRetainTool } from "./memory-retain";
43
44
  import { wrapToolWithMetaNotice } from "./output-meta";
44
45
  import { ReadTool } from "./read";
45
46
  import { RecipeTool } from "./recipe";
@@ -69,19 +70,19 @@ export * from "./ast-edit";
69
70
  export * from "./ast-grep";
70
71
  export * from "./bash";
71
72
  export * from "./browser";
72
- export * from "./calculator";
73
73
  export * from "./checkpoint";
74
74
  export * from "./debug";
75
75
  export * from "./eval";
76
76
  export * from "./find";
77
77
  export * from "./gh";
78
- export * from "./hindsight-recall";
79
- export * from "./hindsight-reflect";
80
- export * from "./hindsight-retain";
81
78
  export * from "./image-gen";
82
79
  export * from "./inspect-image";
83
80
  export * from "./irc";
84
81
  export * from "./job";
82
+ export * from "./memory-edit";
83
+ export * from "./memory-recall";
84
+ export * from "./memory-reflect";
85
+ export * from "./memory-retain";
85
86
  export * from "./read";
86
87
  export * from "./recipe";
87
88
  export * from "./render-mermaid";
@@ -154,6 +155,8 @@ export interface ToolSession {
154
155
  getSessionId?: () => string | null;
155
156
  /** Get Hindsight runtime state for this agent session. */
156
157
  getHindsightSessionState?: () => HindsightSessionState | undefined;
158
+ /** Get Mnemosyne runtime state for this agent session. */
159
+ getMnemosyneSessionState?: () => MnemosyneSessionState | undefined;
157
160
  /** Agent identity used for IRC routing. Returns the registry id (e.g. "0-Main", "0-AuthLoader"). */
158
161
  getAgentId?: () => string | null;
159
162
  /** Look up a registered tool by name (used by the eval js backend's tool bridge). */
@@ -286,7 +289,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
286
289
  ask: AskTool.createIf,
287
290
  debug: DebugTool.createIf,
288
291
  eval: s => new EvalTool(s),
289
- calc: s => new CalculatorTool(s),
290
292
  ssh: loadSshTool,
291
293
  github: GithubTool.createIf,
292
294
  find: s => new FindTool(s),
@@ -304,9 +306,10 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
304
306
  web_search: s => new WebSearchTool(s),
305
307
  search_tool_bm25: SearchToolBm25Tool.createIf,
306
308
  write: s => new WriteTool(s),
307
- retain: HindsightRetainTool.createIf,
308
- recall: HindsightRecallTool.createIf,
309
- reflect: HindsightReflectTool.createIf,
309
+ memory_edit: MemoryEditTool.createIf,
310
+ retain: MemoryRetainTool.createIf,
311
+ recall: MemoryRecallTool.createIf,
312
+ reflect: MemoryReflectTool.createIf,
310
313
  };
311
314
 
312
315
  export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
@@ -420,7 +423,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
420
423
  ) {
421
424
  requestedTools.push("recipe");
422
425
  }
423
- if (session.settings.get("memory.backend") === "hindsight") {
426
+ if (["hindsight", "mnemosyne"].includes(session.settings.get("memory.backend") ?? "")) {
424
427
  for (const name of ["recall", "retain", "reflect"]) {
425
428
  if (!requestedTools.includes(name)) requestedTools.push(name);
426
429
  }
@@ -455,7 +458,6 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
455
458
  if (name === "web_search") return session.settings.get("web_search.enabled");
456
459
  // search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
457
460
  if (name === "search_tool_bm25") return discoveryActive;
458
- if (name === "calc") return session.settings.get("calc.enabled");
459
461
  if (name === "browser") return session.settings.get("browser.enabled");
460
462
  if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
461
463
  if (name === "irc") {
@@ -467,7 +469,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
467
469
  }
468
470
  if (name === "recipe") return session.settings.get("recipe.enabled");
469
471
  if (name === "retain" || name === "recall" || name === "reflect") {
470
- return session.settings.get("memory.backend") === "hindsight";
472
+ return ["hindsight", "mnemosyne"].includes(session.settings.get("memory.backend") ?? "");
471
473
  }
472
474
  if (name === "task") {
473
475
  const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
@@ -0,0 +1,59 @@
1
+ import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
+ import * as z from "zod/v4";
3
+ import memoryEditDescription from "../prompts/tools/memory-edit.md" with { type: "text" };
4
+ import type { ToolSession } from ".";
5
+
6
+ const memoryEditSchema = z.object({
7
+ op: z.enum(["update", "forget", "invalidate"]).describe("memory edit operation"),
8
+ id: z.string().describe("memory id from recall output"),
9
+ content: z.string().optional().describe("replacement content for update"),
10
+ importance: z.number().optional().describe("replacement importance for update, clamped to [0, 1]"),
11
+ replacement_id: z.string().optional().describe("replacement memory id for invalidate"),
12
+ });
13
+
14
+ export type MemoryEditParams = z.infer<typeof memoryEditSchema>;
15
+
16
+ export class MemoryEditTool implements AgentTool<typeof memoryEditSchema> {
17
+ readonly name = "memory_edit";
18
+ readonly approval = "read" as const;
19
+ readonly label = "Memory Edit";
20
+ readonly description = memoryEditDescription;
21
+ readonly parameters = memoryEditSchema;
22
+ readonly strict = true;
23
+ readonly loadMode = "discoverable";
24
+ readonly summary = "Update, forget, or invalidate Mnemosyne memories";
25
+
26
+ constructor(private readonly session: ToolSession) {}
27
+
28
+ static createIf(session: ToolSession): MemoryEditTool | null {
29
+ const backend = session.settings.get("memory.backend");
30
+ if (backend !== "mnemosyne") return null;
31
+ return new MemoryEditTool(session);
32
+ }
33
+
34
+ async execute(_id: string, params: MemoryEditParams): Promise<AgentToolResult> {
35
+ const state = this.session.getMnemosyneSessionState?.();
36
+ if (!state) {
37
+ throw new Error("Mnemosyne backend is not initialised for this session.");
38
+ }
39
+ if (params.op === "update" && params.content === undefined && params.importance === undefined) {
40
+ throw new Error("memory_edit update requires content or importance.");
41
+ }
42
+
43
+ const importance = params.importance === undefined ? undefined : Math.max(0, Math.min(1, params.importance));
44
+ const result = state.editScopedMemory(params.op, params.id, {
45
+ content: params.content,
46
+ importance,
47
+ replacementId: params.replacement_id,
48
+ });
49
+ const location = result.bank ? ` in bank ${result.bank}${result.store ? ` (${result.store})` : ""}` : "";
50
+ const text =
51
+ result.status === "not_found"
52
+ ? `Memory ${params.id} was not found${location}.`
53
+ : `Memory ${params.id} ${result.status}${location}.`;
54
+ return {
55
+ content: [{ type: "text", text }],
56
+ details: result,
57
+ };
58
+ }
59
+ }
@@ -0,0 +1,100 @@
1
+ import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
+ import { logger, untilAborted } from "@oh-my-pi/pi-utils";
3
+ import * as z from "zod/v4";
4
+ import { formatCurrentTime, formatMemories } from "../hindsight/content";
5
+ import recallDescription from "../prompts/tools/recall.md" with { type: "text" };
6
+ import type { ToolSession } from ".";
7
+
8
+ const memoryRecallSchema = z.object({
9
+ query: z.string().describe("natural language search query"),
10
+ });
11
+
12
+ export type MemoryRecallParams = z.infer<typeof memoryRecallSchema>;
13
+
14
+ export class MemoryRecallTool implements AgentTool<typeof memoryRecallSchema> {
15
+ readonly name = "recall";
16
+ readonly approval = "read" as const;
17
+ readonly label = "Recall";
18
+ readonly description = recallDescription;
19
+ readonly parameters = memoryRecallSchema;
20
+ readonly strict = true;
21
+ readonly loadMode = "discoverable";
22
+ readonly summary = "Search memory for relevant prior context";
23
+
24
+ constructor(private readonly session: ToolSession) {}
25
+
26
+ static createIf(session: ToolSession): MemoryRecallTool | null {
27
+ const backend = session.settings.get("memory.backend");
28
+ if (backend !== "hindsight" && backend !== "mnemosyne") return null;
29
+ return new MemoryRecallTool(session);
30
+ }
31
+
32
+ async execute(_id: string, params: MemoryRecallParams, signal?: AbortSignal): Promise<AgentToolResult> {
33
+ return untilAborted(signal, async () => {
34
+ const backend = this.session.settings.get("memory.backend");
35
+ if (backend === "mnemosyne") {
36
+ const state = this.session.getMnemosyneSessionState?.();
37
+ if (!state) {
38
+ throw new Error("Mnemosyne backend is not initialised for this session.");
39
+ }
40
+ try {
41
+ const results = state.recallResultsScoped(params.query);
42
+ if (results.length === 0) {
43
+ return {
44
+ content: [{ type: "text", text: "No relevant memories found." }],
45
+ details: {},
46
+ };
47
+ }
48
+ const formatted = state.formatScopedRecallWithIds(results);
49
+ return {
50
+ content: [
51
+ {
52
+ type: "text",
53
+ text: `Found ${results.length} relevant ${results.length === 1 ? "memory" : "memories"} (as of ${formatCurrentTime()} UTC):\n\n${formatted}`,
54
+ },
55
+ ],
56
+ details: {},
57
+ };
58
+ } catch (err) {
59
+ logger.warn("recall failed", { backend: "mnemosyne", bank: state.config.bank, error: String(err) });
60
+ throw err instanceof Error ? err : new Error(String(err));
61
+ }
62
+ }
63
+
64
+ const state = this.session.getHindsightSessionState?.();
65
+ if (!state) {
66
+ throw new Error("Hindsight backend is not initialised for this session.");
67
+ }
68
+
69
+ try {
70
+ const response = await state.client.recall(state.bankId, params.query, {
71
+ budget: state.config.recallBudget,
72
+ maxTokens: state.config.recallMaxTokens,
73
+ types: state.config.recallTypes.length > 0 ? state.config.recallTypes : undefined,
74
+ tags: state.recallTags,
75
+ tagsMatch: state.recallTagsMatch,
76
+ });
77
+ const results = response.results ?? [];
78
+ if (results.length === 0) {
79
+ return {
80
+ content: [{ type: "text", text: "No relevant memories found." }],
81
+ details: {},
82
+ };
83
+ }
84
+ const formatted = formatMemories(results);
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: `Found ${results.length} relevant ${results.length === 1 ? "memory" : "memories"} (as of ${formatCurrentTime()} UTC):\n\n${formatted}`,
90
+ },
91
+ ],
92
+ details: {},
93
+ };
94
+ } catch (err) {
95
+ logger.warn("recall failed", { bankId: state.bankId, error: String(err) });
96
+ throw err instanceof Error ? err : new Error(String(err));
97
+ }
98
+ });
99
+ }
100
+ }
@@ -0,0 +1,88 @@
1
+ import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
+ import { logger, untilAborted } from "@oh-my-pi/pi-utils";
3
+ import * as z from "zod/v4";
4
+ import { ensureBankMission } from "../hindsight/bank";
5
+ import reflectDescription from "../prompts/tools/reflect.md" with { type: "text" };
6
+ import type { ToolSession } from ".";
7
+
8
+ const memoryReflectSchema = z.object({
9
+ query: z.string().describe("question to answer"),
10
+ context: z.string().describe("optional context").optional(),
11
+ });
12
+
13
+ export type MemoryReflectParams = z.infer<typeof memoryReflectSchema>;
14
+
15
+ export class MemoryReflectTool implements AgentTool<typeof memoryReflectSchema> {
16
+ readonly name = "reflect";
17
+ readonly approval = "read" as const;
18
+ readonly label = "Reflect";
19
+ readonly description = reflectDescription;
20
+ readonly parameters = memoryReflectSchema;
21
+ readonly strict = true;
22
+ readonly loadMode = "discoverable";
23
+ readonly summary = "Synthesize an answer from long-term memory";
24
+
25
+ constructor(private readonly session: ToolSession) {}
26
+
27
+ static createIf(session: ToolSession): MemoryReflectTool | null {
28
+ const backend = session.settings.get("memory.backend");
29
+ if (backend !== "hindsight" && backend !== "mnemosyne") return null;
30
+ return new MemoryReflectTool(session);
31
+ }
32
+
33
+ async execute(_id: string, params: MemoryReflectParams, signal?: AbortSignal): Promise<AgentToolResult> {
34
+ return untilAborted(signal, async () => {
35
+ const backend = this.session.settings.get("memory.backend");
36
+ if (backend === "mnemosyne") {
37
+ const state = this.session.getMnemosyneSessionState?.();
38
+ if (!state) {
39
+ throw new Error("Mnemosyne backend is not initialised for this session.");
40
+ }
41
+
42
+ try {
43
+ const query = params.context?.trim()
44
+ ? `${params.query.trim()}\n\nAdditional context:\n${params.context.trim()}`
45
+ : params.query;
46
+ const results = state.recallResultsScoped(query);
47
+ if (results.length === 0) {
48
+ return {
49
+ content: [{ type: "text", text: "No relevant information found to reflect on." }],
50
+ details: {},
51
+ };
52
+ }
53
+ const summary = state.formatContextScoped(results);
54
+ return {
55
+ content: [{ type: "text", text: `Based on recalled memories:\n\n${summary}` }],
56
+ details: {},
57
+ };
58
+ } catch (err) {
59
+ logger.warn("reflect failed", { backend: "mnemosyne", bank: state.config.bank, error: String(err) });
60
+ throw err instanceof Error ? err : new Error(String(err));
61
+ }
62
+ }
63
+
64
+ const state = this.session.getHindsightSessionState?.();
65
+ if (!state) {
66
+ throw new Error("Hindsight backend is not initialised for this session.");
67
+ }
68
+
69
+ try {
70
+ await ensureBankMission(state.client, state.bankId, state.config, state.missionsSet);
71
+ const response = await state.client.reflect(state.bankId, params.query, {
72
+ context: params.context,
73
+ budget: state.config.recallBudget,
74
+ tags: state.recallTags,
75
+ tagsMatch: state.recallTagsMatch,
76
+ });
77
+ const text = response.text?.trim() || "No relevant information found to reflect on.";
78
+ return {
79
+ content: [{ type: "text", text }],
80
+ details: {},
81
+ };
82
+ } catch (err) {
83
+ logger.warn("reflect failed", { bankId: state.bankId, error: String(err) });
84
+ throw err instanceof Error ? err : new Error(String(err));
85
+ }
86
+ });
87
+ }
88
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Inline TUI renderers for the long-term memory tools (`retain`, `recall`,
3
+ * `reflect`).
4
+ *
5
+ * These keep the transcript terse — one status line plus, for `retain`, one
6
+ * `Remember: …` line per stored item — instead of the generic JSON arg tree,
7
+ * which exploded multi-line memory blobs into an unreadable wall.
8
+ */
9
+ import type { Component } from "@oh-my-pi/pi-tui";
10
+ import { Text } from "@oh-my-pi/pi-tui";
11
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
+ import type { Theme } from "../modes/theme/theme";
13
+ import { Ellipsis, renderStatusLine, truncateToWidth } from "../tui";
14
+ import {
15
+ createCachedComponent,
16
+ formatErrorMessage,
17
+ formatExpandHint,
18
+ PREVIEW_LIMITS,
19
+ replaceTabs,
20
+ type ToolUIStatus,
21
+ } from "./render-utils";
22
+
23
+ // Each stored memory renders as `<bullet> <content>`; the bullet glyph comes
24
+ // from the active theme (`•` by default, a nerd-font dot under nerd themes).
25
+
26
+ interface RetainRenderArgs {
27
+ items?: Array<{ content?: string; context?: string }>;
28
+ }
29
+
30
+ interface QueryRenderArgs {
31
+ query?: string;
32
+ }
33
+
34
+ function retainContents(args: RetainRenderArgs | undefined): string[] {
35
+ return (args?.items ?? []).map(item => replaceTabs((item?.content ?? "").trim())).filter(line => line.length > 0);
36
+ }
37
+
38
+ function resultText(result: { content?: Array<{ type: string; text?: string }> }): string {
39
+ return (result.content?.find(c => c.type === "text")?.text ?? "").trim();
40
+ }
41
+
42
+ /** Single-line query header used by `recall`/`reflect` calls and results. */
43
+ function queryHeader(
44
+ title: string,
45
+ query: string | undefined,
46
+ icon: ToolUIStatus,
47
+ theme: Theme,
48
+ meta?: string[],
49
+ ): string {
50
+ const trimmed = replaceTabs((query ?? "").trim());
51
+ const description = trimmed ? truncateToWidth(trimmed, 80, Ellipsis.Unicode) : undefined;
52
+ return renderStatusLine({ icon, title, description, meta }, theme);
53
+ }
54
+
55
+ function retainComponent(contents: string[], header: string, getExpanded: () => boolean, theme: Theme): Component {
56
+ return createCachedComponent(getExpanded, (width, expanded) => {
57
+ const lines = [header];
58
+ const limit = expanded ? contents.length : PREVIEW_LIMITS.COLLAPSED_ITEMS;
59
+ const shown = contents.slice(0, limit);
60
+ const bullet = theme.format.bullet;
61
+ const contentWidth = Math.max(8, width - 2 - Bun.stringWidth(bullet) - 1);
62
+ for (const content of shown) {
63
+ const value = truncateToWidth(content, contentWidth, Ellipsis.Unicode);
64
+ lines.push(` ${theme.fg("muted", bullet)} ${theme.fg("toolOutput", value)}`);
65
+ }
66
+ const remaining = contents.length - shown.length;
67
+ if (remaining > 0) {
68
+ lines.push(` ${theme.fg("dim", `… ${remaining} more`)} ${formatExpandHint(theme, expanded, true)}`);
69
+ }
70
+ return lines.map(line => truncateToWidth(line, width, Ellipsis.Omit));
71
+ });
72
+ }
73
+
74
+ export const retainToolRenderer = {
75
+ inline: true,
76
+ mergeCallAndResult: true,
77
+ renderCall(args: RetainRenderArgs, options: RenderResultOptions, theme: Theme): Component {
78
+ const contents = retainContents(args);
79
+ const header = renderStatusLine({ icon: "pending", title: "Retain" }, theme);
80
+ return retainComponent(contents, header, () => options.expanded, theme);
81
+ },
82
+ renderResult(
83
+ result: { content: Array<{ type: string; text?: string }>; details?: { count?: number }; isError?: boolean },
84
+ options: RenderResultOptions,
85
+ theme: Theme,
86
+ args?: RetainRenderArgs,
87
+ ): Component {
88
+ if (result.isError) {
89
+ return new Text(formatErrorMessage(resultText(result) || "Retain failed", theme), 0, 0);
90
+ }
91
+ const contents = retainContents(args);
92
+ // `summary` is the tool's own "N memories stored/queued." line; drop the
93
+ // trailing period so it reads cleanly as a status meta segment.
94
+ const summary = resultText(result).replace(/\.$/, "");
95
+ const header = renderStatusLine(
96
+ { icon: "success", title: "Retain", meta: summary ? [summary] : undefined },
97
+ theme,
98
+ );
99
+ return retainComponent(contents, header, () => options.expanded, theme);
100
+ },
101
+ };
102
+
103
+ export const recallToolRenderer = {
104
+ inline: true,
105
+ mergeCallAndResult: true,
106
+ renderCall(args: QueryRenderArgs, _options: RenderResultOptions, theme: Theme): Component {
107
+ return new Text(queryHeader("Recall", args.query, "pending", theme), 0, 0);
108
+ },
109
+ renderResult(
110
+ result: { content: Array<{ type: string; text?: string }>; isError?: boolean },
111
+ options: RenderResultOptions,
112
+ theme: Theme,
113
+ args?: QueryRenderArgs,
114
+ ): Component {
115
+ if (result.isError) {
116
+ return new Text(formatErrorMessage(resultText(result) || "Recall failed", theme), 0, 0);
117
+ }
118
+ const text = resultText(result);
119
+ const match = text.match(/^Found (\d+) relevant/);
120
+ const found = match ? Number(match[1]) : 0;
121
+ const icon: ToolUIStatus = found > 0 ? "success" : "warning";
122
+ const meta = [found > 0 ? `${found} found` : "no matches"];
123
+ const header = queryHeader("Recall", args?.query, icon, theme, meta);
124
+ if (found === 0) {
125
+ return new Text(header, 0, 0);
126
+ }
127
+ // Collapsed view is the header alone; expand to inspect the recalled
128
+ // memories without dumping the whole block into the transcript.
129
+ const body = text.replace(/^[^\n]*\n+/, "");
130
+ return createCachedComponent(
131
+ () => options.expanded,
132
+ (width, expanded) => {
133
+ const lines = [header];
134
+ if (expanded) {
135
+ const bodyLines = body.split("\n").slice(0, PREVIEW_LIMITS.OUTPUT_EXPANDED);
136
+ for (const line of bodyLines) {
137
+ lines.push(` ${theme.fg("muted", replaceTabs(line))}`);
138
+ }
139
+ } else {
140
+ lines.push(` ${formatExpandHint(theme, false, true)}`);
141
+ }
142
+ return lines.map(line => truncateToWidth(line, width, Ellipsis.Omit));
143
+ },
144
+ );
145
+ },
146
+ };
147
+
148
+ export const reflectToolRenderer = {
149
+ inline: true,
150
+ mergeCallAndResult: true,
151
+ renderCall(args: QueryRenderArgs, _options: RenderResultOptions, theme: Theme): Component {
152
+ return new Text(queryHeader("Reflect", args.query, "pending", theme), 0, 0);
153
+ },
154
+ renderResult(
155
+ result: { content: Array<{ type: string; text?: string }>; isError?: boolean },
156
+ options: RenderResultOptions,
157
+ theme: Theme,
158
+ args?: QueryRenderArgs,
159
+ ): Component {
160
+ if (result.isError) {
161
+ return new Text(formatErrorMessage(resultText(result) || "Reflect failed", theme), 0, 0);
162
+ }
163
+ const header = queryHeader("Reflect", args?.query, "success", theme);
164
+ const answer = resultText(result);
165
+ const answerLines = answer.split("\n").filter(line => line.trim().length > 0);
166
+ return createCachedComponent(
167
+ () => options.expanded,
168
+ (width, expanded) => {
169
+ const limit = expanded ? PREVIEW_LIMITS.OUTPUT_EXPANDED : PREVIEW_LIMITS.OUTPUT_COLLAPSED;
170
+ const shown = answerLines.slice(0, limit);
171
+ const lines = [header];
172
+ for (const line of shown) {
173
+ lines.push(` ${theme.fg("toolOutput", replaceTabs(line))}`);
174
+ }
175
+ const remaining = answerLines.length - shown.length;
176
+ if (remaining > 0) {
177
+ lines.push(
178
+ ` ${theme.fg("dim", `… ${remaining} more lines`)} ${formatExpandHint(theme, expanded, true)}`,
179
+ );
180
+ }
181
+ return lines.map(line => truncateToWidth(line, width, Ellipsis.Omit));
182
+ },
183
+ );
184
+ },
185
+ };