@graphorin/memory 0.6.1 → 0.7.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 (227) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +1 -1
  3. package/dist/conflict/index.js +0 -1
  4. package/dist/conflict/pipeline.js +0 -1
  5. package/dist/conflict/pipeline.js.map +1 -1
  6. package/dist/conflict/stages/stage3-heuristic-regex.js +0 -1
  7. package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
  8. package/dist/conflict/types.d.ts +1 -1
  9. package/dist/conflict/types.js.map +1 -1
  10. package/dist/consolidator/index.js +2 -0
  11. package/dist/consolidator/index.js.map +1 -1
  12. package/dist/consolidator/lock.js +7 -1
  13. package/dist/consolidator/lock.js.map +1 -1
  14. package/dist/consolidator/phases/deep.js +19 -12
  15. package/dist/consolidator/phases/deep.js.map +1 -1
  16. package/dist/consolidator/phases/induce.d.ts.map +1 -1
  17. package/dist/consolidator/phases/induce.js +2 -3
  18. package/dist/consolidator/phases/induce.js.map +1 -1
  19. package/dist/consolidator/phases/reflect.js +35 -8
  20. package/dist/consolidator/phases/reflect.js.map +1 -1
  21. package/dist/consolidator/phases/standard.js +199 -28
  22. package/dist/consolidator/phases/standard.js.map +1 -1
  23. package/dist/consolidator/reconcile.js +16 -15
  24. package/dist/consolidator/reconcile.js.map +1 -1
  25. package/dist/consolidator/runtime.d.ts.map +1 -1
  26. package/dist/consolidator/runtime.js +129 -47
  27. package/dist/consolidator/runtime.js.map +1 -1
  28. package/dist/consolidator/scheduler.d.ts +17 -3
  29. package/dist/consolidator/scheduler.d.ts.map +1 -1
  30. package/dist/consolidator/scheduler.js.map +1 -1
  31. package/dist/consolidator/types.d.ts +30 -1
  32. package/dist/consolidator/types.d.ts.map +1 -1
  33. package/dist/consolidator/types.js +10 -0
  34. package/dist/consolidator/types.js.map +1 -1
  35. package/dist/context-engine/compaction/clear-tool-results.d.ts +1 -1
  36. package/dist/context-engine/compaction/clear-tool-results.js +1 -1
  37. package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
  38. package/dist/context-engine/compaction/compactor.d.ts +1 -0
  39. package/dist/context-engine/compaction/compactor.d.ts.map +1 -1
  40. package/dist/context-engine/compaction/compactor.js +32 -8
  41. package/dist/context-engine/compaction/compactor.js.map +1 -1
  42. package/dist/context-engine/compaction/hooks/types.d.ts +1 -1
  43. package/dist/context-engine/compaction/index.d.ts +1 -0
  44. package/dist/context-engine/compaction/templates/marker.d.ts +33 -0
  45. package/dist/context-engine/compaction/templates/marker.d.ts.map +1 -0
  46. package/dist/context-engine/compaction/templates/marker.js +34 -0
  47. package/dist/context-engine/compaction/templates/marker.js.map +1 -0
  48. package/dist/context-engine/compaction/templates/summary-9-section.d.ts +1 -1
  49. package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
  50. package/dist/context-engine/compaction/templates/summary-9-section.js +4 -3
  51. package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
  52. package/dist/context-engine/engine.d.ts +7 -176
  53. package/dist/context-engine/engine.d.ts.map +1 -1
  54. package/dist/context-engine/engine.js +0 -2
  55. package/dist/context-engine/engine.js.map +1 -1
  56. package/dist/context-engine/index.d.ts +8 -49
  57. package/dist/context-engine/io-types.d.ts +144 -0
  58. package/dist/context-engine/io-types.d.ts.map +1 -0
  59. package/dist/context-engine/locale-packs/types.d.ts +1 -1
  60. package/dist/context-engine/locale-packs/types.js.map +1 -1
  61. package/dist/context-engine/privacy-filter.d.ts +1 -2
  62. package/dist/context-engine/privacy-filter.d.ts.map +1 -1
  63. package/dist/context-engine/privacy-filter.js +1 -2
  64. package/dist/context-engine/privacy-filter.js.map +1 -1
  65. package/dist/errors/index.d.ts +2 -2
  66. package/dist/errors/index.js +2 -2
  67. package/dist/errors/index.js.map +1 -1
  68. package/dist/facade.d.ts +43 -52
  69. package/dist/facade.d.ts.map +1 -1
  70. package/dist/facade.js +5 -3
  71. package/dist/facade.js.map +1 -1
  72. package/dist/graph/entity-resolver.js +1 -1
  73. package/dist/graph/entity-resolver.js.map +1 -1
  74. package/dist/index.d.ts +16 -13
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +2 -5
  77. package/dist/index.js.map +1 -1
  78. package/dist/internal/envelope.js +57 -0
  79. package/dist/internal/envelope.js.map +1 -0
  80. package/dist/internal/injection-heuristics.js +0 -0
  81. package/dist/internal/injection-heuristics.js.map +1 -1
  82. package/dist/internal/llm-json.js +40 -0
  83. package/dist/internal/llm-json.js.map +1 -0
  84. package/dist/internal/storage-adapter.d.ts +23 -10
  85. package/dist/internal/storage-adapter.d.ts.map +1 -1
  86. package/dist/memory-interface.d.ts +131 -0
  87. package/dist/memory-interface.d.ts.map +1 -0
  88. package/dist/package.js +1 -1
  89. package/dist/package.js.map +1 -1
  90. package/dist/search/iterative.d.ts.map +1 -1
  91. package/dist/search/iterative.js +2 -3
  92. package/dist/search/iterative.js.map +1 -1
  93. package/dist/search/query-transform.d.ts.map +1 -1
  94. package/dist/search/query-transform.js +2 -3
  95. package/dist/search/query-transform.js.map +1 -1
  96. package/dist/tiers/episodic-memory.js +3 -3
  97. package/dist/tiers/episodic-memory.js.map +1 -1
  98. package/dist/tiers/index.d.ts +2 -2
  99. package/dist/tiers/insight-memory.js +1 -1
  100. package/dist/tiers/insight-memory.js.map +1 -1
  101. package/dist/tiers/procedural-memory.d.ts +1 -1
  102. package/dist/tiers/procedural-memory.js +2 -2
  103. package/dist/tiers/procedural-memory.js.map +1 -1
  104. package/dist/tiers/semantic-memory.d.ts +58 -4
  105. package/dist/tiers/semantic-memory.d.ts.map +1 -1
  106. package/dist/tiers/semantic-memory.js +103 -20
  107. package/dist/tiers/semantic-memory.js.map +1 -1
  108. package/dist/tools/block-tools.d.ts +19 -57
  109. package/dist/tools/block-tools.d.ts.map +1 -1
  110. package/dist/tools/block-tools.js.map +1 -1
  111. package/dist/tools/fact-tools.d.ts +53 -214
  112. package/dist/tools/fact-tools.d.ts.map +1 -1
  113. package/dist/tools/fact-tools.js +3 -2
  114. package/dist/tools/fact-tools.js.map +1 -1
  115. package/dist/tools/recall-tools.d.ts +34 -175
  116. package/dist/tools/recall-tools.d.ts.map +1 -1
  117. package/dist/tools/recall-tools.js +5 -4
  118. package/dist/tools/recall-tools.js.map +1 -1
  119. package/dist/tools/runbook-tools.d.ts +15 -58
  120. package/dist/tools/runbook-tools.d.ts.map +1 -1
  121. package/dist/tools/runbook-tools.js.map +1 -1
  122. package/dist/tools/types.d.ts +1 -1
  123. package/package.json +18 -17
  124. package/src/conflict/index.ts +29 -0
  125. package/src/conflict/locale-packs/en.ts +142 -0
  126. package/src/conflict/locale-packs/index.ts +17 -0
  127. package/src/conflict/locale-packs/types.ts +138 -0
  128. package/src/conflict/pipeline.ts +371 -0
  129. package/src/conflict/stages/helpers.ts +65 -0
  130. package/src/conflict/stages/stage1-exact-dedup.ts +40 -0
  131. package/src/conflict/stages/stage2-embedding-three-zone.ts +54 -0
  132. package/src/conflict/stages/stage3-heuristic-regex.ts +51 -0
  133. package/src/conflict/stages/stage4-subject-predicate.ts +46 -0
  134. package/src/conflict/stages/stage5-defer-to-deep.ts +34 -0
  135. package/src/conflict/types.ts +263 -0
  136. package/src/consolidator/budget.ts +312 -0
  137. package/src/consolidator/decay.ts +246 -0
  138. package/src/consolidator/dlq.ts +61 -0
  139. package/src/consolidator/errors.ts +87 -0
  140. package/src/consolidator/idempotency.ts +27 -0
  141. package/src/consolidator/index.ts +269 -0
  142. package/src/consolidator/lock.ts +141 -0
  143. package/src/consolidator/noise-filter.ts +235 -0
  144. package/src/consolidator/phases/deep.ts +308 -0
  145. package/src/consolidator/phases/induce.ts +423 -0
  146. package/src/consolidator/phases/learned-context.ts +233 -0
  147. package/src/consolidator/phases/light.ts +166 -0
  148. package/src/consolidator/phases/reflect.ts +504 -0
  149. package/src/consolidator/phases/standard.ts +1002 -0
  150. package/src/consolidator/reconcile.ts +273 -0
  151. package/src/consolidator/runtime.ts +1049 -0
  152. package/src/consolidator/scheduler.ts +182 -0
  153. package/src/consolidator/triggers.ts +134 -0
  154. package/src/consolidator/types.ts +622 -0
  155. package/src/context-engine/annotations.ts +161 -0
  156. package/src/context-engine/auto-recall.ts +92 -0
  157. package/src/context-engine/compaction/clear-tool-results.ts +232 -0
  158. package/src/context-engine/compaction/compactor.ts +403 -0
  159. package/src/context-engine/compaction/hooks/reanchor-persona-block.ts +55 -0
  160. package/src/context-engine/compaction/hooks/reanchor-pinned-facts.ts +66 -0
  161. package/src/context-engine/compaction/hooks/reanchor-project-rules.ts +57 -0
  162. package/src/context-engine/compaction/hooks/reanchor-recent-results.ts +116 -0
  163. package/src/context-engine/compaction/hooks/types.ts +68 -0
  164. package/src/context-engine/compaction/index.ts +60 -0
  165. package/src/context-engine/compaction/templates/marker.ts +33 -0
  166. package/src/context-engine/compaction/templates/summary-9-section.ts +223 -0
  167. package/src/context-engine/compaction/thresholds.ts +107 -0
  168. package/src/context-engine/compaction/types.ts +255 -0
  169. package/src/context-engine/engine.ts +970 -0
  170. package/src/context-engine/index.ts +118 -0
  171. package/src/context-engine/io-types.ts +162 -0
  172. package/src/context-engine/locale-packs/en.ts +107 -0
  173. package/src/context-engine/locale-packs/index.ts +19 -0
  174. package/src/context-engine/locale-packs/resolver.ts +197 -0
  175. package/src/context-engine/locale-packs/types.ts +177 -0
  176. package/src/context-engine/metadata.ts +114 -0
  177. package/src/context-engine/preambles/inbound-en.ts +19 -0
  178. package/src/context-engine/privacy-filter.ts +190 -0
  179. package/src/context-engine/templates/base-en.ts +19 -0
  180. package/src/context-engine/templates/composer.ts +91 -0
  181. package/src/context-engine/token-budget.ts +319 -0
  182. package/src/context-engine/token-counter.ts +157 -0
  183. package/src/errors/index.ts +273 -0
  184. package/src/facade.ts +750 -0
  185. package/src/graph/entity-resolver.ts +364 -0
  186. package/src/graph/index.ts +24 -0
  187. package/src/index.ts +259 -0
  188. package/src/internal/contextualize.ts +173 -0
  189. package/src/internal/embedder-binding.ts +53 -0
  190. package/src/internal/envelope.ts +70 -0
  191. package/src/internal/id.ts +23 -0
  192. package/src/internal/injection-heuristics.ts +0 -0
  193. package/src/internal/llm-json.ts +38 -0
  194. package/src/internal/spans.ts +35 -0
  195. package/src/internal/storage-adapter.ts +963 -0
  196. package/src/memory-interface.ts +158 -0
  197. package/src/migration/embedder-migration.ts +237 -0
  198. package/src/migration/index.ts +13 -0
  199. package/src/search/explain.ts +123 -0
  200. package/src/search/fit-weights.ts +109 -0
  201. package/src/search/graph-ppr.ts +38 -0
  202. package/src/search/index.ts +55 -0
  203. package/src/search/iterative.ts +658 -0
  204. package/src/search/query-transform.ts +285 -0
  205. package/src/search/rrf.ts +225 -0
  206. package/src/search/trust.ts +55 -0
  207. package/src/search/types.ts +50 -0
  208. package/src/tiers/episodic-memory.ts +463 -0
  209. package/src/tiers/index.ts +50 -0
  210. package/src/tiers/insight-memory.ts +208 -0
  211. package/src/tiers/procedural-memory.ts +482 -0
  212. package/src/tiers/semantic-memory.ts +1729 -0
  213. package/src/tiers/session-memory.ts +271 -0
  214. package/src/tiers/shared-memory.ts +62 -0
  215. package/src/tiers/working-memory.ts +368 -0
  216. package/src/tools/block-tools.ts +172 -0
  217. package/src/tools/fact-tools.ts +473 -0
  218. package/src/tools/index.ts +109 -0
  219. package/src/tools/recall-tools.ts +303 -0
  220. package/src/tools/runbook-tools.ts +107 -0
  221. package/src/tools/taint.ts +39 -0
  222. package/src/tools/types.ts +37 -0
  223. package/dist/conflict/locale-packs/index.js +0 -4
  224. package/dist/context-engine/compaction/index.js +0 -10
  225. package/dist/context-engine/index.d.ts.map +0 -1
  226. package/dist/context-engine/index.js +0 -25
  227. package/dist/context-engine/locale-packs/index.js +0 -4
@@ -0,0 +1,131 @@
1
+ import { ConflictPipeline } from "./conflict/types.js";
2
+ import "./conflict/index.js";
3
+ import { EpisodicMemory } from "./tiers/episodic-memory.js";
4
+ import { SemanticMemory } from "./tiers/semantic-memory.js";
5
+ import { WorkingMemory } from "./tiers/working-memory.js";
6
+ import { Consolidator } from "./consolidator/runtime.js";
7
+ import { CompactionResult, CompactionSource, CompactionSummarizer } from "./context-engine/compaction/types.js";
8
+ import { AssembleInput, AssembledPrompt, CompileOptions, CompileScope, MemoryContextBlocks, ResolvedContextEngineConfig } from "./context-engine/io-types.js";
9
+ import { InsightMemory } from "./tiers/insight-memory.js";
10
+ import { ProceduralMemory } from "./tiers/procedural-memory.js";
11
+ import { SessionMemory } from "./tiers/session-memory.js";
12
+ import { SharedMemory } from "./tiers/shared-memory.js";
13
+ import { EmbedderProvider, MemoryMetadata, Message, MessageContent, SessionScope, Tool } from "@graphorin/core";
14
+
15
+ //#region src/memory-interface.d.ts
16
+
17
+ /**
18
+ * Public surface of the {@link ContextEngine} instance returned by
19
+ * {@link createContextEngine}.
20
+ *
21
+ * @stable
22
+ */
23
+ interface ContextEngine {
24
+ /** Assemble the layered system prompt for a single step. */
25
+ assemble(memory: Memory, input: AssembleInput): Promise<AssembledPrompt>;
26
+ /**
27
+ * Trigger evaluation primitive used by Phase 12 (agent runtime)
28
+ * at the top of every step. Returns `true` when the in-flight
29
+ * buffer's token count crosses the per-provider trigger
30
+ * threshold. Pass `precomputedTokens` to amortize the count
31
+ * via the per-message cache surfaced by
32
+ * `SessionMemoryStoreExt.totalCachedTokens(scope)` (DEC-131) -
33
+ * the production hot path is an O(1) comparison when the cache
34
+ * is warm.
35
+ */
36
+ shouldCompact(messages: ReadonlyArray<Message>, options?: {
37
+ readonly precomputedTokens?: number;
38
+ /**
39
+ * Index of the first COMPACTABLE message (context-engine-04): the
40
+ * caller's pinned, never-compacted system prefix ends here. The
41
+ * SOTA-4 reclaim floor counts only `messages.slice(from)` older
42
+ * turns as reclaimable - without it a large system prompt is
43
+ * counted as reclaimable and the floor fires the summarizer for
44
+ * near-zero real reclaim. Default `0` (everything compactable).
45
+ */
46
+ readonly compactableFromIndex?: number;
47
+ }): Promise<boolean>;
48
+ /**
49
+ * Run a compaction call. Phase 12 calls this when the trigger
50
+ * fires (`source: 'auto-trigger'`) or the operator invokes
51
+ * `agent.compact(...)` (`source: 'manual'`).
52
+ */
53
+ compactNow(input: {
54
+ readonly scope: SessionScope;
55
+ readonly runId: string;
56
+ readonly sessionId: string;
57
+ readonly agentId: string;
58
+ readonly source: CompactionSource;
59
+ readonly messages: ReadonlyArray<Message>;
60
+ readonly memory: Memory;
61
+ readonly summarizer?: CompactionSummarizer;
62
+ /** Per-call override of the strategy's preserve-recent count (CE-3). */
63
+ readonly preserveRecentTurns?: number;
64
+ /** Topic/tags narrowing for the procedural-rules re-anchor hook (CE-6). */
65
+ readonly procedural?: {
66
+ readonly topic?: string;
67
+ readonly tags?: ReadonlyArray<string>;
68
+ };
69
+ /**
70
+ * The caller's pinned system prefix - the messages EXCLUDED from
71
+ * `messages` before this call (context-engine-04). Used only for
72
+ * accounting: the anti-thrash guard and the "still above threshold"
73
+ * warning must compare against the FULL post-splice context
74
+ * (prefix + summary + preserved + essentials), or a real system
75
+ * prompt defeats the guard and a summarizer call fires every step
76
+ * at the context edge. Never compacted, never returned.
77
+ */
78
+ readonly prefixMessages?: ReadonlyArray<Message>;
79
+ readonly signal?: AbortSignal;
80
+ }): Promise<{
81
+ readonly result: CompactionResult;
82
+ readonly extraContent: ReadonlyArray<MessageContent>;
83
+ readonly hookFailures: ReadonlyArray<{
84
+ readonly hookName: string;
85
+ readonly reason: string;
86
+ }>;
87
+ }>;
88
+ /** Resolved configuration snapshot. */
89
+ config(): ResolvedContextEngineConfig;
90
+ }
91
+ /**
92
+ * The facade returned by {@link createMemory}.
93
+ *
94
+ * @stable
95
+ */
96
+ interface Memory {
97
+ readonly working: WorkingMemory;
98
+ readonly session: SessionMemory;
99
+ readonly episodic: EpisodicMemory;
100
+ readonly semantic: SemanticMemory;
101
+ readonly procedural: ProceduralMemory;
102
+ readonly shared: SharedMemory;
103
+ /**
104
+ * Read surface over reflection insights (P1-1). A no-op (returns
105
+ * empty) when the storage adapter does not expose the optional
106
+ * insight surface.
107
+ */
108
+ readonly insights: InsightMemory;
109
+ readonly tools: ReadonlyArray<Tool>;
110
+ readonly consolidator: Consolidator;
111
+ /** The configured conflict pipeline. Surfaced for tests + CLI tooling. */
112
+ readonly conflictPipeline: ConflictPipeline;
113
+ /** The configured context engine (Phase 10d). */
114
+ readonly contextEngine: ContextEngine;
115
+ /** The active embedder, when configured. `null` otherwise. */
116
+ readonly embedder: EmbedderProvider | null;
117
+ /** The canonical id of the active embedder, when configured. */
118
+ embedderId(): string | null;
119
+ /**
120
+ * Compile a system-prompt block bundle. The bundle carries the
121
+ * static fragments per memory tier; the agent runtime consumes
122
+ * the {@link ContextEngine} surface (`memory.contextEngine`)
123
+ * directly for the full six-layer assembly.
124
+ */
125
+ compile(scope: CompileScope, options?: CompileOptions): Promise<MemoryContextBlocks>;
126
+ /** Counter snapshot consumed by Phase 10d's metadata layer. */
127
+ metadata(scope: SessionScope): Promise<MemoryMetadata>;
128
+ }
129
+ //#endregion
130
+ export { ContextEngine, Memory };
131
+ //# sourceMappingURL=memory-interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-interface.d.ts","names":[],"sources":["../src/memory-interface.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAiHyC,UA3DxB,aAAA,CA2DwB;EAAd;EACA,QAAA,CAAA,MAAA,EA1DR,MA0DQ,EAAA,KAAA,EA1DO,aA0DP,CAAA,EA1DuB,OA0DvB,CA1D+B,eA0D/B,CAAA;EAHrB;;;AAcN;;;;;;;EAYqB,aAAA,CAAA,QAAA,EArEP,aAqEO,CArEO,OAqEP,CAAA,EAAA,OAmBH,CAnBG,EAAA;IACW,SAAA,iBAAA,CAAA,EAAA,MAAA;IAAd;;;;;;;;IAgBwC,SAAA,oBAAA,CAAA,EAAA,MAAA;EAExC,CAAA,CAAA,EA3Eb,OA2Ea,CAAA,OAAA,CAAA;EAAuB;;;;;;oBApErB;;;;qBAIC;uBACE,cAAc;qBAChB;0BACK;;;;;;sBAI2C;;;;;;;;;;;8BAUvC,cAAc;sBACtB;MAChB;qBACe;2BACM,cAAc;2BACd;;;;;;YAGf;;;;;;;UAQK,MAAA;oBACG;oBACA;qBACC;qBACA;uBACE;mBACJ;;;;;;qBAME;kBACH,cAAc;yBACP;;6BAEI;;0BAEH;;qBAEL;;;;;;;;;iBASJ,wBAAwB,iBAAiB,QAAQ;;kBAEhD,eAAe,QAAQ"}
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.6.1";
2
+ var version = "0.7.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
@@ -1 +1 @@
1
- {"version":3,"file":"package.js","names":[],"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"@graphorin/memory\",\n \"version\": \"0.6.1\",\n \"description\": \"Six-tier memory system for the Graphorin framework: createMemory() facade with working / session / episodic / semantic / procedural / shared sub-modules, eleven memory tools wired into @graphorin/tools, hybrid vector + FTS5 search composed through Reciprocal Rank Fusion (k=60 default) with a pluggable reranker hook, a multi-stage conflict resolution pipeline (exact dedup → embedding three-zone → heuristic regex → subject/predicate → defer-to-deep) with an English locale pack default + plug-in locales, an embedder migration runner with three coexistence policies (lock-on-first / multi-active / auto-migrate), per-record embedder_id enforcement, multi-agent attribution, append-only writes with soft-delete + supersede semantics, default-on bi-temporal storage, provenance + quarantine safety gate for synthesized memory, AISpan emission for every operation, and tier-aware memory-modification guard wiring.\",\n \"license\": \"MIT\",\n \"author\": \"Oleksiy Stepurenko\",\n \"homepage\": \"https://github.com/o-stepper/graphorin/tree/main/packages/memory\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/o-stepper/graphorin.git\",\n \"directory\": \"packages/memory\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/o-stepper/graphorin/issues\"\n },\n \"keywords\": [\n \"graphorin\",\n \"ai\",\n \"agents\",\n \"framework\",\n \"memory\",\n \"long-term-memory\",\n \"working-memory\",\n \"session-memory\",\n \"episodic-memory\",\n \"semantic-memory\",\n \"procedural-memory\",\n \"shared-memory\",\n \"rrf\",\n \"hybrid-search\",\n \"rerank\",\n \"conflict-resolution\",\n \"embedder-migration\",\n \"bi-temporal\",\n \"multi-agent\"\n ],\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=22.0.0\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n },\n \"./facade\": {\n \"types\": \"./dist/facade.d.ts\",\n \"import\": \"./dist/facade.js\"\n },\n \"./tiers\": {\n \"types\": \"./dist/tiers/index.d.ts\",\n \"import\": \"./dist/tiers/index.js\"\n },\n \"./tools\": {\n \"types\": \"./dist/tools/index.d.ts\",\n \"import\": \"./dist/tools/index.js\"\n },\n \"./search\": {\n \"types\": \"./dist/search/index.d.ts\",\n \"import\": \"./dist/search/index.js\"\n },\n \"./conflict\": {\n \"types\": \"./dist/conflict/index.d.ts\",\n \"import\": \"./dist/conflict/index.js\"\n },\n \"./migration\": {\n \"types\": \"./dist/migration/index.d.ts\",\n \"import\": \"./dist/migration/index.js\"\n },\n \"./errors\": {\n \"types\": \"./dist/errors/index.d.ts\",\n \"import\": \"./dist/errors/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit && tsc -p tsconfig.tests.json\",\n \"test\": \"vitest run\",\n \"lint\": \"biome check .\",\n \"clean\": \"rimraf dist .turbo *.tsbuildinfo\"\n },\n \"dependencies\": {\n \"@graphorin/core\": \"workspace:*\",\n \"@graphorin/observability\": \"workspace:*\",\n \"@graphorin/security\": \"workspace:*\",\n \"@graphorin/tools\": \"workspace:*\"\n },\n \"peerDependencies\": {\n \"zod\": \"^3.23.0 || ^4.0.0\"\n },\n \"peerDependenciesMeta\": {\n \"zod\": {\n \"optional\": false\n }\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"provenance\": true\n },\n \"devDependencies\": {\n \"@graphorin/store-sqlite\": \"workspace:*\",\n \"@graphorin/embedder-transformersjs\": \"workspace:*\",\n \"@graphorin/triggers\": \"workspace:*\",\n \"@types/better-sqlite3\": \"^7.6.13\",\n \"better-sqlite3\": \"^12.9.0\",\n \"fast-check\": \"^3.23.0\",\n \"zod\": \"^3.25.0\"\n }\n}\n"],"mappings":";cAEa"}
1
+ {"version":3,"file":"package.js","names":[],"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"@graphorin/memory\",\n \"version\": \"0.7.0\",\n \"description\": \"Six-tier memory system for the Graphorin framework: createMemory() facade with working / session / episodic / semantic / procedural / shared sub-modules, eleven memory tools wired into @graphorin/tools, hybrid vector + FTS5 search composed through Reciprocal Rank Fusion (k=60 default) with a pluggable reranker hook, a multi-stage conflict resolution pipeline (exact dedup → embedding three-zone → heuristic regex → subject/predicate → defer-to-deep) with an English locale pack default + plug-in locales, an embedder migration runner with three coexistence policies (lock-on-first / multi-active / auto-migrate), per-record embedder_id enforcement, multi-agent attribution, append-only writes with soft-delete + supersede semantics, default-on bi-temporal storage, provenance + quarantine safety gate for synthesized memory, AISpan emission for every operation, and tier-aware memory-modification guard wiring.\",\n \"license\": \"MIT\",\n \"author\": \"Oleksiy Stepurenko\",\n \"homepage\": \"https://github.com/o-stepper/graphorin/tree/main/packages/memory\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/o-stepper/graphorin.git\",\n \"directory\": \"packages/memory\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/o-stepper/graphorin/issues\"\n },\n \"keywords\": [\n \"graphorin\",\n \"ai\",\n \"agents\",\n \"framework\",\n \"memory\",\n \"long-term-memory\",\n \"working-memory\",\n \"session-memory\",\n \"episodic-memory\",\n \"semantic-memory\",\n \"procedural-memory\",\n \"shared-memory\",\n \"rrf\",\n \"hybrid-search\",\n \"rerank\",\n \"conflict-resolution\",\n \"embedder-migration\",\n \"bi-temporal\",\n \"multi-agent\"\n ],\n \"type\": \"module\",\n \"sideEffects\": false,\n \"engines\": {\n \"node\": \">=22.12.0\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"./facade\": {\n \"types\": \"./dist/facade.d.ts\",\n \"default\": \"./dist/facade.js\"\n },\n \"./tiers\": {\n \"types\": \"./dist/tiers/index.d.ts\",\n \"default\": \"./dist/tiers/index.js\"\n },\n \"./tools\": {\n \"types\": \"./dist/tools/index.d.ts\",\n \"default\": \"./dist/tools/index.js\"\n },\n \"./search\": {\n \"types\": \"./dist/search/index.d.ts\",\n \"default\": \"./dist/search/index.js\"\n },\n \"./conflict\": {\n \"types\": \"./dist/conflict/index.d.ts\",\n \"default\": \"./dist/conflict/index.js\"\n },\n \"./migration\": {\n \"types\": \"./dist/migration/index.d.ts\",\n \"default\": \"./dist/migration/index.js\"\n },\n \"./errors\": {\n \"types\": \"./dist/errors/index.d.ts\",\n \"default\": \"./dist/errors/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\n \"dist\",\n \"src\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit && tsc -p tsconfig.tests.json\",\n \"test\": \"vitest run\",\n \"lint\": \"biome check .\",\n \"clean\": \"rimraf dist .turbo *.tsbuildinfo\"\n },\n \"dependencies\": {\n \"@graphorin/core\": \"workspace:*\",\n \"@graphorin/observability\": \"workspace:*\",\n \"@graphorin/security\": \"workspace:*\",\n \"@graphorin/tools\": \"workspace:*\"\n },\n \"peerDependencies\": {\n \"zod\": \"^3.23.0 || ^4.0.0\"\n },\n \"peerDependenciesMeta\": {\n \"zod\": {\n \"optional\": false\n }\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"provenance\": true\n },\n \"devDependencies\": {\n \"@graphorin/store-sqlite\": \"workspace:*\",\n \"@graphorin/triggers\": \"workspace:*\",\n \"@types/better-sqlite3\": \"^7.6.13\",\n \"better-sqlite3\": \"^12.9.0\",\n \"fast-check\": \"^3.23.0\",\n \"zod\": \"^3.25.0\"\n }\n}\n"],"mappings":";cAEa"}
@@ -1 +1 @@
1
- {"version":3,"file":"iterative.d.ts","names":[],"sources":["../../src/search/iterative.ts"],"sourcesContent":[],"mappings":";;;;;AA0XiE,cArVpD,sBAAA,GAqVoD,CAAA;;;;;;AAclD,cA5VF,sBAAA,GA4VE,CAAA;;AAAkC,cAzVpC,4BAAA,GAyVoC,GAAA;;AAQjD;AAwBA;;;;;AAIW,UA1WM,oBAAA,CA0WN;EAAR,SAAA,IAAA,EAAA,OAAA;EAAO;;;oBArWU;;;;;;;UAQH,qBAAA;;;;;;;;;;;;;;iBAqCD,qBAAA,0BAEL,wBACR;;;;;;UA8Cc,cAAA;;;;;;;;;;;;;;;;;;UAmBA,qBAAA;;oBAEG;;;;;;;;0BAQM;;;;;;;;;;;;;UAcT,eAAA;iCAGH,iCACA,wBACT,QAAQ;;;;;;;;cASA;;;;;;;iBAiBG,iBAAA,0BAEJ;;oBAGU;0BACM;IAEzB;;;;;;;;;;;;;;iBAkCa,UAAA,4BAAsC;;;;;;;;;iBA4BtC,6BAAA,WACJ;;IAET;;;;;;UA4Bc;;iBAEA,cAAc;;;;;;;;;;;;;;;;;;;;;oBAqBX;;;;;;;;UASH;;;;;;mDAMkC,cAAc,QAAQ,cAAc;;iBAEtE;;YAEL;;UAEF;;;;;;;;eAQK,cAAc,cAAc,MAAM,cAAc;;;;;;;UAQ9C,yBAAA;;;;wBAIO;;;;;;;oBAOJ;;;;;;;;;;;;iBAaE,8CAEd,uBAAuB,cACpB,4BACR,QAAQ,yBAAyB"}
1
+ {"version":3,"file":"iterative.d.ts","names":[],"sources":["../../src/search/iterative.ts"],"sourcesContent":[],"mappings":";;;;;AA2XiE,cArVpD,sBAAA,GAqVoD,CAAA;;;;;;AAclD,cA5VF,sBAAA,GA4VE,CAAA;;AAAkC,cAzVpC,4BAAA,GAyVoC,GAAA;;AAQjD;AAwBA;;;;;AAIW,UA1WM,oBAAA,CA0WN;EAAR,SAAA,IAAA,EAAA,OAAA;EAAO;;;oBArWU;;;;;;;UAQH,qBAAA;;;;;;;;;;;;;;iBAqCD,qBAAA,0BAEL,wBACR;;;;;;UA8Cc,cAAA;;;;;;;;;;;;;;;;;;UAmBA,qBAAA;;oBAEG;;;;;;;;0BAQM;;;;;;;;;;;;;UAcT,eAAA;iCAGH,iCACA,wBACT,QAAQ;;;;;;;;cASA;;;;;;;iBAiBG,iBAAA,0BAEJ;;oBAGU;0BACM;IAEzB;;;;;;;;;;;;;;iBAkCa,UAAA,4BAAsC;;;;;;;;;iBA4BtC,6BAAA,WACJ;;IAET;;;;;;UA4Bc;;iBAEA,cAAc;;;;;;;;;;;;;;;;;;;;;oBAqBX;;;;;;;;UASH;;;;;;mDAMkC,cAAc,QAAQ,cAAc;;iBAEtE;;YAEL;;UAEF;;;;;;;;eAQK,cAAc,cAAc,MAAM,cAAc;;;;;;;UAQ9C,yBAAA;;;;wBAIO;;;;;;;oBAOJ;;;;;;;;;;;;iBAaE,8CAEd,uBAAuB,cACpB,4BACR,QAAQ,yBAAyB"}
@@ -1,3 +1,5 @@
1
+ import { stripFence } from "../internal/llm-json.js";
2
+
1
3
  //#region src/search/iterative.ts
2
4
  /** Default hard ceiling on total retrieval passes (gate + cap mandatory). */
3
5
  const DEFAULT_MAX_ITERATIONS = 3;
@@ -319,9 +321,6 @@ function tryJson(text) {
319
321
  return;
320
322
  }
321
323
  }
322
- function stripFence(text) {
323
- return /^```[^\n]*\n([\s\S]*?)\n```/u.exec(text.trim())?.[1] ?? text;
324
- }
325
324
  function isRecord(value) {
326
325
  return value !== null && typeof value === "object" && !Array.isArray(value);
327
326
  }
@@ -1 +1 @@
1
- {"version":3,"file":"iterative.js","names":["signals: string[]","stop: RetrievalGrade","passLists: Array<ReadonlyArray<H>>","queries: string[]","out: H[]"],"sources":["../../src/search/iterative.ts"],"sourcesContent":["/**\n * Agentic / iterative retrieval for hard queries (P2-4) - a **gated**,\n * CRAG/Self-RAG-style grade-then-reformulate loop.\n *\n * Some multi-hop / temporal questions can't be answered from a single\n * retrieval pass; the naive system returns a confident-but-wrong answer\n * instead of retrieving again. This module adds a small loop: grade the\n * retrieved memories for sufficiency and, when weak, reformulate the\n * query and retrieve once more (optionally widening to the P2-1 graph) -\n * up to a hard iteration cap, then **abstain** rather than confabulate.\n *\n * Both CRAG (arXiv:2401.15884) and Self-RAG (arXiv:2310.11511) add\n * latency/cost, so the literature's advice - and this module's design -\n * is to *gate* the loop, not make it default:\n *\n * 1. A cheap, **local** {@link assessQueryDifficulty} heuristic decides\n * whether a query is even a candidate; simple lookups never enter\n * the loop (no provider call) and stay single-shot.\n * 2. The grade/reformulate step needs a {@link RetrievalGrader}; with\n * none configured the loop degrades to a single retrieval pass. The\n * only provider-backed grader, {@link createProviderRetrievalGrader},\n * is **resilient**: any provider error or unparseable output degrades\n * to a \"stop\" grade rather than throwing or looping forever.\n * 3. A mandatory iteration cap bounds the worst case regardless of the\n * grader.\n *\n * The module is provider-agnostic - it imports only `@graphorin/core`\n * types and never performs I/O itself; {@link runIterativeRetrieval} is a\n * pure orchestrator over an injected `retrieve` function + grader, so the\n * loop is unit-testable without a store or a model.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest } from '@graphorin/core';\n\n/** Default hard ceiling on total retrieval passes (gate + cap mandatory). */\nexport const DEFAULT_MAX_ITERATIONS = 3;\n\n/**\n * Absolute clamp on total retrieval passes, applied regardless of the\n * caller's `maxIterations` - a latency guardrail so a mis-configured\n * caller can never unbound the loop.\n */\nexport const MAX_ITERATIONS_CEILING = 5;\n\n/** Default score (0..1) at or above which a query is classified as hard. */\nexport const DEFAULT_DIFFICULTY_THRESHOLD = 0.5;\n\n/** Default output-token ceiling for a single grade call. */\nconst DEFAULT_GRADE_MAX_TOKENS = 256;\n\n/** Default number of retrieved snippets shown to the grader per pass. */\nconst DEFAULT_MAX_GRADE_SNIPPETS = 8;\n\n// ---------------------------------------------------------------------------\n// Difficulty gate (pure, local - no I/O, no provider)\n// ---------------------------------------------------------------------------\n\n/**\n * Outcome of the heuristic difficulty gate. `hard` is the gating\n * decision (`score >= threshold`); `signals` lists which heuristic\n * categories fired, for explainability / tracing.\n *\n * @stable\n */\nexport interface DifficultyAssessment {\n readonly hard: boolean;\n /** Aggregate difficulty score, clamped to `[0, 1]`. */\n readonly score: number;\n /** Heuristic categories that fired (`'multi-hop'`, `'temporal'`, …). */\n readonly signals: ReadonlyArray<string>;\n}\n\n/**\n * Tuning for {@link assessQueryDifficulty}.\n *\n * @stable\n */\nexport interface DifficultyGateOptions {\n /** Minimum score (0..1) to classify the query as hard. Default 0.5. */\n readonly threshold?: number;\n}\n\n/** Relational / referential markers implying a hop to another entity. */\nconst RE_REL_PRONOUN = /\\b(?:who|whom|whose|which)\\b/iu;\nconst RE_OF_CHAIN = /\\bthe\\s+\\w+\\s+of\\b/iu;\nconst RE_POSSESSIVE = /\\b[\\w-]+'s\\b/giu;\nconst RE_DESC_REF =\n /\\bthe\\s+(?:person|people|place|thing|one|company|team|book|restaurant|project|guy|woman|man)\\b/iu;\n/** Temporal ordering / relative-time markers. */\nconst RE_TEMPORAL =\n /\\b(?:before|after|when|while|during|since|until|first|last|earliest|latest|previously|originally|ago|prior|then|recently|used\\s+to)\\b/iu;\n/** Comparison / superlative markers. */\nconst RE_COMPARISON =\n /\\b(?:most|least|more|less|fewer|greater|biggest|largest|smallest|oldest|newest|best|worst|highest|lowest|compared?|versus|vs\\.?|between)\\b/iu;\n/** Clause-joining conjunctions used to count clauses. */\nconst RE_CONJUNCTION = /\\b(?:and|or|but|then)\\b/giu;\n\nconst W_MULTI_HOP = 0.4;\nconst W_TEMPORAL = 0.3;\nconst W_COMPARISON = 0.3;\nconst W_MULTI_CLAUSE = 0.2;\nconst W_LONG = 0.15;\nconst LONG_WORD_COUNT = 12;\n\n/**\n * Score a query's retrieval difficulty with cheap, deterministic, fully\n * **local** heuristics (no I/O). Conservative by design - it prefers to\n * leave a query single-shot (`hard: false`) unless several\n * multi-hop / temporal / comparison signals stack up, so the gate adds\n * passes only where they are likely to help. Used by\n * {@link runIterativeRetrieval} to decide whether to enter the loop.\n *\n * @stable\n */\nexport function assessQueryDifficulty(\n query: string,\n options: DifficultyGateOptions = {},\n): DifficultyAssessment {\n const threshold = options.threshold ?? DEFAULT_DIFFICULTY_THRESHOLD;\n const signals: string[] = [];\n let score = 0;\n\n const possessiveCount = (query.match(RE_POSSESSIVE) ?? []).length;\n const multiHop =\n RE_REL_PRONOUN.test(query) ||\n RE_OF_CHAIN.test(query) ||\n RE_DESC_REF.test(query) ||\n possessiveCount >= 2;\n if (multiHop) {\n signals.push('multi-hop');\n score += W_MULTI_HOP;\n }\n if (RE_TEMPORAL.test(query)) {\n signals.push('temporal');\n score += W_TEMPORAL;\n }\n if (RE_COMPARISON.test(query)) {\n signals.push('comparison');\n score += W_COMPARISON;\n }\n const clauseSignals = (query.match(RE_CONJUNCTION) ?? []).length + countCommas(query);\n if (clauseSignals >= 2 || questionMarkCount(query) >= 2) {\n signals.push('multi-clause');\n score += W_MULTI_CLAUSE;\n }\n if (wordCount(query) >= LONG_WORD_COUNT) {\n signals.push('long');\n score += W_LONG;\n }\n\n const clamped = Math.min(1, score);\n return { hard: clamped >= threshold, score: clamped, signals };\n}\n\n// ---------------------------------------------------------------------------\n// Retrieval grader seam (provider-agnostic)\n// ---------------------------------------------------------------------------\n\n/**\n * Verdict from grading a retrieved candidate set against a query.\n *\n * @stable\n */\nexport interface RetrievalGrade {\n /** Whether the retrieved memories suffice to answer the query. */\n readonly sufficient: boolean;\n /** Grader confidence, clamped to `[0, 1]`. */\n readonly confidence: number;\n /**\n * A single better search query to try next, or `null` when the grader\n * proposes none (the loop then stops / abstains).\n */\n readonly reformulation: string | null;\n /** Optional short rationale (never surfaced in spans). */\n readonly reason?: string;\n}\n\n/**\n * Per-call options for a {@link RetrievalGrader}.\n *\n * @stable\n */\nexport interface RetrievalGradeOptions {\n /** Cancellation signal forwarded to the underlying provider call. */\n readonly signal?: AbortSignal;\n /**\n * Reformulations already attempted (MRET-11). Surfaced to the grader\n * as context so it can propose something genuinely new - the grade\n * itself is ALWAYS judged against the original question, never a\n * reformulation (a narrowed sub-query must not be declared\n * \"sufficient\" while the original multi-hop question is not).\n */\n readonly triedQueries?: ReadonlyArray<string>;\n}\n\n/**\n * Pluggable retrieval-grader seam consumed by the iterative loop. The\n * built-in provider-backed implementation lives in\n * {@link createProviderRetrievalGrader}; advanced callers can supply a\n * bespoke grader (e.g. a deterministic heuristic).\n *\n * Implementations MUST degrade gracefully - return a \"stop\" grade rather\n * than throw - so a grader failure never breaks recall.\n *\n * @stable\n */\nexport interface RetrievalGrader {\n grade(\n query: string,\n snippets: ReadonlyArray<string>,\n options?: RetrievalGradeOptions,\n ): Promise<RetrievalGrade>;\n}\n\n/**\n * System prompt for the retrieval grader. Asks for a bare JSON verdict;\n * {@link parseGrade} also tolerates a chatty model.\n *\n * @internal\n */\nexport const RETRIEVAL_GRADE_SYSTEM_PROMPT =\n 'You are a retrieval grader for a personal memory store. Given a question and a ' +\n 'numbered list of retrieved memories, judge whether they are SUFFICIENT to answer ' +\n 'the question correctly and completely. Respond with ONLY a JSON object: ' +\n '{\"sufficient\": boolean, \"confidence\": number between 0 and 1, \"reformulation\": ' +\n 'string or null, \"reason\": string}. Set \"sufficient\" to true only if the memories ' +\n 'actually contain the answer - do not guess. When insufficient, set \"reformulation\" ' +\n 'to a single alternative search query likely to retrieve the missing information ' +\n '(vary entities, time frame, or specificity); otherwise null. Output only the JSON ' +\n 'object - no prose, no markdown.';\n\n/**\n * Build the grade request. Pure - no I/O. Temperature 0 so the verdict\n * is as stable as the model allows.\n *\n * @stable\n */\nexport function buildGradeRequest(\n query: string,\n snippets: ReadonlyArray<string>,\n options: {\n readonly maxTokens?: number;\n readonly signal?: AbortSignal;\n readonly triedQueries?: ReadonlyArray<string>;\n } = {},\n): ProviderRequest {\n const body =\n snippets.length === 0 ? '(none)' : snippets.map((s, i) => `[${i + 1}] ${s}`).join('\\n');\n const tried =\n options.triedQueries !== undefined && options.triedQueries.length > 0\n ? `\\n\\nQueries already tried (do not repeat them): ${options.triedQueries.join(' | ')}`\n : '';\n const content =\n `Question: ${query}\\n\\nRetrieved memories:\\n${body}${tried}\\n\\n` +\n 'Grade whether these memories are sufficient to answer the question. If not, ' +\n 'propose a single better search query.';\n return {\n messages: [{ role: 'user', content }],\n systemMessage: RETRIEVAL_GRADE_SYSTEM_PROMPT,\n temperature: 0,\n maxTokens: options.maxTokens ?? DEFAULT_GRADE_MAX_TOKENS,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n outputType: { kind: 'structured' },\n };\n}\n\n/**\n * Parse a grader model output into a {@link RetrievalGrade}. Tolerates a\n * bare JSON object, a fenced block, and chatty text around the object.\n *\n * **Fail-safe = stop.** When the verdict can't be read (undefined / empty\n * / unparseable / missing `sufficient`), returns\n * `{ sufficient: true, confidence: 0, reformulation: null }` so a broken\n * grader degrades to a single pass rather than looping or falsely\n * abstaining. A *parsed* `sufficient: false` (a real insufficiency\n * verdict) is preserved.\n *\n * @stable\n */\nexport function parseGrade(text: string | undefined): RetrievalGrade {\n const stop: RetrievalGrade = { sufficient: true, confidence: 0, reformulation: null };\n if (text === undefined) return stop;\n const stripped = stripFence(text).trim();\n if (stripped.length === 0) return stop;\n const obj = tryParseObject(stripped);\n if (obj === null) return stop;\n const sufficient = coerceBool(obj.sufficient);\n if (sufficient === null) return stop;\n const confidence = coerceConfidence(obj.confidence, sufficient);\n const reformulation = sufficient ? null : coerceReformulation(obj.reformulation);\n const reason = coerceReason(obj.reason);\n return {\n sufficient,\n confidence,\n reformulation,\n ...(reason !== undefined ? { reason } : {}),\n };\n}\n\n/**\n * Wrap a {@link Provider} as a {@link RetrievalGrader}. **Resilient**: a\n * provider error or unparseable response degrades to the\n * {@link parseGrade} \"stop\" grade so grading never throws into the loop\n * (and a flaky grader can't drive endless reformulation).\n *\n * @stable\n */\nexport function createProviderRetrievalGrader(\n provider: Provider,\n options: { readonly maxTokens?: number } = {},\n): RetrievalGrader {\n const maxTokens = options.maxTokens ?? DEFAULT_GRADE_MAX_TOKENS;\n return {\n async grade(query, snippets, opts = {}): Promise<RetrievalGrade> {\n try {\n const request = buildGradeRequest(query, snippets, {\n maxTokens,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n ...(opts.triedQueries !== undefined ? { triedQueries: opts.triedQueries } : {}),\n });\n const response = await provider.generate(request);\n return parseGrade(response.text);\n } catch {\n return { sufficient: true, confidence: 0, reformulation: null };\n }\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// The bounded loop (pure orchestration over injected retrieve + grader)\n// ---------------------------------------------------------------------------\n\n/**\n * Result of an iterative retrieval run.\n *\n * @stable\n */\nexport interface IterativeRetrievalResult<H> {\n /** Accumulated hits across all passes (deduped, in discovery order). */\n readonly hits: ReadonlyArray<H>;\n /** Number of retrieval passes performed (≥ 1). */\n readonly iterations: number;\n /** Difficulty-gate verdict (whether the loop was eligible to run). */\n readonly gateHard: boolean;\n /** Final sufficiency verdict. */\n readonly sufficient: boolean;\n /**\n * `true` when the loop exhausted its cap / ran out of reformulations\n * while still judged insufficient - the caller should abstain rather\n * than answer from `hits`.\n */\n readonly abstained: boolean;\n /**\n * `true` when the grader actually judged the result at least once\n * (memory-retrieval-02). `false` on the single-shot path (gate not\n * hard, or no grader configured) - there `sufficient: true` is a\n * DEFAULT, not a verdict, and consumers must not read it as one.\n */\n readonly graded: boolean;\n /** The sequence of queries tried (original first). */\n readonly queries: ReadonlyArray<string>;\n}\n\n/**\n * Dependencies injected into {@link runIterativeRetrieval}. The loop does\n * no I/O of its own - `retrieve` and `grader` own all side effects.\n *\n * @stable\n */\nexport interface IterativeRetrievalDeps<H> {\n /**\n * Run one retrieval pass for `query`. `widen` is `true` on\n * reformulation passes so the caller can broaden recall (e.g. enable\n * P2-1 one-hop graph expansion).\n */\n retrieve(query: string, widen: boolean, signal?: AbortSignal): Promise<ReadonlyArray<H>>;\n /** Snippet shown to the grader for a hit. */\n snippetOf(hit: H): string;\n /** Stable id used to dedup hits across passes. */\n idOf(hit: H): string;\n /** Grader; `null` ⇒ single-shot (no grading, no provider call). */\n grader: RetrievalGrader | null;\n /**\n * Re-fuse the per-pass hit lists into one ranked list (MRET-2).\n * Receives one list per pass in pass order; the result feeds the\n * final `maxResults` cut so a pass-2 find can outrank pass-1 noise.\n * Absent ⇒ the loop falls back to round-robin interleaving (still\n * strictly better than the old discovery-order cut).\n */\n fuse?(lists: ReadonlyArray<ReadonlyArray<H>>): ReadonlyArray<H>;\n}\n\n/**\n * Options for {@link runIterativeRetrieval}.\n *\n * @stable\n */\nexport interface IterativeRetrievalOptions {\n /** Total-pass cap; clamped to `[1, {@link MAX_ITERATIONS_CEILING}]`. */\n readonly maxIterations?: number;\n /** Difficulty-gate tuning. */\n readonly difficulty?: DifficultyGateOptions;\n /** Skip the heuristic gate and force the loop (still capped). */\n readonly forceHard?: boolean;\n /** Max snippets passed to the grader per pass. Default 8. */\n readonly maxGradeSnippets?: number;\n /** Cap on the returned hit count (omitted ⇒ all accumulated). */\n readonly maxResults?: number;\n readonly signal?: AbortSignal;\n}\n\n/**\n * Run the gated grade-then-reformulate loop.\n *\n * Flow: assess difficulty → retrieve (pass 1) → if not hard *or* no\n * grader, return single-shot → else grade; if sufficient, return; if\n * weak and a reformulation is offered and the cap is not hit, retrieve\n * again (widened) and re-grade; otherwise **abstain**.\n *\n * @stable\n */\nexport async function runIterativeRetrieval<H>(\n query: string,\n deps: IterativeRetrievalDeps<H>,\n options: IterativeRetrievalOptions = {},\n): Promise<IterativeRetrievalResult<H>> {\n const gate =\n options.forceHard === true\n ? { hard: true, score: 1, signals: ['forced'] as ReadonlyArray<string> }\n : assessQueryDifficulty(query, options.difficulty ?? {});\n const cap = clampIterations(options.maxIterations);\n const maxSnippets = Math.max(1, options.maxGradeSnippets ?? DEFAULT_MAX_GRADE_SNIPPETS);\n const signal = options.signal;\n\n // MRET-2: hits are tracked PER PASS - RAW, including cross-pass\n // repeats, so rank fusion can reward consensus between passes. The\n // grade window and the final list dedup by id at read time. The old\n // flat append meant pass 1 saturated the first `maxSnippets` forever\n // and the final cut ran in discovery order.\n const passLists: Array<ReadonlyArray<H>> = [];\n const accumulate = (hits: ReadonlyArray<H>): void => {\n passLists.push([...hits]);\n };\n const queries: string[] = [query];\n const tried = new Set<string>([query.trim().toLowerCase()]);\n\n // Pass 1 - always single-shot, no widening.\n accumulate(await deps.retrieve(query, false, signal));\n\n // Gate: the loop is eligible only when the query is judged hard *and* a\n // grader exists. Otherwise stay single-shot - we did not grade, so we\n // make no abstention claim.\n const grader = deps.grader;\n if (!gate.hard || grader === null) {\n // Single-shot: nothing was graded, so `sufficient: true` is a\n // default - `graded: false` tells consumers not to trust it as a\n // verdict (memory-retrieval-02).\n return finalize(deps, passLists, 1, gate.hard, true, false, false, queries, options.maxResults);\n }\n\n let passes = 1;\n for (;;) {\n // MRET-2: the grade window interleaves the TOP hits of every pass\n // (latest pass included) instead of replaying pass 1's head.\n // MRET-11: sufficiency is always judged against the ORIGINAL\n // question; reformulations ride along as already-tried context.\n const window = interleave(passLists, maxSnippets, deps.idOf).map(deps.snippetOf);\n const grade = await grader.grade(query, window, {\n ...(signal !== undefined ? { signal } : {}),\n ...(queries.length > 1 ? { triedQueries: queries.slice(1) } : {}),\n });\n if (grade.sufficient) {\n return finalize(\n deps,\n passLists,\n passes,\n true,\n true,\n false,\n true,\n queries,\n options.maxResults,\n );\n }\n if (passes >= cap) break;\n const next = normalizeReformulation(grade.reformulation, tried);\n if (next === null) break;\n passes += 1;\n queries.push(next);\n tried.add(next.toLowerCase());\n accumulate(await deps.retrieve(next, true, signal));\n }\n // Cap reached or no further reformulation while still insufficient ⇒ abstain.\n return finalize(deps, passLists, passes, true, false, true, true, queries, options.maxResults);\n}\n\n/**\n * Round-robin interleave of per-pass lists, deduped by id: hit 1 of\n * every pass, then hit 2 of every pass, … - guarantees the latest\n * pass's top hits enter a window of any size.\n */\nfunction interleave<H>(\n lists: ReadonlyArray<ReadonlyArray<H>>,\n limit: number,\n idOf: (hit: H) => string,\n): H[] {\n const out: H[] = [];\n const seen = new Set<string>();\n const longest = lists.reduce((max, l) => Math.max(max, l.length), 0);\n for (let rank = 0; rank < longest && out.length < limit; rank += 1) {\n for (const list of lists) {\n const hit = list[rank];\n if (hit === undefined) continue;\n const id = idOf(hit);\n if (seen.has(id)) continue;\n seen.add(id);\n out.push(hit);\n if (out.length >= limit) break;\n }\n }\n return out;\n}\n\nfunction finalize<H>(\n deps: IterativeRetrievalDeps<H>,\n passLists: ReadonlyArray<ReadonlyArray<H>>,\n iterations: number,\n gateHard: boolean,\n sufficient: boolean,\n abstained: boolean,\n graded: boolean,\n queries: ReadonlyArray<string>,\n maxResults: number | undefined,\n): IterativeRetrievalResult<H> {\n // MRET-2: re-rank ACROSS passes before the cut - discovery order let\n // pass-1 noise crowd out the pass-2 hit that actually answers.\n const ranked =\n typeof deps.fuse === 'function'\n ? dedupBy(deps.fuse(passLists), deps.idOf)\n : interleave(passLists, Number.POSITIVE_INFINITY, deps.idOf);\n const capped =\n maxResults !== undefined && maxResults >= 0 ? ranked.slice(0, maxResults) : ranked.slice();\n return {\n hits: capped,\n iterations,\n gateHard,\n sufficient,\n abstained,\n graded,\n queries: [...queries],\n };\n}\n\nfunction dedupBy<H>(hits: ReadonlyArray<H>, idOf: (hit: H) => string): H[] {\n const seen = new Set<string>();\n const out: H[] = [];\n for (const hit of hits) {\n const id = idOf(hit);\n if (seen.has(id)) continue;\n seen.add(id);\n out.push(hit);\n }\n return out;\n}\n\nfunction clampIterations(value: number | undefined): number {\n const n = value ?? DEFAULT_MAX_ITERATIONS;\n if (!Number.isFinite(n)) return DEFAULT_MAX_ITERATIONS;\n return Math.min(MAX_ITERATIONS_CEILING, Math.max(1, Math.floor(n)));\n}\n\n/**\n * Normalise a proposed reformulation: trim, drop empties, and reject a\n * query already tried (case-insensitive) so the loop never re-treads and\n * cannot spin.\n */\nfunction normalizeReformulation(raw: string | null, tried: ReadonlySet<string>): string | null {\n if (raw === null) return null;\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n if (tried.has(trimmed.toLowerCase())) return null;\n return trimmed;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction wordCount(text: string): number {\n const trimmed = text.trim();\n return trimmed.length === 0 ? 0 : trimmed.split(/\\s+/u).length;\n}\n\nfunction countCommas(text: string): number {\n return (text.match(/,/gu) ?? []).length;\n}\n\nfunction questionMarkCount(text: string): number {\n return (text.match(/\\?/gu) ?? []).length;\n}\n\nfunction coerceBool(value: unknown): boolean | null {\n if (typeof value === 'boolean') return value;\n if (typeof value === 'string') {\n const v = value.trim().toLowerCase();\n if (v === 'true' || v === 'yes' || v === 'sufficient') return true;\n if (v === 'false' || v === 'no' || v === 'insufficient') return false;\n }\n return null;\n}\n\nfunction coerceConfidence(value: unknown, sufficient: boolean): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return Math.min(1, Math.max(0, value));\n }\n return sufficient ? 0.8 : 0.3;\n}\n\nfunction coerceReformulation(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n if (trimmed.length === 0) return null;\n return trimmed.length > 1024 ? trimmed.slice(0, 1024) : trimmed;\n}\n\nfunction coerceReason(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined;\n const trimmed = value.trim();\n return trimmed.length === 0 ? undefined : trimmed;\n}\n\nfunction tryParseObject(text: string): Record<string, unknown> | null {\n const direct = tryJson(text);\n if (isRecord(direct)) return direct;\n const start = text.indexOf('{');\n const end = text.lastIndexOf('}');\n if (start >= 0 && end > start) {\n const sliced = tryJson(text.slice(start, end + 1));\n if (isRecord(sliced)) return sliced;\n }\n return null;\n}\n\nfunction tryJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n\nfunction stripFence(text: string): string {\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n```/u.exec(text.trim());\n return match?.[1] ?? text;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"iterative.js","names":["signals: string[]","stop: RetrievalGrade","passLists: Array<ReadonlyArray<H>>","queries: string[]","out: H[]"],"sources":["../../src/search/iterative.ts"],"sourcesContent":["/**\n * Agentic / iterative retrieval for hard queries (P2-4) - a **gated**,\n * CRAG/Self-RAG-style grade-then-reformulate loop.\n *\n * Some multi-hop / temporal questions can't be answered from a single\n * retrieval pass; the naive system returns a confident-but-wrong answer\n * instead of retrieving again. This module adds a small loop: grade the\n * retrieved memories for sufficiency and, when weak, reformulate the\n * query and retrieve once more (optionally widening to the P2-1 graph) -\n * up to a hard iteration cap, then **abstain** rather than confabulate.\n *\n * Both CRAG (arXiv:2401.15884) and Self-RAG (arXiv:2310.11511) add\n * latency/cost, so the literature's advice - and this module's design -\n * is to *gate* the loop, not make it default:\n *\n * 1. A cheap, **local** {@link assessQueryDifficulty} heuristic decides\n * whether a query is even a candidate; simple lookups never enter\n * the loop (no provider call) and stay single-shot.\n * 2. The grade/reformulate step needs a {@link RetrievalGrader}; with\n * none configured the loop degrades to a single retrieval pass. The\n * only provider-backed grader, {@link createProviderRetrievalGrader},\n * is **resilient**: any provider error or unparseable output degrades\n * to a \"stop\" grade rather than throwing or looping forever.\n * 3. A mandatory iteration cap bounds the worst case regardless of the\n * grader.\n *\n * The module is provider-agnostic - it imports only `@graphorin/core`\n * types and never performs I/O itself; {@link runIterativeRetrieval} is a\n * pure orchestrator over an injected `retrieve` function + grader, so the\n * loop is unit-testable without a store or a model.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest } from '@graphorin/core';\nimport { stripFence } from '../internal/llm-json.js';\n\n/** Default hard ceiling on total retrieval passes (gate + cap mandatory). */\nexport const DEFAULT_MAX_ITERATIONS = 3;\n\n/**\n * Absolute clamp on total retrieval passes, applied regardless of the\n * caller's `maxIterations` - a latency guardrail so a mis-configured\n * caller can never unbound the loop.\n */\nexport const MAX_ITERATIONS_CEILING = 5;\n\n/** Default score (0..1) at or above which a query is classified as hard. */\nexport const DEFAULT_DIFFICULTY_THRESHOLD = 0.5;\n\n/** Default output-token ceiling for a single grade call. */\nconst DEFAULT_GRADE_MAX_TOKENS = 256;\n\n/** Default number of retrieved snippets shown to the grader per pass. */\nconst DEFAULT_MAX_GRADE_SNIPPETS = 8;\n\n// ---------------------------------------------------------------------------\n// Difficulty gate (pure, local - no I/O, no provider)\n// ---------------------------------------------------------------------------\n\n/**\n * Outcome of the heuristic difficulty gate. `hard` is the gating\n * decision (`score >= threshold`); `signals` lists which heuristic\n * categories fired, for explainability / tracing.\n *\n * @stable\n */\nexport interface DifficultyAssessment {\n readonly hard: boolean;\n /** Aggregate difficulty score, clamped to `[0, 1]`. */\n readonly score: number;\n /** Heuristic categories that fired (`'multi-hop'`, `'temporal'`, …). */\n readonly signals: ReadonlyArray<string>;\n}\n\n/**\n * Tuning for {@link assessQueryDifficulty}.\n *\n * @stable\n */\nexport interface DifficultyGateOptions {\n /** Minimum score (0..1) to classify the query as hard. Default 0.5. */\n readonly threshold?: number;\n}\n\n/** Relational / referential markers implying a hop to another entity. */\nconst RE_REL_PRONOUN = /\\b(?:who|whom|whose|which)\\b/iu;\nconst RE_OF_CHAIN = /\\bthe\\s+\\w+\\s+of\\b/iu;\nconst RE_POSSESSIVE = /\\b[\\w-]+'s\\b/giu;\nconst RE_DESC_REF =\n /\\bthe\\s+(?:person|people|place|thing|one|company|team|book|restaurant|project|guy|woman|man)\\b/iu;\n/** Temporal ordering / relative-time markers. */\nconst RE_TEMPORAL =\n /\\b(?:before|after|when|while|during|since|until|first|last|earliest|latest|previously|originally|ago|prior|then|recently|used\\s+to)\\b/iu;\n/** Comparison / superlative markers. */\nconst RE_COMPARISON =\n /\\b(?:most|least|more|less|fewer|greater|biggest|largest|smallest|oldest|newest|best|worst|highest|lowest|compared?|versus|vs\\.?|between)\\b/iu;\n/** Clause-joining conjunctions used to count clauses. */\nconst RE_CONJUNCTION = /\\b(?:and|or|but|then)\\b/giu;\n\nconst W_MULTI_HOP = 0.4;\nconst W_TEMPORAL = 0.3;\nconst W_COMPARISON = 0.3;\nconst W_MULTI_CLAUSE = 0.2;\nconst W_LONG = 0.15;\nconst LONG_WORD_COUNT = 12;\n\n/**\n * Score a query's retrieval difficulty with cheap, deterministic, fully\n * **local** heuristics (no I/O). Conservative by design - it prefers to\n * leave a query single-shot (`hard: false`) unless several\n * multi-hop / temporal / comparison signals stack up, so the gate adds\n * passes only where they are likely to help. Used by\n * {@link runIterativeRetrieval} to decide whether to enter the loop.\n *\n * @stable\n */\nexport function assessQueryDifficulty(\n query: string,\n options: DifficultyGateOptions = {},\n): DifficultyAssessment {\n const threshold = options.threshold ?? DEFAULT_DIFFICULTY_THRESHOLD;\n const signals: string[] = [];\n let score = 0;\n\n const possessiveCount = (query.match(RE_POSSESSIVE) ?? []).length;\n const multiHop =\n RE_REL_PRONOUN.test(query) ||\n RE_OF_CHAIN.test(query) ||\n RE_DESC_REF.test(query) ||\n possessiveCount >= 2;\n if (multiHop) {\n signals.push('multi-hop');\n score += W_MULTI_HOP;\n }\n if (RE_TEMPORAL.test(query)) {\n signals.push('temporal');\n score += W_TEMPORAL;\n }\n if (RE_COMPARISON.test(query)) {\n signals.push('comparison');\n score += W_COMPARISON;\n }\n const clauseSignals = (query.match(RE_CONJUNCTION) ?? []).length + countCommas(query);\n if (clauseSignals >= 2 || questionMarkCount(query) >= 2) {\n signals.push('multi-clause');\n score += W_MULTI_CLAUSE;\n }\n if (wordCount(query) >= LONG_WORD_COUNT) {\n signals.push('long');\n score += W_LONG;\n }\n\n const clamped = Math.min(1, score);\n return { hard: clamped >= threshold, score: clamped, signals };\n}\n\n// ---------------------------------------------------------------------------\n// Retrieval grader seam (provider-agnostic)\n// ---------------------------------------------------------------------------\n\n/**\n * Verdict from grading a retrieved candidate set against a query.\n *\n * @stable\n */\nexport interface RetrievalGrade {\n /** Whether the retrieved memories suffice to answer the query. */\n readonly sufficient: boolean;\n /** Grader confidence, clamped to `[0, 1]`. */\n readonly confidence: number;\n /**\n * A single better search query to try next, or `null` when the grader\n * proposes none (the loop then stops / abstains).\n */\n readonly reformulation: string | null;\n /** Optional short rationale (never surfaced in spans). */\n readonly reason?: string;\n}\n\n/**\n * Per-call options for a {@link RetrievalGrader}.\n *\n * @stable\n */\nexport interface RetrievalGradeOptions {\n /** Cancellation signal forwarded to the underlying provider call. */\n readonly signal?: AbortSignal;\n /**\n * Reformulations already attempted (MRET-11). Surfaced to the grader\n * as context so it can propose something genuinely new - the grade\n * itself is ALWAYS judged against the original question, never a\n * reformulation (a narrowed sub-query must not be declared\n * \"sufficient\" while the original multi-hop question is not).\n */\n readonly triedQueries?: ReadonlyArray<string>;\n}\n\n/**\n * Pluggable retrieval-grader seam consumed by the iterative loop. The\n * built-in provider-backed implementation lives in\n * {@link createProviderRetrievalGrader}; advanced callers can supply a\n * bespoke grader (e.g. a deterministic heuristic).\n *\n * Implementations MUST degrade gracefully - return a \"stop\" grade rather\n * than throw - so a grader failure never breaks recall.\n *\n * @stable\n */\nexport interface RetrievalGrader {\n grade(\n query: string,\n snippets: ReadonlyArray<string>,\n options?: RetrievalGradeOptions,\n ): Promise<RetrievalGrade>;\n}\n\n/**\n * System prompt for the retrieval grader. Asks for a bare JSON verdict;\n * {@link parseGrade} also tolerates a chatty model.\n *\n * @internal\n */\nexport const RETRIEVAL_GRADE_SYSTEM_PROMPT =\n 'You are a retrieval grader for a personal memory store. Given a question and a ' +\n 'numbered list of retrieved memories, judge whether they are SUFFICIENT to answer ' +\n 'the question correctly and completely. Respond with ONLY a JSON object: ' +\n '{\"sufficient\": boolean, \"confidence\": number between 0 and 1, \"reformulation\": ' +\n 'string or null, \"reason\": string}. Set \"sufficient\" to true only if the memories ' +\n 'actually contain the answer - do not guess. When insufficient, set \"reformulation\" ' +\n 'to a single alternative search query likely to retrieve the missing information ' +\n '(vary entities, time frame, or specificity); otherwise null. Output only the JSON ' +\n 'object - no prose, no markdown.';\n\n/**\n * Build the grade request. Pure - no I/O. Temperature 0 so the verdict\n * is as stable as the model allows.\n *\n * @stable\n */\nexport function buildGradeRequest(\n query: string,\n snippets: ReadonlyArray<string>,\n options: {\n readonly maxTokens?: number;\n readonly signal?: AbortSignal;\n readonly triedQueries?: ReadonlyArray<string>;\n } = {},\n): ProviderRequest {\n const body =\n snippets.length === 0 ? '(none)' : snippets.map((s, i) => `[${i + 1}] ${s}`).join('\\n');\n const tried =\n options.triedQueries !== undefined && options.triedQueries.length > 0\n ? `\\n\\nQueries already tried (do not repeat them): ${options.triedQueries.join(' | ')}`\n : '';\n const content =\n `Question: ${query}\\n\\nRetrieved memories:\\n${body}${tried}\\n\\n` +\n 'Grade whether these memories are sufficient to answer the question. If not, ' +\n 'propose a single better search query.';\n return {\n messages: [{ role: 'user', content }],\n systemMessage: RETRIEVAL_GRADE_SYSTEM_PROMPT,\n temperature: 0,\n maxTokens: options.maxTokens ?? DEFAULT_GRADE_MAX_TOKENS,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n outputType: { kind: 'structured' },\n };\n}\n\n/**\n * Parse a grader model output into a {@link RetrievalGrade}. Tolerates a\n * bare JSON object, a fenced block, and chatty text around the object.\n *\n * **Fail-safe = stop.** When the verdict can't be read (undefined / empty\n * / unparseable / missing `sufficient`), returns\n * `{ sufficient: true, confidence: 0, reformulation: null }` so a broken\n * grader degrades to a single pass rather than looping or falsely\n * abstaining. A *parsed* `sufficient: false` (a real insufficiency\n * verdict) is preserved.\n *\n * @stable\n */\nexport function parseGrade(text: string | undefined): RetrievalGrade {\n const stop: RetrievalGrade = { sufficient: true, confidence: 0, reformulation: null };\n if (text === undefined) return stop;\n const stripped = stripFence(text).trim();\n if (stripped.length === 0) return stop;\n const obj = tryParseObject(stripped);\n if (obj === null) return stop;\n const sufficient = coerceBool(obj.sufficient);\n if (sufficient === null) return stop;\n const confidence = coerceConfidence(obj.confidence, sufficient);\n const reformulation = sufficient ? null : coerceReformulation(obj.reformulation);\n const reason = coerceReason(obj.reason);\n return {\n sufficient,\n confidence,\n reformulation,\n ...(reason !== undefined ? { reason } : {}),\n };\n}\n\n/**\n * Wrap a {@link Provider} as a {@link RetrievalGrader}. **Resilient**: a\n * provider error or unparseable response degrades to the\n * {@link parseGrade} \"stop\" grade so grading never throws into the loop\n * (and a flaky grader can't drive endless reformulation).\n *\n * @stable\n */\nexport function createProviderRetrievalGrader(\n provider: Provider,\n options: { readonly maxTokens?: number } = {},\n): RetrievalGrader {\n const maxTokens = options.maxTokens ?? DEFAULT_GRADE_MAX_TOKENS;\n return {\n async grade(query, snippets, opts = {}): Promise<RetrievalGrade> {\n try {\n const request = buildGradeRequest(query, snippets, {\n maxTokens,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n ...(opts.triedQueries !== undefined ? { triedQueries: opts.triedQueries } : {}),\n });\n const response = await provider.generate(request);\n return parseGrade(response.text);\n } catch {\n return { sufficient: true, confidence: 0, reformulation: null };\n }\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// The bounded loop (pure orchestration over injected retrieve + grader)\n// ---------------------------------------------------------------------------\n\n/**\n * Result of an iterative retrieval run.\n *\n * @stable\n */\nexport interface IterativeRetrievalResult<H> {\n /** Accumulated hits across all passes (deduped, in discovery order). */\n readonly hits: ReadonlyArray<H>;\n /** Number of retrieval passes performed (≥ 1). */\n readonly iterations: number;\n /** Difficulty-gate verdict (whether the loop was eligible to run). */\n readonly gateHard: boolean;\n /** Final sufficiency verdict. */\n readonly sufficient: boolean;\n /**\n * `true` when the loop exhausted its cap / ran out of reformulations\n * while still judged insufficient - the caller should abstain rather\n * than answer from `hits`.\n */\n readonly abstained: boolean;\n /**\n * `true` when the grader actually judged the result at least once\n * (memory-retrieval-02). `false` on the single-shot path (gate not\n * hard, or no grader configured) - there `sufficient: true` is a\n * DEFAULT, not a verdict, and consumers must not read it as one.\n */\n readonly graded: boolean;\n /** The sequence of queries tried (original first). */\n readonly queries: ReadonlyArray<string>;\n}\n\n/**\n * Dependencies injected into {@link runIterativeRetrieval}. The loop does\n * no I/O of its own - `retrieve` and `grader` own all side effects.\n *\n * @stable\n */\nexport interface IterativeRetrievalDeps<H> {\n /**\n * Run one retrieval pass for `query`. `widen` is `true` on\n * reformulation passes so the caller can broaden recall (e.g. enable\n * P2-1 one-hop graph expansion).\n */\n retrieve(query: string, widen: boolean, signal?: AbortSignal): Promise<ReadonlyArray<H>>;\n /** Snippet shown to the grader for a hit. */\n snippetOf(hit: H): string;\n /** Stable id used to dedup hits across passes. */\n idOf(hit: H): string;\n /** Grader; `null` ⇒ single-shot (no grading, no provider call). */\n grader: RetrievalGrader | null;\n /**\n * Re-fuse the per-pass hit lists into one ranked list (MRET-2).\n * Receives one list per pass in pass order; the result feeds the\n * final `maxResults` cut so a pass-2 find can outrank pass-1 noise.\n * Absent ⇒ the loop falls back to round-robin interleaving (still\n * strictly better than the old discovery-order cut).\n */\n fuse?(lists: ReadonlyArray<ReadonlyArray<H>>): ReadonlyArray<H>;\n}\n\n/**\n * Options for {@link runIterativeRetrieval}.\n *\n * @stable\n */\nexport interface IterativeRetrievalOptions {\n /** Total-pass cap; clamped to `[1, {@link MAX_ITERATIONS_CEILING}]`. */\n readonly maxIterations?: number;\n /** Difficulty-gate tuning. */\n readonly difficulty?: DifficultyGateOptions;\n /** Skip the heuristic gate and force the loop (still capped). */\n readonly forceHard?: boolean;\n /** Max snippets passed to the grader per pass. Default 8. */\n readonly maxGradeSnippets?: number;\n /** Cap on the returned hit count (omitted ⇒ all accumulated). */\n readonly maxResults?: number;\n readonly signal?: AbortSignal;\n}\n\n/**\n * Run the gated grade-then-reformulate loop.\n *\n * Flow: assess difficulty → retrieve (pass 1) → if not hard *or* no\n * grader, return single-shot → else grade; if sufficient, return; if\n * weak and a reformulation is offered and the cap is not hit, retrieve\n * again (widened) and re-grade; otherwise **abstain**.\n *\n * @stable\n */\nexport async function runIterativeRetrieval<H>(\n query: string,\n deps: IterativeRetrievalDeps<H>,\n options: IterativeRetrievalOptions = {},\n): Promise<IterativeRetrievalResult<H>> {\n const gate =\n options.forceHard === true\n ? { hard: true, score: 1, signals: ['forced'] as ReadonlyArray<string> }\n : assessQueryDifficulty(query, options.difficulty ?? {});\n const cap = clampIterations(options.maxIterations);\n const maxSnippets = Math.max(1, options.maxGradeSnippets ?? DEFAULT_MAX_GRADE_SNIPPETS);\n const signal = options.signal;\n\n // MRET-2: hits are tracked PER PASS - RAW, including cross-pass\n // repeats, so rank fusion can reward consensus between passes. The\n // grade window and the final list dedup by id at read time. The old\n // flat append meant pass 1 saturated the first `maxSnippets` forever\n // and the final cut ran in discovery order.\n const passLists: Array<ReadonlyArray<H>> = [];\n const accumulate = (hits: ReadonlyArray<H>): void => {\n passLists.push([...hits]);\n };\n const queries: string[] = [query];\n const tried = new Set<string>([query.trim().toLowerCase()]);\n\n // Pass 1 - always single-shot, no widening.\n accumulate(await deps.retrieve(query, false, signal));\n\n // Gate: the loop is eligible only when the query is judged hard *and* a\n // grader exists. Otherwise stay single-shot - we did not grade, so we\n // make no abstention claim.\n const grader = deps.grader;\n if (!gate.hard || grader === null) {\n // Single-shot: nothing was graded, so `sufficient: true` is a\n // default - `graded: false` tells consumers not to trust it as a\n // verdict (memory-retrieval-02).\n return finalize(deps, passLists, 1, gate.hard, true, false, false, queries, options.maxResults);\n }\n\n let passes = 1;\n for (;;) {\n // MRET-2: the grade window interleaves the TOP hits of every pass\n // (latest pass included) instead of replaying pass 1's head.\n // MRET-11: sufficiency is always judged against the ORIGINAL\n // question; reformulations ride along as already-tried context.\n const window = interleave(passLists, maxSnippets, deps.idOf).map(deps.snippetOf);\n const grade = await grader.grade(query, window, {\n ...(signal !== undefined ? { signal } : {}),\n ...(queries.length > 1 ? { triedQueries: queries.slice(1) } : {}),\n });\n if (grade.sufficient) {\n return finalize(\n deps,\n passLists,\n passes,\n true,\n true,\n false,\n true,\n queries,\n options.maxResults,\n );\n }\n if (passes >= cap) break;\n const next = normalizeReformulation(grade.reformulation, tried);\n if (next === null) break;\n passes += 1;\n queries.push(next);\n tried.add(next.toLowerCase());\n accumulate(await deps.retrieve(next, true, signal));\n }\n // Cap reached or no further reformulation while still insufficient ⇒ abstain.\n return finalize(deps, passLists, passes, true, false, true, true, queries, options.maxResults);\n}\n\n/**\n * Round-robin interleave of per-pass lists, deduped by id: hit 1 of\n * every pass, then hit 2 of every pass, … - guarantees the latest\n * pass's top hits enter a window of any size.\n */\nfunction interleave<H>(\n lists: ReadonlyArray<ReadonlyArray<H>>,\n limit: number,\n idOf: (hit: H) => string,\n): H[] {\n const out: H[] = [];\n const seen = new Set<string>();\n const longest = lists.reduce((max, l) => Math.max(max, l.length), 0);\n for (let rank = 0; rank < longest && out.length < limit; rank += 1) {\n for (const list of lists) {\n const hit = list[rank];\n if (hit === undefined) continue;\n const id = idOf(hit);\n if (seen.has(id)) continue;\n seen.add(id);\n out.push(hit);\n if (out.length >= limit) break;\n }\n }\n return out;\n}\n\nfunction finalize<H>(\n deps: IterativeRetrievalDeps<H>,\n passLists: ReadonlyArray<ReadonlyArray<H>>,\n iterations: number,\n gateHard: boolean,\n sufficient: boolean,\n abstained: boolean,\n graded: boolean,\n queries: ReadonlyArray<string>,\n maxResults: number | undefined,\n): IterativeRetrievalResult<H> {\n // MRET-2: re-rank ACROSS passes before the cut - discovery order let\n // pass-1 noise crowd out the pass-2 hit that actually answers.\n const ranked =\n typeof deps.fuse === 'function'\n ? dedupBy(deps.fuse(passLists), deps.idOf)\n : interleave(passLists, Number.POSITIVE_INFINITY, deps.idOf);\n const capped =\n maxResults !== undefined && maxResults >= 0 ? ranked.slice(0, maxResults) : ranked.slice();\n return {\n hits: capped,\n iterations,\n gateHard,\n sufficient,\n abstained,\n graded,\n queries: [...queries],\n };\n}\n\nfunction dedupBy<H>(hits: ReadonlyArray<H>, idOf: (hit: H) => string): H[] {\n const seen = new Set<string>();\n const out: H[] = [];\n for (const hit of hits) {\n const id = idOf(hit);\n if (seen.has(id)) continue;\n seen.add(id);\n out.push(hit);\n }\n return out;\n}\n\nfunction clampIterations(value: number | undefined): number {\n const n = value ?? DEFAULT_MAX_ITERATIONS;\n if (!Number.isFinite(n)) return DEFAULT_MAX_ITERATIONS;\n return Math.min(MAX_ITERATIONS_CEILING, Math.max(1, Math.floor(n)));\n}\n\n/**\n * Normalise a proposed reformulation: trim, drop empties, and reject a\n * query already tried (case-insensitive) so the loop never re-treads and\n * cannot spin.\n */\nfunction normalizeReformulation(raw: string | null, tried: ReadonlySet<string>): string | null {\n if (raw === null) return null;\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n if (tried.has(trimmed.toLowerCase())) return null;\n return trimmed;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction wordCount(text: string): number {\n const trimmed = text.trim();\n return trimmed.length === 0 ? 0 : trimmed.split(/\\s+/u).length;\n}\n\nfunction countCommas(text: string): number {\n return (text.match(/,/gu) ?? []).length;\n}\n\nfunction questionMarkCount(text: string): number {\n return (text.match(/\\?/gu) ?? []).length;\n}\n\nfunction coerceBool(value: unknown): boolean | null {\n if (typeof value === 'boolean') return value;\n if (typeof value === 'string') {\n const v = value.trim().toLowerCase();\n if (v === 'true' || v === 'yes' || v === 'sufficient') return true;\n if (v === 'false' || v === 'no' || v === 'insufficient') return false;\n }\n return null;\n}\n\nfunction coerceConfidence(value: unknown, sufficient: boolean): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return Math.min(1, Math.max(0, value));\n }\n return sufficient ? 0.8 : 0.3;\n}\n\nfunction coerceReformulation(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n if (trimmed.length === 0) return null;\n return trimmed.length > 1024 ? trimmed.slice(0, 1024) : trimmed;\n}\n\nfunction coerceReason(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined;\n const trimmed = value.trim();\n return trimmed.length === 0 ? undefined : trimmed;\n}\n\nfunction tryParseObject(text: string): Record<string, unknown> | null {\n const direct = tryJson(text);\n if (isRecord(direct)) return direct;\n const start = text.indexOf('{');\n const end = text.lastIndexOf('}');\n if (start >= 0 && end > start) {\n const sliced = tryJson(text.slice(start, end + 1));\n if (isRecord(sliced)) return sliced;\n }\n return null;\n}\n\nfunction tryJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"query-transform.d.ts","names":[],"sources":["../../src/search/query-transform.ts"],"sourcesContent":[],"mappings":";;;;;cA2Ba,0BAAA;;;;;;;;;;;;;;UAkBI,gBAAA;;;;;;iDASH,wBACT,QAAQ;;;;;;wCAM2B,wBAAwB;;;;;;;UAQ/C,qBAAA;;oBAEG;;;;;;;;;cAUP;;;;;;;;cAcA;;;;;;;;iBAYG,qBAAA;;oBAG4C;IACzD;;;;;;iBAqBa,gBAAA;;oBAE4C;IACzD;;;;;;;;;;;iBAoBa,kBAAA,yCAA2D;;;;;;;iBAyB3D,iBAAA;;;;;;;;;;iBAeA,8BAAA,WACJ;;;IAET"}
1
+ {"version":3,"file":"query-transform.d.ts","names":[],"sources":["../../src/search/query-transform.ts"],"sourcesContent":[],"mappings":";;;;;cA4Ba,0BAAA;;;;;;;;;;;;;;UAkBI,gBAAA;;;;;;iDASH,wBACT,QAAQ;;;;;;wCAM2B,wBAAwB;;;;;;;UAQ/C,qBAAA;;oBAEG;;;;;;;;;cAUP;;;;;;;;cAcA;;;;;;;;iBAYG,qBAAA;;oBAG4C;IACzD;;;;;;iBAqBa,gBAAA;;oBAE4C;IACzD;;;;;;;;;;;iBAoBa,kBAAA,yCAA2D;;;;;;;iBAyB3D,iBAAA;;;;;;;;;;iBAeA,8BAAA,WACJ;;;IAET"}
@@ -1,3 +1,5 @@
1
+ import { stripFence } from "../internal/llm-json.js";
2
+
1
3
  //#region src/search/query-transform.ts
2
4
  /** Default ceiling on reworded variants a provider-backed transformer requests. */
3
5
  const DEFAULT_MAX_QUERY_VARIANTS = 5;
@@ -172,9 +174,6 @@ function sliceJson(text) {
172
174
  if (objStart >= 0 && objEnd > objStart) return text.slice(objStart, objEnd + 1);
173
175
  return null;
174
176
  }
175
- function stripFence(text) {
176
- return /^```[^\n]*\n([\s\S]*?)\n```/u.exec(text.trim())?.[1] ?? text;
177
- }
178
177
  function isRecord(value) {
179
178
  return value !== null && typeof value === "object";
180
179
  }
@@ -1 +1 @@
1
- {"version":3,"file":"query-transform.js","names":["out: string[]"],"sources":["../../src/search/query-transform.ts"],"sourcesContent":["/**\n * Query transformation for hybrid retrieval (P2-3) - **multi-query /\n * RAG-Fusion** and optional **HyDE**.\n *\n * Auto-recall is a regex trigger and the default search is single-shot:\n * the user's phrasing rarely matches how a memory was stored, so a\n * relevant fact is missed at the retrieval step. This module fans a\n * query into a few reworded variants (one cheap LLM call) and/or writes\n * a hypothetical answer passage to embed (HyDE, arXiv:2212.10496); the\n * caller retrieves each and fuses the lists through the existing RRF\n * reranker. Variant generation is the only thing that touches a\n * provider, so it is **opt-in**: with no transformer configured the\n * default search path stays fully offline and single-shot.\n *\n * The module is provider-agnostic - it imports only `@graphorin/core`\n * types and never performs I/O itself. The concrete `Provider` is\n * injected by the caller via {@link createProviderQueryTransformer},\n * which is resilient: any provider error or unparseable output degrades\n * to \"no variants\" / \"no hypothetical\" rather than throwing into the\n * search hot path.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest } from '@graphorin/core';\n\n/** Default ceiling on reworded variants a provider-backed transformer requests. */\nexport const DEFAULT_MAX_QUERY_VARIANTS = 5;\n\n/** Default output-token ceiling for a single transform call. */\nconst DEFAULT_TRANSFORM_MAX_TOKENS = 256;\n\n/**\n * Pluggable query-transformation seam consumed by\n * `SemanticMemory.search(..., { multiQuery, hyde })`. The built-in\n * provider-backed implementation lives in\n * {@link createProviderQueryTransformer}; advanced callers can supply a\n * bespoke one (e.g. a deterministic synonym expander) to\n * `new SemanticMemory({ queryTransformer })`.\n *\n * Implementations MUST degrade gracefully - return `[]` / `null` rather\n * than throw - so a transform failure never breaks recall.\n *\n * @stable\n */\nexport interface QueryTransformer {\n /**\n * Rewrite a query into up to `count` **additional** reworded variants\n * (the original query is retained by the caller). Returns `[]` when\n * transformation is unavailable or the model returns nothing usable.\n */\n expand(\n query: string,\n count: number,\n options?: QueryTransformOptions,\n ): Promise<ReadonlyArray<string>>;\n /**\n * Generate a single short hypothetical-answer passage to embed (HyDE),\n * or `null` when unavailable. The caller embeds the passage and fuses\n * its vector neighbours into the result.\n */\n hypothetical(query: string, options?: QueryTransformOptions): Promise<string | null>;\n}\n\n/**\n * Per-call options for a {@link QueryTransformer}.\n *\n * @stable\n */\nexport interface QueryTransformOptions {\n /** Cancellation signal forwarded to the underlying provider call. */\n readonly signal?: AbortSignal;\n}\n\n/**\n * System prompt for multi-query variant generation. Asks for a bare\n * JSON array of standalone rephrasings that preserve the original\n * intent - {@link parseQueryVariants} also tolerates a chatty model.\n *\n * @internal\n */\nexport const QUERY_EXPANSION_SYSTEM_PROMPT =\n 'You rewrite a search query into alternative phrasings to improve recall over a ' +\n 'personal memory store. Preserve the original intent; vary vocabulary, specificity, ' +\n 'and perspective so different wordings of the same stored memory are matched. ' +\n 'Return ONLY a JSON array of strings - each a standalone query, no numbering, no ' +\n 'commentary, no duplicates of the original.';\n\n/**\n * System prompt for HyDE. Asks for a short, plausible hypothetical\n * answer (not a question rephrase) whose embedding sits near the\n * passage that would actually answer the query.\n *\n * @internal\n */\nexport const HYDE_SYSTEM_PROMPT =\n 'Write a short, plausible hypothetical answer to the question, phrased as a ' +\n 'first-person statement of fact as it might be recorded in a memory store. One to ' +\n 'three sentences. Output only the passage - no preamble, no caveats, no markdown.';\n\n/**\n * Build the multi-query expansion request. Pure - no I/O. A higher\n * temperature is used deliberately so the variants diverge (the\n * downstream retrieval + RRF fusion stays deterministic).\n *\n * @stable\n */\nexport function buildExpansionRequest(\n query: string,\n count: number,\n options: { readonly maxTokens?: number; readonly signal?: AbortSignal } = {},\n): ProviderRequest {\n return {\n messages: [\n {\n role: 'user',\n content: `Original query: ${query}\\nReturn up to ${count} alternative phrasings as a JSON array of strings.`,\n },\n ],\n systemMessage: QUERY_EXPANSION_SYSTEM_PROMPT,\n temperature: 0.7,\n maxTokens: options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n outputType: { kind: 'structured' },\n };\n}\n\n/**\n * Build the HyDE pseudo-document request. Pure - no I/O.\n *\n * @stable\n */\nexport function buildHydeRequest(\n query: string,\n options: { readonly maxTokens?: number; readonly signal?: AbortSignal } = {},\n): ProviderRequest {\n return {\n messages: [{ role: 'user', content: query }],\n systemMessage: HYDE_SYSTEM_PROMPT,\n temperature: 0.3,\n maxTokens: options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n };\n}\n\n/**\n * Parse the variant-generation model output into a deduped, capped list\n * of reworded queries. Tolerates a JSON array, a `{ \"variants\": [...] }`\n * / `{ \"queries\": [...] }` wrapper, fenced blocks, and (as a last\n * resort) a newline / numbered list - so a chatty model never breaks\n * recall. Empty strings and case-insensitive duplicates are dropped;\n * the result is capped at `max`.\n *\n * @stable\n */\nexport function parseQueryVariants(text: string | undefined, max: number): ReadonlyArray<string> {\n if (max <= 0 || text === undefined) return [];\n const candidate = stripFence(text).trim();\n if (candidate.length === 0) return [];\n const raw = extractVariantStrings(candidate);\n const seen = new Set<string>();\n const out: string[] = [];\n for (const item of raw) {\n const trimmed = item.trim();\n if (trimmed.length === 0) continue;\n const key = trimmed.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(trimmed);\n if (out.length >= max) break;\n }\n return out;\n}\n\n/**\n * Parse the HyDE model output into a single passage, or `null` when the\n * model returned nothing usable. Strips a fenced block and trims.\n *\n * @stable\n */\nexport function parseHypothetical(text: string | undefined): string | null {\n if (text === undefined) return null;\n const trimmed = stripFence(text).trim();\n return trimmed.length === 0 ? null : trimmed;\n}\n\n/**\n * Wrap a {@link Provider} as a {@link QueryTransformer}. Both methods\n * are **resilient**: a provider error or unparseable response degrades\n * to `[]` / `null` so transformation never throws into `search`. The\n * `maxVariants` ceiling caps the variants requested regardless of the\n * caller's `multiQuery` value (a latency guardrail).\n *\n * @stable\n */\nexport function createProviderQueryTransformer(\n provider: Provider,\n options: { readonly maxVariants?: number; readonly maxTokens?: number } = {},\n): QueryTransformer {\n const maxVariants = Math.max(0, options.maxVariants ?? DEFAULT_MAX_QUERY_VARIANTS);\n const maxTokens = options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS;\n return {\n async expand(query, count, opts = {}): Promise<ReadonlyArray<string>> {\n const want = Math.min(Math.max(0, count), maxVariants);\n if (want === 0 || query.trim().length === 0) return [];\n try {\n const request = buildExpansionRequest(query, want, {\n maxTokens,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n const response = await provider.generate(request);\n return parseQueryVariants(response.text, want);\n } catch {\n return [];\n }\n },\n async hypothetical(query, opts = {}): Promise<string | null> {\n if (query.trim().length === 0) return null;\n try {\n const request = buildHydeRequest(query, {\n maxTokens,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n const response = await provider.generate(request);\n return parseHypothetical(response.text);\n } catch {\n return null;\n }\n },\n };\n}\n\n/** Extract the raw (pre-dedup) variant strings from tolerant model output. */\nfunction extractVariantStrings(candidate: string): ReadonlyArray<string> {\n const parsed = tryParseJson(candidate);\n if (parsed !== undefined) {\n const arr = Array.isArray(parsed) ? parsed : isRecord(parsed) ? pickArray(parsed) : null;\n if (arr !== null) {\n return arr.filter((v): v is string => typeof v === 'string');\n }\n }\n // Fallback - split a newline / numbered list and strip list markers.\n return candidate.split('\\n').map((line) =>\n line\n .replace(/^\\s*(?:[-*•]|\\d+[.)])\\s*/u, '')\n .replace(/^[\"']|[\"']$/gu, '')\n .trim(),\n );\n}\n\nfunction pickArray(record: Record<string, unknown>): ReadonlyArray<unknown> | null {\n if (Array.isArray(record.variants)) return record.variants;\n if (Array.isArray(record.queries)) return record.queries;\n return null;\n}\n\nfunction tryParseJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n const slice = sliceJson(text);\n if (slice === null) return undefined;\n try {\n return JSON.parse(slice);\n } catch {\n return undefined;\n }\n }\n}\n\n/** Slice the widest `[...]` array or `{...}` object out of chatty text. */\nfunction sliceJson(text: string): string | null {\n const arrStart = text.indexOf('[');\n const arrEnd = text.lastIndexOf(']');\n if (arrStart >= 0 && arrEnd > arrStart) return text.slice(arrStart, arrEnd + 1);\n const objStart = text.indexOf('{');\n const objEnd = text.lastIndexOf('}');\n if (objStart >= 0 && objEnd > objStart) return text.slice(objStart, objEnd + 1);\n return null;\n}\n\nfunction stripFence(text: string): string {\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n```/u.exec(text.trim());\n return match?.[1] ?? text;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object';\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"query-transform.js","names":["out: string[]"],"sources":["../../src/search/query-transform.ts"],"sourcesContent":["/**\n * Query transformation for hybrid retrieval (P2-3) - **multi-query /\n * RAG-Fusion** and optional **HyDE**.\n *\n * Auto-recall is a regex trigger and the default search is single-shot:\n * the user's phrasing rarely matches how a memory was stored, so a\n * relevant fact is missed at the retrieval step. This module fans a\n * query into a few reworded variants (one cheap LLM call) and/or writes\n * a hypothetical answer passage to embed (HyDE, arXiv:2212.10496); the\n * caller retrieves each and fuses the lists through the existing RRF\n * reranker. Variant generation is the only thing that touches a\n * provider, so it is **opt-in**: with no transformer configured the\n * default search path stays fully offline and single-shot.\n *\n * The module is provider-agnostic - it imports only `@graphorin/core`\n * types and never performs I/O itself. The concrete `Provider` is\n * injected by the caller via {@link createProviderQueryTransformer},\n * which is resilient: any provider error or unparseable output degrades\n * to \"no variants\" / \"no hypothetical\" rather than throwing into the\n * search hot path.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest } from '@graphorin/core';\nimport { stripFence } from '../internal/llm-json.js';\n\n/** Default ceiling on reworded variants a provider-backed transformer requests. */\nexport const DEFAULT_MAX_QUERY_VARIANTS = 5;\n\n/** Default output-token ceiling for a single transform call. */\nconst DEFAULT_TRANSFORM_MAX_TOKENS = 256;\n\n/**\n * Pluggable query-transformation seam consumed by\n * `SemanticMemory.search(..., { multiQuery, hyde })`. The built-in\n * provider-backed implementation lives in\n * {@link createProviderQueryTransformer}; advanced callers can supply a\n * bespoke one (e.g. a deterministic synonym expander) to\n * `new SemanticMemory({ queryTransformer })`.\n *\n * Implementations MUST degrade gracefully - return `[]` / `null` rather\n * than throw - so a transform failure never breaks recall.\n *\n * @stable\n */\nexport interface QueryTransformer {\n /**\n * Rewrite a query into up to `count` **additional** reworded variants\n * (the original query is retained by the caller). Returns `[]` when\n * transformation is unavailable or the model returns nothing usable.\n */\n expand(\n query: string,\n count: number,\n options?: QueryTransformOptions,\n ): Promise<ReadonlyArray<string>>;\n /**\n * Generate a single short hypothetical-answer passage to embed (HyDE),\n * or `null` when unavailable. The caller embeds the passage and fuses\n * its vector neighbours into the result.\n */\n hypothetical(query: string, options?: QueryTransformOptions): Promise<string | null>;\n}\n\n/**\n * Per-call options for a {@link QueryTransformer}.\n *\n * @stable\n */\nexport interface QueryTransformOptions {\n /** Cancellation signal forwarded to the underlying provider call. */\n readonly signal?: AbortSignal;\n}\n\n/**\n * System prompt for multi-query variant generation. Asks for a bare\n * JSON array of standalone rephrasings that preserve the original\n * intent - {@link parseQueryVariants} also tolerates a chatty model.\n *\n * @internal\n */\nexport const QUERY_EXPANSION_SYSTEM_PROMPT =\n 'You rewrite a search query into alternative phrasings to improve recall over a ' +\n 'personal memory store. Preserve the original intent; vary vocabulary, specificity, ' +\n 'and perspective so different wordings of the same stored memory are matched. ' +\n 'Return ONLY a JSON array of strings - each a standalone query, no numbering, no ' +\n 'commentary, no duplicates of the original.';\n\n/**\n * System prompt for HyDE. Asks for a short, plausible hypothetical\n * answer (not a question rephrase) whose embedding sits near the\n * passage that would actually answer the query.\n *\n * @internal\n */\nexport const HYDE_SYSTEM_PROMPT =\n 'Write a short, plausible hypothetical answer to the question, phrased as a ' +\n 'first-person statement of fact as it might be recorded in a memory store. One to ' +\n 'three sentences. Output only the passage - no preamble, no caveats, no markdown.';\n\n/**\n * Build the multi-query expansion request. Pure - no I/O. A higher\n * temperature is used deliberately so the variants diverge (the\n * downstream retrieval + RRF fusion stays deterministic).\n *\n * @stable\n */\nexport function buildExpansionRequest(\n query: string,\n count: number,\n options: { readonly maxTokens?: number; readonly signal?: AbortSignal } = {},\n): ProviderRequest {\n return {\n messages: [\n {\n role: 'user',\n content: `Original query: ${query}\\nReturn up to ${count} alternative phrasings as a JSON array of strings.`,\n },\n ],\n systemMessage: QUERY_EXPANSION_SYSTEM_PROMPT,\n temperature: 0.7,\n maxTokens: options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n outputType: { kind: 'structured' },\n };\n}\n\n/**\n * Build the HyDE pseudo-document request. Pure - no I/O.\n *\n * @stable\n */\nexport function buildHydeRequest(\n query: string,\n options: { readonly maxTokens?: number; readonly signal?: AbortSignal } = {},\n): ProviderRequest {\n return {\n messages: [{ role: 'user', content: query }],\n systemMessage: HYDE_SYSTEM_PROMPT,\n temperature: 0.3,\n maxTokens: options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n };\n}\n\n/**\n * Parse the variant-generation model output into a deduped, capped list\n * of reworded queries. Tolerates a JSON array, a `{ \"variants\": [...] }`\n * / `{ \"queries\": [...] }` wrapper, fenced blocks, and (as a last\n * resort) a newline / numbered list - so a chatty model never breaks\n * recall. Empty strings and case-insensitive duplicates are dropped;\n * the result is capped at `max`.\n *\n * @stable\n */\nexport function parseQueryVariants(text: string | undefined, max: number): ReadonlyArray<string> {\n if (max <= 0 || text === undefined) return [];\n const candidate = stripFence(text).trim();\n if (candidate.length === 0) return [];\n const raw = extractVariantStrings(candidate);\n const seen = new Set<string>();\n const out: string[] = [];\n for (const item of raw) {\n const trimmed = item.trim();\n if (trimmed.length === 0) continue;\n const key = trimmed.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(trimmed);\n if (out.length >= max) break;\n }\n return out;\n}\n\n/**\n * Parse the HyDE model output into a single passage, or `null` when the\n * model returned nothing usable. Strips a fenced block and trims.\n *\n * @stable\n */\nexport function parseHypothetical(text: string | undefined): string | null {\n if (text === undefined) return null;\n const trimmed = stripFence(text).trim();\n return trimmed.length === 0 ? null : trimmed;\n}\n\n/**\n * Wrap a {@link Provider} as a {@link QueryTransformer}. Both methods\n * are **resilient**: a provider error or unparseable response degrades\n * to `[]` / `null` so transformation never throws into `search`. The\n * `maxVariants` ceiling caps the variants requested regardless of the\n * caller's `multiQuery` value (a latency guardrail).\n *\n * @stable\n */\nexport function createProviderQueryTransformer(\n provider: Provider,\n options: { readonly maxVariants?: number; readonly maxTokens?: number } = {},\n): QueryTransformer {\n const maxVariants = Math.max(0, options.maxVariants ?? DEFAULT_MAX_QUERY_VARIANTS);\n const maxTokens = options.maxTokens ?? DEFAULT_TRANSFORM_MAX_TOKENS;\n return {\n async expand(query, count, opts = {}): Promise<ReadonlyArray<string>> {\n const want = Math.min(Math.max(0, count), maxVariants);\n if (want === 0 || query.trim().length === 0) return [];\n try {\n const request = buildExpansionRequest(query, want, {\n maxTokens,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n const response = await provider.generate(request);\n return parseQueryVariants(response.text, want);\n } catch {\n return [];\n }\n },\n async hypothetical(query, opts = {}): Promise<string | null> {\n if (query.trim().length === 0) return null;\n try {\n const request = buildHydeRequest(query, {\n maxTokens,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n const response = await provider.generate(request);\n return parseHypothetical(response.text);\n } catch {\n return null;\n }\n },\n };\n}\n\n/** Extract the raw (pre-dedup) variant strings from tolerant model output. */\nfunction extractVariantStrings(candidate: string): ReadonlyArray<string> {\n const parsed = tryParseJson(candidate);\n if (parsed !== undefined) {\n const arr = Array.isArray(parsed) ? parsed : isRecord(parsed) ? pickArray(parsed) : null;\n if (arr !== null) {\n return arr.filter((v): v is string => typeof v === 'string');\n }\n }\n // Fallback - split a newline / numbered list and strip list markers.\n return candidate.split('\\n').map((line) =>\n line\n .replace(/^\\s*(?:[-*•]|\\d+[.)])\\s*/u, '')\n .replace(/^[\"']|[\"']$/gu, '')\n .trim(),\n );\n}\n\nfunction pickArray(record: Record<string, unknown>): ReadonlyArray<unknown> | null {\n if (Array.isArray(record.variants)) return record.variants;\n if (Array.isArray(record.queries)) return record.queries;\n return null;\n}\n\nfunction tryParseJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n const slice = sliceJson(text);\n if (slice === null) return undefined;\n try {\n return JSON.parse(slice);\n } catch {\n return undefined;\n }\n }\n}\n\n/** Slice the widest `[...]` array or `{...}` object out of chatty text. */\nfunction sliceJson(text: string): string | null {\n const arrStart = text.indexOf('[');\n const arrEnd = text.lastIndexOf(']');\n if (arrStart >= 0 && arrEnd > arrStart) return text.slice(arrStart, arrEnd + 1);\n const objStart = text.indexOf('{');\n const objEnd = text.lastIndexOf('}');\n if (objStart >= 0 && objEnd > objStart) return text.slice(objStart, objEnd + 1);\n return null;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object';\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { withMemorySpan } from "../internal/spans.js";
2
2
  import { QuarantinePromotionRefusedError } from "../errors/index.js";
3
- import { newMemoryId } from "../internal/id.js";
4
3
  import { detectMemoryInjection } from "../internal/injection-heuristics.js";
4
+ import { newMemoryId } from "../internal/id.js";
5
5
 
6
6
  //#region src/tiers/episodic-memory.ts
7
7
  const DEFAULT_WEIGHTS = Object.freeze({
@@ -119,7 +119,7 @@ var EpisodicMemory = class {
119
119
  "memory.episodic.episode_id": episodeId
120
120
  }, async () => {
121
121
  if (typeof this.#store.episodic.archive !== "function") throw new TypeError("[graphorin/memory] EpisodicMemory.archive(...) requires a storage adapter that implements `episodic.archive(id)`. The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via EpisodicMemoryStoreExt.");
122
- await this.#store.episodic.archive(episodeId, reason);
122
+ await this.#store.episodic.archive(episodeId, reason, scope);
123
123
  });
124
124
  }
125
125
  /**
@@ -165,7 +165,7 @@ var EpisodicMemory = class {
165
165
  }
166
166
  }
167
167
  span.setAttributes({ "memory.episodic.validate.forced": force });
168
- await store.setStatus(episodeId, "active", reason);
168
+ await store.setStatus(episodeId, "active", reason, scope);
169
169
  });
170
170
  }
171
171
  async #tryVectorSearch(scope, query, topK, asOf, includeQuarantined) {
@@ -1 +1 @@
1
- {"version":3,"file":"episodic-memory.js","names":["DEFAULT_WEIGHTS: EpisodeRetrievalWeights","#store","#tracer","#embedder","#embedderIdProvider","episode: Episode","#tryVectorSearch"],"sources":["../../src/tiers/episodic-memory.ts"],"sourcesContent":["import type {\n EmbedderProvider,\n Episode,\n MemoryHit,\n MemoryOwner,\n MemoryProvenance,\n MemoryStatus,\n Sensitivity,\n SessionScope,\n Tracer,\n} from '@graphorin/core';\nimport { QuarantinePromotionRefusedError } from '../errors/index.js';\nimport { newMemoryId } from '../internal/id.js';\nimport { detectMemoryInjection } from '../internal/injection-heuristics.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/**\n * Author-time episode payload. The framework derives `id`,\n * `kind: 'episodic'`, `userId`, `createdAt`, `updatedAt`, and the\n * `embedder_id` from the surrounding `EpisodicMemory.record(...)`\n * call.\n *\n * @stable\n */\nexport interface EpisodeInput {\n readonly summary: string;\n readonly startedAt: string;\n readonly endedAt: string;\n /** Optional importance score in `[0, 1]`. */\n readonly importance?: number;\n readonly sensitivity?: Sensitivity;\n readonly tags?: ReadonlyArray<string>;\n /**\n * Trust-provenance tag (P1-4). Episodes auto-formed by the\n * consolidator pass `'extraction'` so they land quarantined; omit\n * (defaults to first-party `active`) for user-authored episodes.\n */\n readonly provenance?: MemoryProvenance;\n /**\n * Retrieval-trust state (P1-4). Defaults to `active`; the\n * consolidator records auto-formed episodes as `'quarantined'` so\n * they are excluded from action-driving recall until validated.\n */\n readonly status?: MemoryStatus;\n /** Principal dimension (D3). `'agent'` on auto-formed episodes. */\n readonly owner?: MemoryOwner;\n}\n\n/**\n * Triple-signal episode retrieval weights. Defaults match DEC-105:\n * `recency 0.3`, `relevance 0.5`, `importance 0.2`. Implementations\n * normalize the weighted sum back to `[0, 1]`.\n *\n * @stable\n */\nexport interface EpisodeRetrievalWeights {\n readonly recency: number;\n readonly relevance: number;\n readonly importance: number;\n}\n\nconst DEFAULT_WEIGHTS: EpisodeRetrievalWeights = Object.freeze({\n recency: 0.3,\n relevance: 0.5,\n importance: 0.2,\n});\n\n/**\n * Per-call options accepted by {@link EpisodicMemory.search}.\n *\n * @stable\n */\nexport interface EpisodeSearchOptions {\n readonly topK?: number;\n readonly signal?: AbortSignal;\n readonly weights?: EpisodeRetrievalWeights;\n readonly dateRange?: { readonly from?: string; readonly to?: string };\n /**\n * Point-in-time (\"as of\") read. When set, only episodes that had\n * started by this instant (`started_at <= asOf`) are returned.\n * ISO-8601. Absent ⇒ current behaviour is unchanged. P0-2.\n *\n * @stable\n */\n readonly asOf?: string;\n /**\n * Include quarantined episodes in the result set (P1-4). Defaults to\n * `false`: action-driving recall never returns quarantined rows. Set\n * `true` only for the validation / inspector path - never for\n * auto-recall fed back into the model. Auto-formed episodes (P1-2)\n * land quarantined, so this is how an operator surfaces them for\n * review.\n *\n * @stable\n */\n readonly includeQuarantined?: boolean;\n}\n\n/**\n * `EpisodicMemory` - record + retrieve summarized stretches of past\n * activity. Stored embeddings power triple-signal retrieval (recency\n * × relevance × importance).\n *\n * @stable\n */\nexport class EpisodicMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n readonly #embedder: EmbedderProvider | null;\n readonly #embedderIdProvider: () => string | null;\n\n constructor(args: {\n store: MemoryStoreAdapter;\n tracer: Tracer;\n embedder: EmbedderProvider | null;\n embedderIdProvider: () => string | null;\n }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n this.#embedder = args.embedder;\n this.#embedderIdProvider = args.embedderIdProvider;\n }\n\n /** Persist an episode + its embedding (when an embedder is configured). */\n async record(scope: SessionScope, input: EpisodeInput): Promise<Episode> {\n return withMemorySpan(this.#tracer, 'memory.write.episodic', scope, {}, async (span) => {\n const now = new Date().toISOString();\n const episode: Episode = {\n id: newMemoryId('ep'),\n kind: 'episodic',\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n sensitivity: input.sensitivity ?? 'internal',\n summary: input.summary,\n startedAt: input.startedAt,\n endedAt: input.endedAt,\n ...(input.importance !== undefined ? { importance: input.importance } : {}),\n ...(input.tags !== undefined ? { tags: Object.freeze([...input.tags]) } : {}),\n ...(input.provenance !== undefined ? { provenance: input.provenance } : {}),\n ...(input.status !== undefined ? { status: input.status } : {}),\n ...(input.owner !== undefined ? { owner: input.owner } : {}),\n createdAt: now,\n updatedAt: now,\n };\n const embedderId = this.#embedderIdProvider();\n if (\n this.#embedder !== null &&\n embedderId !== null &&\n typeof this.#store.episodic.putWithEmbedding === 'function'\n ) {\n // PS-10: a stored episode summary is the `passage` role for E5.\n const [vector] = await this.#embedder.embed([input.summary], { taskType: 'passage' });\n if (vector !== undefined) {\n await this.#store.episodic.putWithEmbedding(episode, {\n embedding: { embedderId, vector },\n });\n } else {\n await this.#store.episodic.put(episode);\n }\n } else {\n await this.#store.episodic.put(episode);\n }\n span.setAttributes({\n 'memory.episodic.summary_length': input.summary.length,\n ...(input.importance !== undefined\n ? { 'memory.episodic.importance': input.importance }\n : {}),\n });\n return episode;\n });\n }\n\n /** Lookup a single episode by id. */\n async get(id: string): Promise<Episode | null> {\n return this.#store.episodic.get(id);\n }\n\n /**\n * Triple-signal episode retrieval (`recency × relevance ×\n * importance`). The vector signal is computed on demand when an\n * embedder is configured AND the storage adapter exposes\n * `searchVector`; otherwise the FTS5 BM25 score is fed into the\n * relevance term as a normalized fallback.\n */\n async search(\n scope: SessionScope,\n query: string,\n opts: EpisodeSearchOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<Episode>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.episodic',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const topK = opts.topK ?? 10;\n const weights = opts.weights ?? DEFAULT_WEIGHTS;\n const ftsHits = await this.#store.episodic.search(scope, {\n query,\n topK: topK * 2,\n ...(opts.asOf !== undefined ? { asOf: opts.asOf } : {}),\n // MRET-4: forward the date filter - it was accepted by the\n // recall_episodes tool and silently dropped right here.\n ...(opts.dateRange !== undefined ? { dateRange: opts.dateRange } : {}),\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n const vectorHits = await this.#tryVectorSearch(\n scope,\n query,\n topK * 2,\n opts.asOf,\n opts.includeQuarantined,\n );\n // MRET-4: the vector leg's store signature is positional (no\n // dateRange parameter) - apply the same overlap semantics to its\n // hits so both legs agree before the merge.\n const from = opts.dateRange?.from !== undefined ? Date.parse(opts.dateRange.from) : null;\n const to = opts.dateRange?.to !== undefined ? Date.parse(opts.dateRange.to) : null;\n const rangedVectorHits =\n from !== null || to !== null\n ? vectorHits.filter((h) => {\n if (from !== null && Date.parse(h.record.endedAt) < from) return false;\n if (to !== null && Date.parse(h.record.startedAt) > to) return false;\n return true;\n })\n : vectorHits;\n const merged = mergeRecency(ftsHits, rangedVectorHits, weights);\n const finalHits = merged.slice(0, topK);\n span.setAttributes({\n 'memory.search.episodic.fts_count': ftsHits.length,\n 'memory.search.episodic.vector_count': vectorHits.length,\n 'memory.search.episodic.final_count': finalHits.length,\n ...(opts.asOf !== undefined ? { 'memory.search.episodic.as_of': opts.asOf } : {}),\n });\n return finalHits;\n },\n );\n }\n\n /**\n * Soft-archive an episode. Storage adapters that implement\n * `EpisodicMemoryStoreExt.archive(...)` mark the row archived in\n * place. Adapters that do not expose the extension surface a\n * friendly `TypeError` so the operator can opt the storage layer\n * in (or call `episodic.put(...)` with the archived state set\n * manually).\n */\n async archive(scope: SessionScope, episodeId: string, reason?: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.episodic',\n scope,\n { 'memory.episodic.action': 'archive', 'memory.episodic.episode_id': episodeId },\n async () => {\n if (typeof this.#store.episodic.archive !== 'function') {\n throw new TypeError(\n '[graphorin/memory] EpisodicMemory.archive(...) requires a storage adapter that implements `episodic.archive(id)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via EpisodicMemoryStoreExt.',\n );\n }\n await this.#store.episodic.archive(episodeId, reason);\n },\n );\n }\n\n /**\n * Most-recent episodes by end time (newest first), with no embedding / FTS\n * query (MCON-1). Requires `EpisodicMemoryStoreExt.listRecent` - the default\n * `@graphorin/store-sqlite` adapter implements it. Optionally includes\n * quarantined episodes (the importance source for the reflection gate).\n */\n async listRecent(\n scope: SessionScope,\n limit: number,\n opts: { includeQuarantined?: boolean } = {},\n ): Promise<ReadonlyArray<Episode>> {\n return withMemorySpan(this.#tracer, 'memory.read.episodic', scope, {}, async (span) => {\n const store = this.#store.episodic;\n if (typeof store.listRecent !== 'function') {\n throw new Error(\n '[graphorin/memory] EpisodicMemory.listRecent requires EpisodicMemoryStoreExt.listRecent. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in.',\n );\n }\n const out = await store.listRecent(scope, limit, opts);\n span.setAttributes({ 'memory.read.episodic.count': out.length });\n return out;\n });\n }\n\n /** List the most recent episodes (no embedding required). */\n async recent(scope: SessionScope, opts: { topK?: number } = {}): Promise<ReadonlyArray<Episode>> {\n return this.listRecent(scope, opts.topK ?? 10, {});\n }\n\n /**\n * Promote a quarantined episode into default recall (MCON-2). Mirrors\n * {@link SemanticMemory.validate}: re-derives the injection verdict from the\n * stored summary and **refuses** promotion of an injection-flagged episode\n * (`QuarantinePromotionRefusedError`) unless an operator passes\n * `{ force: true }` from a trusted, non-agent context.\n */\n async validate(\n scope: SessionScope,\n episodeId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.episodic',\n scope,\n { 'memory.episodic.action': 'validate', 'memory.episodic.episode_id': episodeId },\n async (span) => {\n const store = this.#store.episodic;\n if (typeof store.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] EpisodicMemory.validate(...) requires a storage adapter that implements `episodic.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via EpisodicMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n const existing = await store.get(episodeId);\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.summary);\n if (injection.flagged) {\n span.setAttributes({ 'memory.episodic.validate.refused': true });\n throw new QuarantinePromotionRefusedError(episodeId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.episodic.validate.forced': force });\n await store.setStatus(episodeId, 'active', reason);\n },\n );\n }\n\n async #tryVectorSearch(\n scope: SessionScope,\n query: string,\n topK: number,\n asOf?: string,\n includeQuarantined?: boolean,\n ): Promise<ReadonlyArray<MemoryHit<Episode>>> {\n const embedderId = this.#embedderIdProvider();\n if (\n this.#embedder === null ||\n embedderId === null ||\n typeof this.#store.episodic.searchVector !== 'function'\n ) {\n return [];\n }\n // PS-10: a search query is the `query` role for E5.\n const [vector] = await this.#embedder.embed([query], { taskType: 'query' });\n if (vector === undefined) return [];\n return this.#store.episodic.searchVector(\n scope,\n vector,\n embedderId,\n topK,\n asOf,\n includeQuarantined,\n );\n }\n}\n\nfunction mergeRecency(\n ftsHits: ReadonlyArray<MemoryHit<Episode>>,\n vectorHits: ReadonlyArray<MemoryHit<Episode>>,\n weights: EpisodeRetrievalWeights,\n): ReadonlyArray<MemoryHit<Episode>> {\n const now = Date.now();\n const aggregates = new Map<\n string,\n {\n record: Episode;\n relevance: number;\n recency: number;\n importance: number;\n score: number;\n signals: Record<string, number>;\n }\n >();\n for (const hit of ftsHits) {\n const id = hit.record.id;\n const recency = computeRecency(now, hit.record);\n const importance = hit.record.importance ?? 0;\n const relevance = normalizeRelevance(hit.score);\n const score =\n weights.recency * recency + weights.relevance * relevance + weights.importance * importance;\n aggregates.set(id, {\n record: hit.record,\n relevance,\n recency,\n importance,\n score,\n signals: { fts: relevance, recency, importance },\n });\n }\n for (const hit of vectorHits) {\n const id = hit.record.id;\n const cosine = clamp01(hit.score);\n const existing = aggregates.get(id);\n if (existing === undefined) {\n const recency = computeRecency(now, hit.record);\n const importance = hit.record.importance ?? 0;\n const score =\n weights.recency * recency + weights.relevance * cosine + weights.importance * importance;\n aggregates.set(id, {\n record: hit.record,\n relevance: cosine,\n recency,\n importance,\n score,\n signals: { vector: cosine, recency, importance },\n });\n } else {\n const blended = Math.max(existing.relevance, cosine);\n existing.relevance = blended;\n existing.score =\n weights.recency * existing.recency +\n weights.relevance * blended +\n weights.importance * existing.importance;\n existing.signals.vector = cosine;\n existing.signals['rrf-blend'] = blended;\n }\n }\n const out = Array.from(aggregates.values()).map((a) => ({\n record: a.record,\n score: a.score,\n signals: Object.freeze({ ...a.signals }),\n }));\n out.sort((a, b) => b.score - a.score);\n return out;\n}\n\nfunction computeRecency(now: number, episode: Episode): number {\n const ended = Date.parse(episode.endedAt);\n if (!Number.isFinite(ended)) return 0;\n const elapsedDays = Math.max(0, (now - ended) / (24 * 60 * 60 * 1000));\n return Math.exp(-elapsedDays / 30);\n}\n\nfunction normalizeRelevance(score: number): number {\n // MRET-5/MST-7: the store returns `score = -bm25(...)` - POSITIVE for\n // every FTS match (SQLite bm25 is always negative), so the old\n // `1 / (1 + max(0, -score))` collapsed every lexical hit to exactly\n // 1.0 and the DEC-105 triple-signal ranking degraded to\n // recency + importance. Saturating ratio: 0 at no signal, → 1 as the\n // match strengthens, graduated in between (k = 1 half-point).\n return clamp01(Math.max(0, score) / (Math.max(0, score) + 1));\n}\n\nfunction clamp01(value: number): number {\n if (Number.isNaN(value)) return 0;\n if (value < 0) return 0;\n if (value > 1) return 1;\n return value;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"episodic-memory.js","names":["DEFAULT_WEIGHTS: EpisodeRetrievalWeights","#store","#tracer","#embedder","#embedderIdProvider","episode: Episode","#tryVectorSearch"],"sources":["../../src/tiers/episodic-memory.ts"],"sourcesContent":["import type {\n EmbedderProvider,\n Episode,\n MemoryHit,\n MemoryOwner,\n MemoryProvenance,\n MemoryStatus,\n Sensitivity,\n SessionScope,\n Tracer,\n} from '@graphorin/core';\nimport { QuarantinePromotionRefusedError } from '../errors/index.js';\nimport { newMemoryId } from '../internal/id.js';\nimport { detectMemoryInjection } from '../internal/injection-heuristics.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/**\n * Author-time episode payload. The framework derives `id`,\n * `kind: 'episodic'`, `userId`, `createdAt`, `updatedAt`, and the\n * `embedder_id` from the surrounding `EpisodicMemory.record(...)`\n * call.\n *\n * @stable\n */\nexport interface EpisodeInput {\n readonly summary: string;\n readonly startedAt: string;\n readonly endedAt: string;\n /** Optional importance score in `[0, 1]`. */\n readonly importance?: number;\n readonly sensitivity?: Sensitivity;\n readonly tags?: ReadonlyArray<string>;\n /**\n * Trust-provenance tag (P1-4). Episodes auto-formed by the\n * consolidator pass `'extraction'` so they land quarantined; omit\n * (defaults to first-party `active`) for user-authored episodes.\n */\n readonly provenance?: MemoryProvenance;\n /**\n * Retrieval-trust state (P1-4). Defaults to `active`; the\n * consolidator records auto-formed episodes as `'quarantined'` so\n * they are excluded from action-driving recall until validated.\n */\n readonly status?: MemoryStatus;\n /** Principal dimension (D3). `'agent'` on auto-formed episodes. */\n readonly owner?: MemoryOwner;\n}\n\n/**\n * Triple-signal episode retrieval weights. Defaults match DEC-105:\n * `recency 0.3`, `relevance 0.5`, `importance 0.2`. Implementations\n * normalize the weighted sum back to `[0, 1]`.\n *\n * @stable\n */\nexport interface EpisodeRetrievalWeights {\n readonly recency: number;\n readonly relevance: number;\n readonly importance: number;\n}\n\nconst DEFAULT_WEIGHTS: EpisodeRetrievalWeights = Object.freeze({\n recency: 0.3,\n relevance: 0.5,\n importance: 0.2,\n});\n\n/**\n * Per-call options accepted by {@link EpisodicMemory.search}.\n *\n * @stable\n */\nexport interface EpisodeSearchOptions {\n readonly topK?: number;\n readonly signal?: AbortSignal;\n readonly weights?: EpisodeRetrievalWeights;\n readonly dateRange?: { readonly from?: string; readonly to?: string };\n /**\n * Point-in-time (\"as of\") read. When set, only episodes that had\n * started by this instant (`started_at <= asOf`) are returned.\n * ISO-8601. Absent ⇒ current behaviour is unchanged. P0-2.\n *\n * @stable\n */\n readonly asOf?: string;\n /**\n * Include quarantined episodes in the result set (P1-4). Defaults to\n * `false`: action-driving recall never returns quarantined rows. Set\n * `true` only for the validation / inspector path - never for\n * auto-recall fed back into the model. Auto-formed episodes (P1-2)\n * land quarantined, so this is how an operator surfaces them for\n * review.\n *\n * @stable\n */\n readonly includeQuarantined?: boolean;\n}\n\n/**\n * `EpisodicMemory` - record + retrieve summarized stretches of past\n * activity. Stored embeddings power triple-signal retrieval (recency\n * × relevance × importance).\n *\n * @stable\n */\nexport class EpisodicMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n readonly #embedder: EmbedderProvider | null;\n readonly #embedderIdProvider: () => string | null;\n\n constructor(args: {\n store: MemoryStoreAdapter;\n tracer: Tracer;\n embedder: EmbedderProvider | null;\n embedderIdProvider: () => string | null;\n }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n this.#embedder = args.embedder;\n this.#embedderIdProvider = args.embedderIdProvider;\n }\n\n /** Persist an episode + its embedding (when an embedder is configured). */\n async record(scope: SessionScope, input: EpisodeInput): Promise<Episode> {\n return withMemorySpan(this.#tracer, 'memory.write.episodic', scope, {}, async (span) => {\n const now = new Date().toISOString();\n const episode: Episode = {\n id: newMemoryId('ep'),\n kind: 'episodic',\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n sensitivity: input.sensitivity ?? 'internal',\n summary: input.summary,\n startedAt: input.startedAt,\n endedAt: input.endedAt,\n ...(input.importance !== undefined ? { importance: input.importance } : {}),\n ...(input.tags !== undefined ? { tags: Object.freeze([...input.tags]) } : {}),\n ...(input.provenance !== undefined ? { provenance: input.provenance } : {}),\n ...(input.status !== undefined ? { status: input.status } : {}),\n ...(input.owner !== undefined ? { owner: input.owner } : {}),\n createdAt: now,\n updatedAt: now,\n };\n const embedderId = this.#embedderIdProvider();\n if (\n this.#embedder !== null &&\n embedderId !== null &&\n typeof this.#store.episodic.putWithEmbedding === 'function'\n ) {\n // PS-10: a stored episode summary is the `passage` role for E5.\n const [vector] = await this.#embedder.embed([input.summary], { taskType: 'passage' });\n if (vector !== undefined) {\n await this.#store.episodic.putWithEmbedding(episode, {\n embedding: { embedderId, vector },\n });\n } else {\n await this.#store.episodic.put(episode);\n }\n } else {\n await this.#store.episodic.put(episode);\n }\n span.setAttributes({\n 'memory.episodic.summary_length': input.summary.length,\n ...(input.importance !== undefined\n ? { 'memory.episodic.importance': input.importance }\n : {}),\n });\n return episode;\n });\n }\n\n /** Lookup a single episode by id. */\n async get(id: string): Promise<Episode | null> {\n return this.#store.episodic.get(id);\n }\n\n /**\n * Triple-signal episode retrieval (`recency × relevance ×\n * importance`). The vector signal is computed on demand when an\n * embedder is configured AND the storage adapter exposes\n * `searchVector`; otherwise the FTS5 BM25 score is fed into the\n * relevance term as a normalized fallback.\n */\n async search(\n scope: SessionScope,\n query: string,\n opts: EpisodeSearchOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<Episode>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.episodic',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const topK = opts.topK ?? 10;\n const weights = opts.weights ?? DEFAULT_WEIGHTS;\n const ftsHits = await this.#store.episodic.search(scope, {\n query,\n topK: topK * 2,\n ...(opts.asOf !== undefined ? { asOf: opts.asOf } : {}),\n // MRET-4: forward the date filter - it was accepted by the\n // recall_episodes tool and silently dropped right here.\n ...(opts.dateRange !== undefined ? { dateRange: opts.dateRange } : {}),\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n const vectorHits = await this.#tryVectorSearch(\n scope,\n query,\n topK * 2,\n opts.asOf,\n opts.includeQuarantined,\n );\n // MRET-4: the vector leg's store signature is positional (no\n // dateRange parameter) - apply the same overlap semantics to its\n // hits so both legs agree before the merge.\n const from = opts.dateRange?.from !== undefined ? Date.parse(opts.dateRange.from) : null;\n const to = opts.dateRange?.to !== undefined ? Date.parse(opts.dateRange.to) : null;\n const rangedVectorHits =\n from !== null || to !== null\n ? vectorHits.filter((h) => {\n if (from !== null && Date.parse(h.record.endedAt) < from) return false;\n if (to !== null && Date.parse(h.record.startedAt) > to) return false;\n return true;\n })\n : vectorHits;\n const merged = mergeRecency(ftsHits, rangedVectorHits, weights);\n const finalHits = merged.slice(0, topK);\n span.setAttributes({\n 'memory.search.episodic.fts_count': ftsHits.length,\n 'memory.search.episodic.vector_count': vectorHits.length,\n 'memory.search.episodic.final_count': finalHits.length,\n ...(opts.asOf !== undefined ? { 'memory.search.episodic.as_of': opts.asOf } : {}),\n });\n return finalHits;\n },\n );\n }\n\n /**\n * Soft-archive an episode. Storage adapters that implement\n * `EpisodicMemoryStoreExt.archive(...)` mark the row archived in\n * place. Adapters that do not expose the extension surface a\n * friendly `TypeError` so the operator can opt the storage layer\n * in (or call `episodic.put(...)` with the archived state set\n * manually).\n */\n async archive(scope: SessionScope, episodeId: string, reason?: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.episodic',\n scope,\n { 'memory.episodic.action': 'archive', 'memory.episodic.episode_id': episodeId },\n async () => {\n if (typeof this.#store.episodic.archive !== 'function') {\n throw new TypeError(\n '[graphorin/memory] EpisodicMemory.archive(...) requires a storage adapter that implements `episodic.archive(id)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via EpisodicMemoryStoreExt.',\n );\n }\n await this.#store.episodic.archive(episodeId, reason, scope);\n },\n );\n }\n\n /**\n * Most-recent episodes by end time (newest first), with no embedding / FTS\n * query (MCON-1). Requires `EpisodicMemoryStoreExt.listRecent` - the default\n * `@graphorin/store-sqlite` adapter implements it. Optionally includes\n * quarantined episodes (the importance source for the reflection gate).\n */\n async listRecent(\n scope: SessionScope,\n limit: number,\n opts: { includeQuarantined?: boolean } = {},\n ): Promise<ReadonlyArray<Episode>> {\n return withMemorySpan(this.#tracer, 'memory.read.episodic', scope, {}, async (span) => {\n const store = this.#store.episodic;\n if (typeof store.listRecent !== 'function') {\n throw new Error(\n '[graphorin/memory] EpisodicMemory.listRecent requires EpisodicMemoryStoreExt.listRecent. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in.',\n );\n }\n const out = await store.listRecent(scope, limit, opts);\n span.setAttributes({ 'memory.read.episodic.count': out.length });\n return out;\n });\n }\n\n /** List the most recent episodes (no embedding required). */\n async recent(scope: SessionScope, opts: { topK?: number } = {}): Promise<ReadonlyArray<Episode>> {\n return this.listRecent(scope, opts.topK ?? 10, {});\n }\n\n /**\n * Promote a quarantined episode into default recall (MCON-2). Mirrors\n * {@link SemanticMemory.validate}: re-derives the injection verdict from the\n * stored summary and **refuses** promotion of an injection-flagged episode\n * (`QuarantinePromotionRefusedError`) unless an operator passes\n * `{ force: true }` from a trusted, non-agent context.\n */\n async validate(\n scope: SessionScope,\n episodeId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.episodic',\n scope,\n { 'memory.episodic.action': 'validate', 'memory.episodic.episode_id': episodeId },\n async (span) => {\n const store = this.#store.episodic;\n if (typeof store.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] EpisodicMemory.validate(...) requires a storage adapter that implements `episodic.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via EpisodicMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n const existing = await store.get(episodeId);\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.summary);\n if (injection.flagged) {\n span.setAttributes({ 'memory.episodic.validate.refused': true });\n throw new QuarantinePromotionRefusedError(episodeId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.episodic.validate.forced': force });\n await store.setStatus(episodeId, 'active', reason, scope);\n },\n );\n }\n\n async #tryVectorSearch(\n scope: SessionScope,\n query: string,\n topK: number,\n asOf?: string,\n includeQuarantined?: boolean,\n ): Promise<ReadonlyArray<MemoryHit<Episode>>> {\n const embedderId = this.#embedderIdProvider();\n if (\n this.#embedder === null ||\n embedderId === null ||\n typeof this.#store.episodic.searchVector !== 'function'\n ) {\n return [];\n }\n // PS-10: a search query is the `query` role for E5.\n const [vector] = await this.#embedder.embed([query], { taskType: 'query' });\n if (vector === undefined) return [];\n return this.#store.episodic.searchVector(\n scope,\n vector,\n embedderId,\n topK,\n asOf,\n includeQuarantined,\n );\n }\n}\n\nfunction mergeRecency(\n ftsHits: ReadonlyArray<MemoryHit<Episode>>,\n vectorHits: ReadonlyArray<MemoryHit<Episode>>,\n weights: EpisodeRetrievalWeights,\n): ReadonlyArray<MemoryHit<Episode>> {\n const now = Date.now();\n const aggregates = new Map<\n string,\n {\n record: Episode;\n relevance: number;\n recency: number;\n importance: number;\n score: number;\n signals: Record<string, number>;\n }\n >();\n for (const hit of ftsHits) {\n const id = hit.record.id;\n const recency = computeRecency(now, hit.record);\n const importance = hit.record.importance ?? 0;\n const relevance = normalizeRelevance(hit.score);\n const score =\n weights.recency * recency + weights.relevance * relevance + weights.importance * importance;\n aggregates.set(id, {\n record: hit.record,\n relevance,\n recency,\n importance,\n score,\n signals: { fts: relevance, recency, importance },\n });\n }\n for (const hit of vectorHits) {\n const id = hit.record.id;\n const cosine = clamp01(hit.score);\n const existing = aggregates.get(id);\n if (existing === undefined) {\n const recency = computeRecency(now, hit.record);\n const importance = hit.record.importance ?? 0;\n const score =\n weights.recency * recency + weights.relevance * cosine + weights.importance * importance;\n aggregates.set(id, {\n record: hit.record,\n relevance: cosine,\n recency,\n importance,\n score,\n signals: { vector: cosine, recency, importance },\n });\n } else {\n const blended = Math.max(existing.relevance, cosine);\n existing.relevance = blended;\n existing.score =\n weights.recency * existing.recency +\n weights.relevance * blended +\n weights.importance * existing.importance;\n existing.signals.vector = cosine;\n existing.signals['rrf-blend'] = blended;\n }\n }\n const out = Array.from(aggregates.values()).map((a) => ({\n record: a.record,\n score: a.score,\n signals: Object.freeze({ ...a.signals }),\n }));\n out.sort((a, b) => b.score - a.score);\n return out;\n}\n\nfunction computeRecency(now: number, episode: Episode): number {\n const ended = Date.parse(episode.endedAt);\n if (!Number.isFinite(ended)) return 0;\n const elapsedDays = Math.max(0, (now - ended) / (24 * 60 * 60 * 1000));\n return Math.exp(-elapsedDays / 30);\n}\n\nfunction normalizeRelevance(score: number): number {\n // MRET-5/MST-7: the store returns `score = -bm25(...)` - POSITIVE for\n // every FTS match (SQLite bm25 is always negative), so the old\n // `1 / (1 + max(0, -score))` collapsed every lexical hit to exactly\n // 1.0 and the DEC-105 triple-signal ranking degraded to\n // recency + importance. Saturating ratio: 0 at no signal, → 1 as the\n // match strengthens, graduated in between (k = 1 half-point).\n return clamp01(Math.max(0, score) / (Math.max(0, score) + 1));\n}\n\nfunction clamp01(value: number): number {\n if (Number.isNaN(value)) return 0;\n if (value < 0) return 0;\n if (value > 1) return 1;\n return value;\n}\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import { EpisodeInput, EpisodeRetrievalWeights, EpisodeSearchOptions, EpisodicMemory } from "./episodic-memory.js";
2
- import { FactInput, FactRememberOptions, FactSearchOptions, FusionStrategy, FusionWeights, IterativeRecallResult, IterativeSearchOptions, RememberOutcome, SemanticMemory } from "./semantic-memory.js";
2
+ import { FactInput, FactRememberOptions, FactSearchOptions, FusionStrategy, FusionWeights, IterativeRecallResult, IterativeSearchOptions, RememberOutcome, SemanticMemory, SemanticSearchDefaults } from "./semantic-memory.js";
3
3
  import { BlockDefinition, BlockSpec, WorkingMemory, defineBlock } from "./working-memory.js";
4
4
  import { InsightListOptions, InsightMemory, InsightSearchOptions, capInsightsBelowFacts } from "./insight-memory.js";
5
5
  import { InduceOptions, ProceduralMemory, RuleActivationContext, RuleInput } from "./procedural-memory.js";
6
6
  import { SessionCompactionPolicy, SessionCompactionResult, SessionMemory } from "./session-memory.js";
7
7
  import { SharedMemory } from "./shared-memory.js";
8
- export { type BlockDefinition, type BlockSpec, type EpisodeInput, type EpisodeRetrievalWeights, type EpisodeSearchOptions, EpisodicMemory, type FactInput, type FactRememberOptions, type FactSearchOptions, type FusionStrategy, type FusionWeights, type InduceOptions, type InsightListOptions, InsightMemory, type InsightSearchOptions, type IterativeRecallResult, type IterativeSearchOptions, ProceduralMemory, type RememberOutcome, type RuleActivationContext, type RuleInput, SemanticMemory, type SessionCompactionPolicy, type SessionCompactionResult, SessionMemory, SharedMemory, WorkingMemory, capInsightsBelowFacts, defineBlock };
8
+ export { type BlockDefinition, type BlockSpec, type EpisodeInput, type EpisodeRetrievalWeights, type EpisodeSearchOptions, EpisodicMemory, type FactInput, type FactRememberOptions, type FactSearchOptions, type FusionStrategy, type FusionWeights, type InduceOptions, type InsightListOptions, InsightMemory, type InsightSearchOptions, type IterativeRecallResult, type IterativeSearchOptions, ProceduralMemory, type RememberOutcome, type RuleActivationContext, type RuleInput, SemanticMemory, type SemanticSearchDefaults, type SessionCompactionPolicy, type SessionCompactionResult, SessionMemory, SharedMemory, WorkingMemory, capInsightsBelowFacts, defineBlock };
@@ -81,7 +81,7 @@ var InsightMemory = class {
81
81
  }
82
82
  }
83
83
  span.setAttributes({ "memory.insight.validate.forced": force });
84
- await store.setStatus(insightId, "active", reason);
84
+ await store.setStatus(insightId, "active", reason, scope);
85
85
  });
86
86
  }
87
87
  };