@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,439 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/cognition/faculties/known.entity.tracker.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* KnownEntityTracker — the cross-modal binder, and owner of the known-entity dossier.
|
|
7
|
+
*
|
|
8
|
+
* This is the faculty `base.sense.engine.ts` anticipates: it subscribes to every
|
|
9
|
+
* `senses.*.percept` and, per `sourceEntityId`, accretes a dossier — the node for anything
|
|
10
|
+
* the Will has come to know (someone or something). It maintains the *perceptual /
|
|
11
|
+
* subconscious* layer of that knowledge:
|
|
12
|
+
*
|
|
13
|
+
* - familiarity — mere-exposure: rises with each encounter, decays in absence
|
|
14
|
+
* - encounterCount, lastSeen
|
|
15
|
+
* - name — when the channel supplies one (e.g. TextMessage.speakerName)
|
|
16
|
+
* - kind — sentient | thing
|
|
17
|
+
* - resolutionConfidence — how identified this referent is (drives curiosity, Phase 3)
|
|
18
|
+
*
|
|
19
|
+
* The *conscious* layer (facts learned in reasoning, a felt valence) is written separately
|
|
20
|
+
* via `knownEntityUpdates` (Phase 2.2). The executive joins both, plus the social triple,
|
|
21
|
+
* in `extractKnownEntities`. Identity is provisional: the `keid` is the referent the
|
|
22
|
+
* senses supply — a dossier can exist long before the Will knows the entity's name.
|
|
23
|
+
*
|
|
24
|
+
* Part of the Social/Perceptual layer — runs every tick, synchronous, deterministic (R2):
|
|
25
|
+
* all accretion/decay is a pure function of percepts + sim-tick.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type {
|
|
29
|
+
Duration,
|
|
30
|
+
Tick,
|
|
31
|
+
SimulationContext,
|
|
32
|
+
ReadonlySimulationState,
|
|
33
|
+
StateCommands,
|
|
34
|
+
} from '#core/types'
|
|
35
|
+
import type { SimulationEngine, EngineResult, CognitiveEngine } from '#cognition/types'
|
|
36
|
+
import type { CognitiveEventSchema } from '#cognition/schema.registry'
|
|
37
|
+
import type { CognitiveEvent, CognitiveBus } from '#cognition/bus'
|
|
38
|
+
import type { Percept } from '#senses/index'
|
|
39
|
+
import { GenerativeModel } from '#cognition/generative.model'
|
|
40
|
+
import { readEffectiveParams } from '#cognition/persona.prior'
|
|
41
|
+
|
|
42
|
+
export interface KnownEntityTrackerConfig {
|
|
43
|
+
/** How fast familiarity rises per encounter (saturating toward 1). Channel A: openness. */
|
|
44
|
+
familiarityGrowthRate?: number
|
|
45
|
+
/** How fast familiarity fades per tick without an encounter. */
|
|
46
|
+
familiarityDecayRate?: number
|
|
47
|
+
/** Gain on the curiosity-to-resolve drive — how readily the pull-to-know fires. Channel A: openness. */
|
|
48
|
+
curiosityGain?: number
|
|
49
|
+
/** EMA weight per action outcome — how fast a reliability judgment is revised. Channel A: analytical. */
|
|
50
|
+
reliabilityRate?: number
|
|
51
|
+
/** Maximum dossiers retained (lowest familiarity pruned). */
|
|
52
|
+
maxTracked?: number
|
|
53
|
+
bus?: CognitiveBus
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface KnownEntity {
|
|
57
|
+
keid: string
|
|
58
|
+
kind: 'sentient' | 'thing'
|
|
59
|
+
name?: string
|
|
60
|
+
/** Mere-exposure familiarity 0–1 — rises per encounter, decays in absence. */
|
|
61
|
+
familiarity: number
|
|
62
|
+
/** Felt affective tone toward the entity (−1..1). Set by the conscious layer (2.2). */
|
|
63
|
+
valence: number
|
|
64
|
+
/**
|
|
65
|
+
* Track-record reliability 0–1 — does it perform/behave as expected? An EMA of action
|
|
66
|
+
* outcomes targeting this entity. General (a car/tool/place/person can have one), distinct
|
|
67
|
+
* from a sentient's *social* reputation. 0.5 = unknown.
|
|
68
|
+
*/
|
|
69
|
+
reliability: number
|
|
70
|
+
encounterCount: number
|
|
71
|
+
lastSeenTick: Tick
|
|
72
|
+
/** 0–1: how identified/coherent this referent is (a name + repeated encounters raise it). */
|
|
73
|
+
resolutionConfidence: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Percept domains whose entities are minds. Everything else defaults to a thing. */
|
|
77
|
+
const SENTIENT_DOMAINS = new Set<string>([ 'audition' ])
|
|
78
|
+
|
|
79
|
+
// Per-entity curiosity (3.b): a *familiar* (≥) yet *unresolved* (<) referent earns a
|
|
80
|
+
// specific "get to know them" pull; it subsides once resolution reaches RESOLVED.
|
|
81
|
+
const CURIOUS_FAMILIARITY = 0.5
|
|
82
|
+
const CURIOUS_RESOLUTION = 0.4
|
|
83
|
+
const CURIOUS_RESOLVED = 0.6
|
|
84
|
+
|
|
85
|
+
// Forgetting (Phase 4). (Reliability rate is now a Channel-A developable field — analytical.)
|
|
86
|
+
const FORGET_FLOOR = 0.02 // below this familiarity, an unidentified blip is forgotten
|
|
87
|
+
|
|
88
|
+
// Recognition (Phase 5) — guards against conflating two *different* people who share a
|
|
89
|
+
// name. Only recognise a still-thin handle (≤) into a known person, and never when both
|
|
90
|
+
// were active at the same time (two people talking at once ⇒ distinct, not one on two
|
|
91
|
+
// handles). Better to keep two records of one person (harmless) than fuse two people.
|
|
92
|
+
const RECOGNITION_MERGE_MAX_ENCOUNTERS = 8
|
|
93
|
+
const RECOGNITION_CONCURRENCY_WINDOW = 20
|
|
94
|
+
|
|
95
|
+
export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
|
|
96
|
+
readonly name = 'known-entity-tracker'
|
|
97
|
+
|
|
98
|
+
private _growthRate: number
|
|
99
|
+
private _decayRate: number
|
|
100
|
+
private _curiosityGain: number
|
|
101
|
+
private _reliabilityRate: number
|
|
102
|
+
private _maxTracked: number
|
|
103
|
+
|
|
104
|
+
private _dossiers = new Map<string, KnownEntity>()
|
|
105
|
+
// Recognition (Phase 5): alias keid → the canonical keid it was fused into. Incoming
|
|
106
|
+
// references are redirected so an aliased referent never re-forms its own dossier.
|
|
107
|
+
private _aliases = new Map<string, string>()
|
|
108
|
+
/** True after dossiers have been rehydrated from persisted state on first tick. */
|
|
109
|
+
private _restored = false
|
|
110
|
+
|
|
111
|
+
// Buffered from senses.*.percept events; drained each tick in react().
|
|
112
|
+
private _pendingEncounters: Array<{ keid: string; domain: string; name?: string }> = []
|
|
113
|
+
// Buffered from known.entity.learned (the conscious / reasoning write-path, Phase 2.2).
|
|
114
|
+
private _pendingConscious: Array<{ keid: string; name?: string; feeling?: number }> = []
|
|
115
|
+
// Buffered from action.outcome — the reliability track-record signal (Phase 4).
|
|
116
|
+
private _pendingOutcomes: Array<{ keid: string; signal: number }> = []
|
|
117
|
+
|
|
118
|
+
private _bus: CognitiveBus | null = null
|
|
119
|
+
private readonly _model = new GenerativeModel()
|
|
120
|
+
|
|
121
|
+
constructor( config: KnownEntityTrackerConfig = {} ){
|
|
122
|
+
this._growthRate = config.familiarityGrowthRate ?? 0.15
|
|
123
|
+
this._decayRate = config.familiarityDecayRate ?? 0.005
|
|
124
|
+
this._curiosityGain = config.curiosityGain ?? 1.0
|
|
125
|
+
this._reliabilityRate = config.reliabilityRate ?? 0.2
|
|
126
|
+
this._maxTracked = config.maxTracked ?? 50
|
|
127
|
+
this._bus = config.bus ?? null
|
|
128
|
+
}
|
|
129
|
+
attachBus( bus: CognitiveBus ): void { this._bus = bus }
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Effective config = base engine-config-known-entity ⊕ persona-prior (Channel A). Read
|
|
133
|
+
* each tick so the tracker's dispositions *develop*: openness raises familiarity growth +
|
|
134
|
+
* the curiosity pull; analytical sharpens how fast reliability judgments are revised.
|
|
135
|
+
*/
|
|
136
|
+
private _readConfigFromState( state: ReadonlySimulationState ): void {
|
|
137
|
+
const p = readEffectiveParams( state, 'engine-config-known-entity' )
|
|
138
|
+
if( p.familiarityGrowthRate != null ) this._growthRate = p.familiarityGrowthRate
|
|
139
|
+
if( p.curiosityGain != null ) this._curiosityGain = p.curiosityGain
|
|
140
|
+
if( p.reliabilityRate != null ) this._reliabilityRate = p.reliabilityRate
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Engine interface ─────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
subscribes(): string[] { return [ 'senses.*', 'known.entity.learned', 'action.outcome' ] }
|
|
146
|
+
publishes(): CognitiveEventSchema[] { return [] }
|
|
147
|
+
|
|
148
|
+
onCognitiveEvent( e: CognitiveEvent ): StateCommands | void {
|
|
149
|
+
this._model.observe( e.type, e.salience )
|
|
150
|
+
|
|
151
|
+
// Conscious learning (the reasoning write-path): a learned name + felt valence.
|
|
152
|
+
if( e.type === 'known.entity.learned' ){
|
|
153
|
+
const u = e.payload as { keid?: string; name?: string; feeling?: number }
|
|
154
|
+
if( u?.keid && u.keid !== 'agent-self' )
|
|
155
|
+
this._pendingConscious.push({ keid: u.keid, name: u.name, feeling: u.feeling })
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Acting on/with an entity and seeing how it went — the reliability track-record.
|
|
160
|
+
if( e.type === 'action.outcome' ){
|
|
161
|
+
const o = e.payload as { targetEntityId?: string; success?: boolean; outcomeQuality?: number }
|
|
162
|
+
if( o?.targetEntityId && o.targetEntityId !== 'agent-self' )
|
|
163
|
+
this._pendingOutcomes.push({ keid: o.targetEntityId, signal: o.success ? 1 : 0 })
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Otherwise a senses.<domain>.percept — bind the perceived entity (sourceEntityId).
|
|
168
|
+
const p = e.payload as Percept | undefined
|
|
169
|
+
const keid = p?.sourceEntityId
|
|
170
|
+
if( !keid || keid === 'agent-self' ) return
|
|
171
|
+
|
|
172
|
+
// A channel-supplied display name rides on the raw input (e.g. TextMessage.speakerName).
|
|
173
|
+
const raw = p?.raw as { speakerName?: unknown } | undefined
|
|
174
|
+
const name = typeof raw?.speakerName === 'string' ? raw.speakerName : undefined
|
|
175
|
+
|
|
176
|
+
this._pendingEncounters.push({ keid, domain: p!.domain, name })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
snapshot(): Record<string, unknown> {
|
|
180
|
+
return { trackedEntities: this._dossiers.size }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async react(
|
|
184
|
+
_delta: Duration,
|
|
185
|
+
tick: Tick,
|
|
186
|
+
state: ReadonlySimulationState,
|
|
187
|
+
_ctx: SimulationContext,
|
|
188
|
+
): Promise<EngineResult> {
|
|
189
|
+
const commands: StateCommands = { set: [], metrics: [], delete: [] }
|
|
190
|
+
|
|
191
|
+
// Channel A: effective dispositions = base ⊕ persona-prior (developed from traits).
|
|
192
|
+
this._readConfigFromState( state )
|
|
193
|
+
|
|
194
|
+
// Rehydrate from persisted known-entity dossiers on the first tick after a
|
|
195
|
+
// snapshot/PMA restore — parity with attachment/reputation/theory-of-mind.
|
|
196
|
+
if( !this._restored ){
|
|
197
|
+
this._restoreFromState( state )
|
|
198
|
+
this._restored = true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Familiarity fades a little each tick — absence dims the sense of an entity.
|
|
202
|
+
for( const d of this._dossiers.values() )
|
|
203
|
+
if( d.familiarity > 0 )
|
|
204
|
+
d.familiarity = Math.max( 0, d.familiarity - this._decayRate )
|
|
205
|
+
|
|
206
|
+
// Drain encounters — each percept is an exposure that re-warms familiarity.
|
|
207
|
+
let touched = false
|
|
208
|
+
for( const enc of this._pendingEncounters.splice( 0 ) ){
|
|
209
|
+
const d = this._getOrCreate( enc.keid, enc.domain, tick )
|
|
210
|
+
d.encounterCount += 1
|
|
211
|
+
d.familiarity = Math.min( 1, d.familiarity + this._growthRate * ( 1 - d.familiarity ) )
|
|
212
|
+
d.lastSeenTick = tick
|
|
213
|
+
if( enc.name && !d.name ) d.name = enc.name // learn a name the channel offers
|
|
214
|
+
d.resolutionConfidence = this._resolution( d )
|
|
215
|
+
touched = true
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Drain conscious updates — a name learned in reasoning, and a felt valence (eased
|
|
219
|
+
// toward, not slammed). A known someone can exist here with no perceptual encounter.
|
|
220
|
+
for( const u of this._pendingConscious.splice( 0 ) ){
|
|
221
|
+
const d = this._getOrCreate( u.keid, 'audition', tick ) // a known *someone*
|
|
222
|
+
if( u.name ) d.name = u.name
|
|
223
|
+
if( u.feeling != null )
|
|
224
|
+
d.valence = Math.max( -1, Math.min( 1, d.valence + 0.5 * ( u.feeling - d.valence ) ) )
|
|
225
|
+
d.resolutionConfidence = this._resolution( d )
|
|
226
|
+
touched = true
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Drain action outcomes — reliability is a track record, earned by how the entity
|
|
230
|
+
// performs when acted on (general: a tool, a place, or a person). Only updates an
|
|
231
|
+
// already-known referent (acting on something you've never perceived doesn't conjure it).
|
|
232
|
+
for( const o of this._pendingOutcomes.splice( 0 ) ){
|
|
233
|
+
const d = this._dossiers.get( o.keid )
|
|
234
|
+
if( !d ) continue
|
|
235
|
+
d.reliability = Math.max( 0, Math.min( 1, d.reliability + this._reliabilityRate * ( o.signal - d.reliability ) ) )
|
|
236
|
+
touched = true
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Recognition (Phase 5): two referents the Will has resolved to the *same name* are very
|
|
240
|
+
// likely the same someone — fuse them. Human-mind-like: provisional (two people *can*
|
|
241
|
+
// share a name — it may be wrong) and reversible (the alias entity can be removed);
|
|
242
|
+
// deterministic (normalised-name match; the more-familiar referent is canonical, ties by
|
|
243
|
+
// keid order). The alias record lets reads of the triple/beliefs under the old keid
|
|
244
|
+
// resolve to the canonical, without destructive re-keying.
|
|
245
|
+
if( this._recognise( commands ) ) touched = true
|
|
246
|
+
|
|
247
|
+
this._prune()
|
|
248
|
+
|
|
249
|
+
// Forgetting (Phase 4): an unidentified blip that has faded out of familiarity is let
|
|
250
|
+
// go — dropped from memory and the persisted entity deleted. A named/resolved entity is
|
|
251
|
+
// identity-constitutive and kept (it rides the attachment×salience PMA, not forgotten).
|
|
252
|
+
for( const d of [ ...this._dossiers.values() ] )
|
|
253
|
+
if( d.familiarity < FORGET_FLOOR && !d.name && d.resolutionConfidence < CURIOUS_RESOLUTION ){
|
|
254
|
+
this._dossiers.delete( d.keid )
|
|
255
|
+
commands.delete!.push( `ke-${d.keid}` )
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Persist dossiers (the perceptual layer of the known-entity node).
|
|
259
|
+
for( const d of this._dossiers.values() ){
|
|
260
|
+
if( d.encounterCount === 0 && !d.name ) continue // keep a named-but-unseen someone
|
|
261
|
+
commands.set!.push({
|
|
262
|
+
id: `ke-${d.keid}`,
|
|
263
|
+
type: 'known-entity',
|
|
264
|
+
metadata: {
|
|
265
|
+
keid: d.keid,
|
|
266
|
+
kind: d.kind,
|
|
267
|
+
name: d.name,
|
|
268
|
+
familiarity: d.familiarity,
|
|
269
|
+
valence: d.valence,
|
|
270
|
+
reliability: d.reliability,
|
|
271
|
+
encounterCount: d.encounterCount,
|
|
272
|
+
lastSeenTick: d.lastSeenTick,
|
|
273
|
+
resolutionConfidence: d.resolutionConfidence,
|
|
274
|
+
},
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
commands.metrics!.push([ 'known_entity.tracked', this._dossiers.size ])
|
|
279
|
+
|
|
280
|
+
// Curiosity-to-resolve (Phase 3.a): a felt pull to learn more about the entities the
|
|
281
|
+
// Will keeps meeting but barely knows. Peaks for a *familiar yet unresolved* referent
|
|
282
|
+
// (high familiarity × low resolution); near-zero for strangers (unfamiliar) and for the
|
|
283
|
+
// well-known (resolved). The aggregate is the strongest such case — GoalManager turns a
|
|
284
|
+
// sustained drive into an epistemic "get to know them" goal that resolves as the Will
|
|
285
|
+
// learns (keid-tagged beliefs raise resolution, lowering the drive — a clean loop).
|
|
286
|
+
let curiosity = 0
|
|
287
|
+
for( const d of this._dossiers.values() )
|
|
288
|
+
curiosity = Math.max( curiosity, d.familiarity * ( 1 - d.resolutionConfidence ) )
|
|
289
|
+
// Channel A (openness): a more open/curious Will feels the pull-to-know more readily.
|
|
290
|
+
commands.metrics!.push([ 'drive.curiosity_resolve', Math.min( 1, curiosity * this._curiosityGain ) ])
|
|
291
|
+
|
|
292
|
+
// Per-entity curiosity (Phase 3.b): the vivid "who *was* that?". For a familiar-yet-
|
|
293
|
+
// unresolved someone, raise a specific pull — an attention.demand that GoalManager turns
|
|
294
|
+
// into a per-keid goal "get to know <name|someone>", completing on *that* referent's
|
|
295
|
+
// resolution. Cleared once resolved. (keid is sanitised for the metric-condition parser,
|
|
296
|
+
// which only accepts [\w.] names — keids may contain ':' e.g. web:42.)
|
|
297
|
+
for( const d of this._dossiers.values() ){
|
|
298
|
+
if( d.familiarity < CURIOUS_FAMILIARITY ) continue // only ones that matter
|
|
299
|
+
const skeid = d.keid.replace( /[^\w.]/g, '_' )
|
|
300
|
+
commands.metrics!.push([ `known_entity.${skeid}.resolution`, d.resolutionConfidence ])
|
|
301
|
+
|
|
302
|
+
if( d.resolutionConfidence < CURIOUS_RESOLUTION )
|
|
303
|
+
commands.set!.push({
|
|
304
|
+
id: `curiosity-${d.keid}`,
|
|
305
|
+
type: 'attention.demand',
|
|
306
|
+
metadata: {
|
|
307
|
+
generatesGoal: true,
|
|
308
|
+
goalDescription: `Get to know ${d.name ?? 'someone I keep encountering'}`,
|
|
309
|
+
goalPriority: 0.45,
|
|
310
|
+
goalTags: [ 'curiosity', 'known-entity', `keid:${d.keid}` ],
|
|
311
|
+
goalCompletionType: 'metric',
|
|
312
|
+
goalCompletionCondition: `known_entity.${skeid}.resolution >= ${CURIOUS_RESOLVED}`,
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
else if( state.entities.has( `curiosity-${d.keid}` ) )
|
|
316
|
+
commands.delete!.push( `curiosity-${d.keid}` ) // resolved → the pull subsides
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if( touched && this._bus ){
|
|
320
|
+
const predErr = this._model.observe( 'known_entity.count', this._dossiers.size )
|
|
321
|
+
if( !predErr.gated )
|
|
322
|
+
this._bus.publish({ type: 'known.entity.updated', version: 1, sourceEngine: this.name,
|
|
323
|
+
salience: Math.max( 0.2, predErr.salience ), payload: { tracked: this._dossiers.size } })
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { commands }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── Public API ───────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
/** The dossier for a referent, if the Will has one. */
|
|
332
|
+
getDossier( keid: string ): KnownEntity | undefined { return this._dossiers.get( keid ) }
|
|
333
|
+
|
|
334
|
+
// ── Internal ─────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
/** Resolution confidence: a learned name plus repeated encounters identify a referent. */
|
|
337
|
+
private _resolution( d: KnownEntity ): number {
|
|
338
|
+
return Math.min( 1, ( d.name ? 0.4 : 0 ) + Math.min( 0.6, d.encounterCount * 0.05 ) )
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Fuse dossiers that have resolved to the same name into one canonical referent. Returns
|
|
343
|
+
* true if any merge happened. Pure + deterministic.
|
|
344
|
+
*/
|
|
345
|
+
private _recognise( commands: StateCommands ): boolean {
|
|
346
|
+
const byName = new Map<string, KnownEntity[]>()
|
|
347
|
+
for( const d of this._dossiers.values() )
|
|
348
|
+
if( d.name ){
|
|
349
|
+
const n = d.name.trim().toLowerCase()
|
|
350
|
+
const g = byName.get( n ); g ? g.push( d ) : byName.set( n, [ d ] )
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let merged = false
|
|
354
|
+
for( const group of byName.values() ){
|
|
355
|
+
if( group.length < 2 ) continue
|
|
356
|
+
// canonical = most familiar; ties broken lexicographically for replay stability.
|
|
357
|
+
group.sort( ( a, b ) => b.familiarity - a.familiarity || ( a.keid < b.keid ? -1 : 1 ) )
|
|
358
|
+
const canon = group[0]!
|
|
359
|
+
for( const alias of group.slice( 1 ) ){
|
|
360
|
+
// Conservative: don't fuse two people who merely share a name. Skip if the absorbed
|
|
361
|
+
// handle is already an established relationship, or if both were active concurrently
|
|
362
|
+
// (two interlocutors at once ⇒ distinct). A same-name match alone is too weak.
|
|
363
|
+
const gap = Math.abs( ( canon.lastSeenTick as unknown as number ) - ( alias.lastSeenTick as unknown as number ) )
|
|
364
|
+
if( alias.encounterCount >= RECOGNITION_MERGE_MAX_ENCOUNTERS || gap < RECOGNITION_CONCURRENCY_WINDOW )
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
canon.encounterCount += alias.encounterCount
|
|
368
|
+
canon.familiarity = Math.max( canon.familiarity, alias.familiarity )
|
|
369
|
+
canon.resolutionConfidence = Math.max( canon.resolutionConfidence, alias.resolutionConfidence )
|
|
370
|
+
canon.lastSeenTick = Math.max( canon.lastSeenTick as unknown as number, alias.lastSeenTick as unknown as number ) as unknown as Tick
|
|
371
|
+
canon.valence = ( canon.valence + alias.valence ) / 2
|
|
372
|
+
canon.reliability = ( canon.reliability + alias.reliability ) / 2
|
|
373
|
+
|
|
374
|
+
this._dossiers.delete( alias.keid )
|
|
375
|
+
this._aliases.set( alias.keid, canon.keid )
|
|
376
|
+
commands.delete!.push( `ke-${alias.keid}` )
|
|
377
|
+
commands.set!.push({ id: `kea-${alias.keid}`, type: 'known-entity-alias',
|
|
378
|
+
metadata: { aliasKeid: alias.keid, canonicalKeid: canon.keid } })
|
|
379
|
+
merged = true
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return merged
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private _getOrCreate( keid: string, domain: string, tick: Tick ): KnownEntity {
|
|
386
|
+
keid = this._aliases.get( keid ) ?? keid // a recognised alias lands on the canonical dossier
|
|
387
|
+
const existing = this._dossiers.get( keid )
|
|
388
|
+
if( existing ) return existing
|
|
389
|
+
|
|
390
|
+
const d: KnownEntity = {
|
|
391
|
+
keid,
|
|
392
|
+
kind: SENTIENT_DOMAINS.has( domain ) ? 'sentient' : 'thing',
|
|
393
|
+
familiarity: 0,
|
|
394
|
+
valence: 0,
|
|
395
|
+
reliability: 0.5, // unknown until acted on
|
|
396
|
+
encounterCount: 0,
|
|
397
|
+
lastSeenTick: tick,
|
|
398
|
+
resolutionConfidence: 0,
|
|
399
|
+
}
|
|
400
|
+
this._dossiers.set( keid, d )
|
|
401
|
+
return d
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Keep the most-familiar dossiers; absence-faded acquaintances fall away (forgetting). */
|
|
405
|
+
private _prune(): void {
|
|
406
|
+
if( this._dossiers.size <= this._maxTracked ) return
|
|
407
|
+
const sorted = [ ...this._dossiers.values() ].sort( ( a, b ) => b.familiarity - a.familiarity )
|
|
408
|
+
for( const d of sorted.slice( this._maxTracked ) ) this._dossiers.delete( d.keid )
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private _restoreFromState( state: ReadonlySimulationState ): void {
|
|
412
|
+
// Recognised aliases first, so incoming references redirect to the canonical referent.
|
|
413
|
+
for( const entity of state.entities.values() )
|
|
414
|
+
if( entity.type === 'known-entity-alias' ){
|
|
415
|
+
const a = entity.metadata?.['aliasKeid'] as string | undefined
|
|
416
|
+
const c = entity.metadata?.['canonicalKeid'] as string | undefined
|
|
417
|
+
if( a && c ) this._aliases.set( a, c )
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
for( const entity of state.entities.values() ){
|
|
421
|
+
if( entity.type !== 'known-entity' ) continue
|
|
422
|
+
const m = entity.metadata ?? {}
|
|
423
|
+
const keid = m['keid'] as string | undefined
|
|
424
|
+
if( !keid || this._dossiers.has( keid ) ) continue
|
|
425
|
+
|
|
426
|
+
this._dossiers.set( keid, {
|
|
427
|
+
keid,
|
|
428
|
+
kind: ( m['kind'] as 'sentient' | 'thing' ) ?? 'sentient',
|
|
429
|
+
name: ( m['name'] as string | undefined ),
|
|
430
|
+
familiarity: ( m['familiarity'] as number ) ?? 0,
|
|
431
|
+
valence: ( m['valence'] as number ) ?? 0,
|
|
432
|
+
reliability: ( m['reliability'] as number ) ?? 0.5,
|
|
433
|
+
encounterCount: ( m['encounterCount'] as number ) ?? 0,
|
|
434
|
+
lastSeenTick: ( m['lastSeenTick'] as Tick ) ?? ( 0 as unknown as Tick ),
|
|
435
|
+
resolutionConfidence: ( m['resolutionConfidence'] as number ) ?? 0,
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/cognition/faculties/loss.evaluator.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LossEvaluator — detects setbacks, failures, and losses.
|
|
7
|
+
*
|
|
8
|
+
* Evaluates:
|
|
9
|
+
* - Failed goals
|
|
10
|
+
* - Lost resources
|
|
11
|
+
* - Relationship damage
|
|
12
|
+
* - Missed opportunities
|
|
13
|
+
* - Degraded state (worsening metrics)
|
|
14
|
+
*
|
|
15
|
+
* Produces: sadness, disappointment, grief
|
|
16
|
+
*
|
|
17
|
+
* Sadness = generalized loss response
|
|
18
|
+
* Disappointment = expected positive outcome that didn't materialize
|
|
19
|
+
* Grief = significant permanent loss (relationship, major goal)
|
|
20
|
+
*
|
|
21
|
+
* Part of Shard 1 (Affective Layer) — runs every tick, synchronous.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { StateCommands } from '#core/types'
|
|
25
|
+
import type { SimulationEngine, CognitiveEngine } from '#cognition/types'
|
|
26
|
+
import type { CognitiveEventSchema } from '#cognition/schema.registry'
|
|
27
|
+
import type { CognitiveEvent, CognitiveBus } from '#cognition/bus'
|
|
28
|
+
import { GenerativeModel } from '#cognition/generative.model'
|
|
29
|
+
|
|
30
|
+
export interface LossEvaluatorConfig {
|
|
31
|
+
/** Threshold for significant loss events */
|
|
32
|
+
significantLossThreshold?: number
|
|
33
|
+
/** How quickly sadness decays without re-triggering */
|
|
34
|
+
decayRate?: number
|
|
35
|
+
bus?: CognitiveBus
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class LossEvaluator implements SimulationEngine, CognitiveEngine {
|
|
39
|
+
readonly name = 'loss-evaluator'
|
|
40
|
+
|
|
41
|
+
private _significantLossThreshold: number
|
|
42
|
+
private _decayRate: number
|
|
43
|
+
|
|
44
|
+
// Track goal regressions (populated externally via entity scanning — kept for future use)
|
|
45
|
+
private _previousGoalProgress = new Map<string, number>()
|
|
46
|
+
|
|
47
|
+
// Track relationship valence history for damage detection (kept for future use)
|
|
48
|
+
private _previousRelationshipValence = new Map<string, number>()
|
|
49
|
+
|
|
50
|
+
// Cached energy / resource state from enriched energy.state.changed payload
|
|
51
|
+
private _energyLevel: number = 100
|
|
52
|
+
private _stressLoad: number = 0
|
|
53
|
+
private _interoceptionComfort: number = 0.5
|
|
54
|
+
private _previousComfort: number = 0.5
|
|
55
|
+
|
|
56
|
+
// Cached loss dimension values — updated each time energy.state.changed fires
|
|
57
|
+
private _cachedGoalLoss: number = 0
|
|
58
|
+
private _cachedRelationshipLoss: number = 0
|
|
59
|
+
|
|
60
|
+
// Cached emotion values for decay across events
|
|
61
|
+
private _previousSadness: number = 0
|
|
62
|
+
private _previousDisappointment: number = 0
|
|
63
|
+
private _previousGrief: number = 0
|
|
64
|
+
|
|
65
|
+
// Fixed deterministic per-event decay step (ms). This previously came from
|
|
66
|
+
// wall-clock Date.now() deltas, but _decayDeltaMs feeds emotion decay into
|
|
67
|
+
// replayable loss metrics, so a wall-clock source would break replay (R2).
|
|
68
|
+
// onCognitiveEvent has no sim clock in scope, so we use a constant estimate.
|
|
69
|
+
// (A true sim-time delta could be plumbed in later if adaptivity is wanted.)
|
|
70
|
+
private _decayDeltaMs: number = 100
|
|
71
|
+
|
|
72
|
+
private _bus: CognitiveBus | null = null
|
|
73
|
+
|
|
74
|
+
private readonly _model = new GenerativeModel()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
constructor( config: LossEvaluatorConfig = {} ){
|
|
78
|
+
this._bus = config.bus ?? null
|
|
79
|
+
this._significantLossThreshold = config.significantLossThreshold ?? 0.5
|
|
80
|
+
this._decayRate = config.decayRate ?? 0.05
|
|
81
|
+
}
|
|
82
|
+
attachBus( bus: CognitiveBus ): void { this._bus = bus }
|
|
83
|
+
|
|
84
|
+
// ── Engine interface ─────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
subscribes(): string[] { return ["energy.state.changed","executive.prediction.formed"] }
|
|
87
|
+
publishes(): CognitiveEventSchema[] { return [] }
|
|
88
|
+
|
|
89
|
+
onCognitiveEvent( e: CognitiveEvent ): StateCommands | void {
|
|
90
|
+
this._model.observe( e.type, e.salience )
|
|
91
|
+
|
|
92
|
+
switch( e.type ){
|
|
93
|
+
case 'energy.state.changed': {
|
|
94
|
+
const p = e.payload as { level: number; zoneCode: number; stressLoad: number; interoceptionComfort: number }
|
|
95
|
+
|
|
96
|
+
// Update energy state from enriched payload
|
|
97
|
+
this._energyLevel = p.level ?? this._energyLevel
|
|
98
|
+
this._stressLoad = p.stressLoad ?? 0
|
|
99
|
+
this._previousComfort = this._interoceptionComfort
|
|
100
|
+
this._interoceptionComfort = p.interoceptionComfort ?? 0.5
|
|
101
|
+
|
|
102
|
+
// Compute loss dimensions from available payload data
|
|
103
|
+
const goalLoss = this._cachedGoalLoss
|
|
104
|
+
const resourceLoss = this._computeResourceLoss()
|
|
105
|
+
const relationshipLoss = this._cachedRelationshipLoss
|
|
106
|
+
const stateDegradation = this._computeStateDegradation()
|
|
107
|
+
|
|
108
|
+
// Weighted loss level
|
|
109
|
+
const lossLevel
|
|
110
|
+
= goalLoss * 0.35
|
|
111
|
+
+ resourceLoss * 0.20
|
|
112
|
+
+ relationshipLoss * 0.30
|
|
113
|
+
+ stateDegradation * 0.15
|
|
114
|
+
|
|
115
|
+
const decayAmount = this._decayRate * ( this._decayDeltaMs / 1000 )
|
|
116
|
+
|
|
117
|
+
// Decompose into specific emotions (with decay when no new triggers)
|
|
118
|
+
const sadness = lossLevel > 0.01
|
|
119
|
+
? Math.min( 1, lossLevel * 1.1 )
|
|
120
|
+
: Math.max( 0, this._previousSadness - decayAmount )
|
|
121
|
+
|
|
122
|
+
const disappointment = goalLoss > 0.1
|
|
123
|
+
? Math.min( 1, goalLoss * 1.3 + relationshipLoss * 0.3 )
|
|
124
|
+
: Math.max( 0, this._previousDisappointment - decayAmount )
|
|
125
|
+
|
|
126
|
+
const grief = ( relationshipLoss > 0.7 || goalLoss > 0.8 )
|
|
127
|
+
? Math.min( 1, ( relationshipLoss + goalLoss ) / 2 )
|
|
128
|
+
: Math.max( 0, this._previousGrief - this._decayRate * 0.3 * ( this._decayDeltaMs / 1000 ) )
|
|
129
|
+
|
|
130
|
+
// Persist for next decay pass
|
|
131
|
+
this._previousSadness = sadness
|
|
132
|
+
this._previousDisappointment = disappointment
|
|
133
|
+
this._previousGrief = grief
|
|
134
|
+
|
|
135
|
+
const commands: StateCommands = {
|
|
136
|
+
metrics: [
|
|
137
|
+
[ 'emotion.sadness', sadness ],
|
|
138
|
+
[ 'emotion.disappointment', disappointment ],
|
|
139
|
+
[ 'emotion.grief', grief ],
|
|
140
|
+
[ 'loss.level', lossLevel ],
|
|
141
|
+
[ 'loss.goal', goalLoss ],
|
|
142
|
+
[ 'loss.resource', resourceLoss ],
|
|
143
|
+
[ 'loss.relationship', relationshipLoss ],
|
|
144
|
+
],
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Publish significant loss and grief events via bus
|
|
148
|
+
const _bus = this._bus
|
|
149
|
+
if( _bus ){
|
|
150
|
+
if( lossLevel > this._significantLossThreshold )
|
|
151
|
+
_bus.publish({ type: 'emotion.sadness.significant', version: 1, sourceEngine: this.name, salience: Math.min( 1, sadness * 1.5 ), payload: { sadness, disappointment, grief, lossLevel } })
|
|
152
|
+
|
|
153
|
+
if( grief > 0.6 )
|
|
154
|
+
_bus.publish({ type: 'emotion.grief.significant', version: 1, sourceEngine: this.name, salience: Math.min( 1, grief * 1.5 ), payload: { grief, relationshipLoss, goalLoss } })
|
|
155
|
+
|
|
156
|
+
// Publish cognitive event — gated by prediction error
|
|
157
|
+
if( sadness > 0.4 ){
|
|
158
|
+
const predErr = this._model.observe( 'emotion.sadness', sadness )
|
|
159
|
+
if( !predErr.gated )
|
|
160
|
+
_bus.publish({ type: 'emotion.sadness.elevated', version: 1, sourceEngine: this.name, salience: Math.min(1, sadness * 1.5), payload: { sadness } })
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return commands
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case 'executive.prediction.formed': {
|
|
168
|
+
const p = e.payload as { predictedDomains: string[]; confidence: number }
|
|
169
|
+
|
|
170
|
+
if( p.predictedDomains.includes('affect') )
|
|
171
|
+
this._model.setPrecision( 'emotion.sadness', 1.0 + p.confidence * 0.5 )
|
|
172
|
+
|
|
173
|
+
// Approximate goal loss from prediction confidence:
|
|
174
|
+
// Low confidence in domains signals uncertain/failing goals
|
|
175
|
+
if( p.predictedDomains.includes('goal') ){
|
|
176
|
+
const goalUncertainty = Math.max( 0, 1 - p.confidence )
|
|
177
|
+
// Blend with cached value — don't overwrite aggressively
|
|
178
|
+
this._cachedGoalLoss = Math.min( 1, this._cachedGoalLoss * 0.8 + goalUncertainty * 0.2 )
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Approximate relationship loss from social-domain prediction
|
|
182
|
+
if( p.predictedDomains.includes('social') ){
|
|
183
|
+
const socialUncertainty = Math.max( 0, 1 - p.confidence )
|
|
184
|
+
this._cachedRelationshipLoss = Math.min( 1, this._cachedRelationshipLoss * 0.8 + socialUncertainty * 0.2 )
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
snapshot(): Record<string, unknown> {
|
|
193
|
+
return {
|
|
194
|
+
energyLevel: this._energyLevel,
|
|
195
|
+
stressLoad: this._stressLoad,
|
|
196
|
+
interoceptionComfort: this._interoceptionComfort,
|
|
197
|
+
cachedGoalLoss: this._cachedGoalLoss,
|
|
198
|
+
cachedRelationshipLoss: this._cachedRelationshipLoss,
|
|
199
|
+
sadness: this._previousSadness,
|
|
200
|
+
disappointment: this._previousDisappointment,
|
|
201
|
+
grief: this._previousGrief,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Loss dimension helpers (event-payload-driven) ─────────
|
|
206
|
+
|
|
207
|
+
private _computeResourceLoss(): number {
|
|
208
|
+
// Low energy combined with high stress = resource loss perception
|
|
209
|
+
const energyDeficit = Math.max( 0, 1 - this._energyLevel / 50 )
|
|
210
|
+
return Math.min( 1,
|
|
211
|
+
energyDeficit * 0.5
|
|
212
|
+
+ this._stressLoad / 100 * 0.5
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private _computeStateDegradation(): number {
|
|
217
|
+
// Interoceptive comfort decrease signals state degradation
|
|
218
|
+
const previousComfort = this._previousComfort
|
|
219
|
+
const comfort = this._interoceptionComfort
|
|
220
|
+
return previousComfort > comfort
|
|
221
|
+
? ( previousComfort - comfort ) * 1.5
|
|
222
|
+
: 0
|
|
223
|
+
}
|
|
224
|
+
}
|