@mindot/will 0.8.0 → 0.10.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/README.md +15 -11
- package/dist/index.d.ts +188 -141
- package/dist/index.js +6985 -3337
- package/dist/index.js.map +1 -1
- package/dist/surface/channels/discord.d.ts +177 -0
- package/dist/surface/channels/discord.js +394 -0
- package/dist/surface/channels/discord.js.map +1 -0
- package/dist/{channels → surface/channels}/whatsapp.d.ts +2 -2
- package/dist/{channels → surface/channels}/whatsapp.js +11 -5
- package/dist/surface/channels/whatsapp.js.map +1 -0
- package/dist/{cli.js → surface/cli.js} +5382 -1573
- package/dist/surface/cli.js.map +1 -0
- package/dist/{mcp → surface/mcp}/effectors.d.ts +1 -1
- package/dist/{mcp → surface/mcp}/effectors.js +10 -4
- package/dist/surface/mcp/effectors.js.map +1 -0
- package/dist/{will-cS6k4uiJ.d.ts → will-evj9_vrd.d.ts} +6658 -5078
- package/package.json +11 -12
- package/src/cognition/agency/consequence.ts +356 -1
- package/src/cognition/agency/conversation.aim.ts +292 -0
- package/src/cognition/agency/engines/action.selector.ts +142 -5
- package/src/cognition/agency/engines/affordance.synthesizer.ts +200 -7
- package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
- package/src/cognition/agency/engines/motor.schema.executor.ts +509 -32
- package/src/cognition/agency/engines/reafference.engine.ts +160 -4
- package/src/cognition/agency/execution.primitives.ts +17 -4
- package/src/cognition/agency/proactive.communicator.ts +19 -3
- package/src/cognition/agency/reconcile.learning.ts +5 -4
- package/src/cognition/agency/restart.ts +66 -0
- package/src/cognition/agency/schemas/innate.ts +96 -1
- package/src/cognition/agency/schemas/repertoire.ts +19 -1
- package/src/cognition/agency/selection.scoring.ts +49 -0
- package/src/cognition/agency/settlement.ts +203 -0
- package/src/cognition/agency/types.ts +73 -1
- package/src/cognition/cache/composition.ts +232 -0
- package/src/cognition/cache/deliberation.cache.ts +219 -0
- package/src/cognition/cache/fingerprint.ts +120 -0
- package/src/cognition/cache/types.ts +105 -0
- package/src/cognition/config.mirror.entities.ts +108 -1
- package/src/cognition/event.schemas.ts +22 -8
- package/src/cognition/faculties/affective.blender.ts +21 -4
- package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
- package/src/cognition/faculties/circadian.oscillator.ts +36 -7
- package/src/cognition/faculties/episodic.consolidator.ts +59 -3
- package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
- package/src/cognition/faculties/executive.engine/commands.ts +189 -14
- package/src/cognition/faculties/executive.engine/context.ts +106 -46
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
- package/src/cognition/faculties/executive.engine/engine.ts +714 -136
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +230 -45
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +320 -65
- package/src/cognition/faculties/executive.engine/facet.ts +81 -26
- package/src/cognition/faculties/executive.engine/gating.ts +14 -14
- package/src/cognition/faculties/executive.engine/parser.ts +120 -38
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +331 -38
- package/src/cognition/faculties/executive.engine/types.ts +84 -12
- package/src/cognition/faculties/exteroception.ts +99 -98
- package/src/cognition/faculties/goal.manager.ts +144 -16
- package/src/cognition/faculties/known.entity.tracker.ts +287 -29
- package/src/cognition/faculties/moral.evaluator.ts +8 -3
- package/src/cognition/faculties/persona.consolidator.ts +141 -0
- package/src/cognition/faculties/planning.engine/engine.ts +4 -4
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
- package/src/cognition/faculties/reputation.tracker.ts +66 -2
- package/src/cognition/faculties/self.model.updater.ts +19 -12
- package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
- package/src/cognition/faculties/social.perception.ts +47 -3
- package/src/cognition/faculties/threat.evaluator.ts +7 -0
- package/src/cognition/faculties/working.memory.ts +16 -21
- package/src/cognition/identity.entity.ts +205 -0
- package/src/cognition/index.ts +7 -0
- package/src/cognition/instruction.handler.ts +1 -1
- package/src/cognition/memory/vector.adapter.ts +12 -3
- package/src/cognition/memory/vector.embedder.ts +45 -4
- package/src/cognition/percept.entity.ts +122 -0
- package/src/cognition/persona.prior.ts +6 -0
- package/src/cognition/sense.boundary.ts +176 -0
- package/src/cognition/senses/audition.engine/engine.ts +492 -53
- package/src/cognition/senses/base.sense.engine.ts +105 -7
- package/src/cognition/senses/index.ts +83 -13
- package/src/cognition/senses/provenance.ts +128 -0
- package/src/cognition/senses/somatosensation.engine.ts +117 -9
- package/src/cognition/social.identity.ts +296 -0
- package/src/cognition/utilities/token.tracker.ts +94 -10
- package/src/core/orchestrator.ts +51 -0
- package/src/index.ts +39 -5
- package/src/llm/gate.ts +48 -0
- package/src/llm/index.ts +127 -33
- package/src/llm/routing.ts +6 -0
- package/src/llm/summarizer.ts +1 -1
- package/src/llm/wire.contracts.ts +57 -0
- package/src/pma/index.ts +68 -54
- package/src/stem/assembly.audit.ts +1 -0
- package/src/stem/guards/identity.coherence.ts +1 -1
- package/src/stem/index.ts +171 -23
- package/src/stem/mind.ts +198 -57
- package/src/stem/policy/arbiter.ts +10 -0
- package/src/stem/profiles/built-in.ts +7 -0
- package/src/{profiles → stem/profiles}/companion.ts +1 -1
- package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
- package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
- package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
- package/src/{profiles → stem/profiles}/index.ts +1 -1
- package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
- package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
- package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
- package/src/stem/tracts/effector/types.ts +110 -0
- package/src/stem/tracts/effector.controller.ts +112 -401
- package/src/stem/tracts/outbox.controller.ts +30 -17
- package/src/stem/tracts/outbox.writer.ts +40 -2
- package/src/stem/tracts/sensory.controller.ts +6 -6
- package/src/stem/tracts/session.logger.ts +6 -1
- package/src/stem/tracts/transport/types.ts +20 -1
- package/src/stem/tracts/transport.controller.ts +33 -9
- package/src/surface/channels/discord.ts +609 -0
- package/src/{channels → surface/channels}/roster.ts +1 -1
- package/src/surface/channels/types.ts +136 -0
- package/src/{channels → surface/channels}/whatsapp.ts +22 -10
- package/src/{cli.ts → surface/cli.ts} +14 -9
- package/src/{host → surface/host}/boot.ts +3 -3
- package/src/{host → surface/host}/utterances.ts +2 -2
- package/src/{mcp → surface/mcp}/effectors.ts +19 -6
- package/src/{mcp → surface/mcp}/server.ts +19 -5
- package/src/{sdk → surface/sdk}/will.ts +194 -33
- package/src/{serve → surface/serve}/server.ts +11 -4
- package/src/types.ts +24 -3
- package/dist/channels/discord.d.ts +0 -69
- package/dist/channels/discord.js +0 -193
- package/dist/channels/discord.js.map +0 -1
- package/dist/channels/whatsapp.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/mcp/effectors.js.map +0 -1
- package/src/channels/discord.ts +0 -214
- package/src/channels/types.ts +0 -46
- package/src/cognition/faculties/executive.engine/messages.ts +0 -102
- package/src/extensions/livestream.ext.ts +0 -570
- package/src/extensions/time.ext.ts +0 -339
- package/src/profiles/built-in.ts +0 -7
- package/src/runners/coherence.runner.ts +0 -49
- package/src/runners/outreach.runner.ts +0 -155
- package/src/runners/social.runner.ts +0 -179
- package/src/runners/thin-shim.runner.ts +0 -205
- /package/dist/{cli.d.ts → surface/cli.d.ts} +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/cognition/cache/deliberation.cache.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DeliberationCache — deterministic fast-path for executive reasoning.
|
|
7
|
+
*
|
|
8
|
+
* Stores past (fingerprint, output) pairs and composes a new output by
|
|
9
|
+
* interpolating neighbors of the *query* fingerprint. Pure, R2-safe, bounded.
|
|
10
|
+
*
|
|
11
|
+
* Determinism discipline:
|
|
12
|
+
* - Patterns live in a flat array; retrieval is a linear scan (k is small and
|
|
13
|
+
* the array is bounded, so determinism beats micro-optimisation).
|
|
14
|
+
* - The similarity that drives confidence, weighting and competence updates is
|
|
15
|
+
* always computed against the query fingerprint the caller passes in — never
|
|
16
|
+
* a self-similarity proxy.
|
|
17
|
+
* - Sort keys are scalars; ties break on storedAtTick (older first).
|
|
18
|
+
* - No hash maps over fingerprints, no Set iteration, no wall-clock, no RNG.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ExecutiveOutputFull } from '#faculties/executive.engine/types'
|
|
22
|
+
import type { Tick } from '#core/types'
|
|
23
|
+
import type {
|
|
24
|
+
DeliberationCacheConfig,
|
|
25
|
+
CachePattern,
|
|
26
|
+
CacheResult,
|
|
27
|
+
ScoredNeighbor,
|
|
28
|
+
DeliberationCacheSnapshot,
|
|
29
|
+
} from './types'
|
|
30
|
+
import { FINGERPRINT_VERSION, fingerprintSimilarity } from './fingerprint'
|
|
31
|
+
import { composeOutput } from './composition'
|
|
32
|
+
|
|
33
|
+
const DEFAULT_CONFIG: Required<DeliberationCacheConfig> = {
|
|
34
|
+
maxPatterns: 5000,
|
|
35
|
+
k: 5,
|
|
36
|
+
minSimilarity: 0.75,
|
|
37
|
+
theta: 0.70,
|
|
38
|
+
tau: 0.5,
|
|
39
|
+
eta: 0.1,
|
|
40
|
+
decayPerCycle: 0.999,
|
|
41
|
+
verifyEveryNHits: 5,
|
|
42
|
+
scopes: [ 'actions' ],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class DeliberationCache {
|
|
46
|
+
readonly name = 'deliberation-cache'
|
|
47
|
+
|
|
48
|
+
private _patterns: CachePattern[] = []
|
|
49
|
+
private _config: Required<DeliberationCacheConfig>
|
|
50
|
+
private _hitCount = 0
|
|
51
|
+
private _missCount = 0
|
|
52
|
+
private _verifyCounter = 0
|
|
53
|
+
|
|
54
|
+
constructor( config: DeliberationCacheConfig = {} ){
|
|
55
|
+
this._config = { ...DEFAULT_CONFIG, ...config }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get size(): number { return this._patterns.length }
|
|
59
|
+
get hitCount(): number { return this._hitCount }
|
|
60
|
+
get missCount(): number { return this._missCount }
|
|
61
|
+
|
|
62
|
+
// ── Retrieval + composition ──────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Retrieve neighbors of `queryFp` and, if confident, compose an output.
|
|
66
|
+
* Confidence ρ = max over neighbors of (competence × similarity), per the
|
|
67
|
+
* research sketch §2.2 — a diffuse cloud of weak matches never triggers a hit.
|
|
68
|
+
*/
|
|
69
|
+
retrieve( queryFp: Float32Array, _tick: Tick ): CacheResult {
|
|
70
|
+
const neighbors = this._retrieveNeighbors( queryFp )
|
|
71
|
+
if( neighbors.length === 0 ){
|
|
72
|
+
this._missCount++
|
|
73
|
+
return { output: null, confidence: 0, neighbors: [], hit: false }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let confidence = 0
|
|
77
|
+
for( const n of neighbors ){
|
|
78
|
+
const score = n.pattern.competence * n.similarity
|
|
79
|
+
if( score > confidence ) confidence = score
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const hit = confidence >= this._config.theta
|
|
83
|
+
if( hit ) this._hitCount++
|
|
84
|
+
else this._missCount++
|
|
85
|
+
|
|
86
|
+
const output = hit
|
|
87
|
+
? composeOutput( neighbors, this._config.tau, this._config.scopes )
|
|
88
|
+
: null
|
|
89
|
+
|
|
90
|
+
return { output, confidence, neighbors, hit }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Store a new (fingerprint, output) pair from the slow (LLM) path. */
|
|
94
|
+
learn( queryFp: Float32Array, output: ExecutiveOutputFull, tick: Tick ): void {
|
|
95
|
+
this._evictIfFull( tick )
|
|
96
|
+
this._patterns.push( {
|
|
97
|
+
fingerprint: new Float32Array( queryFp ),
|
|
98
|
+
output,
|
|
99
|
+
competence: 0.5,
|
|
100
|
+
storedAtTick: tick,
|
|
101
|
+
retrievalCount: 0,
|
|
102
|
+
successCount: 0,
|
|
103
|
+
} )
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Update the competence of the pattern nearest to `queryFp`, from a reafference
|
|
108
|
+
* reward in [0,1]. Called after an action outcome is confirmed.
|
|
109
|
+
*/
|
|
110
|
+
updateCompetence( queryFp: Float32Array, reward: number, _tick: Tick ): void {
|
|
111
|
+
const best = this._findBestMatch( queryFp )
|
|
112
|
+
if( !best ) return
|
|
113
|
+
const r = Math.max( 0, Math.min( 1, reward ) )
|
|
114
|
+
best.retrievalCount++
|
|
115
|
+
if( r > 0.5 ) best.successCount++
|
|
116
|
+
const a = this._config.eta
|
|
117
|
+
best.competence = Math.max( 0, Math.min( 1, best.competence * ( 1 - a ) + r * a ) )
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Decay all competences one executive cycle. Slowly forgets stale patterns. */
|
|
121
|
+
decay(): void {
|
|
122
|
+
const f = this._config.decayPerCycle
|
|
123
|
+
if( f >= 1 ) return
|
|
124
|
+
for( const p of this._patterns ) p.competence *= f
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Deterministic 1-in-N verify schedule. Increments a counter each call. */
|
|
128
|
+
shouldVerify(): boolean {
|
|
129
|
+
if( this._config.verifyEveryNHits <= 0 ) return false
|
|
130
|
+
this._verifyCounter++
|
|
131
|
+
return this._verifyCounter % this._config.verifyEveryNHits === 0
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Snapshot / restore (entity persistence + tests) ──────
|
|
135
|
+
|
|
136
|
+
snapshot(): DeliberationCacheSnapshot {
|
|
137
|
+
return {
|
|
138
|
+
version: FINGERPRINT_VERSION,
|
|
139
|
+
patterns: this._patterns.map( p => ( {
|
|
140
|
+
fingerprint: Array.from( p.fingerprint ),
|
|
141
|
+
output: p.output,
|
|
142
|
+
competence: p.competence,
|
|
143
|
+
storedAtTick: p.storedAtTick,
|
|
144
|
+
retrievalCount: p.retrievalCount,
|
|
145
|
+
successCount: p.successCount,
|
|
146
|
+
} ) ),
|
|
147
|
+
hitCount: this._hitCount,
|
|
148
|
+
missCount: this._missCount,
|
|
149
|
+
verifyCounter: this._verifyCounter,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
restore( snap: DeliberationCacheSnapshot ): void {
|
|
154
|
+
// Version guard: a fingerprint layout change invalidates stored vectors.
|
|
155
|
+
if( snap.version !== FINGERPRINT_VERSION ) return
|
|
156
|
+
this._patterns = snap.patterns.map( p => ( {
|
|
157
|
+
fingerprint: new Float32Array( p.fingerprint ),
|
|
158
|
+
output: p.output,
|
|
159
|
+
competence: p.competence,
|
|
160
|
+
storedAtTick: p.storedAtTick,
|
|
161
|
+
retrievalCount: p.retrievalCount,
|
|
162
|
+
successCount: p.successCount,
|
|
163
|
+
} ) )
|
|
164
|
+
this._hitCount = snap.hitCount ?? 0
|
|
165
|
+
this._missCount = snap.missCount ?? 0
|
|
166
|
+
this._verifyCounter = snap.verifyCounter ?? 0
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Internal ─────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
private _retrieveNeighbors( queryFp: Float32Array ): ScoredNeighbor[] {
|
|
172
|
+
const scored: ScoredNeighbor[] = []
|
|
173
|
+
for( const p of this._patterns ){
|
|
174
|
+
const similarity = fingerprintSimilarity( queryFp, p.fingerprint )
|
|
175
|
+
if( similarity >= this._config.minSimilarity )
|
|
176
|
+
scored.push( { pattern: p, similarity } )
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Rank by (similarity × competence) desc; ties → older pattern first.
|
|
180
|
+
scored.sort( ( a, b ) => {
|
|
181
|
+
const sa = a.similarity * a.pattern.competence
|
|
182
|
+
const sb = b.similarity * b.pattern.competence
|
|
183
|
+
if( sa !== sb ) return sb - sa
|
|
184
|
+
return a.pattern.storedAtTick - b.pattern.storedAtTick
|
|
185
|
+
} )
|
|
186
|
+
|
|
187
|
+
return scored.slice( 0, this._config.k )
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private _findBestMatch( queryFp: Float32Array ): CachePattern | null {
|
|
191
|
+
let best: CachePattern | null = null
|
|
192
|
+
let bestSim = -1
|
|
193
|
+
for( const p of this._patterns ){
|
|
194
|
+
const sim = fingerprintSimilarity( queryFp, p.fingerprint )
|
|
195
|
+
if( sim > bestSim ){ bestSim = sim; best = p }
|
|
196
|
+
}
|
|
197
|
+
return best
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private _evictIfFull( _tick: Tick ): void {
|
|
201
|
+
if( this._patterns.length < this._config.maxPatterns ) return
|
|
202
|
+
// Evict the LEAST competent pattern; ties break on the oldest (lowest
|
|
203
|
+
// storedAtTick). Age is handled by decay() — stale patterns bleed competence
|
|
204
|
+
// toward 0 each cycle, so competence alone already ranks them lowest. Folding
|
|
205
|
+
// recency INTO the score inverts value: a high-competence *old* pattern would
|
|
206
|
+
// score below a low-competence *new* one and be wrongly evicted.
|
|
207
|
+
let evictIdx = 0
|
|
208
|
+
let best = this._patterns[ 0 ]!
|
|
209
|
+
for( let i = 1; i < this._patterns.length; i++ ){
|
|
210
|
+
const p = this._patterns[ i ]!
|
|
211
|
+
if( p.competence < best.competence ||
|
|
212
|
+
( p.competence === best.competence && p.storedAtTick < best.storedAtTick ) ){
|
|
213
|
+
best = p
|
|
214
|
+
evictIdx = i
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
this._patterns.splice( evictIdx, 1 )
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/cognition/cache/fingerprint.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CognitiveFingerprint — extracts a deterministic scalar vector from the
|
|
7
|
+
* frozen simulation state. No wall-clock, no RNG, no iteration-order
|
|
8
|
+
* dependence (values are sorted before they enter fixed slots). The vector
|
|
9
|
+
* captures the operating context that most strongly conditions executive
|
|
10
|
+
* reasoning — the same scalars the prompt's `## Current State` block renders.
|
|
11
|
+
*
|
|
12
|
+
* Dimension layout (fixed order — never reorder without bumping the version):
|
|
13
|
+
* 0–2 physiology energy.level, sleep.pressure, stress.load (metrics, 0–100 → 0–1)
|
|
14
|
+
* 3–5 PAD affect valence (−1..1 → 0..1), arousal, dominance (metrics, 0–1)
|
|
15
|
+
* 6–15 goal priorities top 10 `goal` entities by metadata.priority (0..1)
|
|
16
|
+
* 16–25 belief confidences top 10 `belief` entities by metadata.confidence (0..1)
|
|
17
|
+
* 26–35 wm activations top 10 `working_memory.item` by metadata.activation (0..1)
|
|
18
|
+
*
|
|
19
|
+
* Total: 36 dimensions. Verified against the live writers:
|
|
20
|
+
* - interoception.ts writes energy.level / sleep.pressure (0–100)
|
|
21
|
+
* - stress.regulator.ts writes stress.load
|
|
22
|
+
* - affective.blender.ts writes affect.valence (−1..1) / arousal / dominance (0..1)
|
|
23
|
+
* - goal.manager.ts persists goal entities with metadata.priority
|
|
24
|
+
* - executive.engine/commands.ts + semantic integrator persist belief entities
|
|
25
|
+
* with metadata.confidence
|
|
26
|
+
* - working.memory.ts persists working_memory.item entities with metadata.activation
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { ReadonlySimulationState } from '#core/types'
|
|
30
|
+
|
|
31
|
+
export const FINGERPRINT_DIM = 36
|
|
32
|
+
export const FINGERPRINT_VERSION = 1
|
|
33
|
+
|
|
34
|
+
export function extractFingerprint( state: ReadonlySimulationState ): Float32Array {
|
|
35
|
+
const vec = new Float32Array( FINGERPRINT_DIM )
|
|
36
|
+
let idx = 0
|
|
37
|
+
|
|
38
|
+
// ── Physiology (metrics are 0–100) ───────────────────────
|
|
39
|
+
vec[ idx++ ] = _norm( _metric( state, 'energy.level', 50 ), 0, 100 )
|
|
40
|
+
vec[ idx++ ] = _norm( _metric( state, 'sleep.pressure', 0 ), 0, 100 )
|
|
41
|
+
vec[ idx++ ] = _norm( _metric( state, 'stress.load', 0 ), 0, 100 )
|
|
42
|
+
|
|
43
|
+
// ── PAD affect ───────────────────────────────────────────
|
|
44
|
+
// valence is written on [-1, 1]; map to [0, 1] so a distressed state is not
|
|
45
|
+
// silently clamped to neutral. arousal / dominance are already [0, 1].
|
|
46
|
+
vec[ idx++ ] = _clamp01( ( _metric( state, 'affect.valence', 0 ) + 1 ) / 2 )
|
|
47
|
+
vec[ idx++ ] = _clamp01( _metric( state, 'affect.arousal', 0.3 ) )
|
|
48
|
+
vec[ idx++ ] = _clamp01( _metric( state, 'affect.dominance', 0.5 ) )
|
|
49
|
+
|
|
50
|
+
// ── Top-10 goal priorities ───────────────────────────────
|
|
51
|
+
idx = _topEntityScalars( state, 'goal', 'priority', 0, vec, idx, 10 )
|
|
52
|
+
|
|
53
|
+
// ── Top-10 belief confidences ────────────────────────────
|
|
54
|
+
idx = _topEntityScalars( state, 'belief', 'confidence', 0.5, vec, idx, 10 )
|
|
55
|
+
|
|
56
|
+
// ── Top-10 working-memory activations ────────────────────
|
|
57
|
+
idx = _topEntityScalars( state, 'working_memory.item', 'activation', 0, vec, idx, 10 )
|
|
58
|
+
|
|
59
|
+
// Defensive: zero-fill any remaining slots (never expected).
|
|
60
|
+
while( idx < FINGERPRINT_DIM ) vec[ idx++ ] = 0
|
|
61
|
+
|
|
62
|
+
return vec
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Collect a metadata scalar from every entity of `type`, sort descending,
|
|
67
|
+
* and write the top `count` (clamped to [0,1]) into `vec` starting at `idx`.
|
|
68
|
+
* Sorting makes the slice order deterministic regardless of Map iteration.
|
|
69
|
+
* Returns the next free index.
|
|
70
|
+
*/
|
|
71
|
+
function _topEntityScalars(
|
|
72
|
+
state: ReadonlySimulationState,
|
|
73
|
+
type: string,
|
|
74
|
+
field: string,
|
|
75
|
+
fallback: number,
|
|
76
|
+
vec: Float32Array,
|
|
77
|
+
idx: number,
|
|
78
|
+
count: number,
|
|
79
|
+
): number {
|
|
80
|
+
const vals: number[] = []
|
|
81
|
+
for( const e of state.entities.values() ){
|
|
82
|
+
if( e.type !== type ) continue
|
|
83
|
+
const raw = e.metadata?.[ field ]
|
|
84
|
+
vals.push( typeof raw === 'number' ? raw : fallback )
|
|
85
|
+
}
|
|
86
|
+
vals.sort( ( a, b ) => b - a )
|
|
87
|
+
for( let i = 0; i < count; i++ )
|
|
88
|
+
vec[ idx++ ] = _clamp01( vals[ i ] ?? 0 )
|
|
89
|
+
return idx
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _metric( state: ReadonlySimulationState, key: string, fallback: number ): number {
|
|
93
|
+
const v = state.metrics.get( key )
|
|
94
|
+
return typeof v === 'number' ? v : fallback
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Normalise a value from [min,max] → [0,1]. */
|
|
98
|
+
function _norm( v: number, min: number, max: number ): number {
|
|
99
|
+
if( max === min ) return 0.5
|
|
100
|
+
return Math.max( 0, Math.min( 1, ( v - min ) / ( max - min ) ) )
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _clamp01( v: number ): number {
|
|
104
|
+
if( Number.isNaN( v ) ) return 0
|
|
105
|
+
return Math.max( 0, Math.min( 1, v ) )
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Cosine similarity between two fingerprint vectors. */
|
|
109
|
+
export function fingerprintSimilarity( a: Float32Array, b: Float32Array ): number {
|
|
110
|
+
let dot = 0, na = 0, nb = 0
|
|
111
|
+
for( let i = 0; i < FINGERPRINT_DIM; i++ ){
|
|
112
|
+
const ai = a[ i ] ?? 0
|
|
113
|
+
const bi = b[ i ] ?? 0
|
|
114
|
+
dot += ai * bi
|
|
115
|
+
na += ai * ai
|
|
116
|
+
nb += bi * bi
|
|
117
|
+
}
|
|
118
|
+
const denom = Math.sqrt( na ) * Math.sqrt( nb )
|
|
119
|
+
return denom === 0 ? 0 : dot / denom
|
|
120
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/cognition/cache/types.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DeliberationCache — types and contracts.
|
|
7
|
+
*
|
|
8
|
+
* The cache stores past executive outputs keyed by a deterministic
|
|
9
|
+
* cognitive fingerprint. It is pure, deterministic, and R2-safe:
|
|
10
|
+
* the same state + same history ⇒ same retrieval + same composition.
|
|
11
|
+
*
|
|
12
|
+
* Scope note: Phase 1 caches the ACTIONS block only. The composed output
|
|
13
|
+
* is a valid `ExecutiveOutputFull` carrying the three required fields
|
|
14
|
+
* (actions, reasoning, confidence) plus whatever optional blocks the
|
|
15
|
+
* enabled scopes cover. Everything else stays undefined and the existing
|
|
16
|
+
* downstream (`buildStateCommands`) treats it as "nothing to do", which is
|
|
17
|
+
* exactly the intended Phase-1 behaviour.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { ExecutiveOutputFull } from '#faculties/executive.engine/types'
|
|
21
|
+
|
|
22
|
+
/** Which blocks of the executive output the cache may synthesise. */
|
|
23
|
+
export type CacheScope =
|
|
24
|
+
| 'actions'
|
|
25
|
+
| 'goals'
|
|
26
|
+
| 'beliefs'
|
|
27
|
+
|
|
28
|
+
export interface DeliberationCacheConfig {
|
|
29
|
+
/** Maximum patterns to retain. Lowest (competence × recency) evicted when full. */
|
|
30
|
+
maxPatterns?: number
|
|
31
|
+
/** Neighbors retrieved for composition. */
|
|
32
|
+
k?: number
|
|
33
|
+
/** Minimum similarity for a stored pattern to count as a neighbor. */
|
|
34
|
+
minSimilarity?: number
|
|
35
|
+
/** Confidence threshold θ — cache hit requires ρ ≥ θ. Start conservative. */
|
|
36
|
+
theta?: number
|
|
37
|
+
/** Temperature for softmax weights over neighbors. */
|
|
38
|
+
tau?: number
|
|
39
|
+
/** Learning rate (EMA) for competence updates. */
|
|
40
|
+
eta?: number
|
|
41
|
+
/** Competence decay per executive cycle (applied via decay()). */
|
|
42
|
+
decayPerCycle?: number
|
|
43
|
+
/** Verify 1-in-N cache hits against the LLM (0 = never). */
|
|
44
|
+
verifyEveryNHits?: number
|
|
45
|
+
/** Which output blocks to synthesise. Phase 1 default: ['actions']. */
|
|
46
|
+
scopes?: CacheScope[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A stored pattern in the cache. */
|
|
50
|
+
export interface CachePattern {
|
|
51
|
+
/** Deterministic fingerprint vector (length FINGERPRINT_DIM). */
|
|
52
|
+
fingerprint: Float32Array
|
|
53
|
+
/** The executive output produced for this fingerprint (verbatim from the LLM). */
|
|
54
|
+
output: ExecutiveOutputFull
|
|
55
|
+
/** Competence score (0–1), learned from reafference. */
|
|
56
|
+
competence: number
|
|
57
|
+
/** Tick when stored. */
|
|
58
|
+
storedAtTick: number
|
|
59
|
+
/** Number of times this pattern won a retrieval. */
|
|
60
|
+
retrievalCount: number
|
|
61
|
+
/** Number of retrievals that were followed by a positive reafference. */
|
|
62
|
+
successCount: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A neighbor plus its similarity to the query fingerprint. */
|
|
66
|
+
export interface ScoredNeighbor {
|
|
67
|
+
pattern: CachePattern
|
|
68
|
+
/** Cosine similarity of this pattern's fingerprint to the query. */
|
|
69
|
+
similarity: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Result of a cache retrieve (+ compose when hit). */
|
|
73
|
+
export interface CacheResult {
|
|
74
|
+
/** The composed output — only meaningful when `hit` is true. */
|
|
75
|
+
output: ExecutiveOutputFull | null
|
|
76
|
+
/** Confidence ρ = max_i (competence_i × sim_i). Used for gating. */
|
|
77
|
+
confidence: number
|
|
78
|
+
/** The scored neighbors that were considered. */
|
|
79
|
+
neighbors: ScoredNeighbor[]
|
|
80
|
+
/** Whether this was a cache hit (confidence ≥ θ). */
|
|
81
|
+
hit: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Serialisable snapshot for deterministic persistence / restore.
|
|
86
|
+
*
|
|
87
|
+
* Persistence follows the same pattern as the rolling summarizer: the engine
|
|
88
|
+
* writes this into a state entity (`executive-deliberation-cache`) via
|
|
89
|
+
* StateCommands and rehydrates it on the first tick. Float arrays are stored
|
|
90
|
+
* as plain number[] so they survive JSON round-tripping through state.
|
|
91
|
+
*/
|
|
92
|
+
export interface DeliberationCacheSnapshot {
|
|
93
|
+
version: number
|
|
94
|
+
patterns: Array<{
|
|
95
|
+
fingerprint: number[]
|
|
96
|
+
output: ExecutiveOutputFull
|
|
97
|
+
competence: number
|
|
98
|
+
storedAtTick: number
|
|
99
|
+
retrievalCount: number
|
|
100
|
+
successCount: number
|
|
101
|
+
}>
|
|
102
|
+
hitCount: number
|
|
103
|
+
missCount: number
|
|
104
|
+
verifyCounter: number
|
|
105
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// reason about its own operational parameters.
|
|
14
14
|
|
|
15
15
|
import { WillConfig } from '#stem/mind'
|
|
16
|
+
import type { StateManager } from '#core/state.manager'
|
|
16
17
|
|
|
17
18
|
export interface EngineConfigEntity {
|
|
18
19
|
id: string
|
|
@@ -102,7 +103,6 @@ export function buildEngineConfigEntities( config: WillConfig, executiveInterval
|
|
|
102
103
|
params: {
|
|
103
104
|
maxPerceptsPerTick: 50,
|
|
104
105
|
defaultSalience: 0.3,
|
|
105
|
-
emitPerceptEvents: 1,
|
|
106
106
|
},
|
|
107
107
|
},
|
|
108
108
|
{
|
|
@@ -316,6 +316,24 @@ export function buildEngineConfigEntities( config: WillConfig, executiveInterval
|
|
|
316
316
|
// DOWN from demonstrated `analytical` disposition via the persona-prior mirror,
|
|
317
317
|
// so a more analytical Will deliberates more readily; this is the baseline.
|
|
318
318
|
deliberateThreshold: 0.5,
|
|
319
|
+
// How many focused facets this Will can hold at once before spawning starts
|
|
320
|
+
// evicting (FacetSupervisor). A structural ceiling, not the live budget:
|
|
321
|
+
// attention scales the allowance *within* it each tick, so a tired or loaded
|
|
322
|
+
// mind narrows on its own. The metacog loop develops it via the persona-prior
|
|
323
|
+
// (openness widens, conscientiousness narrows), which is what makes "how many
|
|
324
|
+
// things I can hold at once" a property of this person rather than a constant.
|
|
325
|
+
maxFacets: 10,
|
|
326
|
+
// How long a QUIET thread stays open before the mind considers it finished
|
|
327
|
+
// (FacetSupervisor idle reaper). The sibling of maxFacets — that one is how
|
|
328
|
+
// many threads at once, this one is how long each survives a silence — and
|
|
329
|
+
// it was the only number in the economy no personality could move.
|
|
330
|
+
//
|
|
331
|
+
// ~30 minutes at a typical tick rate. It was hardcoded at 50 ticks, which is
|
|
332
|
+
// THIRTY SECONDS: every pause longer than a person taking a moment to type
|
|
333
|
+
// destroyed the conversation, and the reply landed on a facet that had never
|
|
334
|
+
// heard of them. Generous is safe — maxFacets + eviction bound the population;
|
|
335
|
+
// this only decides when silence means "over".
|
|
336
|
+
facetIdleTtlTicks: 3000,
|
|
319
337
|
},
|
|
320
338
|
},
|
|
321
339
|
{
|
|
@@ -366,6 +384,35 @@ export function buildEngineConfigEntities( config: WillConfig, executiveInterval
|
|
|
366
384
|
switchCost: 0.15,
|
|
367
385
|
riskWeight: 0.20,
|
|
368
386
|
noveltyWeight: 0.10,
|
|
387
|
+
// How hard an act's own live footprint damps doing it again (EXAFFERENCE
|
|
388
|
+
// P5) — how long this mind sits with something it has already said before
|
|
389
|
+
// saying it again. Agreeableness develops it up, demonstrated persistence
|
|
390
|
+
// down, so "gives people room" vs "chases an answer" is a trait rather
|
|
391
|
+
// than a constant.
|
|
392
|
+
repeatDamping: 0.30,
|
|
393
|
+
// Ticks an act keeps satiating the urge to repeat it. Separate from the
|
|
394
|
+
// consequence TTL on purpose: that one is "how long until the world's echo
|
|
395
|
+
// could still arrive" (short, and about perception), this is "how long
|
|
396
|
+
// before saying it again feels right" (a disposition). Same two traits as
|
|
397
|
+
// repeatDamping move it — patience lengthens, persistence shortens.
|
|
398
|
+
repeatWindowTicks: 60,
|
|
399
|
+
// Ticks before a silence starts to mean something — how long this mind
|
|
400
|
+
// gives someone to get back to it before it counts the turn unanswered
|
|
401
|
+
// and learns from that (conversation.aim / ReafferenceEngine).
|
|
402
|
+
//
|
|
403
|
+
// Lives beside repeatWindowTicks rather than in a config of its own
|
|
404
|
+
// because they are two readings of ONE disposition, and splitting them
|
|
405
|
+
// would let a mind tune itself into contradiction — coming back to
|
|
406
|
+
// something in 20 ticks while still calling the silence too fresh to
|
|
407
|
+
// count. Long relative to its neighbours by design: repeatWindowTicks
|
|
408
|
+
// asks "how long before saying it again feels right", this asks "how long
|
|
409
|
+
// before I take not hearing back as information", and at 1s/tick that is
|
|
410
|
+
// four minutes of a real person's time, not one.
|
|
411
|
+
replyWindowTicks: 240,
|
|
412
|
+
// How much a learned read on someone biases acting toward them. SIGNED and
|
|
413
|
+
// unclamped: a warm mind leans toward whoever answers, a dogged one chases
|
|
414
|
+
// the silence. The container will not choose between those.
|
|
415
|
+
socialWeight: 0.30,
|
|
369
416
|
},
|
|
370
417
|
},
|
|
371
418
|
|
|
@@ -473,3 +520,63 @@ export function buildEngineConfigEntities( config: WillConfig, executiveInterval
|
|
|
473
520
|
},
|
|
474
521
|
]
|
|
475
522
|
}
|
|
523
|
+
|
|
524
|
+
// ── The single writer ─────────────────────────────────────────
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Write an `engine.config` entity — MERGING, always. The only sanctioned way to
|
|
528
|
+
* write one; `tests/unit/config.mirror.writer.test.ts` fails on a raw
|
|
529
|
+
* `setEntity({ type: 'engine.config' })` anywhere else.
|
|
530
|
+
*
|
|
531
|
+
* Every whole-entity write to one of these has silently dropped params, three
|
|
532
|
+
* times in one day and each in a different place:
|
|
533
|
+
*
|
|
534
|
+
* • PMALoader replaced `engine-config-executive` with the three behavioural
|
|
535
|
+
* params a PMA carries, dropping `deliberateThreshold` — so `readBaseParams`
|
|
536
|
+
* returned nothing for it and `consolidatePrior` skipped the analytical and
|
|
537
|
+
* decisiveness edges outright, for every Will ever restored from an artifact.
|
|
538
|
+
* • The same loader dropped `emitBlendEvents` from the blender and three params
|
|
539
|
+
* from forgetting.
|
|
540
|
+
* • Snapshot restore replaced the whole mirror, so a Will woke with the config
|
|
541
|
+
* it FIRST hibernated under and could never receive a param added later —
|
|
542
|
+
* `maxFacets` and `deliberateThreshold` were inert on a live Will for its
|
|
543
|
+
* entire life.
|
|
544
|
+
*
|
|
545
|
+
* `precedence` says which side wins on a key both hold. Neither ever drops a key.
|
|
546
|
+
*
|
|
547
|
+
* 'incoming' — the caller is the authority (boot seed; a PMA supplying the
|
|
548
|
+
* tenant's own dispositions). Keys it does not mention survive.
|
|
549
|
+
* 'existing' — state is the authority (post-restore backfill). Only genuinely
|
|
550
|
+
* missing keys are added, so learned and PMA'd values are safe.
|
|
551
|
+
*
|
|
552
|
+
* Returns the keys it actually added or changed, for the caller to log.
|
|
553
|
+
*/
|
|
554
|
+
export function mergeEngineConfig(
|
|
555
|
+
store: StateManager,
|
|
556
|
+
cfg: EngineConfigEntity,
|
|
557
|
+
precedence: 'incoming' | 'existing' = 'incoming',
|
|
558
|
+
): string[] {
|
|
559
|
+
const existing = store.getEntity( cfg.id )
|
|
560
|
+
const current = ( existing?.metadata as { params?: Record<string, unknown> } | undefined )?.params ?? {}
|
|
561
|
+
|
|
562
|
+
const params = precedence === 'existing'
|
|
563
|
+
? { ...cfg.params, ...current } // state wins; fills only what is missing
|
|
564
|
+
: { ...current, ...cfg.params } // caller wins; keeps everything else
|
|
565
|
+
|
|
566
|
+
const changed = Object.keys( params ).filter( k => params[ k ] !== current[ k ] )
|
|
567
|
+
if( existing && changed.length === 0 ) return []
|
|
568
|
+
|
|
569
|
+
// No timestamps: StateManager.setEntity is the single place they are stamped,
|
|
570
|
+
// and it sources them from the SIM clock so entity times replay identically
|
|
571
|
+
// (R2). It also preserves an existing `createdAt`. The write sites this
|
|
572
|
+
// replaced all passed `Date.now()`, which was both redundant and a real
|
|
573
|
+
// determinism hole — the guard test caught it the moment the code moved into
|
|
574
|
+
// `cognition/`, where wall-clock reads are banned.
|
|
575
|
+
store.setEntity({
|
|
576
|
+
id: cfg.id,
|
|
577
|
+
type: 'engine.config',
|
|
578
|
+
metadata: { engine: cfg.engine, params },
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
return changed
|
|
582
|
+
}
|
|
@@ -74,14 +74,6 @@ globalSchemaRegistry.register({
|
|
|
74
74
|
},
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
-
globalSchemaRegistry.register({
|
|
78
|
-
type: 'percept.batch.ingested', version: 1,
|
|
79
|
-
validate( p ){
|
|
80
|
-
if( !isObj(p) ) return 'payload must be object'
|
|
81
|
-
return hasNum( p, 'count')
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
|
|
85
77
|
globalSchemaRegistry.register({
|
|
86
78
|
type: 'social.agents.present', version: 1,
|
|
87
79
|
validate( p ){
|
|
@@ -566,3 +558,25 @@ globalSchemaRegistry.register({
|
|
|
566
558
|
return hasNum( p, 'count')
|
|
567
559
|
},
|
|
568
560
|
})
|
|
561
|
+
|
|
562
|
+
// ── Deliberation cache (fast-path telemetry) ─────────────────
|
|
563
|
+
// Published from the ExecutiveEngine's committed path (onReasoningComplete),
|
|
564
|
+
// never from inside the pure cache. Lets faculties like the PersonaConsolidator
|
|
565
|
+
// react to how automatic the Will is becoming (e.g. a high hit rate could lower
|
|
566
|
+
// the deliberate-effort threshold).
|
|
567
|
+
|
|
568
|
+
globalSchemaRegistry.register({
|
|
569
|
+
type: 'cache.hit', version: 1,
|
|
570
|
+
validate( p ){
|
|
571
|
+
if( !isObj(p) ) return 'payload must be object'
|
|
572
|
+
return hasNum( p, 'confidence') ?? hasNum( p, 'neighborCount')
|
|
573
|
+
},
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
globalSchemaRegistry.register({
|
|
577
|
+
type: 'cache.miss', version: 1,
|
|
578
|
+
validate( p ){
|
|
579
|
+
if( !isObj(p) ) return 'payload must be object'
|
|
580
|
+
return hasNum( p, 'confidence')
|
|
581
|
+
},
|
|
582
|
+
})
|
|
@@ -47,6 +47,21 @@ const ACP_CONFIDENCE = 0.4
|
|
|
47
47
|
* Numeric encoding for dominant emotion metric.
|
|
48
48
|
* Consumers decode using DOMINANT_EMOTION_LABELS[code].
|
|
49
49
|
*/
|
|
50
|
+
/**
|
|
51
|
+
* The entity carrying the non-numeric side of affect — what the mind is feeling,
|
|
52
|
+
* by name.
|
|
53
|
+
*
|
|
54
|
+
* Exported as a CONSTANT because the id is a contract between two files and it
|
|
55
|
+
* silently broke: `context.ts` read `state.entities.get('affective-state')`, an
|
|
56
|
+
* id nothing has ever written, so `dominantEmotion` fell back to 'neutral' in
|
|
57
|
+
* every prompt ever rendered. Measured live: a COO sitting at frustration 1.000
|
|
58
|
+
* and boredom 1.000 was told "Dominant emotion: neutral".
|
|
59
|
+
*
|
|
60
|
+
* Two string literals in two files cannot disagree if there is only one.
|
|
61
|
+
*/
|
|
62
|
+
export const AFFECT_STATE_ID = 'affect-blends'
|
|
63
|
+
export const AFFECT_STATE_TYPE = 'affect.blends'
|
|
64
|
+
|
|
50
65
|
const DOMINANT_EMOTION_CODES: Record<string, number> = {
|
|
51
66
|
'neutral': 0,
|
|
52
67
|
'fear': 1,
|
|
@@ -381,11 +396,13 @@ export class AffectiveBlender implements SimulationEngine, CognitiveEngine {
|
|
|
381
396
|
const dominantCode = DOMINANT_EMOTION_CODES[ dominant ?? 'neutral' ] ?? 0
|
|
382
397
|
commands.metrics!.push([ 'affect.dominant_emotion', dominantCode ])
|
|
383
398
|
|
|
384
|
-
//
|
|
399
|
+
// The mind's own read on what it is feeling, as an entity the executive can
|
|
400
|
+
// render. `dominantEmotion` is written as the LABEL, not the code: the code
|
|
401
|
+
// goes to metrics for telemetry, and a prompt cannot say "I feel 20".
|
|
385
402
|
commands.set!.push({
|
|
386
|
-
id:
|
|
387
|
-
type:
|
|
388
|
-
metadata: { blends },
|
|
403
|
+
id: AFFECT_STATE_ID,
|
|
404
|
+
type: AFFECT_STATE_TYPE,
|
|
405
|
+
metadata: { dominantEmotion: dominant ?? 'neutral', blends },
|
|
389
406
|
})
|
|
390
407
|
|
|
391
408
|
// 8. Track emotion history for blending detection
|