@graphorin/memory 0.5.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 (287) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +401 -0
  4. package/dist/conflict/index.d.ts +3 -0
  5. package/dist/conflict/index.js +7 -0
  6. package/dist/conflict/locale-packs/en.js +262 -0
  7. package/dist/conflict/locale-packs/en.js.map +1 -0
  8. package/dist/conflict/locale-packs/index.js +4 -0
  9. package/dist/conflict/locale-packs/types.d.ts +103 -0
  10. package/dist/conflict/locale-packs/types.d.ts.map +1 -0
  11. package/dist/conflict/locale-packs/types.js +55 -0
  12. package/dist/conflict/locale-packs/types.js.map +1 -0
  13. package/dist/conflict/pipeline.d.ts +35 -0
  14. package/dist/conflict/pipeline.d.ts.map +1 -0
  15. package/dist/conflict/pipeline.js +257 -0
  16. package/dist/conflict/pipeline.js.map +1 -0
  17. package/dist/conflict/stages/helpers.js +66 -0
  18. package/dist/conflict/stages/helpers.js.map +1 -0
  19. package/dist/conflict/stages/stage1-exact-dedup.js +21 -0
  20. package/dist/conflict/stages/stage1-exact-dedup.js.map +1 -0
  21. package/dist/conflict/stages/stage2-embedding-three-zone.js +37 -0
  22. package/dist/conflict/stages/stage2-embedding-three-zone.js.map +1 -0
  23. package/dist/conflict/stages/stage3-heuristic-regex.js +45 -0
  24. package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -0
  25. package/dist/conflict/stages/stage4-subject-predicate.js +31 -0
  26. package/dist/conflict/stages/stage4-subject-predicate.js.map +1 -0
  27. package/dist/conflict/stages/stage5-defer-to-deep.js +22 -0
  28. package/dist/conflict/stages/stage5-defer-to-deep.js.map +1 -0
  29. package/dist/conflict/types.d.ts +222 -0
  30. package/dist/conflict/types.d.ts.map +1 -0
  31. package/dist/conflict/types.js +50 -0
  32. package/dist/conflict/types.js.map +1 -0
  33. package/dist/consolidator/budget.d.ts +1 -0
  34. package/dist/consolidator/budget.js +214 -0
  35. package/dist/consolidator/budget.js.map +1 -0
  36. package/dist/consolidator/decay.d.ts +35 -0
  37. package/dist/consolidator/decay.d.ts.map +1 -0
  38. package/dist/consolidator/decay.js +91 -0
  39. package/dist/consolidator/decay.js.map +1 -0
  40. package/dist/consolidator/dlq.js +58 -0
  41. package/dist/consolidator/dlq.js.map +1 -0
  42. package/dist/consolidator/errors.d.ts +54 -0
  43. package/dist/consolidator/errors.d.ts.map +1 -0
  44. package/dist/consolidator/errors.js +67 -0
  45. package/dist/consolidator/errors.js.map +1 -0
  46. package/dist/consolidator/idempotency.d.ts +1 -0
  47. package/dist/consolidator/idempotency.js +22 -0
  48. package/dist/consolidator/idempotency.js.map +1 -0
  49. package/dist/consolidator/index.d.ts +34 -0
  50. package/dist/consolidator/index.d.ts.map +1 -0
  51. package/dist/consolidator/index.js +163 -0
  52. package/dist/consolidator/index.js.map +1 -0
  53. package/dist/consolidator/lock.d.ts +2 -0
  54. package/dist/consolidator/lock.js +89 -0
  55. package/dist/consolidator/lock.js.map +1 -0
  56. package/dist/consolidator/noise-filter.d.ts +2 -0
  57. package/dist/consolidator/noise-filter.js +173 -0
  58. package/dist/consolidator/noise-filter.js.map +1 -0
  59. package/dist/consolidator/phases/deep.d.ts +4 -0
  60. package/dist/consolidator/phases/deep.js +198 -0
  61. package/dist/consolidator/phases/deep.js.map +1 -0
  62. package/dist/consolidator/phases/induce.d.ts +154 -0
  63. package/dist/consolidator/phases/induce.d.ts.map +1 -0
  64. package/dist/consolidator/phases/induce.js +253 -0
  65. package/dist/consolidator/phases/induce.js.map +1 -0
  66. package/dist/consolidator/phases/light.d.ts +4 -0
  67. package/dist/consolidator/phases/light.js +99 -0
  68. package/dist/consolidator/phases/light.js.map +1 -0
  69. package/dist/consolidator/phases/reflect.js +287 -0
  70. package/dist/consolidator/phases/reflect.js.map +1 -0
  71. package/dist/consolidator/phases/standard.d.ts +8 -0
  72. package/dist/consolidator/phases/standard.js +499 -0
  73. package/dist/consolidator/phases/standard.js.map +1 -0
  74. package/dist/consolidator/reconcile.js +192 -0
  75. package/dist/consolidator/reconcile.js.map +1 -0
  76. package/dist/consolidator/runtime.d.ts +62 -0
  77. package/dist/consolidator/runtime.d.ts.map +1 -0
  78. package/dist/consolidator/runtime.js +580 -0
  79. package/dist/consolidator/runtime.js.map +1 -0
  80. package/dist/consolidator/scheduler.d.ts +111 -0
  81. package/dist/consolidator/scheduler.d.ts.map +1 -0
  82. package/dist/consolidator/scheduler.js +71 -0
  83. package/dist/consolidator/scheduler.js.map +1 -0
  84. package/dist/consolidator/triggers.d.ts +40 -0
  85. package/dist/consolidator/triggers.d.ts.map +1 -0
  86. package/dist/consolidator/triggers.js +94 -0
  87. package/dist/consolidator/triggers.js.map +1 -0
  88. package/dist/consolidator/types.d.ts +397 -0
  89. package/dist/consolidator/types.d.ts.map +1 -0
  90. package/dist/consolidator/types.js +117 -0
  91. package/dist/consolidator/types.js.map +1 -0
  92. package/dist/context-engine/annotations.d.ts +118 -0
  93. package/dist/context-engine/annotations.d.ts.map +1 -0
  94. package/dist/context-engine/annotations.js +102 -0
  95. package/dist/context-engine/annotations.js.map +1 -0
  96. package/dist/context-engine/auto-recall.d.ts +52 -0
  97. package/dist/context-engine/auto-recall.d.ts.map +1 -0
  98. package/dist/context-engine/auto-recall.js +41 -0
  99. package/dist/context-engine/auto-recall.js.map +1 -0
  100. package/dist/context-engine/compaction/clear-tool-results.d.ts +55 -0
  101. package/dist/context-engine/compaction/clear-tool-results.d.ts.map +1 -0
  102. package/dist/context-engine/compaction/clear-tool-results.js +97 -0
  103. package/dist/context-engine/compaction/clear-tool-results.js.map +1 -0
  104. package/dist/context-engine/compaction/compactor.d.ts +52 -0
  105. package/dist/context-engine/compaction/compactor.d.ts.map +1 -0
  106. package/dist/context-engine/compaction/compactor.js +196 -0
  107. package/dist/context-engine/compaction/compactor.js.map +1 -0
  108. package/dist/context-engine/compaction/hooks/reanchor-persona-block.d.ts +15 -0
  109. package/dist/context-engine/compaction/hooks/reanchor-persona-block.d.ts.map +1 -0
  110. package/dist/context-engine/compaction/hooks/reanchor-persona-block.js +27 -0
  111. package/dist/context-engine/compaction/hooks/reanchor-persona-block.js.map +1 -0
  112. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.d.ts +18 -0
  113. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.d.ts.map +1 -0
  114. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js +47 -0
  115. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js.map +1 -0
  116. package/dist/context-engine/compaction/hooks/reanchor-project-rules.d.ts +15 -0
  117. package/dist/context-engine/compaction/hooks/reanchor-project-rules.d.ts.map +1 -0
  118. package/dist/context-engine/compaction/hooks/reanchor-project-rules.js +32 -0
  119. package/dist/context-engine/compaction/hooks/reanchor-project-rules.js.map +1 -0
  120. package/dist/context-engine/compaction/hooks/types.d.ts +42 -0
  121. package/dist/context-engine/compaction/hooks/types.d.ts.map +1 -0
  122. package/dist/context-engine/compaction/index.d.ts +9 -0
  123. package/dist/context-engine/compaction/index.js +9 -0
  124. package/dist/context-engine/compaction/templates/summary-9-section.d.ts +84 -0
  125. package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -0
  126. package/dist/context-engine/compaction/templates/summary-9-section.js +84 -0
  127. package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -0
  128. package/dist/context-engine/compaction/thresholds.d.ts +68 -0
  129. package/dist/context-engine/compaction/thresholds.d.ts.map +1 -0
  130. package/dist/context-engine/compaction/thresholds.js +63 -0
  131. package/dist/context-engine/compaction/thresholds.js.map +1 -0
  132. package/dist/context-engine/compaction/types.d.ts +202 -0
  133. package/dist/context-engine/compaction/types.d.ts.map +1 -0
  134. package/dist/context-engine/engine.d.ts +258 -0
  135. package/dist/context-engine/engine.d.ts.map +1 -0
  136. package/dist/context-engine/engine.js +424 -0
  137. package/dist/context-engine/engine.js.map +1 -0
  138. package/dist/context-engine/index.d.ts +68 -0
  139. package/dist/context-engine/index.d.ts.map +1 -0
  140. package/dist/context-engine/index.js +24 -0
  141. package/dist/context-engine/locale-packs/en.d.ts +14 -0
  142. package/dist/context-engine/locale-packs/en.d.ts.map +1 -0
  143. package/dist/context-engine/locale-packs/en.js +87 -0
  144. package/dist/context-engine/locale-packs/en.js.map +1 -0
  145. package/dist/context-engine/locale-packs/index.js +4 -0
  146. package/dist/context-engine/locale-packs/resolver.d.ts +41 -0
  147. package/dist/context-engine/locale-packs/resolver.d.ts.map +1 -0
  148. package/dist/context-engine/locale-packs/resolver.js +119 -0
  149. package/dist/context-engine/locale-packs/resolver.js.map +1 -0
  150. package/dist/context-engine/locale-packs/types.d.ts +119 -0
  151. package/dist/context-engine/locale-packs/types.d.ts.map +1 -0
  152. package/dist/context-engine/locale-packs/types.js +24 -0
  153. package/dist/context-engine/locale-packs/types.js.map +1 -0
  154. package/dist/context-engine/metadata.d.ts +40 -0
  155. package/dist/context-engine/metadata.d.ts.map +1 -0
  156. package/dist/context-engine/metadata.js +66 -0
  157. package/dist/context-engine/metadata.js.map +1 -0
  158. package/dist/context-engine/preambles/inbound-en.d.ts +20 -0
  159. package/dist/context-engine/preambles/inbound-en.d.ts.map +1 -0
  160. package/dist/context-engine/preambles/inbound-en.js +23 -0
  161. package/dist/context-engine/preambles/inbound-en.js.map +1 -0
  162. package/dist/context-engine/privacy-filter.d.ts +108 -0
  163. package/dist/context-engine/privacy-filter.d.ts.map +1 -0
  164. package/dist/context-engine/privacy-filter.js +113 -0
  165. package/dist/context-engine/privacy-filter.js.map +1 -0
  166. package/dist/context-engine/templates/base-en.d.ts +19 -0
  167. package/dist/context-engine/templates/base-en.d.ts.map +1 -0
  168. package/dist/context-engine/templates/base-en.js +22 -0
  169. package/dist/context-engine/templates/base-en.js.map +1 -0
  170. package/dist/context-engine/templates/composer.d.ts +55 -0
  171. package/dist/context-engine/templates/composer.d.ts.map +1 -0
  172. package/dist/context-engine/templates/composer.js +51 -0
  173. package/dist/context-engine/templates/composer.js.map +1 -0
  174. package/dist/context-engine/token-budget.d.ts +99 -0
  175. package/dist/context-engine/token-budget.d.ts.map +1 -0
  176. package/dist/context-engine/token-budget.js +211 -0
  177. package/dist/context-engine/token-budget.js.map +1 -0
  178. package/dist/context-engine/token-counter.d.ts +54 -0
  179. package/dist/context-engine/token-counter.d.ts.map +1 -0
  180. package/dist/context-engine/token-counter.js +81 -0
  181. package/dist/context-engine/token-counter.js.map +1 -0
  182. package/dist/errors/index.d.ts +171 -0
  183. package/dist/errors/index.d.ts.map +1 -0
  184. package/dist/errors/index.js +206 -0
  185. package/dist/errors/index.js.map +1 -0
  186. package/dist/facade.d.ts +278 -0
  187. package/dist/facade.d.ts.map +1 -0
  188. package/dist/facade.js +265 -0
  189. package/dist/facade.js.map +1 -0
  190. package/dist/graph/entity-resolver.d.ts +135 -0
  191. package/dist/graph/entity-resolver.d.ts.map +1 -0
  192. package/dist/graph/entity-resolver.js +202 -0
  193. package/dist/graph/entity-resolver.js.map +1 -0
  194. package/dist/graph/index.d.ts +1 -0
  195. package/dist/index.d.ts +103 -0
  196. package/dist/index.d.ts.map +1 -0
  197. package/dist/index.js +95 -0
  198. package/dist/index.js.map +1 -0
  199. package/dist/internal/contextualize.d.ts +14 -0
  200. package/dist/internal/contextualize.d.ts.map +1 -0
  201. package/dist/internal/contextualize.js +110 -0
  202. package/dist/internal/contextualize.js.map +1 -0
  203. package/dist/internal/embedder-binding.js +50 -0
  204. package/dist/internal/embedder-binding.js.map +1 -0
  205. package/dist/internal/id.js +24 -0
  206. package/dist/internal/id.js.map +1 -0
  207. package/dist/internal/injection-heuristics.js +64 -0
  208. package/dist/internal/injection-heuristics.js.map +1 -0
  209. package/dist/internal/spans.js +35 -0
  210. package/dist/internal/spans.js.map +1 -0
  211. package/dist/internal/storage-adapter.d.ts +742 -0
  212. package/dist/internal/storage-adapter.d.ts.map +1 -0
  213. package/dist/migration/embedder-migration.d.ts +124 -0
  214. package/dist/migration/embedder-migration.d.ts.map +1 -0
  215. package/dist/migration/embedder-migration.js +109 -0
  216. package/dist/migration/embedder-migration.js.map +1 -0
  217. package/dist/migration/index.d.ts +2 -0
  218. package/dist/migration/index.js +3 -0
  219. package/dist/search/explain.d.ts +74 -0
  220. package/dist/search/explain.d.ts.map +1 -0
  221. package/dist/search/explain.js +50 -0
  222. package/dist/search/explain.js.map +1 -0
  223. package/dist/search/index.d.ts +6 -0
  224. package/dist/search/index.js +6 -0
  225. package/dist/search/iterative.d.ts +223 -0
  226. package/dist/search/iterative.d.ts.map +1 -0
  227. package/dist/search/iterative.js +330 -0
  228. package/dist/search/iterative.js.map +1 -0
  229. package/dist/search/query-transform.d.ts +112 -0
  230. package/dist/search/query-transform.d.ts.map +1 -0
  231. package/dist/search/query-transform.js +184 -0
  232. package/dist/search/query-transform.js.map +1 -0
  233. package/dist/search/rrf.d.ts +95 -0
  234. package/dist/search/rrf.d.ts.map +1 -0
  235. package/dist/search/rrf.js +166 -0
  236. package/dist/search/rrf.js.map +1 -0
  237. package/dist/search/types.d.ts +50 -0
  238. package/dist/search/types.d.ts.map +1 -0
  239. package/dist/tiers/episodic-memory.d.ts +142 -0
  240. package/dist/tiers/episodic-memory.d.ts.map +1 -0
  241. package/dist/tiers/episodic-memory.js +254 -0
  242. package/dist/tiers/episodic-memory.js.map +1 -0
  243. package/dist/tiers/index.d.ts +8 -0
  244. package/dist/tiers/index.js +9 -0
  245. package/dist/tiers/insight-memory.d.ts +71 -0
  246. package/dist/tiers/insight-memory.d.ts.map +1 -0
  247. package/dist/tiers/insight-memory.js +133 -0
  248. package/dist/tiers/insight-memory.js.map +1 -0
  249. package/dist/tiers/procedural-memory.d.ts +147 -0
  250. package/dist/tiers/procedural-memory.d.ts.map +1 -0
  251. package/dist/tiers/procedural-memory.js +267 -0
  252. package/dist/tiers/procedural-memory.js.map +1 -0
  253. package/dist/tiers/semantic-memory.d.ts +461 -0
  254. package/dist/tiers/semantic-memory.d.ts.map +1 -0
  255. package/dist/tiers/semantic-memory.js +631 -0
  256. package/dist/tiers/semantic-memory.js.map +1 -0
  257. package/dist/tiers/session-memory.d.ts +117 -0
  258. package/dist/tiers/session-memory.d.ts.map +1 -0
  259. package/dist/tiers/session-memory.js +163 -0
  260. package/dist/tiers/session-memory.js.map +1 -0
  261. package/dist/tiers/shared-memory.d.ts +29 -0
  262. package/dist/tiers/shared-memory.d.ts.map +1 -0
  263. package/dist/tiers/shared-memory.js +58 -0
  264. package/dist/tiers/shared-memory.js.map +1 -0
  265. package/dist/tiers/working-memory.d.ts +129 -0
  266. package/dist/tiers/working-memory.d.ts.map +1 -0
  267. package/dist/tiers/working-memory.js +229 -0
  268. package/dist/tiers/working-memory.js.map +1 -0
  269. package/dist/tools/block-tools.d.ts +98 -0
  270. package/dist/tools/block-tools.d.ts.map +1 -0
  271. package/dist/tools/block-tools.js +114 -0
  272. package/dist/tools/block-tools.js.map +1 -0
  273. package/dist/tools/fact-tools.d.ts +294 -0
  274. package/dist/tools/fact-tools.d.ts.map +1 -0
  275. package/dist/tools/fact-tools.js +293 -0
  276. package/dist/tools/fact-tools.js.map +1 -0
  277. package/dist/tools/index.d.ts +35 -0
  278. package/dist/tools/index.d.ts.map +1 -0
  279. package/dist/tools/index.js +36 -0
  280. package/dist/tools/index.js.map +1 -0
  281. package/dist/tools/recall-tools.d.ts +209 -0
  282. package/dist/tools/recall-tools.d.ts.map +1 -0
  283. package/dist/tools/recall-tools.js +170 -0
  284. package/dist/tools/recall-tools.js.map +1 -0
  285. package/dist/tools/types.d.ts +40 -0
  286. package/dist/tools/types.d.ts.map +1 -0
  287. package/package.json +119 -0
@@ -0,0 +1,133 @@
1
+ import { withMemorySpan } from "../internal/spans.js";
2
+ import { QuarantinePromotionRefusedError } from "../errors/index.js";
3
+ import { detectMemoryInjection } from "../internal/injection-heuristics.js";
4
+
5
+ //#region src/tiers/insight-memory.ts
6
+ /**
7
+ * `InsightMemory` — list / search reflection insights. A no-op (returns
8
+ * empty) when the storage adapter does not expose the optional
9
+ * `insights` surface.
10
+ *
11
+ * @stable
12
+ */
13
+ var InsightMemory = class {
14
+ #store;
15
+ #tracer;
16
+ constructor(args) {
17
+ this.#store = args.store;
18
+ this.#tracer = args.tracer;
19
+ }
20
+ /** FTS keyword search over insight text. */
21
+ async search(scope, query, opts = {}) {
22
+ return withMemorySpan(this.#tracer, "memory.search.insight", scope, { "memory.search.query_length": query.length }, async (span) => {
23
+ const store = this.#store.insights;
24
+ if (store === void 0) {
25
+ span.setAttributes({ "memory.search.insight.count": 0 });
26
+ return [];
27
+ }
28
+ const hits = await store.search(scope, query, {
29
+ topK: opts.topK ?? 10,
30
+ ...opts.includeQuarantined !== void 0 ? { includeQuarantined: opts.includeQuarantined } : {}
31
+ });
32
+ for (const hit of hits) try {
33
+ await store.bumpSalience(hit.record.id, 1, "retrieval");
34
+ } catch {}
35
+ span.setAttributes({ "memory.search.insight.count": hits.length });
36
+ return hits;
37
+ });
38
+ }
39
+ /** Most-recent insights for the scope (newest first). */
40
+ async list(scope, opts = {}) {
41
+ return withMemorySpan(this.#tracer, "memory.read.insight", scope, {}, async (span) => {
42
+ const store = this.#store.insights;
43
+ if (store === void 0) {
44
+ span.setAttributes({ "memory.read.insight.count": 0 });
45
+ return [];
46
+ }
47
+ const out = await store.list(scope, {
48
+ ...opts.limit !== void 0 ? { limit: opts.limit } : {},
49
+ ...opts.includeQuarantined !== void 0 ? { includeQuarantined: opts.includeQuarantined } : {}
50
+ });
51
+ span.setAttributes({ "memory.read.insight.count": out.length });
52
+ return out;
53
+ });
54
+ }
55
+ /** Lookup a single insight by id. */
56
+ async get(id) {
57
+ const store = this.#store.insights;
58
+ if (store === void 0 || typeof store.get !== "function") return null;
59
+ return store.get(id);
60
+ }
61
+ /**
62
+ * Promote a quarantined insight out of quarantine (MCON-2). Mirrors
63
+ * {@link SemanticMemory.validate}: re-derives the injection verdict from the
64
+ * stored text and **refuses** promotion of an injection-flagged insight
65
+ * unless an operator passes `{ force: true }` from a trusted context.
66
+ */
67
+ async validate(scope, insightId, reason, options) {
68
+ await withMemorySpan(this.#tracer, "memory.write.insight", scope, {
69
+ "memory.insight.action": "validate",
70
+ "memory.insight.insight_id": insightId
71
+ }, async (span) => {
72
+ const store = this.#store.insights;
73
+ if (store === void 0 || typeof store.setStatus !== "function") throw new TypeError("[graphorin/memory] InsightMemory.validate(...) requires a storage adapter that implements `insights.setStatus(id, status)`. The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via InsightMemoryStoreExt.");
74
+ const force = options?.force === true;
75
+ const existing = typeof store.get === "function" ? await store.get(insightId) : null;
76
+ if (existing !== null && !force) {
77
+ const injection = detectMemoryInjection(existing.text);
78
+ if (injection.flagged) {
79
+ span.setAttributes({ "memory.insight.validate.refused": true });
80
+ throw new QuarantinePromotionRefusedError(insightId, injection.markers);
81
+ }
82
+ }
83
+ span.setAttributes({ "memory.insight.validate.forced": force });
84
+ await store.setStatus(insightId, "active", reason);
85
+ });
86
+ }
87
+ };
88
+ /** Tie-break margin keeping a capped insight strictly below its cited fact. */
89
+ const RANK_CAP_EPSILON = 1e-6;
90
+ /**
91
+ * Enforce the P1-1 rank ceiling: an insight may never outrank a primary
92
+ * fact **it cites**. For each insight hit, if any fact it cites is
93
+ * present in `factHits`, its score is lowered to strictly below that
94
+ * cited fact's score — so concatenating the two lists and sorting by
95
+ * score descending can never place the insight above the evidence it
96
+ * was synthesized from. Insights whose cited facts are absent from
97
+ * `factHits` are returned unchanged; this is a relative, not a global,
98
+ * cap (per the execution plan: "never outrank the primary facts they
99
+ * cite").
100
+ *
101
+ * Pure + deterministic — does not mutate its inputs.
102
+ *
103
+ * @stable
104
+ */
105
+ function capInsightsBelowFacts(factHits, insightHits) {
106
+ if (factHits.length === 0 || insightHits.length === 0) return insightHits;
107
+ const factScoreById = /* @__PURE__ */ new Map();
108
+ for (const hit of factHits) {
109
+ const prev = factScoreById.get(hit.record.id);
110
+ if (prev === void 0 || hit.score > prev) factScoreById.set(hit.record.id, hit.score);
111
+ }
112
+ return insightHits.map((hit) => {
113
+ let citedMax = Number.NEGATIVE_INFINITY;
114
+ for (const id of hit.record.cites) {
115
+ const score = factScoreById.get(id);
116
+ if (score !== void 0 && score > citedMax) citedMax = score;
117
+ }
118
+ if (citedMax === Number.NEGATIVE_INFINITY || hit.score < citedMax) return hit;
119
+ const capped = citedMax - RANK_CAP_EPSILON;
120
+ return {
121
+ ...hit,
122
+ score: capped,
123
+ signals: Object.freeze({
124
+ ...hit.signals ?? {},
125
+ "rank-cap": capped
126
+ })
127
+ };
128
+ });
129
+ }
130
+
131
+ //#endregion
132
+ export { InsightMemory, capInsightsBelowFacts };
133
+ //# sourceMappingURL=insight-memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insight-memory.js","names":["#store","#tracer"],"sources":["../../src/tiers/insight-memory.ts"],"sourcesContent":["/**\n * `InsightMemory` — the thin, read-only surface over the reflection\n * insights the consolidator synthesizes (P1-1). Insights are *derived*\n * (quarantined + `provenance: 'reflection'`), carry mandatory citations,\n * and are retrieval-ranked **below the primary facts they cite**. The\n * write + salience-management paths live in the consolidator's\n * reflection pass; this tier only lists / searches for the validation /\n * inspector path.\n *\n * Search is FTS-only — insights are a soft inspector surface, not\n * primary recall, so no embedder is wired here.\n *\n * @packageDocumentation\n */\n\nimport type { Fact, Insight, MemoryHit, SessionScope, Tracer } from '@graphorin/core';\nimport { QuarantinePromotionRefusedError } from '../errors/index.js';\nimport { detectMemoryInjection } from '../internal/injection-heuristics.js';\nimport { withMemorySpan } from '../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../internal/storage-adapter.js';\n\n/** Per-call options accepted by {@link InsightMemory.search}. */\nexport interface InsightSearchOptions {\n readonly topK?: number;\n /**\n * Include quarantined insights (P1-4). Defaults to `false`. Since\n * reflection-synthesized insights *always* land quarantined, set this\n * `true` to surface them for the validation / inspector path — never\n * for auto-recall fed back into the model.\n */\n readonly includeQuarantined?: boolean;\n readonly signal?: AbortSignal;\n}\n\n/** Per-call options accepted by {@link InsightMemory.list}. */\nexport interface InsightListOptions {\n readonly limit?: number;\n /** Include quarantined insights (validation / inspector path). */\n readonly includeQuarantined?: boolean;\n}\n\n/**\n * `InsightMemory` — list / search reflection insights. A no-op (returns\n * empty) when the storage adapter does not expose the optional\n * `insights` surface.\n *\n * @stable\n */\nexport class InsightMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n\n constructor(args: { store: MemoryStoreAdapter; tracer: Tracer }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n }\n\n /** FTS keyword search over insight text. */\n async search(\n scope: SessionScope,\n query: string,\n opts: InsightSearchOptions = {},\n ): Promise<ReadonlyArray<MemoryHit<Insight>>> {\n return withMemorySpan(\n this.#tracer,\n 'memory.search.insight',\n scope,\n { 'memory.search.query_length': query.length },\n async (span) => {\n const store = this.#store.insights;\n if (store === undefined) {\n span.setAttributes({ 'memory.search.insight.count': 0 });\n return [];\n }\n const hits = await store.search(scope, query, {\n topK: opts.topK ?? 10,\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n // MCON-16: retrieval reinforces (ExpeL) — a recalled insight gains\n // +1 salience, offsetting the per-reflection-pass −1 decay, so\n // used insights persist while unused ones slide to the prune\n // floor. Best-effort bookkeeping: never break the read path.\n for (const hit of hits) {\n try {\n await store.bumpSalience(hit.record.id, 1, 'retrieval');\n } catch {\n // Advisory only.\n }\n }\n span.setAttributes({ 'memory.search.insight.count': hits.length });\n return hits;\n },\n );\n }\n\n /** Most-recent insights for the scope (newest first). */\n async list(scope: SessionScope, opts: InsightListOptions = {}): Promise<ReadonlyArray<Insight>> {\n return withMemorySpan(this.#tracer, 'memory.read.insight', scope, {}, async (span) => {\n const store = this.#store.insights;\n if (store === undefined) {\n span.setAttributes({ 'memory.read.insight.count': 0 });\n return [];\n }\n const out = await store.list(scope, {\n ...(opts.limit !== undefined ? { limit: opts.limit } : {}),\n ...(opts.includeQuarantined !== undefined\n ? { includeQuarantined: opts.includeQuarantined }\n : {}),\n });\n span.setAttributes({ 'memory.read.insight.count': out.length });\n return out;\n });\n }\n\n /** Lookup a single insight by id. */\n async get(id: string): Promise<Insight | null> {\n const store = this.#store.insights;\n if (store === undefined || typeof store.get !== 'function') return null;\n return store.get(id);\n }\n\n /**\n * Promote a quarantined insight out of quarantine (MCON-2). Mirrors\n * {@link SemanticMemory.validate}: re-derives the injection verdict from the\n * stored text and **refuses** promotion of an injection-flagged insight\n * unless an operator passes `{ force: true }` from a trusted context.\n */\n async validate(\n scope: SessionScope,\n insightId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.insight',\n scope,\n { 'memory.insight.action': 'validate', 'memory.insight.insight_id': insightId },\n async (span) => {\n const store = this.#store.insights;\n if (store === undefined || typeof store.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] InsightMemory.validate(...) requires a storage adapter that implements `insights.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via InsightMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n const existing = typeof store.get === 'function' ? await store.get(insightId) : null;\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.text);\n if (injection.flagged) {\n span.setAttributes({ 'memory.insight.validate.refused': true });\n throw new QuarantinePromotionRefusedError(insightId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.insight.validate.forced': force });\n await store.setStatus(insightId, 'active', reason);\n },\n );\n }\n}\n\n/** Tie-break margin keeping a capped insight strictly below its cited fact. */\nconst RANK_CAP_EPSILON = 1e-6;\n\n/**\n * Enforce the P1-1 rank ceiling: an insight may never outrank a primary\n * fact **it cites**. For each insight hit, if any fact it cites is\n * present in `factHits`, its score is lowered to strictly below that\n * cited fact's score — so concatenating the two lists and sorting by\n * score descending can never place the insight above the evidence it\n * was synthesized from. Insights whose cited facts are absent from\n * `factHits` are returned unchanged; this is a relative, not a global,\n * cap (per the execution plan: \"never outrank the primary facts they\n * cite\").\n *\n * Pure + deterministic — does not mutate its inputs.\n *\n * @stable\n */\nexport function capInsightsBelowFacts(\n factHits: ReadonlyArray<MemoryHit<Fact>>,\n insightHits: ReadonlyArray<MemoryHit<Insight>>,\n): ReadonlyArray<MemoryHit<Insight>> {\n if (factHits.length === 0 || insightHits.length === 0) return insightHits;\n const factScoreById = new Map<string, number>();\n for (const hit of factHits) {\n const prev = factScoreById.get(hit.record.id);\n if (prev === undefined || hit.score > prev) factScoreById.set(hit.record.id, hit.score);\n }\n return insightHits.map((hit) => {\n let citedMax = Number.NEGATIVE_INFINITY;\n for (const id of hit.record.cites) {\n const score = factScoreById.get(id);\n if (score !== undefined && score > citedMax) citedMax = score;\n }\n // No cited fact present in the fused set, or already below it ⇒ keep.\n if (citedMax === Number.NEGATIVE_INFINITY || hit.score < citedMax) return hit;\n const capped = citedMax - RANK_CAP_EPSILON;\n return {\n ...hit,\n score: capped,\n signals: Object.freeze({ ...(hit.signals ?? {}), 'rank-cap': capped }),\n };\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,147 @@
1
+ import { MemoryStoreAdapter } from "../internal/storage-adapter.js";
2
+ import { Trajectory, WorkflowInducer } from "../consolidator/phases/induce.js";
3
+ import { Rule, RunState, Sensitivity, SessionScope, Tracer } from "@graphorin/core";
4
+
5
+ //#region src/tiers/procedural-memory.d.ts
6
+
7
+ /**
8
+ * Author-time rule payload accepted by {@link ProceduralMemory.define}.
9
+ *
10
+ * @stable
11
+ */
12
+ interface RuleInput {
13
+ readonly text: string;
14
+ /**
15
+ * Free-form predicate evaluated by {@link ProceduralMemory.activate}.
16
+ * The predicate language is intentionally narrow in v0.1: either
17
+ * the literal string `'always'`, or a `'topic=...'` / `'tag=...'`
18
+ * shorthand. Custom predicates should be expressed as a callable
19
+ * matched in `activate(...)`'s `customMatchers` argument.
20
+ */
21
+ readonly condition?: string;
22
+ /** Default `'public'` per DEC-126 — rules are NOT user data. */
23
+ readonly sensitivity?: Sensitivity;
24
+ readonly priority?: number;
25
+ readonly tags?: ReadonlyArray<string>;
26
+ /**
27
+ * Optional structured workflow payload (P2-2). Usually set by
28
+ * {@link ProceduralMemory.induce}, but accepted here so an author can
29
+ * round-trip a hand-written procedure. See {@link Rule.steps}.
30
+ */
31
+ readonly steps?: ReadonlyArray<string>;
32
+ /** Variable names abstracted into {@link RuleInput.steps} (P2-2). */
33
+ readonly variables?: ReadonlyArray<string>;
34
+ /** Verifiable success criteria stored with the procedure (P2-2). */
35
+ readonly successCriteria?: ReadonlyArray<string>;
36
+ }
37
+ /**
38
+ * Options for {@link ProceduralMemory.induce}.
39
+ *
40
+ * @stable
41
+ */
42
+ interface InduceOptions {
43
+ /** Sensitivity of the stored procedure. Default `'internal'`. */
44
+ readonly sensitivity?: Sensitivity;
45
+ /** Priority of the stored procedure. Default {@link INDUCED_PRIORITY} (40). */
46
+ readonly priority?: number;
47
+ }
48
+ /**
49
+ * Predicate context passed to {@link ProceduralMemory.activate}.
50
+ *
51
+ * @stable
52
+ */
53
+ interface RuleActivationContext {
54
+ readonly topic?: string;
55
+ readonly tags?: ReadonlyArray<string>;
56
+ }
57
+ /**
58
+ * `ProceduralMemory` — standing orders activated when the agent's
59
+ * current context matches the rule's predicate. The activation rules
60
+ * are deterministic so the agent runtime + ContextEngine can render
61
+ * the active set into the system prompt every step.
62
+ *
63
+ * P2-2 adds {@link ProceduralMemory.induce}: distil a reusable workflow
64
+ * from a successful agent trajectory and store it **quarantined** (it must
65
+ * not drive actions until validated). Quarantined procedures are excluded
66
+ * from {@link ProceduralMemory.activate} but remain visible to
67
+ * {@link ProceduralMemory.list}.
68
+ *
69
+ * @stable
70
+ */
71
+ declare class ProceduralMemory {
72
+ #private;
73
+ constructor(args: {
74
+ store: MemoryStoreAdapter;
75
+ tracer: Tracer;
76
+ inducer?: WorkflowInducer | null;
77
+ promoteAfterSuccesses?: number | null;
78
+ });
79
+ /**
80
+ * Record the outcome of one demonstrated reuse of a procedure
81
+ * (MCON-2 part 4). A success increments the rule's persistent
82
+ * `successCount`; when `procedurePromotion.afterSuccesses` is
83
+ * configured and a QUARANTINED procedure reaches the threshold it is
84
+ * promoted through {@link validate} — the injection gate still
85
+ * applies, so a flagged text refuses promotion (surfaced as
86
+ * `refused: true`) no matter how many successes accumulate.
87
+ * Failures are observed but not persisted (no negative counter yet).
88
+ *
89
+ * Callers decide what "success" means — typically
90
+ * `checkSuccessCriteria(...)` over the procedure's stored
91
+ * `successCriteria`, or an operator's judgement.
92
+ *
93
+ * @stable
94
+ */
95
+ recordOutcome(scope: SessionScope, ruleId: string, succeeded: boolean): Promise<{
96
+ readonly successCount: number;
97
+ readonly promoted: boolean;
98
+ readonly refused: boolean;
99
+ }>;
100
+ /** Persist a rule. Returns the stored record. */
101
+ define(scope: SessionScope, input: RuleInput): Promise<Rule>;
102
+ /**
103
+ * Induce a reusable procedure (P2-2) from a successful agent trajectory
104
+ * and store it **quarantined** + `provenance: 'induction'` (P1-4). Returns
105
+ * the stored {@link Rule}, or `null` when the trajectory was unsuccessful /
106
+ * empty or the inducer produced nothing inducible.
107
+ *
108
+ * Throws {@link ProcedureInductionNotConfiguredError} when no inducer was
109
+ * configured (`createMemory({ procedureInduction: { provider } })`).
110
+ */
111
+ induce(scope: SessionScope, trajectory: Trajectory, opts?: InduceOptions): Promise<Rule | null>;
112
+ /**
113
+ * Convenience over {@link induce}: distil the {@link Trajectory} from a
114
+ * completed {@link RunState} (the agent's already-emitted run state) and
115
+ * induce a procedure. The success signal is `status === 'completed'`.
116
+ */
117
+ induceFromRun(scope: SessionScope, run: RunState, opts?: InduceOptions): Promise<Rule | null>;
118
+ /** Soft-delete a rule. */
119
+ remove(scope: SessionScope, ruleId: string, reason?: string): Promise<void>;
120
+ /** List every active (non-deleted) rule for the supplied scope. */
121
+ list(scope: SessionScope): Promise<ReadonlyArray<Rule>>;
122
+ /**
123
+ * Return the rules active under `context`. Rules without a
124
+ * `condition` are always active; the bundled predicate vocabulary
125
+ * supports the literals `'always'`, `'topic=<topic>'`, and
126
+ * `'tag=<tag>'`. Anything outside that grammar is treated as
127
+ * always-active so callers do not silently lose rules.
128
+ *
129
+ * **Quarantined procedures are excluded** (P1-4 / P2-2): an induced
130
+ * procedure must not drive actions until validated, so activation — which
131
+ * feeds the system prompt — never surfaces it.
132
+ */
133
+ activate(scope: SessionScope, context?: RuleActivationContext): Promise<ReadonlyArray<Rule>>;
134
+ /**
135
+ * Promote a quarantined (induced) procedure into `activate()` (MCON-2).
136
+ * Mirrors {@link SemanticMemory.validate}: re-derives the injection verdict
137
+ * from the stored rule text and **refuses** promotion of an injection-flagged
138
+ * procedure unless an operator passes `{ force: true }`. Induced procedures
139
+ * drive *actions*, so this gate matters most for them.
140
+ */
141
+ validate(scope: SessionScope, ruleId: string, reason?: string, options?: {
142
+ readonly force?: boolean;
143
+ }): Promise<void>;
144
+ }
145
+ //#endregion
146
+ export { InduceOptions, ProceduralMemory, RuleActivationContext, RuleInput };
147
+ //# sourceMappingURL=procedural-memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"procedural-memory.d.ts","names":[],"sources":["../../src/tiers/procedural-memory.ts"],"sourcesContent":[],"mappings":";;;;;;;;AAkCA;;;AAmBmB,UAnBF,SAAA,CAmBE;EAEI,SAAA,IAAA,EAAA,MAAA;EAEM;;AAQ7B;AAYA;AAmBA;;;EAcc,SAAA,SAAA,CAAA,EAAA,MAAA;EA4BH;EAGN,SAAA,WAAA,CAAA,EAhGoB,WAgGpB;EA8CiB,SAAA,QAAA,CAAA,EAAA,MAAA;EAAqB,SAAA,IAAA,CAAA,EA5IzB,aA4IyB,CAAA,MAAA,CAAA;EAAoB;;;;;EA8ClD,SAAA,KAAA,CAAA,EApLM,aAoLN,CAAA,MAAA,CAAA;EAAR;EAqDM,SAAA,SAAA,CAAA,EAvOY,aAuOZ,CAAA,MAAA,CAAA;EACF;EACC,SAAA,eAAA,CAAA,EAvOmB,aAuOnB,CAAA,MAAA,CAAA;;;;;;;AAmBiC,UAlP1B,aAAA,CAkP0B;EAAR;EAoBxB,SAAA,WAAA,CAAA,EApQc,WAoQd;EACE;EACc,SAAA,QAAA,CAAA,EAAA,MAAA;;;;;;;UA5PV,qBAAA;;kBAEC;;;;;;;;;;;;;;;;cAiBL,gBAAA;;;WAYF;YACC;cACE;;;;;;;;;;;;;;;;;;;uBA4BH,mDAGN;;;;;;gBA8CiB,qBAAqB,YAAY,QAAQ;;;;;;;;;;gBA2CpD,0BACK,mBACN,gBACL,QAAQ;;;;;;uBAqDF,mBACF,iBACC,gBACL,QAAQ;;gBAKS,gDAAgD;;cAalD,eAAe,QAAQ,cAAc;;;;;;;;;;;;kBAoB9C,wBACE,wBACR,QAAQ,cAAc;;;;;;;;kBAehB;;MAIN"}
@@ -0,0 +1,267 @@
1
+ import { withMemorySpan } from "../internal/spans.js";
2
+ import { ProcedureInductionNotConfiguredError, QuarantinePromotionRefusedError } from "../errors/index.js";
3
+ import { runWorkflowInduction, trajectoryFromRunState } from "../consolidator/phases/induce.js";
4
+ import { newMemoryId } from "../internal/id.js";
5
+ import { detectMemoryInjection } from "../internal/injection-heuristics.js";
6
+
7
+ //#region src/tiers/procedural-memory.ts
8
+ /** Default priority for an *induced* procedure — below the author default
9
+ * (50) because an induced, still-quarantined procedure is provisional. */
10
+ const INDUCED_PRIORITY = 40;
11
+ /**
12
+ * `ProceduralMemory` — standing orders activated when the agent's
13
+ * current context matches the rule's predicate. The activation rules
14
+ * are deterministic so the agent runtime + ContextEngine can render
15
+ * the active set into the system prompt every step.
16
+ *
17
+ * P2-2 adds {@link ProceduralMemory.induce}: distil a reusable workflow
18
+ * from a successful agent trajectory and store it **quarantined** (it must
19
+ * not drive actions until validated). Quarantined procedures are excluded
20
+ * from {@link ProceduralMemory.activate} but remain visible to
21
+ * {@link ProceduralMemory.list}.
22
+ *
23
+ * @stable
24
+ */
25
+ var ProceduralMemory = class {
26
+ #store;
27
+ #tracer;
28
+ /** Opt-in workflow inducer (P2-2). `null` ⇒ {@link induce} throws. */
29
+ #inducer;
30
+ /**
31
+ * Promotion-by-demonstrated-success threshold (MCON-2 part 4).
32
+ * `null` ⇒ outcomes are counted but never auto-promote.
33
+ */
34
+ #promoteAfterSuccesses;
35
+ constructor(args) {
36
+ this.#store = args.store;
37
+ this.#tracer = args.tracer;
38
+ this.#inducer = args.inducer ?? null;
39
+ const k = args.promoteAfterSuccesses;
40
+ this.#promoteAfterSuccesses = typeof k === "number" && Number.isFinite(k) && k >= 1 ? Math.floor(k) : null;
41
+ }
42
+ /**
43
+ * Record the outcome of one demonstrated reuse of a procedure
44
+ * (MCON-2 part 4). A success increments the rule's persistent
45
+ * `successCount`; when `procedurePromotion.afterSuccesses` is
46
+ * configured and a QUARANTINED procedure reaches the threshold it is
47
+ * promoted through {@link validate} — the injection gate still
48
+ * applies, so a flagged text refuses promotion (surfaced as
49
+ * `refused: true`) no matter how many successes accumulate.
50
+ * Failures are observed but not persisted (no negative counter yet).
51
+ *
52
+ * Callers decide what "success" means — typically
53
+ * `checkSuccessCriteria(...)` over the procedure's stored
54
+ * `successCriteria`, or an operator's judgement.
55
+ *
56
+ * @stable
57
+ */
58
+ async recordOutcome(scope, ruleId, succeeded) {
59
+ return withMemorySpan(this.#tracer, "memory.write.procedural", scope, {
60
+ "memory.procedural.action": "record-outcome",
61
+ "memory.procedural.rule_id": ruleId
62
+ }, async (span) => {
63
+ const store = this.#store.procedural;
64
+ if (!succeeded || typeof store.recordSuccess !== "function") {
65
+ span.setAttributes({ "memory.procedural.outcome.counted": false });
66
+ return {
67
+ successCount: 0,
68
+ promoted: false,
69
+ refused: false
70
+ };
71
+ }
72
+ const successCount = await store.recordSuccess(ruleId);
73
+ span.setAttributes({ "memory.procedural.outcome.success_count": successCount });
74
+ const threshold = this.#promoteAfterSuccesses;
75
+ if (threshold === null || successCount < threshold) return {
76
+ successCount,
77
+ promoted: false,
78
+ refused: false
79
+ };
80
+ const existing = (await store.list(scope)).find((rule) => rule.id === ruleId) ?? null;
81
+ if (existing === null || existing.status !== "quarantined") return {
82
+ successCount,
83
+ promoted: false,
84
+ refused: false
85
+ };
86
+ try {
87
+ await this.validate(scope, ruleId, `demonstrated-success: ${successCount} >= ${threshold}`);
88
+ span.setAttributes({ "memory.procedural.outcome.promoted": true });
89
+ return {
90
+ successCount,
91
+ promoted: true,
92
+ refused: false
93
+ };
94
+ } catch (err) {
95
+ if (err instanceof QuarantinePromotionRefusedError) {
96
+ span.setAttributes({ "memory.procedural.outcome.refused": true });
97
+ return {
98
+ successCount,
99
+ promoted: false,
100
+ refused: true
101
+ };
102
+ }
103
+ throw err;
104
+ }
105
+ });
106
+ }
107
+ /** Persist a rule. Returns the stored record. */
108
+ async define(scope, input) {
109
+ return withMemorySpan(this.#tracer, "memory.write.procedural", scope, {}, async (span) => {
110
+ const now = (/* @__PURE__ */ new Date()).toISOString();
111
+ const rule = {
112
+ id: newMemoryId("rule"),
113
+ kind: "procedural",
114
+ userId: scope.userId,
115
+ ...scope.sessionId !== void 0 ? { sessionId: scope.sessionId } : {},
116
+ ...scope.agentId !== void 0 ? { agentId: scope.agentId } : {},
117
+ sensitivity: input.sensitivity ?? "public",
118
+ text: input.text,
119
+ ...input.condition !== void 0 ? { condition: input.condition } : {},
120
+ priority: input.priority ?? 50,
121
+ ...input.tags !== void 0 ? { tags: Object.freeze([...input.tags]) } : {},
122
+ ...input.steps !== void 0 ? { steps: Object.freeze([...input.steps]) } : {},
123
+ ...input.variables !== void 0 ? { variables: Object.freeze([...input.variables]) } : {},
124
+ ...input.successCriteria !== void 0 ? { successCriteria: Object.freeze([...input.successCriteria]) } : {},
125
+ createdAt: now,
126
+ updatedAt: now
127
+ };
128
+ await this.#store.procedural.add(rule);
129
+ span.setAttributes({
130
+ "memory.procedural.priority": rule.priority,
131
+ "memory.procedural.text_length": input.text.length
132
+ });
133
+ return rule;
134
+ });
135
+ }
136
+ /**
137
+ * Induce a reusable procedure (P2-2) from a successful agent trajectory
138
+ * and store it **quarantined** + `provenance: 'induction'` (P1-4). Returns
139
+ * the stored {@link Rule}, or `null` when the trajectory was unsuccessful /
140
+ * empty or the inducer produced nothing inducible.
141
+ *
142
+ * Throws {@link ProcedureInductionNotConfiguredError} when no inducer was
143
+ * configured (`createMemory({ procedureInduction: { provider } })`).
144
+ */
145
+ async induce(scope, trajectory, opts = {}) {
146
+ const inducer = this.#inducer;
147
+ if (inducer === null) throw new ProcedureInductionNotConfiguredError();
148
+ return withMemorySpan(this.#tracer, "memory.write.procedural", scope, { "memory.procedural.action": "induce" }, async (span) => {
149
+ const induced = await runWorkflowInduction(trajectory, inducer, {});
150
+ if (induced === null) {
151
+ span.setAttributes({ "memory.procedural.induced": 0 });
152
+ return null;
153
+ }
154
+ const now = (/* @__PURE__ */ new Date()).toISOString();
155
+ const rule = {
156
+ id: newMemoryId("rule"),
157
+ kind: "procedural",
158
+ userId: scope.userId,
159
+ ...scope.sessionId !== void 0 ? { sessionId: scope.sessionId } : {},
160
+ ...scope.agentId !== void 0 ? { agentId: scope.agentId } : {},
161
+ sensitivity: opts.sensitivity ?? "internal",
162
+ text: renderProcedureText(induced),
163
+ priority: opts.priority ?? INDUCED_PRIORITY,
164
+ steps: Object.freeze([...induced.steps]),
165
+ variables: Object.freeze([...induced.variables]),
166
+ successCriteria: Object.freeze([...induced.successCriteria]),
167
+ provenance: "induction",
168
+ status: "quarantined",
169
+ createdAt: now,
170
+ updatedAt: now
171
+ };
172
+ await this.#store.procedural.add(rule);
173
+ span.setAttributes({
174
+ "memory.procedural.induced": 1,
175
+ "memory.procedural.steps": induced.steps.length,
176
+ "memory.procedural.variables": induced.variables.length
177
+ });
178
+ return rule;
179
+ });
180
+ }
181
+ /**
182
+ * Convenience over {@link induce}: distil the {@link Trajectory} from a
183
+ * completed {@link RunState} (the agent's already-emitted run state) and
184
+ * induce a procedure. The success signal is `status === 'completed'`.
185
+ */
186
+ async induceFromRun(scope, run, opts = {}) {
187
+ return this.induce(scope, trajectoryFromRunState(run), opts);
188
+ }
189
+ /** Soft-delete a rule. */
190
+ async remove(scope, ruleId, reason) {
191
+ await withMemorySpan(this.#tracer, "memory.write.procedural", scope, {
192
+ "memory.procedural.action": "remove",
193
+ "memory.procedural.rule_id": ruleId
194
+ }, async () => {
195
+ await this.#store.procedural.remove(ruleId, reason);
196
+ });
197
+ }
198
+ /** List every active (non-deleted) rule for the supplied scope. */
199
+ async list(scope) {
200
+ return withMemorySpan(this.#tracer, "memory.read.procedural", scope, {}, async (span) => {
201
+ const out = await this.#store.procedural.list(scope);
202
+ span.setAttributes({ "memory.read.procedural.count": out.length });
203
+ return out;
204
+ });
205
+ }
206
+ /**
207
+ * Return the rules active under `context`. Rules without a
208
+ * `condition` are always active; the bundled predicate vocabulary
209
+ * supports the literals `'always'`, `'topic=<topic>'`, and
210
+ * `'tag=<tag>'`. Anything outside that grammar is treated as
211
+ * always-active so callers do not silently lose rules.
212
+ *
213
+ * **Quarantined procedures are excluded** (P1-4 / P2-2): an induced
214
+ * procedure must not drive actions until validated, so activation — which
215
+ * feeds the system prompt — never surfaces it.
216
+ */
217
+ async activate(scope, context = {}) {
218
+ return (await this.list(scope)).filter((rule) => rule.status !== "quarantined" && predicateMatches(rule, context)).sort((a, b) => b.priority - a.priority);
219
+ }
220
+ /**
221
+ * Promote a quarantined (induced) procedure into `activate()` (MCON-2).
222
+ * Mirrors {@link SemanticMemory.validate}: re-derives the injection verdict
223
+ * from the stored rule text and **refuses** promotion of an injection-flagged
224
+ * procedure unless an operator passes `{ force: true }`. Induced procedures
225
+ * drive *actions*, so this gate matters most for them.
226
+ */
227
+ async validate(scope, ruleId, reason, options) {
228
+ await withMemorySpan(this.#tracer, "memory.write.procedural", scope, {
229
+ "memory.procedural.action": "validate",
230
+ "memory.procedural.rule_id": ruleId
231
+ }, async (span) => {
232
+ const store = this.#store.procedural;
233
+ if (typeof store.setStatus !== "function") throw new TypeError("[graphorin/memory] ProceduralMemory.validate(...) requires a storage adapter that implements `procedural.setStatus(id, status)`. The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via ProceduralMemoryStoreExt.");
234
+ const force = options?.force === true;
235
+ const existing = (await store.list(scope)).find((rule) => rule.id === ruleId) ?? null;
236
+ if (existing !== null && !force) {
237
+ const injection = detectMemoryInjection(existing.text);
238
+ if (injection.flagged) {
239
+ span.setAttributes({ "memory.procedural.validate.refused": true });
240
+ throw new QuarantinePromotionRefusedError(ruleId, injection.markers);
241
+ }
242
+ }
243
+ span.setAttributes({ "memory.procedural.validate.forced": force });
244
+ await store.setStatus(ruleId, "active", reason);
245
+ });
246
+ }
247
+ };
248
+ /** Render an induced procedure into the human-readable `Rule.text`. */
249
+ function renderProcedureText(procedure) {
250
+ return [procedure.title, ...procedure.steps.map((step, i) => `${i + 1}. ${step}`)].join("\n");
251
+ }
252
+ function predicateMatches(rule, context) {
253
+ if (rule.condition === void 0 || rule.condition === "always") return true;
254
+ if (rule.condition.startsWith("topic=")) {
255
+ const wanted = rule.condition.slice(6).trim();
256
+ return context.topic === wanted;
257
+ }
258
+ if (rule.condition.startsWith("tag=")) {
259
+ const wanted = rule.condition.slice(4).trim();
260
+ return (context.tags ?? []).includes(wanted);
261
+ }
262
+ return true;
263
+ }
264
+
265
+ //#endregion
266
+ export { ProceduralMemory };
267
+ //# sourceMappingURL=procedural-memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"procedural-memory.js","names":["#store","#tracer","#inducer","#promoteAfterSuccesses","rule: Rule"],"sources":["../../src/tiers/procedural-memory.ts"],"sourcesContent":["import type {\n MemoryProvenance,\n MemoryStatus,\n Rule,\n RunState,\n Sensitivity,\n SessionScope,\n Tracer,\n} from '@graphorin/core';\nimport {\n type InducedProcedure,\n runWorkflowInduction,\n type Trajectory,\n trajectoryFromRunState,\n type WorkflowInducer,\n} from '../consolidator/phases/induce.js';\nimport {\n ProcedureInductionNotConfiguredError,\n QuarantinePromotionRefusedError,\n} 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/** Default priority for an *induced* procedure — below the author default\n * (50) because an induced, still-quarantined procedure is provisional. */\nconst INDUCED_PRIORITY = 40;\n\n/**\n * Author-time rule payload accepted by {@link ProceduralMemory.define}.\n *\n * @stable\n */\nexport interface RuleInput {\n readonly text: string;\n /**\n * Free-form predicate evaluated by {@link ProceduralMemory.activate}.\n * The predicate language is intentionally narrow in v0.1: either\n * the literal string `'always'`, or a `'topic=...'` / `'tag=...'`\n * shorthand. Custom predicates should be expressed as a callable\n * matched in `activate(...)`'s `customMatchers` argument.\n */\n readonly condition?: string;\n /** Default `'public'` per DEC-126 — rules are NOT user data. */\n readonly sensitivity?: Sensitivity;\n readonly priority?: number;\n readonly tags?: ReadonlyArray<string>;\n /**\n * Optional structured workflow payload (P2-2). Usually set by\n * {@link ProceduralMemory.induce}, but accepted here so an author can\n * round-trip a hand-written procedure. See {@link Rule.steps}.\n */\n readonly steps?: ReadonlyArray<string>;\n /** Variable names abstracted into {@link RuleInput.steps} (P2-2). */\n readonly variables?: ReadonlyArray<string>;\n /** Verifiable success criteria stored with the procedure (P2-2). */\n readonly successCriteria?: ReadonlyArray<string>;\n}\n\n/**\n * Options for {@link ProceduralMemory.induce}.\n *\n * @stable\n */\nexport interface InduceOptions {\n /** Sensitivity of the stored procedure. Default `'internal'`. */\n readonly sensitivity?: Sensitivity;\n /** Priority of the stored procedure. Default {@link INDUCED_PRIORITY} (40). */\n readonly priority?: number;\n}\n\n/**\n * Predicate context passed to {@link ProceduralMemory.activate}.\n *\n * @stable\n */\nexport interface RuleActivationContext {\n readonly topic?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * `ProceduralMemory` — standing orders activated when the agent's\n * current context matches the rule's predicate. The activation rules\n * are deterministic so the agent runtime + ContextEngine can render\n * the active set into the system prompt every step.\n *\n * P2-2 adds {@link ProceduralMemory.induce}: distil a reusable workflow\n * from a successful agent trajectory and store it **quarantined** (it must\n * not drive actions until validated). Quarantined procedures are excluded\n * from {@link ProceduralMemory.activate} but remain visible to\n * {@link ProceduralMemory.list}.\n *\n * @stable\n */\nexport class ProceduralMemory {\n readonly #store: MemoryStoreAdapter;\n readonly #tracer: Tracer;\n /** Opt-in workflow inducer (P2-2). `null` ⇒ {@link induce} throws. */\n readonly #inducer: WorkflowInducer | null;\n /**\n * Promotion-by-demonstrated-success threshold (MCON-2 part 4).\n * `null` ⇒ outcomes are counted but never auto-promote.\n */\n readonly #promoteAfterSuccesses: number | null;\n\n constructor(args: {\n store: MemoryStoreAdapter;\n tracer: Tracer;\n inducer?: WorkflowInducer | null;\n promoteAfterSuccesses?: number | null;\n }) {\n this.#store = args.store;\n this.#tracer = args.tracer;\n this.#inducer = args.inducer ?? null;\n const k = args.promoteAfterSuccesses;\n this.#promoteAfterSuccesses =\n typeof k === 'number' && Number.isFinite(k) && k >= 1 ? Math.floor(k) : null;\n }\n\n /**\n * Record the outcome of one demonstrated reuse of a procedure\n * (MCON-2 part 4). A success increments the rule's persistent\n * `successCount`; when `procedurePromotion.afterSuccesses` is\n * configured and a QUARANTINED procedure reaches the threshold it is\n * promoted through {@link validate} — the injection gate still\n * applies, so a flagged text refuses promotion (surfaced as\n * `refused: true`) no matter how many successes accumulate.\n * Failures are observed but not persisted (no negative counter yet).\n *\n * Callers decide what \"success\" means — typically\n * `checkSuccessCriteria(...)` over the procedure's stored\n * `successCriteria`, or an operator's judgement.\n *\n * @stable\n */\n async recordOutcome(\n scope: SessionScope,\n ruleId: string,\n succeeded: boolean,\n ): Promise<{\n readonly successCount: number;\n readonly promoted: boolean;\n readonly refused: boolean;\n }> {\n return withMemorySpan(\n this.#tracer,\n 'memory.write.procedural',\n scope,\n { 'memory.procedural.action': 'record-outcome', 'memory.procedural.rule_id': ruleId },\n async (span) => {\n const store = this.#store.procedural;\n if (!succeeded || typeof store.recordSuccess !== 'function') {\n span.setAttributes({ 'memory.procedural.outcome.counted': false });\n return { successCount: 0, promoted: false, refused: false };\n }\n const successCount = await store.recordSuccess(ruleId);\n span.setAttributes({ 'memory.procedural.outcome.success_count': successCount });\n const threshold = this.#promoteAfterSuccesses;\n if (threshold === null || successCount < threshold) {\n return { successCount, promoted: false, refused: false };\n }\n const existing = (await store.list(scope)).find((rule) => rule.id === ruleId) ?? null;\n if (existing === null || existing.status !== 'quarantined') {\n return { successCount, promoted: false, refused: false };\n }\n try {\n await this.validate(\n scope,\n ruleId,\n `demonstrated-success: ${successCount} >= ${threshold}`,\n );\n span.setAttributes({ 'memory.procedural.outcome.promoted': true });\n return { successCount, promoted: true, refused: false };\n } catch (err) {\n if (err instanceof QuarantinePromotionRefusedError) {\n span.setAttributes({ 'memory.procedural.outcome.refused': true });\n return { successCount, promoted: false, refused: true };\n }\n throw err;\n }\n },\n );\n }\n\n /** Persist a rule. Returns the stored record. */\n async define(scope: SessionScope, input: RuleInput): Promise<Rule> {\n return withMemorySpan(this.#tracer, 'memory.write.procedural', scope, {}, async (span) => {\n const now = new Date().toISOString();\n const rule: Rule = {\n id: newMemoryId('rule'),\n kind: 'procedural',\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n sensitivity: input.sensitivity ?? 'public',\n text: input.text,\n ...(input.condition !== undefined ? { condition: input.condition } : {}),\n priority: input.priority ?? 50,\n ...(input.tags !== undefined ? { tags: Object.freeze([...input.tags]) } : {}),\n ...(input.steps !== undefined ? { steps: Object.freeze([...input.steps]) } : {}),\n ...(input.variables !== undefined\n ? { variables: Object.freeze([...input.variables]) }\n : {}),\n ...(input.successCriteria !== undefined\n ? { successCriteria: Object.freeze([...input.successCriteria]) }\n : {}),\n createdAt: now,\n updatedAt: now,\n };\n await this.#store.procedural.add(rule);\n span.setAttributes({\n 'memory.procedural.priority': rule.priority,\n 'memory.procedural.text_length': input.text.length,\n });\n return rule;\n });\n }\n\n /**\n * Induce a reusable procedure (P2-2) from a successful agent trajectory\n * and store it **quarantined** + `provenance: 'induction'` (P1-4). Returns\n * the stored {@link Rule}, or `null` when the trajectory was unsuccessful /\n * empty or the inducer produced nothing inducible.\n *\n * Throws {@link ProcedureInductionNotConfiguredError} when no inducer was\n * configured (`createMemory({ procedureInduction: { provider } })`).\n */\n async induce(\n scope: SessionScope,\n trajectory: Trajectory,\n opts: InduceOptions = {},\n ): Promise<Rule | null> {\n const inducer = this.#inducer;\n if (inducer === null) throw new ProcedureInductionNotConfiguredError();\n return withMemorySpan(\n this.#tracer,\n 'memory.write.procedural',\n scope,\n { 'memory.procedural.action': 'induce' },\n async (span) => {\n const induced = await runWorkflowInduction(trajectory, inducer, {});\n if (induced === null) {\n span.setAttributes({ 'memory.procedural.induced': 0 });\n return null;\n }\n const now = new Date().toISOString();\n const rule: Rule = {\n id: newMemoryId('rule'),\n kind: 'procedural',\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n // Induced from a run ⇒ user-derived, so 'internal' (not the public\n // default for author-defined rules).\n sensitivity: opts.sensitivity ?? 'internal',\n text: renderProcedureText(induced),\n priority: opts.priority ?? INDUCED_PRIORITY,\n steps: Object.freeze([...induced.steps]),\n variables: Object.freeze([...induced.variables]),\n successCriteria: Object.freeze([...induced.successCriteria]),\n // Highest-poisoning-risk write in the system — procedures drive\n // actions, so it lands quarantined + provenance-tagged (P1-4).\n provenance: 'induction' satisfies MemoryProvenance,\n status: 'quarantined' satisfies MemoryStatus,\n createdAt: now,\n updatedAt: now,\n };\n await this.#store.procedural.add(rule);\n span.setAttributes({\n 'memory.procedural.induced': 1,\n 'memory.procedural.steps': induced.steps.length,\n 'memory.procedural.variables': induced.variables.length,\n });\n return rule;\n },\n );\n }\n\n /**\n * Convenience over {@link induce}: distil the {@link Trajectory} from a\n * completed {@link RunState} (the agent's already-emitted run state) and\n * induce a procedure. The success signal is `status === 'completed'`.\n */\n async induceFromRun(\n scope: SessionScope,\n run: RunState,\n opts: InduceOptions = {},\n ): Promise<Rule | null> {\n return this.induce(scope, trajectoryFromRunState(run), opts);\n }\n\n /** Soft-delete a rule. */\n async remove(scope: SessionScope, ruleId: string, reason?: string): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.procedural',\n scope,\n { 'memory.procedural.action': 'remove', 'memory.procedural.rule_id': ruleId },\n async () => {\n await this.#store.procedural.remove(ruleId, reason);\n },\n );\n }\n\n /** List every active (non-deleted) rule for the supplied scope. */\n async list(scope: SessionScope): Promise<ReadonlyArray<Rule>> {\n return withMemorySpan(this.#tracer, 'memory.read.procedural', scope, {}, async (span) => {\n const out = await this.#store.procedural.list(scope);\n span.setAttributes({ 'memory.read.procedural.count': out.length });\n return out;\n });\n }\n\n /**\n * Return the rules active under `context`. Rules without a\n * `condition` are always active; the bundled predicate vocabulary\n * supports the literals `'always'`, `'topic=<topic>'`, and\n * `'tag=<tag>'`. Anything outside that grammar is treated as\n * always-active so callers do not silently lose rules.\n *\n * **Quarantined procedures are excluded** (P1-4 / P2-2): an induced\n * procedure must not drive actions until validated, so activation — which\n * feeds the system prompt — never surfaces it.\n */\n async activate(\n scope: SessionScope,\n context: RuleActivationContext = {},\n ): Promise<ReadonlyArray<Rule>> {\n const rules = await this.list(scope);\n return rules\n .filter((rule) => rule.status !== 'quarantined' && predicateMatches(rule, context))\n .sort((a, b) => b.priority - a.priority);\n }\n\n /**\n * Promote a quarantined (induced) procedure into `activate()` (MCON-2).\n * Mirrors {@link SemanticMemory.validate}: re-derives the injection verdict\n * from the stored rule text and **refuses** promotion of an injection-flagged\n * procedure unless an operator passes `{ force: true }`. Induced procedures\n * drive *actions*, so this gate matters most for them.\n */\n async validate(\n scope: SessionScope,\n ruleId: string,\n reason?: string,\n options?: { readonly force?: boolean },\n ): Promise<void> {\n await withMemorySpan(\n this.#tracer,\n 'memory.write.procedural',\n scope,\n { 'memory.procedural.action': 'validate', 'memory.procedural.rule_id': ruleId },\n async (span) => {\n const store = this.#store.procedural;\n if (typeof store.setStatus !== 'function') {\n throw new TypeError(\n '[graphorin/memory] ProceduralMemory.validate(...) requires a storage adapter that implements `procedural.setStatus(id, status)`. ' +\n 'The default `@graphorin/store-sqlite` adapter implements it; custom adapters can opt in via ProceduralMemoryStoreExt.',\n );\n }\n const force = options?.force === true;\n // No store get-by-id for rules — list() already surfaces quarantined.\n const existing = (await store.list(scope)).find((rule) => rule.id === ruleId) ?? null;\n if (existing !== null && !force) {\n const injection = detectMemoryInjection(existing.text);\n if (injection.flagged) {\n span.setAttributes({ 'memory.procedural.validate.refused': true });\n throw new QuarantinePromotionRefusedError(ruleId, injection.markers);\n }\n }\n span.setAttributes({ 'memory.procedural.validate.forced': force });\n await store.setStatus(ruleId, 'active', reason);\n },\n );\n }\n}\n\n/** Render an induced procedure into the human-readable `Rule.text`. */\nfunction renderProcedureText(procedure: InducedProcedure): string {\n const lines = [procedure.title, ...procedure.steps.map((step, i) => `${i + 1}. ${step}`)];\n return lines.join('\\n');\n}\n\nfunction predicateMatches(rule: Rule, context: RuleActivationContext): boolean {\n if (rule.condition === undefined || rule.condition === 'always') return true;\n if (rule.condition.startsWith('topic=')) {\n const wanted = rule.condition.slice('topic='.length).trim();\n return context.topic === wanted;\n }\n if (rule.condition.startsWith('tag=')) {\n const wanted = rule.condition.slice('tag='.length).trim();\n return (context.tags ?? []).includes(wanted);\n }\n return true;\n}\n"],"mappings":"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"}