@graphorin/memory 0.6.0 → 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 +83 -0
  2. package/README.md +24 -7
  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 +17 -15
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +5 -7
  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 +6 -0
  89. package/dist/package.js.map +1 -0
  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,271 @@
1
+ import type {
2
+ AgentRegistryEntry,
3
+ MemoryHit,
4
+ Message,
5
+ MessageRef,
6
+ SessionListOptions,
7
+ SessionMessageWithMetadata,
8
+ SessionScope,
9
+ Tracer,
10
+ } from '@graphorin/core';
11
+ import { newMemoryId } from '../internal/id.js';
12
+ import { withMemorySpan } from '../internal/spans.js';
13
+ import type { MemoryStoreAdapter } from '../internal/storage-adapter.js';
14
+
15
+ /**
16
+ * Per-session compaction policy. The default `0.9` matches DEC-104:
17
+ * compaction kicks in once cached message tokens exceed
18
+ * `0.9 * contextWindow`.
19
+ *
20
+ * @stable
21
+ */
22
+ export interface SessionCompactionPolicy {
23
+ /** Default `0.9`. */
24
+ readonly compactAtRatio?: number;
25
+ /** Default `8192`. */
26
+ readonly contextWindowTokens?: number;
27
+ }
28
+
29
+ /**
30
+ * Snapshot returned by {@link SessionMemory.compact}. The
31
+ * minimum-viable rendering simply reports counts; the full
32
+ * LLM-summarized cutoff (Phase 10c Consolidator) replaces this
33
+ * implementation later.
34
+ *
35
+ * @stable
36
+ */
37
+ export interface SessionCompactionResult {
38
+ readonly removed: number;
39
+ readonly summarized: number;
40
+ readonly summary?: string;
41
+ }
42
+
43
+ /**
44
+ * `SessionMemory` - append-only message log per session. Owns the
45
+ * `session_messages` storage by single-source-of-truth (DEC-147); the
46
+ * `@graphorin/sessions` package wraps this surface in Phase 11.
47
+ *
48
+ * @stable
49
+ */
50
+ export class SessionMemory {
51
+ readonly #store: MemoryStoreAdapter;
52
+ readonly #tracer: Tracer;
53
+ readonly #compactionPolicy: SessionCompactionPolicy;
54
+
55
+ constructor(args: {
56
+ store: MemoryStoreAdapter;
57
+ tracer: Tracer;
58
+ compactionPolicy?: SessionCompactionPolicy;
59
+ }) {
60
+ this.#store = args.store;
61
+ this.#tracer = args.tracer;
62
+ this.#compactionPolicy = args.compactionPolicy ?? {};
63
+ }
64
+
65
+ /** Persist a message. Returns the storage reference. */
66
+ async push(scope: SessionScope, message: Message): Promise<MessageRef> {
67
+ return withMemorySpan(
68
+ this.#tracer,
69
+ 'memory.write.session',
70
+ scope,
71
+ {
72
+ 'memory.session.message_role': message.role,
73
+ ...(message.role === 'assistant' && message.agentId !== undefined
74
+ ? { 'memory.session.agent_id': message.agentId }
75
+ : {}),
76
+ },
77
+ async (span) => {
78
+ const ref = await this.#store.session.push(scope, message);
79
+ span.setAttributes({
80
+ 'memory.session.sequence': ref.sequence,
81
+ 'memory.session.message_id': ref.messageId,
82
+ });
83
+ return ref;
84
+ },
85
+ );
86
+ }
87
+
88
+ /** List messages for the supplied scope. */
89
+ async list(scope: SessionScope, opts: SessionListOptions = {}): Promise<ReadonlyArray<Message>> {
90
+ return withMemorySpan(
91
+ this.#tracer,
92
+ 'memory.read.session',
93
+ scope,
94
+ {
95
+ ...(opts.lastN !== undefined ? { 'memory.session.last_n': opts.lastN } : {}),
96
+ ...(opts.agentId !== undefined ? { 'memory.session.agent_id': opts.agentId } : {}),
97
+ ...(opts.role !== undefined ? { 'memory.session.role': opts.role } : {}),
98
+ },
99
+ async (span) => {
100
+ const out = await this.#store.session.list(scope, opts);
101
+ span.setAttributes({ 'memory.read.session.count': out.length });
102
+ return out;
103
+ },
104
+ );
105
+ }
106
+
107
+ /**
108
+ * RP-5: list messages with their persisted identity (stored id / sequence /
109
+ * `createdAt`) so an exporter preserves message identity + chronology.
110
+ * Delegates to the store when it supports the richer read.
111
+ */
112
+ async listWithMetadata(
113
+ scope: SessionScope,
114
+ opts: SessionListOptions = {},
115
+ ): Promise<ReadonlyArray<SessionMessageWithMetadata>> {
116
+ const store = this.#store.session;
117
+ if (store.listWithMetadata !== undefined) {
118
+ return store.listWithMetadata(scope, opts);
119
+ }
120
+ // A store without the richer read: fabricate a unique id + the current time
121
+ // (the pre-RP-5 behaviour, just centralised here).
122
+ const messages = await store.list(scope, opts);
123
+ return messages.map((message, i) => ({
124
+ message,
125
+ messageId: newMemoryId('msg'),
126
+ sequence: i + 1,
127
+ createdAt: new Date().toISOString(),
128
+ }));
129
+ }
130
+
131
+ /** Hybrid (FTS5) search over the session messages. */
132
+ async search(
133
+ scope: SessionScope,
134
+ query: string,
135
+ opts: { topK?: number; signal?: AbortSignal } = {},
136
+ ): Promise<ReadonlyArray<MemoryHit>> {
137
+ return withMemorySpan(
138
+ this.#tracer,
139
+ 'memory.search.session',
140
+ scope,
141
+ { 'memory.search.query_length': query.length },
142
+ async (span) => {
143
+ const out = await this.#store.session.search(scope, query, {
144
+ query,
145
+ ...(opts.topK !== undefined ? { topK: opts.topK } : {}),
146
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
147
+ });
148
+ span.setAttributes({ 'memory.search.session.count': out.length });
149
+ return out;
150
+ },
151
+ );
152
+ }
153
+
154
+ /**
155
+ * NOT IMPLEMENTED (MRET-12) - always resolves `{ flushed: 0 }` and
156
+ * performs no work. The consolidator pipeline (extraction → facts /
157
+ * episodes) superseded the planned "silent flush turn"; this method
158
+ * remains only for contract stability. Do not branch on its counter.
159
+ */
160
+ async flushImportant(
161
+ scope: SessionScope,
162
+ opts: { silent?: boolean } = {},
163
+ ): Promise<{ flushed: number }> {
164
+ void opts;
165
+ return withMemorySpan(
166
+ this.#tracer,
167
+ 'memory.write.session',
168
+ scope,
169
+ { 'memory.session.action': 'flush-important', 'memory.session.implemented': false },
170
+ async () => ({ flushed: 0 }),
171
+ );
172
+ }
173
+
174
+ /**
175
+ * NOT IMPLEMENTED (MRET-12) - always resolves
176
+ * `{ removed: 0, summarized: 0 }` and deletes / summarizes nothing.
177
+ * Session-context compaction is owned by the context engine
178
+ * (`memory.contextEngine.compactNow`, driven by the agent runtime);
179
+ * this tier-level method previously FABRICATED counts (it reported
180
+ * `total - keepLastN` as "removed" while removing nothing - with the
181
+ * default `keepLastN: 0` it claimed to have compacted the whole
182
+ * session). It now reports the truth until a real message splice
183
+ * exists at this layer.
184
+ */
185
+ async compact(
186
+ scope: SessionScope,
187
+ opts: { keepLastN?: number } = {},
188
+ ): Promise<SessionCompactionResult> {
189
+ void opts;
190
+ return withMemorySpan(
191
+ this.#tracer,
192
+ 'memory.write.session',
193
+ scope,
194
+ { 'memory.session.action': 'compact', 'memory.session.implemented': false },
195
+ async (span) => {
196
+ span.setAttributes({
197
+ 'memory.session.compact.removed': 0,
198
+ 'memory.session.compact.summarized': 0,
199
+ });
200
+ return { removed: 0, summarized: 0 };
201
+ },
202
+ );
203
+ }
204
+
205
+ /**
206
+ * Returns `true` when the cached message tokens exceed
207
+ * `compactAtRatio * contextWindow` (default `0.9` per DEC-104). The
208
+ * second argument can be either:
209
+ *
210
+ * - a `number` - interpreted as the live `contextWindow` size in
211
+ * tokens (matches the documented memory-system spec signature);
212
+ * - an options bag - `{ usedTokens?, contextWindow? }`. When
213
+ * `usedTokens` is supplied the call is purely arithmetic; when
214
+ * omitted, the storage adapter's per-message `token_count`
215
+ * cache (DEC-131) is consulted via `totalCachedTokens(scope)`,
216
+ * falling back to a heuristic (~4 chars/token) for cache
217
+ * misses.
218
+ *
219
+ * @stable
220
+ */
221
+ async shouldCompact(
222
+ scope: SessionScope,
223
+ contextWindowOrOptions: number | { usedTokens?: number; contextWindow?: number } = {},
224
+ ): Promise<boolean> {
225
+ const options =
226
+ typeof contextWindowOrOptions === 'number'
227
+ ? { contextWindow: contextWindowOrOptions }
228
+ : contextWindowOrOptions;
229
+ const ratio = this.#compactionPolicy.compactAtRatio ?? 0.9;
230
+ const window = options.contextWindow ?? this.#compactionPolicy.contextWindowTokens ?? 8192;
231
+ if (options.usedTokens !== undefined) {
232
+ return options.usedTokens / window > ratio;
233
+ }
234
+ if (typeof this.#store.session.totalCachedTokens === 'function') {
235
+ const cached = await this.#store.session.totalCachedTokens(scope);
236
+ if (cached !== null) {
237
+ return cached / window > ratio;
238
+ }
239
+ }
240
+ const messages = await this.#store.session.list(scope, {});
241
+ let used = 0;
242
+ for (const m of messages) used += approximateTokenLength(m);
243
+ return used / window > ratio;
244
+ }
245
+
246
+ /**
247
+ * NOT IMPLEMENTED (MRET-12) - always resolves `[]`. The agent
248
+ * registry lives in `@graphorin/sessions` and has never been
249
+ * threaded into this tier; the previous JSDoc claimed the default
250
+ * sqlite adapter "resolves" registry rows here, which was false. Use
251
+ * the sessions facade for participant attribution.
252
+ */
253
+ async attributedFor(scope: SessionScope): Promise<ReadonlyArray<AgentRegistryEntry>> {
254
+ void scope;
255
+ return [];
256
+ }
257
+ }
258
+
259
+ function approximateTokenLength(message: Message): number {
260
+ if (message.role === 'system') {
261
+ return Math.ceil(message.content.length / 4);
262
+ }
263
+ if (typeof message.content === 'string') return Math.ceil(message.content.length / 4);
264
+ let total = 0;
265
+ for (const part of message.content) {
266
+ if (part.type === 'text' || part.type === 'reasoning') {
267
+ total += Math.ceil(part.text.length / 4);
268
+ }
269
+ }
270
+ return total;
271
+ }
@@ -0,0 +1,62 @@
1
+ import type { MemoryRecord, Tracer } from '@graphorin/core';
2
+ import { withMemorySpan } from '../internal/spans.js';
3
+ import type { MemoryStoreAdapter } from '../internal/storage-adapter.js';
4
+
5
+ /**
6
+ * `SharedMemory` - many-to-many attach mode for blocks / facts /
7
+ * rules across multiple agents. Storage adapters represent
8
+ * attachments as a join table (`shared_attachments` in
9
+ * `@graphorin/store-sqlite`).
10
+ *
11
+ * @stable
12
+ */
13
+ export class SharedMemory {
14
+ readonly #store: MemoryStoreAdapter;
15
+ readonly #tracer: Tracer;
16
+
17
+ constructor(args: { store: MemoryStoreAdapter; tracer: Tracer }) {
18
+ this.#store = args.store;
19
+ this.#tracer = args.tracer;
20
+ }
21
+
22
+ /** Attach `recordId` to `agentId`. Idempotent. */
23
+ async attach(recordId: string, agentId: string, userId: string): Promise<void> {
24
+ await withMemorySpan(
25
+ this.#tracer,
26
+ 'memory.write.shared',
27
+ { userId, agentId },
28
+ { 'memory.shared.action': 'attach', 'memory.shared.record_id': recordId },
29
+ async () => {
30
+ await this.#store.shared.attach(recordId, agentId);
31
+ },
32
+ );
33
+ }
34
+
35
+ /** Detach `recordId` from `agentId`. Idempotent. */
36
+ async detach(recordId: string, agentId: string, userId: string): Promise<void> {
37
+ await withMemorySpan(
38
+ this.#tracer,
39
+ 'memory.write.shared',
40
+ { userId, agentId },
41
+ { 'memory.shared.action': 'detach', 'memory.shared.record_id': recordId },
42
+ async () => {
43
+ await this.#store.shared.detach(recordId, agentId);
44
+ },
45
+ );
46
+ }
47
+
48
+ /** List every attachment for `agentId`. */
49
+ async listFor(agentId: string, userId: string): Promise<ReadonlyArray<MemoryRecord>> {
50
+ return withMemorySpan(
51
+ this.#tracer,
52
+ 'memory.read.shared',
53
+ { userId, agentId },
54
+ {},
55
+ async (span) => {
56
+ const out = await this.#store.shared.listFor(agentId);
57
+ span.setAttributes({ 'memory.read.shared.count': out.length });
58
+ return out;
59
+ },
60
+ );
61
+ }
62
+ }
@@ -0,0 +1,368 @@
1
+ import type { Block, Sensitivity, SessionScope, Tracer, ZodLikeSchema } from '@graphorin/core';
2
+ import {
3
+ WorkingBlockOverflowError,
4
+ WorkingBlockReadOnlyError,
5
+ WorkingBlockReplaceMismatchError,
6
+ } from '../errors/index.js';
7
+ import { newMemoryId } from '../internal/id.js';
8
+ import { withMemorySpan } from '../internal/spans.js';
9
+ import type { MemoryStoreAdapter } from '../internal/storage-adapter.js';
10
+
11
+ /**
12
+ * Author-time block specification accepted by
13
+ * {@link defineBlock} and {@link WorkingMemory.define}.
14
+ *
15
+ * @stable
16
+ */
17
+ export interface BlockSpec {
18
+ readonly label: string;
19
+ readonly description?: string;
20
+ readonly schema?: ZodLikeSchema<unknown>;
21
+ /** Maximum length of the rendered value in characters. */
22
+ readonly charLimit: number;
23
+ /** Default `'internal'`. */
24
+ readonly sensitivity?: Sensitivity;
25
+ /** Default `false`. */
26
+ readonly readOnly?: boolean;
27
+ /**
28
+ * Per-block default value. Applied on first definition only; later
29
+ * runs preserve any value already in storage.
30
+ */
31
+ readonly defaultValue?: string;
32
+ /** Default `'truncate'` - `'reject'` opt-in for strict use cases. */
33
+ readonly overflowPolicy?: 'truncate' | 'reject';
34
+ /** Optional free-form labels surfaced through `tags`. */
35
+ readonly tags?: ReadonlyArray<string>;
36
+ }
37
+
38
+ /**
39
+ * Frozen block definition returned by {@link defineBlock}. Re-used by
40
+ * the agent runtime / context engine to know which blocks should be
41
+ * compiled into the system prompt every step.
42
+ *
43
+ * @stable
44
+ */
45
+ export interface BlockDefinition {
46
+ readonly label: string;
47
+ readonly description?: string;
48
+ readonly schema?: ZodLikeSchema<unknown>;
49
+ readonly charLimit: number;
50
+ readonly sensitivity: Sensitivity;
51
+ readonly readOnly: boolean;
52
+ readonly defaultValue?: string;
53
+ readonly overflowPolicy: 'truncate' | 'reject';
54
+ readonly tags?: ReadonlyArray<string>;
55
+ }
56
+
57
+ /**
58
+ * Build a frozen {@link BlockDefinition}. Surfaced as `blocks.define(...)`
59
+ * for ergonomic call-sites; the underlying object is the same shape
60
+ * the {@link WorkingMemory.define} method accepts.
61
+ *
62
+ * @stable
63
+ */
64
+ export function defineBlock(spec: BlockSpec): BlockDefinition {
65
+ if (!spec.label || spec.label.length === 0) {
66
+ throw new TypeError('[graphorin/memory] BlockSpec.label must be a non-empty string.');
67
+ }
68
+ if (!Number.isInteger(spec.charLimit) || spec.charLimit <= 0) {
69
+ throw new TypeError(
70
+ `[graphorin/memory] BlockSpec.charLimit must be a positive integer, got ${String(spec.charLimit)}.`,
71
+ );
72
+ }
73
+ return Object.freeze({
74
+ label: spec.label,
75
+ ...(spec.description !== undefined ? { description: spec.description } : {}),
76
+ ...(spec.schema !== undefined ? { schema: spec.schema } : {}),
77
+ charLimit: spec.charLimit,
78
+ sensitivity: spec.sensitivity ?? 'internal',
79
+ readOnly: spec.readOnly ?? false,
80
+ ...(spec.defaultValue !== undefined ? { defaultValue: spec.defaultValue } : {}),
81
+ overflowPolicy: spec.overflowPolicy ?? 'truncate',
82
+ ...(spec.tags !== undefined ? { tags: Object.freeze([...spec.tags]) } : {}),
83
+ });
84
+ }
85
+
86
+ /**
87
+ * `WorkingMemory` - labeled, character-bounded blocks rendered into
88
+ * every system prompt. Operations:
89
+ *
90
+ * - `define(spec)`: idempotently registers a block definition; the
91
+ * block's row is created in storage on the next `write` call so
92
+ * operators can change `description` between runs without
93
+ * triggering a write.
94
+ * - `read(scope, label)` / `list(scope)`: surface the current block
95
+ * contents.
96
+ * - `write(scope, label, value)`: full replace.
97
+ * - `append(scope, label, content)`: append-with-newline.
98
+ * - `replace(scope, label, oldUnique, newText)`: targeted replace
99
+ * with a uniqueness check.
100
+ * - `compile(scope)`: render the active blocks for the context
101
+ * engine (used by Phase 10d).
102
+ *
103
+ * @stable
104
+ */
105
+ export class WorkingMemory {
106
+ readonly #store: MemoryStoreAdapter;
107
+ readonly #tracer: Tracer;
108
+ readonly #defs: Map<string, BlockDefinition>;
109
+
110
+ constructor(args: { store: MemoryStoreAdapter; tracer: Tracer }) {
111
+ this.#store = args.store;
112
+ this.#tracer = args.tracer;
113
+ this.#defs = new Map();
114
+ }
115
+
116
+ /** Register a block definition. Returns the same definition object. */
117
+ define(definition: BlockDefinition): BlockDefinition {
118
+ this.#defs.set(definition.label, definition);
119
+ return definition;
120
+ }
121
+
122
+ /** Snapshot of every registered definition. */
123
+ definitions(): ReadonlyArray<BlockDefinition> {
124
+ return Array.from(this.#defs.values());
125
+ }
126
+
127
+ /** Lookup a definition by label. */
128
+ definitionFor(label: string): BlockDefinition | undefined {
129
+ return this.#defs.get(label);
130
+ }
131
+
132
+ /** List active (non-deleted) blocks for the supplied scope. */
133
+ async list(scope: SessionScope): Promise<ReadonlyArray<Block>> {
134
+ return withMemorySpan(this.#tracer, 'memory.read.working', scope, {}, async (span) => {
135
+ const out = await this.#store.working.list(scope);
136
+ span.setAttributes({ 'memory.read.working.count': out.length });
137
+ return out;
138
+ });
139
+ }
140
+
141
+ /** Read a single block's value (or `null` when absent). */
142
+ async read(scope: SessionScope, label: string): Promise<string | null> {
143
+ return withMemorySpan(
144
+ this.#tracer,
145
+ 'memory.read.working',
146
+ scope,
147
+ { 'memory.block.label': label },
148
+ async (span) => {
149
+ const block = await this.#store.working.get(scope, label);
150
+ span.setAttributes({ 'memory.read.working.found': block !== null });
151
+ if (block !== null) return block.value;
152
+ // MST-8: a defined-but-unwritten block answers with its declared
153
+ // defaultValue (previously advertised, copied into the
154
+ // definition, and then never read by anything).
155
+ const definition = this.#defs.get(label);
156
+ return definition?.defaultValue ?? null;
157
+ },
158
+ );
159
+ }
160
+
161
+ /** Replace a block's value entirely. Honours overflow policy. */
162
+ async write(scope: SessionScope, label: string, value: string): Promise<Block> {
163
+ return this.#mutate(scope, label, async () => value);
164
+ }
165
+
166
+ /** Append `content` to a block (with a newline separator). */
167
+ async append(scope: SessionScope, label: string, content: string): Promise<Block> {
168
+ return this.#mutate(scope, label, async (current) => {
169
+ if (current === '' || current === undefined) return content;
170
+ return `${current}\n${content}`;
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Replace the unique substring `oldUnique` inside the block's value
176
+ * with `newText`. Throws `WorkingBlockReplaceMismatchError` when
177
+ * the substring is missing or appears more than once.
178
+ */
179
+ async replace(
180
+ scope: SessionScope,
181
+ label: string,
182
+ oldUnique: string,
183
+ newText: string,
184
+ ): Promise<Block> {
185
+ return this.#mutate(scope, label, async (current) => {
186
+ const haystack = current ?? '';
187
+ const occurrences = countOccurrences(haystack, oldUnique);
188
+ if (occurrences !== 1) {
189
+ throw new WorkingBlockReplaceMismatchError(label, occurrences);
190
+ }
191
+ return haystack.replace(oldUnique, newText);
192
+ });
193
+ }
194
+
195
+ /** Run `mutator(current) => next` and persist the result. */
196
+ async rethink(
197
+ scope: SessionScope,
198
+ label: string,
199
+ mutator: (current: string) => string | Promise<string>,
200
+ ): Promise<Block> {
201
+ return this.#mutate(scope, label, async (current) => mutator(current ?? ''));
202
+ }
203
+
204
+ /** Soft-delete a block. */
205
+ async forget(scope: SessionScope, label: string, reason?: string): Promise<void> {
206
+ await withMemorySpan(
207
+ this.#tracer,
208
+ 'memory.write.working',
209
+ scope,
210
+ { 'memory.block.label': label, 'memory.write.working.action': 'forget' },
211
+ async () => {
212
+ await this.#store.working.delete(scope, label, reason);
213
+ },
214
+ );
215
+ }
216
+
217
+ /**
218
+ * Attach a working block to an additional agent. Backed by the
219
+ * adapter's `shared.attach(...)` join table so multi-agent crews
220
+ * can share the same block without duplicating storage.
221
+ */
222
+ async attach(scope: SessionScope, blockId: string, agentId: string): Promise<void> {
223
+ await withMemorySpan(
224
+ this.#tracer,
225
+ 'memory.write.working',
226
+ scope,
227
+ {
228
+ 'memory.write.working.action': 'attach',
229
+ 'memory.block.id': blockId,
230
+ 'memory.write.working.target_agent_id': agentId,
231
+ },
232
+ async () => {
233
+ await this.#store.shared.attach(blockId, agentId);
234
+ },
235
+ );
236
+ }
237
+
238
+ /** Detach a working block from an agent. */
239
+ async detach(scope: SessionScope, blockId: string, agentId: string): Promise<void> {
240
+ await withMemorySpan(
241
+ this.#tracer,
242
+ 'memory.write.working',
243
+ scope,
244
+ {
245
+ 'memory.write.working.action': 'detach',
246
+ 'memory.block.id': blockId,
247
+ 'memory.write.working.target_agent_id': agentId,
248
+ },
249
+ async () => {
250
+ await this.#store.shared.detach(blockId, agentId);
251
+ },
252
+ );
253
+ }
254
+
255
+ /**
256
+ * Render a deterministic `<memory_blocks>` XML fragment for the
257
+ * supplied scope. The full layered system prompt (six layers) is
258
+ * produced by Phase 10d's ContextEngine; this method ships the
259
+ * minimum-viable rendering used by the smoke acceptance criteria.
260
+ *
261
+ * The optional `agentId` argument is reserved for the per-agent
262
+ * filtering Phase 10d wires through. In Phase 10a the argument is
263
+ * accepted but ignored - the rendering is scope-wide.
264
+ */
265
+ async compile(scope: SessionScope, agentId?: string): Promise<string> {
266
+ void agentId;
267
+ const blocks = await this.list(scope);
268
+ if (blocks.length === 0) return '';
269
+ const lines = ['<memory_blocks>'];
270
+ for (const block of blocks) {
271
+ const description =
272
+ block.description !== undefined ? ` description="${escapeXml(block.description)}"` : '';
273
+ lines.push(` <block label="${escapeXml(block.label)}"${description}>`);
274
+ lines.push(` ${escapeXml(block.value)}`);
275
+ lines.push(' </block>');
276
+ }
277
+ lines.push('</memory_blocks>');
278
+ return lines.join('\n');
279
+ }
280
+
281
+ async #mutate(
282
+ scope: SessionScope,
283
+ label: string,
284
+ nextValueFn: (current: string | undefined) => Promise<string>,
285
+ ): Promise<Block> {
286
+ return withMemorySpan(
287
+ this.#tracer,
288
+ 'memory.write.working',
289
+ scope,
290
+ { 'memory.block.label': label },
291
+ async (span) => {
292
+ const definition = this.#requireDefinition(label);
293
+ if (definition.readOnly === true) {
294
+ // MRET-14: a dedicated kind - the old
295
+ // WorkingBlockReplaceMismatchError(label, 0) read as "substring
296
+ // matched 0 times" and misled replace-retry callers.
297
+ throw new WorkingBlockReadOnlyError(label);
298
+ }
299
+ const existing = await this.#store.working.get(scope, label);
300
+ // MST-8: first materialization starts from the declared default,
301
+ // so an append/replace on an unwritten block composes with it.
302
+ const candidate = await nextValueFn(existing?.value ?? definition.defaultValue);
303
+ const enforcedValue = enforceCharLimit(candidate, definition);
304
+ const now = new Date().toISOString();
305
+ const id = existing?.id ?? newMemoryId('block');
306
+ const block: Block = {
307
+ id,
308
+ kind: 'working',
309
+ userId: scope.userId,
310
+ ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),
311
+ ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),
312
+ sensitivity: definition.sensitivity,
313
+ label,
314
+ ...(definition.description !== undefined ? { description: definition.description } : {}),
315
+ value: enforcedValue,
316
+ charLimit: definition.charLimit,
317
+ readOnly: definition.readOnly,
318
+ ...(definition.tags !== undefined ? { tags: definition.tags } : {}),
319
+ createdAt: existing?.createdAt ?? now,
320
+ updatedAt: now,
321
+ };
322
+ await this.#store.working.upsert(scope, block);
323
+ span.setAttributes({
324
+ 'memory.write.working.action': existing === null ? 'create' : 'update',
325
+ 'memory.write.working.length': enforcedValue.length,
326
+ });
327
+ return block;
328
+ },
329
+ );
330
+ }
331
+
332
+ #requireDefinition(label: string): BlockDefinition {
333
+ const def = this.#defs.get(label);
334
+ if (def !== undefined) return def;
335
+ throw new TypeError(
336
+ `[graphorin/memory] working block '${label}' was not defined. ` +
337
+ 'Call memory.working.define(blocks.define({...})) before writing to it.',
338
+ );
339
+ }
340
+ }
341
+
342
+ function enforceCharLimit(value: string, definition: BlockDefinition): string {
343
+ if (value.length <= definition.charLimit) return value;
344
+ if (definition.overflowPolicy === 'reject') {
345
+ throw new WorkingBlockOverflowError(definition.label, value.length, definition.charLimit);
346
+ }
347
+ return value.slice(0, definition.charLimit);
348
+ }
349
+
350
+ function countOccurrences(haystack: string, needle: string): number {
351
+ if (needle.length === 0) return 0;
352
+ let count = 0;
353
+ let idx = 0;
354
+ for (;;) {
355
+ const next = haystack.indexOf(needle, idx);
356
+ if (next === -1) return count;
357
+ count += 1;
358
+ idx = next + needle.length;
359
+ }
360
+ }
361
+
362
+ function escapeXml(value: string): string {
363
+ return value
364
+ .replace(/&/g, '&amp;')
365
+ .replace(/</g, '&lt;')
366
+ .replace(/>/g, '&gt;')
367
+ .replace(/"/g, '&quot;');
368
+ }