@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.
- package/LICENSE +202 -0
- package/README.md +777 -0
- package/dist/index.d.ts +8144 -0
- package/dist/index.js +26231 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/src/cognition/agency/access.grants.ts +59 -0
- package/src/cognition/agency/competence.codec.ts +89 -0
- package/src/cognition/agency/engines/action.selector.ts +462 -0
- package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
- package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
- package/src/cognition/agency/engines/instruction.intake.ts +54 -0
- package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
- package/src/cognition/agency/engines/reafference.engine.ts +241 -0
- package/src/cognition/agency/execution.primitives.ts +118 -0
- package/src/cognition/agency/index.ts +58 -0
- package/src/cognition/agency/proactive.communicator.ts +274 -0
- package/src/cognition/agency/reconcile.learning.ts +82 -0
- package/src/cognition/agency/schemas/external.ts +59 -0
- package/src/cognition/agency/schemas/innate.ts +111 -0
- package/src/cognition/agency/schemas/repertoire.ts +246 -0
- package/src/cognition/agency/selection.scoring.ts +191 -0
- package/src/cognition/agency/types.ts +165 -0
- package/src/cognition/bus.ts +391 -0
- package/src/cognition/completion.inbox.ts +86 -0
- package/src/cognition/config.mirror.entities.ts +475 -0
- package/src/cognition/conversation.memory.ts +81 -0
- package/src/cognition/event.log.ts +116 -0
- package/src/cognition/event.schemas.ts +568 -0
- package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
- package/src/cognition/faculties/affective.blender.ts +562 -0
- package/src/cognition/faculties/attachment.evaluator.ts +432 -0
- package/src/cognition/faculties/attention.allocator.ts +422 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
- package/src/cognition/faculties/bias.detector.ts +363 -0
- package/src/cognition/faculties/circadian.oscillator.ts +294 -0
- package/src/cognition/faculties/confidence.calibrator.ts +324 -0
- package/src/cognition/faculties/dream.simulator.ts +251 -0
- package/src/cognition/faculties/empathy.simulator.ts +253 -0
- package/src/cognition/faculties/energy.regulator.ts +320 -0
- package/src/cognition/faculties/episodic.consolidator.ts +728 -0
- package/src/cognition/faculties/executive.engine/commands.ts +459 -0
- package/src/cognition/faculties/executive.engine/config.ts +46 -0
- package/src/cognition/faculties/executive.engine/context.ts +605 -0
- package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
- package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
- package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
- package/src/cognition/faculties/executive.engine/facet.ts +628 -0
- package/src/cognition/faculties/executive.engine/gating.ts +222 -0
- package/src/cognition/faculties/executive.engine/index.ts +6 -0
- package/src/cognition/faculties/executive.engine/messages.ts +102 -0
- package/src/cognition/faculties/executive.engine/parser.ts +380 -0
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
- package/src/cognition/faculties/executive.engine/types.ts +323 -0
- package/src/cognition/faculties/exteroception.ts +338 -0
- package/src/cognition/faculties/forgetting.curve.ts +202 -0
- package/src/cognition/faculties/frustration.evaluator.ts +280 -0
- package/src/cognition/faculties/goal.manager.ts +1008 -0
- package/src/cognition/faculties/inhibition.controller.ts +351 -0
- package/src/cognition/faculties/interoception.ts +417 -0
- package/src/cognition/faculties/introspection.engine.ts +246 -0
- package/src/cognition/faculties/known.entity.tracker.ts +439 -0
- package/src/cognition/faculties/loss.evaluator.ts +224 -0
- package/src/cognition/faculties/moral.evaluator.ts +437 -0
- package/src/cognition/faculties/novelty.detector.ts +254 -0
- package/src/cognition/faculties/persona.consolidator.ts +845 -0
- package/src/cognition/faculties/planning.engine/engine.ts +859 -0
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
- package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
- package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
- package/src/cognition/faculties/planning.engine/types.ts +143 -0
- package/src/cognition/faculties/reputation.tracker.ts +294 -0
- package/src/cognition/faculties/reward.evaluator.ts +304 -0
- package/src/cognition/faculties/self.model.updater.ts +677 -0
- package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
- package/src/cognition/faculties/semantic.engine/index.ts +6 -0
- package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
- package/src/cognition/faculties/semantic.engine/types.ts +53 -0
- package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
- package/src/cognition/faculties/social.perception.ts +333 -0
- package/src/cognition/faculties/spaced.repetition.ts +605 -0
- package/src/cognition/faculties/stress.regulator.ts +375 -0
- package/src/cognition/faculties/task.switcher.ts +239 -0
- package/src/cognition/faculties/theory.of.mind.ts +333 -0
- package/src/cognition/faculties/threat.evaluator.ts +312 -0
- package/src/cognition/faculties/working.memory.ts +406 -0
- package/src/cognition/generative.model.ts +344 -0
- package/src/cognition/heartbeat.ts +70 -0
- package/src/cognition/index.ts +291 -0
- package/src/cognition/instruction.handler.ts +146 -0
- package/src/cognition/memory/index.ts +35 -0
- package/src/cognition/memory/vector.adapter.ts +316 -0
- package/src/cognition/memory/vector.content.ts +47 -0
- package/src/cognition/memory/vector.embedder.ts +212 -0
- package/src/cognition/memory/vector.index.ts +459 -0
- package/src/cognition/memory/vector.types.ts +65 -0
- package/src/cognition/orchestrator.ts +142 -0
- package/src/cognition/persona.prior.ts +265 -0
- package/src/cognition/schema.registry.ts +145 -0
- package/src/cognition/senses/audition.engine/engine.ts +976 -0
- package/src/cognition/senses/audition.engine/salience.ts +59 -0
- package/src/cognition/senses/base.sense.engine.ts +117 -0
- package/src/cognition/senses/gustation.engine.ts +24 -0
- package/src/cognition/senses/index.ts +172 -0
- package/src/cognition/senses/olfaction.engine.ts +24 -0
- package/src/cognition/senses/somatosensation.engine.ts +23 -0
- package/src/cognition/senses/vision.engine.ts +23 -0
- package/src/cognition/types.ts +63 -0
- package/src/cognition/utilities/token.tracker.ts +492 -0
- package/src/core/abstracts.ts +60 -0
- package/src/core/async.engine.ts +539 -0
- package/src/core/clock.ts +196 -0
- package/src/core/completion.recorder.ts +122 -0
- package/src/core/conflict.detector.ts +140 -0
- package/src/core/distributed.ts +626 -0
- package/src/core/event.bus.ts +292 -0
- package/src/core/inbound.recorder.ts +100 -0
- package/src/core/index.ts +167 -0
- package/src/core/logger.ts +85 -0
- package/src/core/metrics.ts +107 -0
- package/src/core/orchestrator.ts +625 -0
- package/src/core/replay.ts +589 -0
- package/src/core/scenario.ts +120 -0
- package/src/core/serialization.ts +448 -0
- package/src/core/simulation.ts +197 -0
- package/src/core/snapshot.manager.ts +293 -0
- package/src/core/state.manager.ts +283 -0
- package/src/core/types.ts +274 -0
- package/src/core/utils.ts +82 -0
- package/src/core/wall.clock.ts +23 -0
- package/src/extensions/livestream.ext.ts +570 -0
- package/src/extensions/time.ext.ts +339 -0
- package/src/index.ts +91 -0
- package/src/llm/gate.ts +140 -0
- package/src/llm/index.ts +716 -0
- package/src/llm/summarizer.ts +170 -0
- package/src/llm/wire.contracts.ts +67 -0
- package/src/pma/eval.ts +651 -0
- package/src/pma/index.ts +1082 -0
- package/src/profiles/built-in.ts +7 -0
- package/src/profiles/companion.ts +28 -0
- package/src/profiles/company-brain.ts +54 -0
- package/src/profiles/customer-service.ts +30 -0
- package/src/profiles/game-npc.ts +20 -0
- package/src/profiles/index.ts +42 -0
- package/src/profiles/smart-home.ts +34 -0
- package/src/runners/coherence.runner.ts +49 -0
- package/src/runners/outreach.runner.ts +158 -0
- package/src/runners/social.runner.ts +182 -0
- package/src/runners/thin-shim.runner.ts +195 -0
- package/src/sdk/will.ts +334 -0
- package/src/stem/assembly.audit.ts +88 -0
- package/src/stem/distribution.ts +203 -0
- package/src/stem/guards/identity.coherence.ts +152 -0
- package/src/stem/guards/identity.guard.ts +227 -0
- package/src/stem/index.ts +1145 -0
- package/src/stem/mind.ts +1090 -0
- package/src/stem/tracts/ack.reconciler.ts +55 -0
- package/src/stem/tracts/biography.writer.ts +190 -0
- package/src/stem/tracts/effector.controller.ts +143 -0
- package/src/stem/tracts/health.reporter.ts +113 -0
- package/src/stem/tracts/inbound.queue.ts +54 -0
- package/src/stem/tracts/outbox.controller.ts +167 -0
- package/src/stem/tracts/outbox.writer.ts +133 -0
- package/src/stem/tracts/pma.controller.ts +109 -0
- package/src/stem/tracts/replay.controller.ts +177 -0
- package/src/stem/tracts/sensory.controller.ts +168 -0
- package/src/stem/tracts/session.logger.ts +157 -0
- package/src/stem/tracts/transport/index.ts +38 -0
- package/src/stem/tracts/transport/loopback.transport.ts +86 -0
- package/src/stem/tracts/transport/socketio.transport.ts +200 -0
- package/src/stem/tracts/transport/stream.transport.ts +152 -0
- package/src/stem/tracts/transport/types.ts +193 -0
- package/src/stem/tracts/transport.controller.ts +380 -0
- package/src/types.ts +116 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/cognition/memory/vector.index.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vector index interface — allows swapping different implementations.
|
|
7
|
+
*
|
|
8
|
+
* Implementations:
|
|
9
|
+
* - HNSWIndex (in-memory, deterministic)
|
|
10
|
+
* - QdrantClient (cloud)
|
|
11
|
+
* - PgVectorClient (Postgres)
|
|
12
|
+
* - PineconeClient (cloud)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { VectorRecord, VectorQueryResult } from '#memory/vector.types'
|
|
16
|
+
import type { SeededPRNG } from '#core/types'
|
|
17
|
+
import { createPRNG } from '#core/utils'
|
|
18
|
+
|
|
19
|
+
/** Fixed fallback seed so an unwired index is still deterministic per insertion order. */
|
|
20
|
+
const DEFAULT_HNSW_SEED = 42
|
|
21
|
+
|
|
22
|
+
export interface VectorIndex {
|
|
23
|
+
/** Insert a vector into the index */
|
|
24
|
+
insert( record: VectorRecord ): Promise<void>
|
|
25
|
+
|
|
26
|
+
/** Search for k nearest neighbors. Similarity-only — see VectorQueryFilter. */
|
|
27
|
+
search(
|
|
28
|
+
vector: number[],
|
|
29
|
+
k: number,
|
|
30
|
+
filter?: { minSimilarity?: number }
|
|
31
|
+
): Promise<VectorQueryResult[]>
|
|
32
|
+
|
|
33
|
+
/** Delete a vector from the index */
|
|
34
|
+
delete( id: string ): Promise<boolean>
|
|
35
|
+
|
|
36
|
+
/** Get current size of index */
|
|
37
|
+
readonly size: number
|
|
38
|
+
|
|
39
|
+
/** Iterate all indexed ids in insertion order — lets callers rebuild an
|
|
40
|
+
* external id-set (e.g. the adapter's dedup/eviction set) after a load. */
|
|
41
|
+
keys?(): IterableIterator<string>
|
|
42
|
+
|
|
43
|
+
/** Serialize index to bytes for persistence (optional) */
|
|
44
|
+
serialize?(): Uint8Array
|
|
45
|
+
|
|
46
|
+
/** Deserialize index from bytes (optional) */
|
|
47
|
+
deserialize?( bytes: Uint8Array ): Promise<void>
|
|
48
|
+
|
|
49
|
+
/** Clear all entries */
|
|
50
|
+
clear(): Promise<void>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface HNSWIndexConfig {
|
|
54
|
+
dimensions?: number
|
|
55
|
+
similarityMetric?: 'cosine' | 'euclidean' | 'dot'
|
|
56
|
+
hnswM?: number
|
|
57
|
+
hnswEfConstruction?: number
|
|
58
|
+
/** Base-layer search beam width. Must be ≫ k for good recall; the effective
|
|
59
|
+
* beam is max(k, hnswEfSearch). Default 64. */
|
|
60
|
+
hnswEfSearch?: number
|
|
61
|
+
/** Seed for the level-assignment PRNG. Required for deterministic replay. */
|
|
62
|
+
seed?: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* HNSW-based vector index for semantic similarity search.
|
|
67
|
+
*
|
|
68
|
+
* In-memory index with optional persistence via StorageAdapter.
|
|
69
|
+
* Designed to be rebuilt from _store on snapshot restore.
|
|
70
|
+
* Deterministic for given insertion order (important for replay).
|
|
71
|
+
*/
|
|
72
|
+
export class HNSWIndex implements VectorIndex {
|
|
73
|
+
private _nodes: Map<string, HNSWNode> = new Map()
|
|
74
|
+
private _entryPoint: string | null = null
|
|
75
|
+
private _maxLevel: number = 0
|
|
76
|
+
private _config: HNSWIndexConfig
|
|
77
|
+
private _seed: number
|
|
78
|
+
private _prng: SeededPRNG
|
|
79
|
+
|
|
80
|
+
constructor( config: HNSWIndexConfig = {} ){
|
|
81
|
+
this._config = {
|
|
82
|
+
dimensions: config.dimensions ?? 1536,
|
|
83
|
+
similarityMetric: config.similarityMetric ?? 'cosine',
|
|
84
|
+
hnswM: config.hnswM ?? 16,
|
|
85
|
+
hnswEfConstruction: config.hnswEfConstruction ?? 200,
|
|
86
|
+
hnswEfSearch: config.hnswEfSearch ?? 64
|
|
87
|
+
}
|
|
88
|
+
this._seed = config.seed ?? DEFAULT_HNSW_SEED
|
|
89
|
+
this._prng = createPRNG( this._seed )
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get size(): number {
|
|
93
|
+
return this._nodes.size
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Insertion-ordered ids (Map preserves insertion order; deserialize repopulates
|
|
97
|
+
* _nodes in the original order), so callers get oldest-first iteration. */
|
|
98
|
+
keys(): IterableIterator<string> {
|
|
99
|
+
return this._nodes.keys()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async insert( record: VectorRecord ): Promise<void> {
|
|
103
|
+
if( this._nodes.has( record.id ) )
|
|
104
|
+
await this.delete( record.id )
|
|
105
|
+
|
|
106
|
+
const level = this._randomLevel()
|
|
107
|
+
const node: HNSWNode = {
|
|
108
|
+
id: record.id,
|
|
109
|
+
vector: record.vector,
|
|
110
|
+
level,
|
|
111
|
+
connections: new Map()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for( let l = 0; l <= level; l++ )
|
|
115
|
+
node.connections.set( l, new Set() )
|
|
116
|
+
|
|
117
|
+
// Register the node up front so neighbour-list pruning below can resolve
|
|
118
|
+
// `record.id` to its node while wiring bidirectional links. Safe: no
|
|
119
|
+
// level-l edge points at the new node until *after* this insert's own
|
|
120
|
+
// _searchLayer call for level l, so it can never appear in its own
|
|
121
|
+
// candidate sets (which would change the deterministic graph).
|
|
122
|
+
this._nodes.set( record.id, node )
|
|
123
|
+
|
|
124
|
+
if( this._entryPoint === null ){
|
|
125
|
+
this._entryPoint = record.id
|
|
126
|
+
this._maxLevel = level
|
|
127
|
+
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const entryNode = this._nodes.get( this._entryPoint )!
|
|
132
|
+
let currNode = entryNode
|
|
133
|
+
|
|
134
|
+
for( let l = this._maxLevel; l > level; l-- ){
|
|
135
|
+
const candidates = this._searchLayer( currNode, record.vector, l, 1 )
|
|
136
|
+
currNode = candidates[0] ?? currNode
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for( let l = Math.min( level, this._maxLevel ); l >= 0; l-- ){
|
|
140
|
+
const candidates = this._searchLayer( currNode, record.vector, l, this._config.hnswEfConstruction! )
|
|
141
|
+
const neighbors = this._selectNeighbors( record.vector, candidates, this._config.hnswM! )
|
|
142
|
+
|
|
143
|
+
// Canonical HNSW degree caps: M for upper layers, M*2 at the base layer.
|
|
144
|
+
const maxConn = l === 0 ? this._config.hnswM! * 2 : this._config.hnswM!
|
|
145
|
+
|
|
146
|
+
for( const neighbor of neighbors ){
|
|
147
|
+
if( neighbor.id === record.id ) continue // never self-link
|
|
148
|
+
|
|
149
|
+
node.connections.get( l )!.add( neighbor.id )
|
|
150
|
+
|
|
151
|
+
const nset = neighbor.connections.get( l )!
|
|
152
|
+
nset.add( record.id )
|
|
153
|
+
// Shrink the touched neighbour back to its cap. Without this, popular
|
|
154
|
+
// nodes accumulate back-links without bound → memory bloat and an
|
|
155
|
+
// ever-slower _searchLayer (FN6). Re-selection is deterministic.
|
|
156
|
+
if( nset.size > maxConn )
|
|
157
|
+
this._shrinkConnections( neighbor, l, maxConn )
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
currNode = candidates[0] ?? currNode
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if( level > this._maxLevel ){
|
|
164
|
+
this._maxLevel = level
|
|
165
|
+
this._entryPoint = record.id
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Re-select a node's `level` connections down to `cap`, keeping the closest
|
|
171
|
+
* by distance. Canonical HNSW neighbour pruning: bounds node degree so the
|
|
172
|
+
* graph stays sparse and search stays fast. Asymmetric drops (the removed
|
|
173
|
+
* peer may still link back) are expected and self-correct when that peer is
|
|
174
|
+
* itself pruned — matching reference implementations.
|
|
175
|
+
*/
|
|
176
|
+
private _shrinkConnections( node: HNSWNode, level: number, cap: number ): void {
|
|
177
|
+
const set = node.connections.get( level )
|
|
178
|
+
if( !set || set.size <= cap ) return
|
|
179
|
+
|
|
180
|
+
const connected: HNSWNode[] = []
|
|
181
|
+
for( const id of set ){
|
|
182
|
+
const n = this._nodes.get( id )
|
|
183
|
+
if( n ) connected.push( n )
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const kept = this._selectNeighbors( node.vector, connected, cap )
|
|
187
|
+
node.connections.set( level, new Set( kept.map( n => n.id ) ) )
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async search(
|
|
191
|
+
vector: number[],
|
|
192
|
+
k: number,
|
|
193
|
+
filter?: { minSimilarity?: number }
|
|
194
|
+
): Promise<VectorQueryResult[]> {
|
|
195
|
+
if( this._entryPoint === null || this._nodes.size === 0 ) return []
|
|
196
|
+
|
|
197
|
+
const entryNode = this._nodes.get( this._entryPoint )!
|
|
198
|
+
let currNode = entryNode
|
|
199
|
+
|
|
200
|
+
for( let l = this._maxLevel; l > 0; l-- ){
|
|
201
|
+
const candidates = this._searchLayer( currNode, vector, l, 1 )
|
|
202
|
+
currNode = candidates[0] ?? currNode
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Explore a base-layer beam much wider than k (canonical HNSW efSearch ≫ k),
|
|
206
|
+
// then keep the top-k. With ef==k the greedy walk under-explored and capped
|
|
207
|
+
// recall; widening it surfaces true nearest neighbours that a k-width beam
|
|
208
|
+
// skipped. _searchLayer returns nearest-first, so slicing keeps the best k.
|
|
209
|
+
const ef = Math.max( k, this._config.hnswEfSearch ?? 64 )
|
|
210
|
+
const candidates = this._searchLayer( currNode, vector, 0, ef )
|
|
211
|
+
const minSimilarity = filter?.minSimilarity ?? 0.35
|
|
212
|
+
|
|
213
|
+
const results: VectorQueryResult[] = []
|
|
214
|
+
for( const node of candidates ){
|
|
215
|
+
const similarity = this._computeSimilarity( vector, node.vector )
|
|
216
|
+
if( similarity >= minSimilarity )
|
|
217
|
+
results.push({ episodeId: node.id, similarity })
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return results.slice( 0, k )
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async delete( id: string ): Promise<boolean> {
|
|
224
|
+
const node = this._nodes.get( id )
|
|
225
|
+
if( !node ) return false
|
|
226
|
+
|
|
227
|
+
for( const [ level, neighbors ] of node.connections )
|
|
228
|
+
for( const neighborId of neighbors ){
|
|
229
|
+
const neighbor = this._nodes.get( neighborId )
|
|
230
|
+
if( neighbor )
|
|
231
|
+
neighbor.connections.get( level )?.delete( id )
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this._nodes.delete( id )
|
|
235
|
+
|
|
236
|
+
// Reselect the entry point when we removed it. The old code grabbed an
|
|
237
|
+
// arbitrary `keys().next()` node — often a level-0 node — while _maxLevel
|
|
238
|
+
// stayed high, so the top-down descent began at a node with no high-level
|
|
239
|
+
// connections and returned only itself (compounding FN4). Promote the
|
|
240
|
+
// highest-level remaining node and resync _maxLevel to it. Deterministic:
|
|
241
|
+
// Map iteration is insertion order, ties resolve to the first inserted.
|
|
242
|
+
if( this._entryPoint === id ){
|
|
243
|
+
this._entryPoint = null
|
|
244
|
+
this._maxLevel = 0
|
|
245
|
+
for( const n of this._nodes.values() )
|
|
246
|
+
if( this._entryPoint === null || n.level > this._maxLevel ){
|
|
247
|
+
this._entryPoint = n.id
|
|
248
|
+
this._maxLevel = n.level
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return true
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async clear(): Promise<void> {
|
|
256
|
+
this._nodes.clear()
|
|
257
|
+
this._entryPoint = null
|
|
258
|
+
this._maxLevel = 0
|
|
259
|
+
// Reset the PRNG so a rebuild from the same insertion order (e.g.
|
|
260
|
+
// rebuildFromStore on snapshot restore) reproduces the identical graph.
|
|
261
|
+
this._prng = createPRNG( this._seed )
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
serialize(): Uint8Array {
|
|
265
|
+
const data = {
|
|
266
|
+
config: this._config,
|
|
267
|
+
seed: this._seed,
|
|
268
|
+
// Persist the live PRNG state so inserts after a load continue the
|
|
269
|
+
// same deterministic sequence rather than restarting from the seed.
|
|
270
|
+
// Mask to 32 bits: the Mulberry32 accumulator grows unbounded but only
|
|
271
|
+
// its low 32 bits affect output, and createPRNG re-applies `>>> 0` on
|
|
272
|
+
// load — storing the masked value keeps serialize/restore round-trips
|
|
273
|
+
// byte-stable.
|
|
274
|
+
prngState: this._prng.state >>> 0,
|
|
275
|
+
entryPoint: this._entryPoint,
|
|
276
|
+
maxLevel: this._maxLevel,
|
|
277
|
+
nodes: Array.from( this._nodes.entries() ).map( ( [ id, node ] ) => ( {
|
|
278
|
+
id,
|
|
279
|
+
vector: node.vector,
|
|
280
|
+
level: node.level,
|
|
281
|
+
connections: Array.from( node.connections.entries() ).map( ( [ level, neighbors ] ) => ( {
|
|
282
|
+
level,
|
|
283
|
+
neighbors: Array.from( neighbors )
|
|
284
|
+
} ) )
|
|
285
|
+
} ) )
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const encoder = new TextEncoder()
|
|
289
|
+
return encoder.encode( JSON.stringify( data ) )
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async deserialize( bytes: Uint8Array ): Promise<void> {
|
|
293
|
+
const decoder = new TextDecoder()
|
|
294
|
+
const data = JSON.parse( decoder.decode( bytes ) ) as {
|
|
295
|
+
config: HNSWIndexConfig
|
|
296
|
+
seed?: number
|
|
297
|
+
prngState?: number
|
|
298
|
+
entryPoint: string | null
|
|
299
|
+
maxLevel: number
|
|
300
|
+
nodes: Array<{
|
|
301
|
+
id: string
|
|
302
|
+
vector: number[]
|
|
303
|
+
level: number
|
|
304
|
+
connections: Array<{ level: number; neighbors: string[] }>
|
|
305
|
+
}>
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this._config = data.config
|
|
309
|
+
this._seed = data.seed ?? this._seed
|
|
310
|
+
// createPRNG sets its internal state directly from the argument, so
|
|
311
|
+
// re-seeding with the persisted state resumes the exact sequence.
|
|
312
|
+
this._prng = createPRNG( data.prngState ?? this._seed )
|
|
313
|
+
this._entryPoint = data.entryPoint
|
|
314
|
+
this._maxLevel = data.maxLevel
|
|
315
|
+
this._nodes.clear()
|
|
316
|
+
|
|
317
|
+
for( const nodeData of data.nodes ){
|
|
318
|
+
const connections = new Map<number, Set<string>>()
|
|
319
|
+
for( const conn of nodeData.connections )
|
|
320
|
+
connections.set( conn.level, new Set( conn.neighbors ) )
|
|
321
|
+
|
|
322
|
+
this._nodes.set( nodeData.id, {
|
|
323
|
+
id: nodeData.id,
|
|
324
|
+
vector: nodeData.vector,
|
|
325
|
+
level: nodeData.level,
|
|
326
|
+
connections
|
|
327
|
+
} )
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── Private methods ───────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
private _randomLevel(): number {
|
|
334
|
+
let level = 0
|
|
335
|
+
while( this._prng.next() < 1 / this._config.hnswM! && level < 16 )
|
|
336
|
+
level++
|
|
337
|
+
|
|
338
|
+
return level
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private _searchLayer(
|
|
342
|
+
entry: HNSWNode,
|
|
343
|
+
query: number[],
|
|
344
|
+
level: number,
|
|
345
|
+
ef: number
|
|
346
|
+
): HNSWNode[] {
|
|
347
|
+
const visited = new Set<string>()
|
|
348
|
+
const candidates: Array<{ node: HNSWNode; dist: number }> = []
|
|
349
|
+
const results: Array<{ node: HNSWNode; dist: number }> = []
|
|
350
|
+
|
|
351
|
+
const entryDist = this._distance( query, entry.vector )
|
|
352
|
+
candidates.push( { node: entry, dist: entryDist } )
|
|
353
|
+
results.push( { node: entry, dist: entryDist } )
|
|
354
|
+
visited.add( entry.id )
|
|
355
|
+
|
|
356
|
+
while( candidates.length > 0 ){
|
|
357
|
+
candidates.sort( ( a, b ) => a.dist - b.dist )
|
|
358
|
+
const best = candidates[0]!
|
|
359
|
+
|
|
360
|
+
if( results.length >= ef && best.dist > results[results.length - 1]!.dist )
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
candidates.shift()
|
|
364
|
+
|
|
365
|
+
// Expand the node we just popped, not the fixed entry node. Using
|
|
366
|
+
// `entry` here collapsed the search into a single-hop scan of the
|
|
367
|
+
// entry's immediate neighbourhood and never walked the graph, badly
|
|
368
|
+
// degrading recall. Traverse `best`'s connections so the greedy walk
|
|
369
|
+
// actually hops across the layer.
|
|
370
|
+
const neighbors = best.node.connections.get( level ) ?? new Set()
|
|
371
|
+
for( const neighborId of neighbors ){
|
|
372
|
+
if( visited.has( neighborId ) ) continue
|
|
373
|
+
|
|
374
|
+
const neighbor = this._nodes.get( neighborId )
|
|
375
|
+
if( !neighbor ) continue
|
|
376
|
+
|
|
377
|
+
visited.add( neighborId )
|
|
378
|
+
const dist = this._distance( query, neighbor.vector )
|
|
379
|
+
|
|
380
|
+
const candidate = { node: neighbor, dist }
|
|
381
|
+
|
|
382
|
+
if( results.length < ef || dist < results[results.length - 1]!.dist ){
|
|
383
|
+
candidates.push( candidate )
|
|
384
|
+
results.push( candidate )
|
|
385
|
+
results.sort( ( a, b ) => a.dist - b.dist )
|
|
386
|
+
|
|
387
|
+
if( results.length > ef ) results.pop()
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return results.map( r => r.node )
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private _selectNeighbors( query: number[], candidates: HNSWNode[], m: number ): HNSWNode[] {
|
|
396
|
+
const withDist = candidates.map( node => ( {
|
|
397
|
+
node,
|
|
398
|
+
dist: this._distance( query, node.vector )
|
|
399
|
+
} ) )
|
|
400
|
+
|
|
401
|
+
withDist.sort( ( a, b ) => a.dist - b.dist )
|
|
402
|
+
|
|
403
|
+
return withDist.slice( 0, m ).map( r => r.node )
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private _distance( a: number[], b: number[] ): number {
|
|
407
|
+
switch( this._config.similarityMetric ){
|
|
408
|
+
case 'cosine': return 1 - this._cosineSimilarity( a, b )
|
|
409
|
+
case 'euclidean': return this._euclideanDistance( a, b )
|
|
410
|
+
case 'dot': return -this._dotProduct( a, b )
|
|
411
|
+
default: return 1 - this._cosineSimilarity( a, b )
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private _computeSimilarity( a: number[], b: number[] ): number {
|
|
416
|
+
switch( this._config.similarityMetric ){
|
|
417
|
+
case 'cosine': return this._cosineSimilarity( a, b )
|
|
418
|
+
case 'euclidean': return 1 / ( 1 + this._euclideanDistance( a, b ) )
|
|
419
|
+
case 'dot': return this._dotProduct( a, b )
|
|
420
|
+
default: return this._cosineSimilarity( a, b )
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private _cosineSimilarity( a: number[], b: number[] ): number {
|
|
425
|
+
let dot = 0, magA = 0, magB = 0
|
|
426
|
+
for( let i = 0; i < a.length; i++ ){
|
|
427
|
+
dot += a[i]! * b[i]!
|
|
428
|
+
magA += a[i]! * a[i]!
|
|
429
|
+
magB += b[i]! * b[i]!
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return dot / ( Math.sqrt( magA ) * Math.sqrt( magB ) + 1e-8 )
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private _euclideanDistance( a: number[], b: number[] ): number {
|
|
436
|
+
let sum = 0
|
|
437
|
+
for( let i = 0; i < a.length; i++ ){
|
|
438
|
+
const diff = a[i]! - b[i]!
|
|
439
|
+
sum += diff * diff
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return Math.sqrt( sum )
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private _dotProduct( a: number[], b: number[] ): number {
|
|
446
|
+
let sum = 0
|
|
447
|
+
for( let i = 0; i < a.length; i++ )
|
|
448
|
+
sum += a[i]! * b[i]!
|
|
449
|
+
|
|
450
|
+
return sum
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
interface HNSWNode {
|
|
455
|
+
id: string
|
|
456
|
+
vector: number[]
|
|
457
|
+
level: number
|
|
458
|
+
connections: Map<number, Set<string>>
|
|
459
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/cognition/memory/vector.types.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vector memory types for semantic episodic retrieval.
|
|
7
|
+
*
|
|
8
|
+
* Provides similarity search over consolidated episodic memories
|
|
9
|
+
* without replacing the existing _store array or StateManager.
|
|
10
|
+
* Acts as a read-optimized secondary index.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Tick } from '#core/types'
|
|
14
|
+
|
|
15
|
+
export interface VectorMemoryConfig {
|
|
16
|
+
/** Dimension of embedding vectors (default 1536 for OpenAI text-embedding-3-small) */
|
|
17
|
+
dimensions?: number
|
|
18
|
+
/** Similarity metric: 'cosine', 'euclidean', or 'dot' */
|
|
19
|
+
similarityMetric?: 'cosine' | 'euclidean' | 'dot'
|
|
20
|
+
/** Maximum number of episodes to index (older entries evicted) */
|
|
21
|
+
maxIndexedEpisodes?: number
|
|
22
|
+
/** Minimum similarity threshold for query results (0-1). Default 0.35, tuned
|
|
23
|
+
* for real sentence embeddings (text-embedding-3-small); raise for higher
|
|
24
|
+
* precision. */
|
|
25
|
+
minSimilarity?: number
|
|
26
|
+
/** Seed for the index's level-assignment PRNG — required for deterministic replay */
|
|
27
|
+
seed?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface VectorRecord {
|
|
31
|
+
id: string // matches EpisodicMemory.id
|
|
32
|
+
vector: number[]
|
|
33
|
+
embeddingModel: string // e.g., 'text-embedding-3-small'
|
|
34
|
+
createdAt: number
|
|
35
|
+
metadata: {
|
|
36
|
+
tick: Tick
|
|
37
|
+
sourceType: string
|
|
38
|
+
/** Encode-time affective valence (-1..1). Stamped here so index backends that
|
|
39
|
+
* CAN filter/rank on metadata (pgvector, Qdrant) may do affective filtering
|
|
40
|
+
* server-side. The HNSW path is similarity-only, so its mood-congruent recall
|
|
41
|
+
* (EpisodicConsolidator.semanticQuery affectiveBias) re-ranks on the
|
|
42
|
+
* authoritative resolved-episode valence instead. */
|
|
43
|
+
emotionalValence: number
|
|
44
|
+
tags: string[]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface VectorQueryResult {
|
|
49
|
+
episodeId: string
|
|
50
|
+
similarity: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Query knobs for vector search.
|
|
55
|
+
*
|
|
56
|
+
* The HNSW index is **similarity-only**: it ranks by vector distance and
|
|
57
|
+
* applies a `minSimilarity` floor, nothing else. Metadata is stored on
|
|
58
|
+
* VectorRecord but is NOT indexed, so any metadata-based narrowing
|
|
59
|
+
* (sourceType / valence / tags / tick range) must be done by the caller
|
|
60
|
+
* AFTER the search returns. See episodic.consolidator.semanticQuery.
|
|
61
|
+
*/
|
|
62
|
+
export interface VectorQueryFilter {
|
|
63
|
+
minSimilarity?: number
|
|
64
|
+
maxResults?: number
|
|
65
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/cognition/orchestrator.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CognitiveOrchestrator — extends DefaultOrchestrator with the three concerns
|
|
7
|
+
* that belong to the cognitive layer:
|
|
8
|
+
*
|
|
9
|
+
* 1. Bus wiring — subscribe engines to the bus on addEngine; call attachBus()
|
|
10
|
+
* so engines hold a direct reference for publishing (Phase F constructor injection).
|
|
11
|
+
* 2. Cold-start snapshot — broadcast engine states before the first tick.
|
|
12
|
+
* 3. HeartbeatPublisher — registered as onAfterTick middleware; owns bus flush
|
|
13
|
+
* + clock.tick delivery.
|
|
14
|
+
*
|
|
15
|
+
* Context enrichment (cognitiveBus in SimulationContext) has been removed:
|
|
16
|
+
* engines now hold the bus directly via constructor injection or attachBus().
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { SimulationContext, Tick, ReadonlySimulationState } from '#core/types'
|
|
20
|
+
import type { SimulationEngine, OrchestratorConfig } from '#core/orchestrator'
|
|
21
|
+
import type { SimulationClock } from '#core/clock'
|
|
22
|
+
import type { EventBus } from '#core/event.bus'
|
|
23
|
+
import type { StateManager } from '#core/state.manager'
|
|
24
|
+
import type { CognitiveBus } from '#cognition/bus'
|
|
25
|
+
import { DefaultOrchestrator } from '#core/orchestrator'
|
|
26
|
+
import { createProductionBus } from '#cognition/bus'
|
|
27
|
+
import { CompletionInbox } from '#cognition/completion.inbox'
|
|
28
|
+
import { isCognitiveEngine } from '#cognition/types'
|
|
29
|
+
import { HeartbeatPublisher } from '#cognition/heartbeat'
|
|
30
|
+
|
|
31
|
+
export interface CognitiveOrchestratorConfig extends OrchestratorConfig {
|
|
32
|
+
/**
|
|
33
|
+
* The shared CognitiveBus instance.
|
|
34
|
+
* When omitted, CognitiveOrchestrator creates a production bus internally.
|
|
35
|
+
* Pass an explicit bus only when you need a shared or test bus.
|
|
36
|
+
*/
|
|
37
|
+
cognitiveBus?: CognitiveBus
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class CognitiveOrchestrator extends DefaultOrchestrator {
|
|
41
|
+
private readonly _bus: CognitiveBus
|
|
42
|
+
private readonly _heartbeat: HeartbeatPublisher
|
|
43
|
+
private readonly _inbox: CompletionInbox
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
clock: SimulationClock,
|
|
47
|
+
eventBus: EventBus,
|
|
48
|
+
stateManager: StateManager,
|
|
49
|
+
config: CognitiveOrchestratorConfig
|
|
50
|
+
){
|
|
51
|
+
super( clock, eventBus, stateManager, config )
|
|
52
|
+
|
|
53
|
+
this._bus = config.cognitiveBus ?? createProductionBus()
|
|
54
|
+
this._heartbeat = new HeartbeatPublisher( this._bus )
|
|
55
|
+
this._inbox = new CompletionInbox()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The tick-boundary landing queue for async completion effects. */
|
|
59
|
+
get completionInbox(): CompletionInbox { return this._inbox }
|
|
60
|
+
|
|
61
|
+
// ── Phase 2 hook ─────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
protected override async _onAfterPhase1(
|
|
64
|
+
tick: Tick,
|
|
65
|
+
state: ReadonlySimulationState,
|
|
66
|
+
_ctx: SimulationContext
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
// Pass 0: land async completion effects (facet decisions staged at LLM-
|
|
69
|
+
// promise resolution). FIRST — so listener effects (plan mutations, outbox
|
|
70
|
+
// writes) and any bus events those listeners publish deliver in THIS phase,
|
|
71
|
+
// keeping each decision's landing atomic at one tick boundary.
|
|
72
|
+
this._inbox.drain( tick )
|
|
73
|
+
|
|
74
|
+
// Pass 1: deliver events engines queued during react()
|
|
75
|
+
this._bus.flush()
|
|
76
|
+
|
|
77
|
+
// Publish clock.tick (adaptive — may be a no-op during quiet periods)
|
|
78
|
+
this._heartbeat.publishTick( tick, state )
|
|
79
|
+
|
|
80
|
+
// Pass 2: deliver clock.tick to subscribers
|
|
81
|
+
this._bus.flush()
|
|
82
|
+
|
|
83
|
+
// Phase 2 commit: apply all StateCommands returned by onCognitiveEvent() handlers
|
|
84
|
+
for( const commands of this._bus.drainCommands() )
|
|
85
|
+
this._stateManager.applyCommands( commands )
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected override _enginesTick(): SimulationEngine[] {
|
|
89
|
+
return this._engines.filter( e => e.react !== undefined )
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Engine wiring ────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
override addEngine( engine: SimulationEngine ): void {
|
|
95
|
+
super.addEngine( engine )
|
|
96
|
+
|
|
97
|
+
// Phase F: give the engine a direct bus reference for publishing
|
|
98
|
+
if( 'attachBus' in engine && typeof ( engine as any ).attachBus === 'function' ){
|
|
99
|
+
( engine as any ).attachBus( this._bus )
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Tick-boundary landing: engines that stage async completion effects
|
|
103
|
+
// (the ExecutiveEngine, for its facets) receive the shared inbox.
|
|
104
|
+
if( 'attachCompletionInbox' in engine && typeof ( engine as any ).attachCompletionInbox === 'function' ){
|
|
105
|
+
( engine as any ).attachCompletionInbox( this._inbox )
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Wire delivery: subscribe the engine to its declared topics
|
|
109
|
+
if( isCognitiveEngine( engine ) ){
|
|
110
|
+
const topics = engine.subscribes()
|
|
111
|
+
if( topics.length > 0 ){
|
|
112
|
+
const acceptsFn = engine.acceptsVersions?.bind( engine )
|
|
113
|
+
this._bus.subscribe( engine.name, topics, ev => engine.onCognitiveEvent( ev ), acceptsFn )
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
override removeEngine( name: string ): boolean {
|
|
119
|
+
const removed = super.removeEngine( name )
|
|
120
|
+
if( removed ) this._bus.unsubscribe( name )
|
|
121
|
+
|
|
122
|
+
return removed
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Cold-start snapshot ──────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
override async start( context: SimulationContext ): Promise<void> {
|
|
128
|
+
for( const engine of this._engines )
|
|
129
|
+
if( isCognitiveEngine( engine ) )
|
|
130
|
+
this._bus.publish({
|
|
131
|
+
type: 'engine.snapshot',
|
|
132
|
+
version: 1,
|
|
133
|
+
sourceEngine: engine.name,
|
|
134
|
+
salience: 0,
|
|
135
|
+
payload: engine.snapshot()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
this._bus.flush()
|
|
139
|
+
|
|
140
|
+
await super.start( context )
|
|
141
|
+
}
|
|
142
|
+
}
|