@mindot/will 0.1.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 (178) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +777 -0
  3. package/dist/index.d.ts +8144 -0
  4. package/dist/index.js +26231 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +73 -0
  7. package/src/cognition/agency/access.grants.ts +59 -0
  8. package/src/cognition/agency/competence.codec.ts +89 -0
  9. package/src/cognition/agency/engines/action.selector.ts +462 -0
  10. package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
  11. package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
  12. package/src/cognition/agency/engines/instruction.intake.ts +54 -0
  13. package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
  14. package/src/cognition/agency/engines/reafference.engine.ts +241 -0
  15. package/src/cognition/agency/execution.primitives.ts +118 -0
  16. package/src/cognition/agency/index.ts +58 -0
  17. package/src/cognition/agency/proactive.communicator.ts +274 -0
  18. package/src/cognition/agency/reconcile.learning.ts +82 -0
  19. package/src/cognition/agency/schemas/external.ts +59 -0
  20. package/src/cognition/agency/schemas/innate.ts +111 -0
  21. package/src/cognition/agency/schemas/repertoire.ts +246 -0
  22. package/src/cognition/agency/selection.scoring.ts +191 -0
  23. package/src/cognition/agency/types.ts +165 -0
  24. package/src/cognition/bus.ts +391 -0
  25. package/src/cognition/completion.inbox.ts +86 -0
  26. package/src/cognition/config.mirror.entities.ts +475 -0
  27. package/src/cognition/conversation.memory.ts +81 -0
  28. package/src/cognition/event.log.ts +116 -0
  29. package/src/cognition/event.schemas.ts +568 -0
  30. package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
  31. package/src/cognition/faculties/affective.blender.ts +562 -0
  32. package/src/cognition/faculties/attachment.evaluator.ts +432 -0
  33. package/src/cognition/faculties/attention.allocator.ts +422 -0
  34. package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
  35. package/src/cognition/faculties/bias.detector.ts +363 -0
  36. package/src/cognition/faculties/circadian.oscillator.ts +294 -0
  37. package/src/cognition/faculties/confidence.calibrator.ts +324 -0
  38. package/src/cognition/faculties/dream.simulator.ts +251 -0
  39. package/src/cognition/faculties/empathy.simulator.ts +253 -0
  40. package/src/cognition/faculties/energy.regulator.ts +320 -0
  41. package/src/cognition/faculties/episodic.consolidator.ts +728 -0
  42. package/src/cognition/faculties/executive.engine/commands.ts +459 -0
  43. package/src/cognition/faculties/executive.engine/config.ts +46 -0
  44. package/src/cognition/faculties/executive.engine/context.ts +605 -0
  45. package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
  46. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
  47. package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
  48. package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
  49. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
  50. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
  51. package/src/cognition/faculties/executive.engine/facet.ts +628 -0
  52. package/src/cognition/faculties/executive.engine/gating.ts +222 -0
  53. package/src/cognition/faculties/executive.engine/index.ts +6 -0
  54. package/src/cognition/faculties/executive.engine/messages.ts +102 -0
  55. package/src/cognition/faculties/executive.engine/parser.ts +380 -0
  56. package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
  57. package/src/cognition/faculties/executive.engine/types.ts +323 -0
  58. package/src/cognition/faculties/exteroception.ts +338 -0
  59. package/src/cognition/faculties/forgetting.curve.ts +202 -0
  60. package/src/cognition/faculties/frustration.evaluator.ts +280 -0
  61. package/src/cognition/faculties/goal.manager.ts +1008 -0
  62. package/src/cognition/faculties/inhibition.controller.ts +351 -0
  63. package/src/cognition/faculties/interoception.ts +417 -0
  64. package/src/cognition/faculties/introspection.engine.ts +246 -0
  65. package/src/cognition/faculties/known.entity.tracker.ts +439 -0
  66. package/src/cognition/faculties/loss.evaluator.ts +224 -0
  67. package/src/cognition/faculties/moral.evaluator.ts +437 -0
  68. package/src/cognition/faculties/novelty.detector.ts +254 -0
  69. package/src/cognition/faculties/persona.consolidator.ts +845 -0
  70. package/src/cognition/faculties/planning.engine/engine.ts +859 -0
  71. package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
  72. package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
  73. package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
  74. package/src/cognition/faculties/planning.engine/types.ts +143 -0
  75. package/src/cognition/faculties/reputation.tracker.ts +294 -0
  76. package/src/cognition/faculties/reward.evaluator.ts +304 -0
  77. package/src/cognition/faculties/self.model.updater.ts +677 -0
  78. package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
  79. package/src/cognition/faculties/semantic.engine/index.ts +6 -0
  80. package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
  81. package/src/cognition/faculties/semantic.engine/types.ts +53 -0
  82. package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
  83. package/src/cognition/faculties/social.perception.ts +333 -0
  84. package/src/cognition/faculties/spaced.repetition.ts +605 -0
  85. package/src/cognition/faculties/stress.regulator.ts +375 -0
  86. package/src/cognition/faculties/task.switcher.ts +239 -0
  87. package/src/cognition/faculties/theory.of.mind.ts +333 -0
  88. package/src/cognition/faculties/threat.evaluator.ts +312 -0
  89. package/src/cognition/faculties/working.memory.ts +406 -0
  90. package/src/cognition/generative.model.ts +344 -0
  91. package/src/cognition/heartbeat.ts +70 -0
  92. package/src/cognition/index.ts +291 -0
  93. package/src/cognition/instruction.handler.ts +146 -0
  94. package/src/cognition/memory/index.ts +35 -0
  95. package/src/cognition/memory/vector.adapter.ts +316 -0
  96. package/src/cognition/memory/vector.content.ts +47 -0
  97. package/src/cognition/memory/vector.embedder.ts +212 -0
  98. package/src/cognition/memory/vector.index.ts +459 -0
  99. package/src/cognition/memory/vector.types.ts +65 -0
  100. package/src/cognition/orchestrator.ts +142 -0
  101. package/src/cognition/persona.prior.ts +265 -0
  102. package/src/cognition/schema.registry.ts +145 -0
  103. package/src/cognition/senses/audition.engine/engine.ts +976 -0
  104. package/src/cognition/senses/audition.engine/salience.ts +59 -0
  105. package/src/cognition/senses/base.sense.engine.ts +117 -0
  106. package/src/cognition/senses/gustation.engine.ts +24 -0
  107. package/src/cognition/senses/index.ts +172 -0
  108. package/src/cognition/senses/olfaction.engine.ts +24 -0
  109. package/src/cognition/senses/somatosensation.engine.ts +23 -0
  110. package/src/cognition/senses/vision.engine.ts +23 -0
  111. package/src/cognition/types.ts +63 -0
  112. package/src/cognition/utilities/token.tracker.ts +492 -0
  113. package/src/core/abstracts.ts +60 -0
  114. package/src/core/async.engine.ts +539 -0
  115. package/src/core/clock.ts +196 -0
  116. package/src/core/completion.recorder.ts +122 -0
  117. package/src/core/conflict.detector.ts +140 -0
  118. package/src/core/distributed.ts +626 -0
  119. package/src/core/event.bus.ts +292 -0
  120. package/src/core/inbound.recorder.ts +100 -0
  121. package/src/core/index.ts +167 -0
  122. package/src/core/logger.ts +85 -0
  123. package/src/core/metrics.ts +107 -0
  124. package/src/core/orchestrator.ts +625 -0
  125. package/src/core/replay.ts +589 -0
  126. package/src/core/scenario.ts +120 -0
  127. package/src/core/serialization.ts +448 -0
  128. package/src/core/simulation.ts +197 -0
  129. package/src/core/snapshot.manager.ts +293 -0
  130. package/src/core/state.manager.ts +283 -0
  131. package/src/core/types.ts +274 -0
  132. package/src/core/utils.ts +82 -0
  133. package/src/core/wall.clock.ts +23 -0
  134. package/src/extensions/livestream.ext.ts +570 -0
  135. package/src/extensions/time.ext.ts +339 -0
  136. package/src/index.ts +91 -0
  137. package/src/llm/gate.ts +140 -0
  138. package/src/llm/index.ts +716 -0
  139. package/src/llm/summarizer.ts +170 -0
  140. package/src/llm/wire.contracts.ts +67 -0
  141. package/src/pma/eval.ts +651 -0
  142. package/src/pma/index.ts +1082 -0
  143. package/src/profiles/built-in.ts +7 -0
  144. package/src/profiles/companion.ts +28 -0
  145. package/src/profiles/company-brain.ts +54 -0
  146. package/src/profiles/customer-service.ts +30 -0
  147. package/src/profiles/game-npc.ts +20 -0
  148. package/src/profiles/index.ts +42 -0
  149. package/src/profiles/smart-home.ts +34 -0
  150. package/src/runners/coherence.runner.ts +49 -0
  151. package/src/runners/outreach.runner.ts +158 -0
  152. package/src/runners/social.runner.ts +182 -0
  153. package/src/runners/thin-shim.runner.ts +195 -0
  154. package/src/sdk/will.ts +334 -0
  155. package/src/stem/assembly.audit.ts +88 -0
  156. package/src/stem/distribution.ts +203 -0
  157. package/src/stem/guards/identity.coherence.ts +152 -0
  158. package/src/stem/guards/identity.guard.ts +227 -0
  159. package/src/stem/index.ts +1145 -0
  160. package/src/stem/mind.ts +1090 -0
  161. package/src/stem/tracts/ack.reconciler.ts +55 -0
  162. package/src/stem/tracts/biography.writer.ts +190 -0
  163. package/src/stem/tracts/effector.controller.ts +143 -0
  164. package/src/stem/tracts/health.reporter.ts +113 -0
  165. package/src/stem/tracts/inbound.queue.ts +54 -0
  166. package/src/stem/tracts/outbox.controller.ts +167 -0
  167. package/src/stem/tracts/outbox.writer.ts +133 -0
  168. package/src/stem/tracts/pma.controller.ts +109 -0
  169. package/src/stem/tracts/replay.controller.ts +177 -0
  170. package/src/stem/tracts/sensory.controller.ts +168 -0
  171. package/src/stem/tracts/session.logger.ts +157 -0
  172. package/src/stem/tracts/transport/index.ts +38 -0
  173. package/src/stem/tracts/transport/loopback.transport.ts +86 -0
  174. package/src/stem/tracts/transport/socketio.transport.ts +200 -0
  175. package/src/stem/tracts/transport/stream.transport.ts +152 -0
  176. package/src/stem/tracts/transport/types.ts +193 -0
  177. package/src/stem/tracts/transport.controller.ts +380 -0
  178. package/src/types.ts +116 -0
@@ -0,0 +1,316 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/memory/vector.adapter.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * VectorMemoryAdapter — primary interface for cognitive engines.
7
+ *
8
+ * Provides semantic similarity search over episodic memory.
9
+ * Accepts any VectorIndex implementation (HNSW, Qdrant, pgvector, Pinecone)
10
+ * and any EmbeddingProvider (OpenAI, local, mock).
11
+ *
12
+ * Follows the StorageAdapter pattern from replay.ts — receives storage
13
+ * in constructor for persistence of the index itself.
14
+ */
15
+
16
+ import { logger } from '#core/logger'
17
+ import { wallClock } from '#core/wall.clock'
18
+ import type { StorageAdapter } from '#core/abstracts'
19
+ import type { EpisodicMemory } from '#faculties/episodic.consolidator'
20
+ import type { EmbeddingProvider } from '#memory/vector.embedder'
21
+ import type { VectorRecord, VectorQueryFilter, VectorQueryResult, VectorMemoryConfig } from '#memory/vector.types'
22
+ import type { VectorIndex } from '#memory/vector.index'
23
+ import { BunStorageAdapter } from '#core/abstracts'
24
+ import { HNSWIndex } from '#memory/vector.index'
25
+ import { episodeContentToText } from '#memory/vector.content'
26
+
27
+ export interface VectorMemoryAdapter {
28
+ /** Index an episodic memory (called during consolidation) */
29
+ index( episode: EpisodicMemory, content: unknown ): Promise<void>
30
+
31
+ /** Index multiple episodes in batch */
32
+ indexBatch( episodes: Array<{ episode: EpisodicMemory; content: unknown }> ): Promise<void>
33
+
34
+ /** Search for semantically similar episodes — returns ID + similarity, caller resolves from store */
35
+ search( query: unknown, filter?: VectorQueryFilter ): Promise<VectorQueryResult[]>
36
+
37
+ /** Search with embedding vector directly */
38
+ searchWithVector( embedding: number[], filter?: VectorQueryFilter ): Promise<VectorQueryResult[]>
39
+
40
+ /** Delete an episode from the index (when pruned from _store) */
41
+ delete( episodeId: string ): Promise<void>
42
+
43
+ /** Rebuild entire index from store (called on snapshot restore when no persisted index exists) */
44
+ rebuildFromStore( store: EpisodicMemory[] ): Promise<void>
45
+
46
+ /** Persist index to storage */
47
+ persist(): Promise<void>
48
+
49
+ /** Load index from storage */
50
+ load(): Promise<void>
51
+
52
+ /** Get current index size */
53
+ readonly size: number
54
+ }
55
+
56
+ export class DefaultVectorMemoryAdapter implements VectorMemoryAdapter {
57
+ private _index: VectorIndex
58
+ private _embedder: EmbeddingProvider
59
+ private _storage: StorageAdapter
60
+ private _persistPath: string
61
+ private _metaPath: string
62
+ private _maxIndexedEpisodes: number
63
+ private _indexedIds: Set<string> = new Set()
64
+ private _dirty: boolean = false
65
+ private _persistDebounceTimer: NodeJS.Timeout | null = null
66
+ private _minSimilarity: number
67
+ /** Per-id access recency (insert + search hit) for LRU-style eviction. A plain
68
+ * monotonic counter — not persisted; rebuilt from insertion order on load. */
69
+ private _accessTick: Map<string, number> = new Map()
70
+ private _accessClock: number = 0
71
+
72
+ constructor(
73
+ embedder: EmbeddingProvider,
74
+ config: VectorMemoryConfig & { persistPath?: string } = {},
75
+ storage: StorageAdapter = new BunStorageAdapter(),
76
+ indexImpl: VectorIndex = new HNSWIndex( config )
77
+ ){
78
+ this._embedder = embedder
79
+ this._storage = storage
80
+ this._index = indexImpl
81
+ this._persistPath = config.persistPath ?? './data/vector_index'
82
+ this._metaPath = `${this._persistPath}.meta`
83
+ this._maxIndexedEpisodes = config.maxIndexedEpisodes ?? 10000
84
+ // 0.35 suits real sentence embeddings (e.g. text-embedding-3-small), where
85
+ // genuinely related-but-reworded memories commonly score 0.3–0.5 cosine. The
86
+ // old 0.65 floor was high enough that semantic recall returned nothing for
87
+ // most relevant memories and quietly fell back to recency. Tunable per Will.
88
+ this._minSimilarity = config.minSimilarity ?? 0.35
89
+ }
90
+
91
+ /** Record that `id` was just inserted or recalled, so eviction keeps the
92
+ * memories the Will actually uses (LRU) and drops the genuinely cold ones. */
93
+ private _touch( id: string ): void {
94
+ this._accessTick.set( id, ++this._accessClock )
95
+ }
96
+
97
+ get size(): number {
98
+ return this._index.size
99
+ }
100
+
101
+ async index( episode: EpisodicMemory, content: unknown ): Promise<void> {
102
+ if( this._indexedIds.has( episode.id ) ) return
103
+
104
+ if( this._index.size >= this._maxIndexedEpisodes )
105
+ await this._evictColdest()
106
+
107
+ const embedding = await this._embedder.embed( episodeContentToText( content ), 'index' )
108
+
109
+ const record: VectorRecord = {
110
+ id: episode.id,
111
+ vector: embedding,
112
+ embeddingModel: this._embedder.modelName,
113
+ createdAt: wallClock(), // determinism-ok: secondary index telemetry, rebuilt from _store, never in replay state
114
+ metadata: {
115
+ tick: episode.timestamp,
116
+ sourceType: episode.sourceType,
117
+ emotionalValence: episode.affectiveContext.valence,
118
+ tags: episode.tags
119
+ }
120
+ }
121
+
122
+ await this._index.insert( record )
123
+ this._indexedIds.add( episode.id )
124
+ this._touch( episode.id )
125
+ this._dirty = true
126
+ this._schedulePersist()
127
+ }
128
+
129
+ async indexBatch(
130
+ episodes: Array<{ episode: EpisodicMemory; content: unknown }>
131
+ ): Promise<void> {
132
+ const newEpisodes = episodes.filter( e => !this._indexedIds.has( e.episode.id ) )
133
+ if( newEpisodes.length === 0 ) return
134
+
135
+ // `size > 0` guards against an infinite loop when the batch alone exceeds
136
+ // the cap — once the index is drained there is nothing left to evict.
137
+ while( this._index.size > 0 && this._index.size + newEpisodes.length > this._maxIndexedEpisodes )
138
+ await this._evictColdest()
139
+
140
+ const contents = newEpisodes.map( e => episodeContentToText( e.content ) )
141
+ const embeddings = await this._embedder.embedBatch( contents, 'index' )
142
+
143
+ for( let i = 0; i < newEpisodes.length; i++ ){
144
+ const { episode } = newEpisodes[i]!
145
+ const embedding = embeddings[i]!
146
+
147
+ const record: VectorRecord = {
148
+ id: episode.id,
149
+ vector: embedding,
150
+ embeddingModel: this._embedder.modelName,
151
+ createdAt: wallClock(), // determinism-ok: secondary index telemetry, rebuilt from _store, never in replay state
152
+ metadata: {
153
+ tick: episode.timestamp,
154
+ sourceType: episode.sourceType,
155
+ emotionalValence: episode.affectiveContext.valence,
156
+ tags: episode.tags
157
+ }
158
+ }
159
+
160
+ await this._index.insert( record )
161
+ this._indexedIds.add( episode.id )
162
+ this._touch( episode.id )
163
+ }
164
+
165
+ this._dirty = true
166
+ this._schedulePersist()
167
+ }
168
+
169
+ async search( query: unknown, filter?: VectorQueryFilter ): Promise<VectorQueryResult[]> {
170
+ const embedding = await this._embedder.embed( episodeContentToText( query ), 'recall' )
171
+ return this.searchWithVector( embedding, filter )
172
+ }
173
+
174
+ async searchWithVector( embedding: number[], filter?: VectorQueryFilter ): Promise<VectorQueryResult[]> {
175
+ // HNSW search is similarity-only (see VectorQueryFilter). Metadata-based
176
+ // narrowing, if any, is the caller's job post-search.
177
+ const results = await this._index.search( embedding, filter?.maxResults ?? 10, {
178
+ minSimilarity: filter?.minSimilarity ?? this._minSimilarity,
179
+ } )
180
+ // Recall warms the cache: bump access recency so frequently-recalled memories
181
+ // survive eviction (LRU) — the index keeps what the Will actually uses.
182
+ for( const r of results ) this._touch( r.episodeId )
183
+ return results
184
+ }
185
+
186
+ async delete( episodeId: string ): Promise<void> {
187
+ if( await this._index.delete( episodeId ) ){
188
+ this._indexedIds.delete( episodeId )
189
+ this._accessTick.delete( episodeId )
190
+ this._dirty = true
191
+ this._schedulePersist()
192
+ }
193
+ }
194
+
195
+ async rebuildFromStore( store: EpisodicMemory[] ): Promise<void> {
196
+ await this._index.clear()
197
+ this._indexedIds.clear()
198
+ this._accessTick.clear()
199
+ this._accessClock = 0
200
+
201
+ const episodesWithContent = store.map( episode => ( {
202
+ episode,
203
+ content: episode.content
204
+ } ) )
205
+
206
+ await this.indexBatch( episodesWithContent )
207
+ this._dirty = true
208
+ await this.persist()
209
+ }
210
+
211
+ async persist(): Promise<void> {
212
+ if( !this._dirty ) return
213
+
214
+ if( this._index.serialize ){
215
+ const serialized = this._index.serialize()
216
+ await this._storage.write( this._persistPath, serialized )
217
+ // Stamp the embedding model + dimensions next to the index so a later
218
+ // load() can detect drift (see load()) and rebuild instead of querying
219
+ // vectors that live in a different model's space.
220
+ await this._storage.write(
221
+ this._metaPath,
222
+ JSON.stringify({ model: this._embedder.modelName, dimensions: this._embedder.dimensions })
223
+ )
224
+ }
225
+
226
+ this._dirty = false
227
+ }
228
+
229
+ async load(): Promise<void> {
230
+ try {
231
+ const exists = await this._storage.exists( this._persistPath )
232
+ if( !exists ) return
233
+
234
+ // Guard against embedding-model / dimension drift. A persisted index holds
235
+ // vectors in the model's space at build time; querying it with a different
236
+ // model (or dimension count) silently corrupts similarity — mismatched
237
+ // lengths read past array ends → NaN, so recall quietly returns nothing.
238
+ // On a mismatch we skip the load and leave the index empty, which makes the
239
+ // restore path rebuild it from the store with the current embedder.
240
+ // A missing meta file (index persisted before this stamp existed) is treated
241
+ // as "unknown but trusted" — we proceed rather than force a mass re-embed.
242
+ if( await this._storage.exists( this._metaPath ) ){
243
+ try {
244
+ const meta = JSON.parse( await this._storage.read( this._metaPath ) ) as { model?: string; dimensions?: number }
245
+ if( meta.model !== this._embedder.modelName || meta.dimensions !== this._embedder.dimensions ){
246
+ logger.warn(
247
+ `[VectorMemoryAdapter] Persisted index was built with ${meta.model}/${meta.dimensions}d ` +
248
+ `but current embedder is ${this._embedder.modelName}/${this._embedder.dimensions}d — ` +
249
+ `discarding stale index (will rebuild from store).`
250
+ )
251
+ return
252
+ }
253
+ }
254
+ catch { /* unreadable meta — fall through to a best-effort load */ }
255
+ }
256
+
257
+ const bytes = await this._storage.readBytes( this._persistPath )
258
+
259
+ if( this._index.deserialize ){
260
+ await this._index.deserialize( bytes )
261
+ // Rebuild the dedup/eviction id-set from the loaded index. Without this,
262
+ // _indexedIds stays empty after a restore while the index is full, so
263
+ // _evictOldest (which iterates _indexedIds) becomes a no-op and the
264
+ // indexBatch eviction loop spins forever once the index is at its cap.
265
+ this._indexedIds.clear()
266
+ this._accessTick.clear()
267
+ this._accessClock = 0
268
+ if( this._index.keys )
269
+ for( const id of this._index.keys() ){
270
+ this._indexedIds.add( id )
271
+ // Seed access recency in insertion order so a freshly-loaded index
272
+ // evicts oldest-first until real recalls warm specific entries.
273
+ this._touch( id )
274
+ }
275
+ }
276
+ }
277
+ catch( err ){
278
+ logger.warn( `[VectorMemoryAdapter] Failed to load index:`, err )
279
+ }
280
+ }
281
+
282
+ private async _evictColdest(): Promise<void> {
283
+ // Evict the least-recently-used entries (lowest access tick), not merely the
284
+ // oldest-inserted: a memory that is recalled often stays warm and survives,
285
+ // while genuinely cold ones go first — so the bounded index keeps what the
286
+ // Will actually uses. Never-touched entries fall back to insertion order
287
+ // (their access tick was seeded at insert/load). Drop ~10% of the cap (≥1)
288
+ // per call to amortise the rank over many inserts; each call strictly shrinks
289
+ // the index, so the indexBatch eviction loop is guaranteed to terminate.
290
+ const target = Math.max( 1, Math.floor( this._maxIndexedEpisodes * 0.1 ) )
291
+
292
+ const victims = Array.from( this._indexedIds )
293
+ .sort( ( a, b ) => ( this._accessTick.get( a ) ?? 0 ) - ( this._accessTick.get( b ) ?? 0 ) )
294
+ .slice( 0, target )
295
+
296
+ for( const id of victims ){
297
+ await this._index.delete( id )
298
+ this._indexedIds.delete( id )
299
+ this._accessTick.delete( id )
300
+ }
301
+
302
+ if( victims.length > 0 ) this._dirty = true
303
+ }
304
+
305
+ private _schedulePersist(): void {
306
+ if( this._persistDebounceTimer )
307
+ clearTimeout( this._persistDebounceTimer )
308
+
309
+ this._persistDebounceTimer = setTimeout( () => {
310
+ this.persist().catch( err => {
311
+ logger.error( `[VectorMemoryAdapter] Persist failed:`, err )
312
+ } )
313
+ this._persistDebounceTimer = null
314
+ }, 5000 )
315
+ }
316
+ }
@@ -0,0 +1,47 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/memory/vector.content.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * Projects an episode's stored content into a clean text string for embedding.
7
+ *
8
+ * Episodes are consolidated from working-memory items, so `content` is usually
9
+ * the raw WM `entity.metadata` object — a mix of the actual semantic text
10
+ * (summary / userMessage / willReply) and structural noise (wmType, numeric
11
+ * activation, attendedCount, tags). Embedding the whole JSON blob dilutes the
12
+ * vector with that noise and weakens recall. This extracts the human-meaningful
13
+ * text, falling back to JSON only for shapes we don't recognise so it never
14
+ * throws and never silently drops data.
15
+ *
16
+ * Applied at every write path (index / indexBatch, hence rebuildFromStore) so
17
+ * indexed vectors are consistent regardless of how they were built. Query
18
+ * strings are already clean text and pass through unchanged.
19
+ */
20
+ export function episodeContentToText( content: unknown ): string {
21
+ if( typeof content === 'string' ) return content
22
+
23
+ if( content && typeof content === 'object' ){
24
+ const m = content as Record<string, unknown>
25
+
26
+ // Conversation exchanges carry the actual dialogue — by far the strongest
27
+ // semantic signal — so prefer it over the pre-baked summary string.
28
+ const user = typeof m['userMessage'] === 'string' ? m['userMessage'] as string : ''
29
+ const reply = typeof m['willReply'] === 'string' ? m['willReply'] as string : ''
30
+ if( user || reply )
31
+ return [ user, reply ].filter( Boolean ).join( ' → ' )
32
+
33
+ // Most WM items carry a top-level human summary.
34
+ if( typeof m['summary'] === 'string' && m['summary'] ) return m['summary'] as string
35
+
36
+ // Descriptor items nest their payload (e.g. plan: { content: { summary } }).
37
+ const nested = m['content']
38
+ if( typeof nested === 'string' && nested ) return nested
39
+ if( nested && typeof nested === 'object' ){
40
+ const nm = nested as Record<string, unknown>
41
+ if( typeof nm['summary'] === 'string' && nm['summary'] ) return nm['summary'] as string
42
+ }
43
+ }
44
+
45
+ // Unknown shape — stable JSON so we still embed something deterministic.
46
+ try { return JSON.stringify( content ) ?? '' } catch { return String( content ) }
47
+ }
@@ -0,0 +1,212 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/memory/vector.embedder.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * Embedding provider interface — abstracts different embedding models.
7
+ *
8
+ * Supports:
9
+ * - Local models (via Transformers.js or Ollama)
10
+ * - Cloud providers (OpenAI, Anthropic, Cohere)
11
+ * - Mock embedder for testing/deterministic replay
12
+ */
13
+
14
+ import type { TokenTracker } from '#cognition/utilities/token.tracker'
15
+
16
+ export interface EmbeddingProvider {
17
+ readonly modelName: string
18
+ readonly dimensions: number
19
+
20
+ /** Generate embedding for a single piece of content. `fn` tags the call for
21
+ * cost attribution: 'recall' (query) vs 'index' (write). */
22
+ embed( content: unknown, fn?: string ): Promise<number[]>
23
+
24
+ /** Generate embeddings for multiple items (batched for efficiency). */
25
+ embedBatch( contents: unknown[], fn?: string ): Promise<number[][]>
26
+
27
+ /** Check if two embeddings are semantically equivalent (for replay validation) */
28
+ areEquivalent( embedding1: number[], embedding2: number[], tolerance?: number ): boolean
29
+ }
30
+
31
+ /**
32
+ * OpenAI-compatible embedder (works with OpenAI, Azure, LocalAI, Ollama)
33
+ */
34
+ export class OpenAICompatibleEmbedder implements EmbeddingProvider {
35
+ readonly modelName: string
36
+ readonly dimensions: number
37
+
38
+ private _apiUrl: string
39
+ private _apiKey: string | null
40
+ private _maxConcurrency: number
41
+ private _timeoutMs: number
42
+ private _tokenTracker: TokenTracker | null
43
+
44
+ constructor( config: {
45
+ modelName: string
46
+ dimensions: number
47
+ apiUrl: string
48
+ apiKey?: string | null
49
+ /** Max embedding requests in flight at once for embedBatch(). Default 8. */
50
+ maxConcurrency?: number
51
+ /** @deprecated use maxConcurrency — kept as its fallback for back-compat. */
52
+ batchSize?: number
53
+ /** Per-request timeout in ms before the connection is aborted. Default 30s. */
54
+ timeoutMs?: number
55
+ /**
56
+ * Per-Will token tracker. When provided, each embedding call records its
57
+ * input-token usage under the 'embedding' category so memory-vector spend is
58
+ * visible alongside LLM spend instead of being a silent COGS leak.
59
+ */
60
+ tokenTracker?: TokenTracker | null
61
+ } ){
62
+ this.modelName = config.modelName
63
+ this.dimensions = config.dimensions
64
+ this._apiUrl = config.apiUrl
65
+ this._apiKey = config.apiKey ?? null
66
+ this._maxConcurrency = Math.max( 1, config.maxConcurrency ?? config.batchSize ?? 8 )
67
+ this._timeoutMs = config.timeoutMs ?? 30_000
68
+ this._tokenTracker = config.tokenTracker ?? null
69
+ }
70
+
71
+ async embed( content: unknown, fn: string = 'recall' ): Promise<number[]> {
72
+ let response: Response
73
+ try {
74
+ response = await fetch( `${this._apiUrl}/embeddings`, {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ ...( this._apiKey ? { 'Authorization': `Bearer ${this._apiKey}` } : {} )
79
+ },
80
+ body: JSON.stringify({
81
+ model: this.modelName,
82
+ input: typeof content === 'string' ? content : JSON.stringify( content )
83
+ }),
84
+ // Abort a hung connection instead of waiting forever (FN16).
85
+ signal: AbortSignal.timeout( this._timeoutMs ),
86
+ } )
87
+ }
88
+ catch( err ){
89
+ // AbortSignal.timeout rejects with a DOMException named 'TimeoutError'.
90
+ if( err instanceof Error && err.name === 'TimeoutError' )
91
+ throw new Error( `Embedding request timed out after ${this._timeoutMs}ms` )
92
+ throw err
93
+ }
94
+
95
+ if( !response.ok )
96
+ throw new Error( `Embedding failed: ${response.status} ${response.statusText}` )
97
+
98
+ const data = await response.json() as {
99
+ data?: Array<{ embedding?: number[] }>
100
+ usage?: { prompt_tokens?: number; total_tokens?: number }
101
+ }
102
+
103
+ // Guard the response shape: the old `data.data[0]!.embedding` threw an
104
+ // opaque TypeError on an empty/malformed body (FN16).
105
+ const embedding = data?.data?.[ 0 ]?.embedding
106
+ if( !Array.isArray( embedding ) || embedding.length === 0 )
107
+ throw new Error( `Embedding response was empty or malformed for model ${this.modelName}` )
108
+
109
+ // Meter embedding token usage (input-only — embeddings have no completion).
110
+ // Recorded under the 'embedding' category so per-Will dashboards can split
111
+ // memory-vector spend from LLM reasoning spend.
112
+ const usedTok = data?.usage?.total_tokens ?? data?.usage?.prompt_tokens ?? 0
113
+ if( usedTok > 0 ){
114
+ this._tokenTracker?.recordUsage({
115
+ model: this.modelName,
116
+ promptTokens: usedTok,
117
+ completionTokens: 0,
118
+ totalTokens: usedTok,
119
+ category: 'embedding',
120
+ attribute: 'memory',
121
+ function: fn, // 'recall' (query) | 'index' (write)
122
+ scope: this.modelName,
123
+ tick: 0,
124
+ latencyMs: 0,
125
+ })
126
+ }
127
+
128
+ return embedding
129
+ }
130
+
131
+ async embedBatch( contents: unknown[], fn: string = 'index' ): Promise<number[][]> {
132
+ // Bounded fan-out: cap concurrent requests at _maxConcurrency instead of
133
+ // firing all of them at once (FN16), while preserving input order.
134
+ const results: number[][] = new Array( contents.length )
135
+ let next = 0
136
+
137
+ const worker = async (): Promise<void> => {
138
+ for( let i = next++; i < contents.length; i = next++ )
139
+ results[ i ] = await this.embed( contents[ i ], fn )
140
+ }
141
+
142
+ await Promise.all(
143
+ Array.from( { length: Math.min( this._maxConcurrency, contents.length ) }, () => worker() )
144
+ )
145
+
146
+ return results
147
+ }
148
+
149
+ areEquivalent( embedding1: number[], embedding2: number[], tolerance: number = 1e-6 ): boolean {
150
+ if( embedding1.length !== embedding2.length ) return false
151
+
152
+ for( let i = 0; i < embedding1.length; i++ ){
153
+ if( Math.abs( embedding1[i]! - embedding2[i]! ) > tolerance ) return false
154
+ }
155
+ return true
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Deterministic mock embedder for testing and replay.
161
+ * Uses content hashing to produce stable embeddings.
162
+ */
163
+ export class MockEmbedder implements EmbeddingProvider {
164
+ readonly modelName = 'mock'
165
+ readonly dimensions = 128
166
+
167
+ private _seed: number
168
+
169
+ constructor( seed: number = 42 ){
170
+ this._seed = seed
171
+ }
172
+
173
+ async embed( content: unknown, _fn: string = 'recall' ): Promise<number[]> {
174
+ const str = typeof content === 'string' ? content : JSON.stringify( content )
175
+ const hash = this._hashString( str )
176
+ const embedding: number[] = []
177
+
178
+ let state = hash
179
+ for( let i = 0; i < this.dimensions; i++ ){
180
+ state = this._next( state )
181
+ embedding.push( ( state % 200000 ) / 200000 - 1 )
182
+ }
183
+
184
+ return embedding
185
+ }
186
+
187
+ async embedBatch( contents: unknown[], fn: string = 'index' ): Promise<number[][]> {
188
+ return Promise.all( contents.map( c => this.embed( c, fn ) ) )
189
+ }
190
+
191
+ areEquivalent( embedding1: number[], embedding2: number[], tolerance: number = 1e-6 ): boolean {
192
+ if( embedding1.length !== embedding2.length ) return false
193
+ for( let i = 0; i < embedding1.length; i++ ){
194
+ if( Math.abs( embedding1[i]! - embedding2[i]! ) > tolerance ) return false
195
+ }
196
+ return true
197
+ }
198
+
199
+ private _hashString( str: string ): number {
200
+ let hash = 2166136261
201
+ for( let i = 0; i < str.length; i++ ){
202
+ hash ^= str.charCodeAt( i )
203
+ hash = Math.imul( hash, 16777619 )
204
+ }
205
+ return hash >>> 0
206
+ }
207
+
208
+ private _next( state: number ): number {
209
+ state = Math.imul( state, 1103515245 ) + 12345
210
+ return state >>> 0
211
+ }
212
+ }