@mindot/will 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +777 -0
  3. package/dist/index.d.ts +8144 -0
  4. package/dist/index.js +26231 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +73 -0
  7. package/src/cognition/agency/access.grants.ts +59 -0
  8. package/src/cognition/agency/competence.codec.ts +89 -0
  9. package/src/cognition/agency/engines/action.selector.ts +462 -0
  10. package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
  11. package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
  12. package/src/cognition/agency/engines/instruction.intake.ts +54 -0
  13. package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
  14. package/src/cognition/agency/engines/reafference.engine.ts +241 -0
  15. package/src/cognition/agency/execution.primitives.ts +118 -0
  16. package/src/cognition/agency/index.ts +58 -0
  17. package/src/cognition/agency/proactive.communicator.ts +274 -0
  18. package/src/cognition/agency/reconcile.learning.ts +82 -0
  19. package/src/cognition/agency/schemas/external.ts +59 -0
  20. package/src/cognition/agency/schemas/innate.ts +111 -0
  21. package/src/cognition/agency/schemas/repertoire.ts +246 -0
  22. package/src/cognition/agency/selection.scoring.ts +191 -0
  23. package/src/cognition/agency/types.ts +165 -0
  24. package/src/cognition/bus.ts +391 -0
  25. package/src/cognition/completion.inbox.ts +86 -0
  26. package/src/cognition/config.mirror.entities.ts +475 -0
  27. package/src/cognition/conversation.memory.ts +81 -0
  28. package/src/cognition/event.log.ts +116 -0
  29. package/src/cognition/event.schemas.ts +568 -0
  30. package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
  31. package/src/cognition/faculties/affective.blender.ts +562 -0
  32. package/src/cognition/faculties/attachment.evaluator.ts +432 -0
  33. package/src/cognition/faculties/attention.allocator.ts +422 -0
  34. package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
  35. package/src/cognition/faculties/bias.detector.ts +363 -0
  36. package/src/cognition/faculties/circadian.oscillator.ts +294 -0
  37. package/src/cognition/faculties/confidence.calibrator.ts +324 -0
  38. package/src/cognition/faculties/dream.simulator.ts +251 -0
  39. package/src/cognition/faculties/empathy.simulator.ts +253 -0
  40. package/src/cognition/faculties/energy.regulator.ts +320 -0
  41. package/src/cognition/faculties/episodic.consolidator.ts +728 -0
  42. package/src/cognition/faculties/executive.engine/commands.ts +459 -0
  43. package/src/cognition/faculties/executive.engine/config.ts +46 -0
  44. package/src/cognition/faculties/executive.engine/context.ts +605 -0
  45. package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
  46. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
  47. package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
  48. package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
  49. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
  50. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
  51. package/src/cognition/faculties/executive.engine/facet.ts +628 -0
  52. package/src/cognition/faculties/executive.engine/gating.ts +222 -0
  53. package/src/cognition/faculties/executive.engine/index.ts +6 -0
  54. package/src/cognition/faculties/executive.engine/messages.ts +102 -0
  55. package/src/cognition/faculties/executive.engine/parser.ts +380 -0
  56. package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
  57. package/src/cognition/faculties/executive.engine/types.ts +323 -0
  58. package/src/cognition/faculties/exteroception.ts +338 -0
  59. package/src/cognition/faculties/forgetting.curve.ts +202 -0
  60. package/src/cognition/faculties/frustration.evaluator.ts +280 -0
  61. package/src/cognition/faculties/goal.manager.ts +1008 -0
  62. package/src/cognition/faculties/inhibition.controller.ts +351 -0
  63. package/src/cognition/faculties/interoception.ts +417 -0
  64. package/src/cognition/faculties/introspection.engine.ts +246 -0
  65. package/src/cognition/faculties/known.entity.tracker.ts +439 -0
  66. package/src/cognition/faculties/loss.evaluator.ts +224 -0
  67. package/src/cognition/faculties/moral.evaluator.ts +437 -0
  68. package/src/cognition/faculties/novelty.detector.ts +254 -0
  69. package/src/cognition/faculties/persona.consolidator.ts +845 -0
  70. package/src/cognition/faculties/planning.engine/engine.ts +859 -0
  71. package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
  72. package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
  73. package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
  74. package/src/cognition/faculties/planning.engine/types.ts +143 -0
  75. package/src/cognition/faculties/reputation.tracker.ts +294 -0
  76. package/src/cognition/faculties/reward.evaluator.ts +304 -0
  77. package/src/cognition/faculties/self.model.updater.ts +677 -0
  78. package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
  79. package/src/cognition/faculties/semantic.engine/index.ts +6 -0
  80. package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
  81. package/src/cognition/faculties/semantic.engine/types.ts +53 -0
  82. package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
  83. package/src/cognition/faculties/social.perception.ts +333 -0
  84. package/src/cognition/faculties/spaced.repetition.ts +605 -0
  85. package/src/cognition/faculties/stress.regulator.ts +375 -0
  86. package/src/cognition/faculties/task.switcher.ts +239 -0
  87. package/src/cognition/faculties/theory.of.mind.ts +333 -0
  88. package/src/cognition/faculties/threat.evaluator.ts +312 -0
  89. package/src/cognition/faculties/working.memory.ts +406 -0
  90. package/src/cognition/generative.model.ts +344 -0
  91. package/src/cognition/heartbeat.ts +70 -0
  92. package/src/cognition/index.ts +291 -0
  93. package/src/cognition/instruction.handler.ts +146 -0
  94. package/src/cognition/memory/index.ts +35 -0
  95. package/src/cognition/memory/vector.adapter.ts +316 -0
  96. package/src/cognition/memory/vector.content.ts +47 -0
  97. package/src/cognition/memory/vector.embedder.ts +212 -0
  98. package/src/cognition/memory/vector.index.ts +459 -0
  99. package/src/cognition/memory/vector.types.ts +65 -0
  100. package/src/cognition/orchestrator.ts +142 -0
  101. package/src/cognition/persona.prior.ts +265 -0
  102. package/src/cognition/schema.registry.ts +145 -0
  103. package/src/cognition/senses/audition.engine/engine.ts +976 -0
  104. package/src/cognition/senses/audition.engine/salience.ts +59 -0
  105. package/src/cognition/senses/base.sense.engine.ts +117 -0
  106. package/src/cognition/senses/gustation.engine.ts +24 -0
  107. package/src/cognition/senses/index.ts +172 -0
  108. package/src/cognition/senses/olfaction.engine.ts +24 -0
  109. package/src/cognition/senses/somatosensation.engine.ts +23 -0
  110. package/src/cognition/senses/vision.engine.ts +23 -0
  111. package/src/cognition/types.ts +63 -0
  112. package/src/cognition/utilities/token.tracker.ts +492 -0
  113. package/src/core/abstracts.ts +60 -0
  114. package/src/core/async.engine.ts +539 -0
  115. package/src/core/clock.ts +196 -0
  116. package/src/core/completion.recorder.ts +122 -0
  117. package/src/core/conflict.detector.ts +140 -0
  118. package/src/core/distributed.ts +626 -0
  119. package/src/core/event.bus.ts +292 -0
  120. package/src/core/inbound.recorder.ts +100 -0
  121. package/src/core/index.ts +167 -0
  122. package/src/core/logger.ts +85 -0
  123. package/src/core/metrics.ts +107 -0
  124. package/src/core/orchestrator.ts +625 -0
  125. package/src/core/replay.ts +589 -0
  126. package/src/core/scenario.ts +120 -0
  127. package/src/core/serialization.ts +448 -0
  128. package/src/core/simulation.ts +197 -0
  129. package/src/core/snapshot.manager.ts +293 -0
  130. package/src/core/state.manager.ts +283 -0
  131. package/src/core/types.ts +274 -0
  132. package/src/core/utils.ts +82 -0
  133. package/src/core/wall.clock.ts +23 -0
  134. package/src/extensions/livestream.ext.ts +570 -0
  135. package/src/extensions/time.ext.ts +339 -0
  136. package/src/index.ts +91 -0
  137. package/src/llm/gate.ts +140 -0
  138. package/src/llm/index.ts +716 -0
  139. package/src/llm/summarizer.ts +170 -0
  140. package/src/llm/wire.contracts.ts +67 -0
  141. package/src/pma/eval.ts +651 -0
  142. package/src/pma/index.ts +1082 -0
  143. package/src/profiles/built-in.ts +7 -0
  144. package/src/profiles/companion.ts +28 -0
  145. package/src/profiles/company-brain.ts +54 -0
  146. package/src/profiles/customer-service.ts +30 -0
  147. package/src/profiles/game-npc.ts +20 -0
  148. package/src/profiles/index.ts +42 -0
  149. package/src/profiles/smart-home.ts +34 -0
  150. package/src/runners/coherence.runner.ts +49 -0
  151. package/src/runners/outreach.runner.ts +158 -0
  152. package/src/runners/social.runner.ts +182 -0
  153. package/src/runners/thin-shim.runner.ts +195 -0
  154. package/src/sdk/will.ts +334 -0
  155. package/src/stem/assembly.audit.ts +88 -0
  156. package/src/stem/distribution.ts +203 -0
  157. package/src/stem/guards/identity.coherence.ts +152 -0
  158. package/src/stem/guards/identity.guard.ts +227 -0
  159. package/src/stem/index.ts +1145 -0
  160. package/src/stem/mind.ts +1090 -0
  161. package/src/stem/tracts/ack.reconciler.ts +55 -0
  162. package/src/stem/tracts/biography.writer.ts +190 -0
  163. package/src/stem/tracts/effector.controller.ts +143 -0
  164. package/src/stem/tracts/health.reporter.ts +113 -0
  165. package/src/stem/tracts/inbound.queue.ts +54 -0
  166. package/src/stem/tracts/outbox.controller.ts +167 -0
  167. package/src/stem/tracts/outbox.writer.ts +133 -0
  168. package/src/stem/tracts/pma.controller.ts +109 -0
  169. package/src/stem/tracts/replay.controller.ts +177 -0
  170. package/src/stem/tracts/sensory.controller.ts +168 -0
  171. package/src/stem/tracts/session.logger.ts +157 -0
  172. package/src/stem/tracts/transport/index.ts +38 -0
  173. package/src/stem/tracts/transport/loopback.transport.ts +86 -0
  174. package/src/stem/tracts/transport/socketio.transport.ts +200 -0
  175. package/src/stem/tracts/transport/stream.transport.ts +152 -0
  176. package/src/stem/tracts/transport/types.ts +193 -0
  177. package/src/stem/tracts/transport.controller.ts +380 -0
  178. package/src/types.ts +116 -0
@@ -0,0 +1,55 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/ack.reconciler.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // AckReconciler — idempotent dedup for the dual ack paths (Section 3).
6
+ //
7
+ // An outbound envelope can be acked twice: once via the socket.io emit callback
8
+ // (fast, best-effort) and once via a discrete inbound event (durable across
9
+ // reconnects — see SocketIoTransport). Both carry the same correlationId. This
10
+ // keyed deduper guarantees the ack is applied exactly once: the FIRST ack for a
11
+ // correlationId wins, every later one is a no-op.
12
+ //
13
+ // It is a pure deduper — classification (delivery vs result) and application
14
+ // (confirmDelivery / confirmExecution) stay in TransportController, which holds
15
+ // the collaborators. Bounded by a FIFO eviction window so it never grows without
16
+ // limit on a long-lived Will.
17
+ // ─────────────────────────────────────────────────────────────
18
+
19
+ const DEFAULT_MAX_TRACKED = 10_000
20
+
21
+ export class AckReconciler {
22
+ private readonly _seen = new Set<string>()
23
+ private readonly _order: string[] = []
24
+ private readonly _max: number
25
+
26
+ constructor( maxTracked: number = DEFAULT_MAX_TRACKED ){
27
+ this._max = maxTracked
28
+ }
29
+
30
+ /**
31
+ * Record an ack for `correlationId`. Returns true the FIRST time (caller should
32
+ * apply the ack), false on every subsequent call (caller drops it).
33
+ */
34
+ shouldApply( correlationId: string ): boolean {
35
+ if( this._seen.has( correlationId ) ) return false
36
+
37
+ this._seen.add( correlationId )
38
+ this._order.push( correlationId )
39
+ if( this._order.length > this._max ){
40
+ const evicted = this._order.shift()
41
+ if( evicted !== undefined ) this._seen.delete( evicted )
42
+ }
43
+ return true
44
+ }
45
+
46
+ /** Whether a correlationId has already been applied (without recording it). */
47
+ has( correlationId: string ): boolean { return this._seen.has( correlationId ) }
48
+
49
+ get size(): number { return this._seen.size }
50
+
51
+ clear(): void {
52
+ this._seen.clear()
53
+ this._order.length = 0
54
+ }
55
+ }
@@ -0,0 +1,190 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/biography.writer.ts — per-Will session-biography writers
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // BiographyWriter owns the session-biography disk writers extracted from
6
+ // WillStem (R5-f): the behavioral session summary and the emotional
7
+ // biography (real-time spike/sustained-arousal events + end-of-session
8
+ // summary). All output is appended as JSONL under
9
+ // `data/wills/{willId}/profiles/`.
10
+ //
11
+ // WillStem calls these from its lifecycle (pause/archive) and tick loop
12
+ // (emotion-spike detection). The writers read (and, for the emotional
13
+ // summary, flush) the instance's `_sessionBehavior` aggregate; they touch
14
+ // only WillInstance fields, so they take the resolved instance directly
15
+ // and derive the Will id from `instance.config.id`.
16
+ //
17
+ // Behaviour is preserved verbatim from the original WillStem methods;
18
+ // this is a pure extract-collaborator refactor.
19
+ // ─────────────────────────────────────────────────────────────
20
+
21
+ import { logger } from '#core/logger'
22
+ import { appendFileSync, mkdirSync } from 'node:fs'
23
+ import { join } from 'node:path'
24
+ import { fileLoggingEnabled } from '#stem/tracts/transport/stream.transport'
25
+ import type { WillInstance } from '#stem/index'
26
+
27
+ export class BiographyWriter {
28
+ /** Profile dirs already ensured this process — avoids an mkdir syscall per write. */
29
+ private readonly _ensuredDirs = new Set<string>()
30
+
31
+ /**
32
+ * Append one JSONL record to a Will's profiles/ dir. Dev-only (gated by
33
+ * `fileLoggingEnabled()`, matching SessionLogger) — in production this concern
34
+ * rides the `session_log`/transport path, not ephemeral local disk. The dir is
35
+ * ensured once per process, not per write.
36
+ */
37
+ private _append( willId: string, file: string, record: unknown ): void {
38
+ if( !fileLoggingEnabled() ) return
39
+
40
+ const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
41
+ const dir = join( dataDir, 'wills', willId, 'profiles' )
42
+ try {
43
+ if( !this._ensuredDirs.has( dir ) ){
44
+ mkdirSync( dir, { recursive: true } )
45
+ this._ensuredDirs.add( dir )
46
+ }
47
+ appendFileSync( join( dir, file ), JSON.stringify( record ) + '\n', 'utf8' )
48
+ } catch( err ){
49
+ logger.error( `[biography] ${file} write failed:`, err )
50
+ }
51
+ }
52
+ /**
53
+ * Compute and append a behavioral session summary to `behavioral.jsonl`.
54
+ * Captures action distribution, top/novel/impulsive action counts, avg
55
+ * confidence, valence/arousal ranges, and goal completion for the session.
56
+ * Topics discussed (TODO 7.2) intentionally omitted — requires NLP analysis
57
+ * on conversation content not available in state metrics.
58
+ */
59
+ writeSessionSummary( instance: WillInstance ): void {
60
+ const sb = instance._sessionBehavior
61
+ if( !sb ) return
62
+
63
+ const willId = instance.config.id
64
+
65
+ // Action type distribution from the agency repertoire's enactment counts.
66
+ const actionDist: Record<string, number> = {}
67
+ for( const skill of instance.cognition.schemaRepertoire.skills().values() )
68
+ if( skill.enactments > 0 ) actionDist[ skill.schema ] = skill.enactments
69
+
70
+ const topAction = Object.entries( actionDist )
71
+ .sort( ( a, b ) => b[1] - a[1] )[ 0 ]?.[ 0 ] ?? 'none'
72
+
73
+ // Action types tried exactly once signal exploratory/novel behavior
74
+ const novelActionCount = Object.values( actionDist ).filter( c => c === 1 ).length
75
+
76
+ const avgConfidence = sb.confidenceCount > 0
77
+ ? sb.confidenceSum / sb.confidenceCount
78
+ : null
79
+
80
+ const completionRate = sb.goalsTotal > 0
81
+ ? sb.goalsCompleted / sb.goalsTotal
82
+ : null
83
+
84
+ const summary = {
85
+ type: 'session_summary',
86
+ wallTime: Date.now(),
87
+ sessionId: instance.sessionLogger?.sessionId ?? 'unknown',
88
+ willId,
89
+ startTick: sb.startTick,
90
+ endTick: instance.tickCount,
91
+ tickCount: instance.tickCount - sb.startTick,
92
+ actionDist,
93
+ topAction,
94
+ avgConfidence,
95
+ novelActionCount,
96
+ impulsiveActionCount: sb.impulsiveActionCount,
97
+ valenceRange: { min: sb.valenceMin, max: sb.valenceMax, spread: sb.valenceMax - sb.valenceMin },
98
+ arousalRange: { min: sb.arousalMin, max: sb.arousalMax, spread: sb.arousalMax - sb.arousalMin },
99
+ goalsTotal: sb.goalsTotal,
100
+ goalsCompleted: sb.goalsCompleted,
101
+ completionRate,
102
+ }
103
+
104
+ this._append( willId, 'behavioral.jsonl', summary )
105
+ }
106
+
107
+ /**
108
+ * Append one emotional event entry to `emotional_biography.jsonl`.
109
+ * Called real-time on spike detection and sustained-arousal episode end.
110
+ *
111
+ * @param evtType 'spike' | 'sustained_high_arousal'
112
+ * @param fields Event-specific fields (tick, dimension, from/to/delta, etc.)
113
+ */
114
+ writeEmotionalEvent(
115
+ instance: WillInstance,
116
+ evtType: 'spike' | 'sustained_high_arousal',
117
+ fields: Record<string, unknown>
118
+ ): void {
119
+ const willId = instance.config.id
120
+
121
+ const entry = {
122
+ type: evtType,
123
+ wallTime: Date.now(),
124
+ sessionId: instance.sessionLogger?.sessionId ?? 'unknown',
125
+ willId,
126
+ ...fields,
127
+ }
128
+
129
+ this._append( willId, 'emotional_biography.jsonl', entry )
130
+ }
131
+
132
+ /**
133
+ * Compute and append a session emotional biography summary to
134
+ * `emotional_biography.jsonl`. Called at session end (pause or archive).
135
+ * Flushes any open sustained-arousal streak, then writes a `session_summary`
136
+ * entry covering dominant mood, valence/arousal arcs, spike count, and the
137
+ * number of completed sustained-high-arousal episodes.
138
+ *
139
+ * PMA tooling can read across all `session_summary` entries to build a cross-
140
+ * session emotional biography.
141
+ */
142
+ writeEmotionalBiographySummary( instance: WillInstance ): void {
143
+ const sb = instance._sessionBehavior
144
+ if( !sb || sb.avgValenceCount === 0 ) return
145
+
146
+ const willId = instance.config.id
147
+
148
+ // Flush any open sustained-arousal streak before writing the summary.
149
+ if( sb.highArousalStreak >= 10 ){
150
+ sb.sustainedEpisodes++
151
+ this.writeEmotionalEvent( instance, 'sustained_high_arousal', {
152
+ startTick: instance.tickCount - sb.highArousalStreak,
153
+ durationTicks: sb.highArousalStreak,
154
+ tick: instance.tickCount,
155
+ })
156
+ sb.highArousalStreak = 0
157
+ }
158
+
159
+ const avgValence = sb.avgValenceSum / sb.avgValenceCount
160
+ const dominantMood = avgValence > 0.15 ? 'positive' : avgValence < -0.10 ? 'negative' : 'neutral'
161
+
162
+ const summary = {
163
+ type: 'session_summary',
164
+ wallTime: Date.now(),
165
+ sessionId: instance.sessionLogger?.sessionId ?? 'unknown',
166
+ willId,
167
+ startTick: sb.startTick,
168
+ endTick: instance.tickCount,
169
+ tickCount: instance.tickCount - sb.startTick,
170
+ dominantMood,
171
+ avgValence: Math.round( avgValence * 1000 ) / 1000,
172
+ valenceArc: {
173
+ start: Math.round( sb.valenceStart * 1000 ) / 1000,
174
+ end: Math.round( sb.valenceEnd * 1000 ) / 1000,
175
+ min: Math.round( sb.valenceMin * 1000 ) / 1000,
176
+ max: Math.round( sb.valenceMax * 1000 ) / 1000,
177
+ },
178
+ arousalArc: {
179
+ start: Math.round( sb.arousalStart * 1000 ) / 1000,
180
+ end: Math.round( sb.arousalEnd * 1000 ) / 1000,
181
+ min: Math.round( sb.arousalMin * 1000 ) / 1000,
182
+ max: Math.round( sb.arousalMax * 1000 ) / 1000,
183
+ },
184
+ spikeCount: sb.spikeCount,
185
+ sustainedHighArousalEpisodes: sb.sustainedEpisodes,
186
+ }
187
+
188
+ this._append( willId, 'emotional_biography.jsonl', summary )
189
+ }
190
+ }
@@ -0,0 +1,143 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/effector.controller.ts — per-Will external-effector subsystem
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // effectorController owns the external-effector machinery extracted from
6
+ // WillStem (R5-d): the runtime allow-list (setAllowed), the host
7
+ // execution-feedback / reafference loop (confirmExecution), buffering of
8
+ // externally-dispatched invocations off the `agency.invocation` bus event
9
+ // (bufferInvocation), and draining that buffer for the delivery layer (drain).
10
+ //
11
+ // Agency-native (no legacy effector/decision.record stack): the MotorSchemaExecutor
12
+ // holds a host-owned action as an 'awaiting' `agency.intent` and publishes
13
+ // `agency.invocation` (carrying the intentId as the correlation handle). The host
14
+ // executes it and acks; `confirmExecution` turns that ack into an `agency.outcome`
15
+ // via `reconcileInvocation`, which the ReafferenceEngine consumes — learning the
16
+ // real result, freeing the awaiting intent, and (when the intent was a plan's
17
+ // frontier step) emitting the `action.outcome` the PlanningEngine advances on.
18
+ //
19
+ // The ops touch only WillInstance fields (no Will id needed), so these
20
+ // methods take the resolved instance directly. WillStem still validates
21
+ // existence via _get(id) before delegating.
22
+ // ─────────────────────────────────────────────────────────────
23
+
24
+ import { logger } from '#core/logger'
25
+ import { reconcileInvocation } from '#agency/reconcile.learning'
26
+ import type { effectorInvocation } from '#types'
27
+ import type { WillInstance } from '#stem/index'
28
+
29
+ export class effectorController {
30
+ /**
31
+ * Update the set of allowed communication effectors at runtime via AccessGrants
32
+ * (the permission / sense gate the senses + reply path read).
33
+ */
34
+ setAllowed( instance: WillInstance, effectors: string[] | null ): void {
35
+ instance.cognition.accessGrants.setAllowed( effectors )
36
+ }
37
+
38
+ /**
39
+ * Buffer a host-owned effector invocation for delivery. Called from the WillStem
40
+ * `agency.invocation` bus subscription with the event payload
41
+ * (`{ schema, intentId, targetEntityId, parameters, tick }`). The awaiting
42
+ * `agency.intent` id is the **correlation handle** (`decisionRecordId`): the host
43
+ * echoes it on its result-ack, and `confirmExecution` uses it to find the intent.
44
+ */
45
+ bufferInvocation( instance: WillInstance, payload: Record<string, unknown> ): void {
46
+ const intentId = ( payload.intentId as string ) ?? ''
47
+ instance.pendingEffectorInvocations.push({
48
+ id: intentId,
49
+ decisionRecordId: intentId, // correlation handle — the awaiting agency.intent id
50
+ effectorName: ( payload.schema as string ) ?? '',
51
+ parameters: ( payload.parameters as Record<string, unknown> ) ?? {},
52
+ targetEntityId: payload.targetEntityId as string | undefined,
53
+ reasoning: ( payload.reasoning as string ) ?? '',
54
+ tick: ( payload.tick as number ) ?? 0,
55
+ timestamp: Date.now()
56
+ })
57
+ }
58
+
59
+ /**
60
+ * Drain all pending external effector invocations.
61
+ * Called by the SSE delivery layer each tick after drainOutbox.
62
+ */
63
+ drain( instance: WillInstance ): effectorInvocation[] {
64
+ return instance.pendingEffectorInvocations.splice( 0 )
65
+ }
66
+
67
+ // ── External Effector Feedback (agency-native reafference) ─────────────────
68
+ /**
69
+ * Called by the host/WorldInterface after executing a host-owned effector.
70
+ * `invocationId` is the correlation handle the host echoed — the awaiting
71
+ * `agency.intent`'s id (= the `decisionRecordId` field of the effectorInvocation).
72
+ *
73
+ * It reconciles the ack into an `agency.outcome` (via `reconcileInvocation`),
74
+ * carrying the intent's efference copy (predicted reward/valence) so surprise is
75
+ * honest, and its plan provenance (planId/stepId) when it was a plan's frontier
76
+ * step. The ReafferenceEngine consumes that next tick: it learns the real result,
77
+ * frees the awaiting intent, and — for a plan-tagged outcome — emits the
78
+ * `action.outcome` the PlanningEngine advances on. No decision.record, no legacy
79
+ * `effector.confirmed`: the agency pipeline owns the loop end to end.
80
+ */
81
+ confirmExecution(
82
+ instance: WillInstance,
83
+ invocationId: string,
84
+ result: {
85
+ success: boolean
86
+ description: string
87
+ metrics?: Record<string, number>
88
+ },
89
+ ): void {
90
+ const tick = instance.tickCount
91
+
92
+ const intent = instance.simulation.stateManager.snapshot().entities.get( invocationId )
93
+ if( !intent || intent.type !== 'agency.intent' ){
94
+ // No awaiting intent for this id. Expected when the executor's 15-tick await
95
+ // timeout already abandoned it (writing a failed outcome + advancing any plan)
96
+ // before the host's late ack arrived, or the id is unknown. Drop the straggler.
97
+ logger.warn( `[effector] confirmExecution: no awaiting agency.intent "${invocationId}" (timed out / already reconciled?) — ignored` )
98
+ return
99
+ }
100
+
101
+ const m = ( intent.metadata ?? {} ) as Record<string, unknown>
102
+ const schema = ( m['schema'] as string ) ?? 'unknown'
103
+ const predicted = {
104
+ reward: num( m['predictedReward'], num( m['expectedReward'], 0.5 ) ),
105
+ valence: num( m['predictedValence'], num( m['expectedValence'], 0 ) ),
106
+ }
107
+ const provenance = { planId: m['planId'] as string | undefined, stepId: m['stepId'] as string | undefined }
108
+
109
+ // Reconcile → agency.outcome. ReafferenceEngine consumes it next tick (learn +
110
+ // free the intent + emit the plan's action.outcome when plan-tagged).
111
+ instance.simulation.stateManager.setEntity(
112
+ reconcileInvocation( invocationId, schema, result, tick, predicted, provenance )
113
+ )
114
+
115
+ // Optional host-supplied metric deltas (e.g. the world moved a body metric).
116
+ // Validate each value is a finite number before it touches simulation state —
117
+ // a NaN/Infinity/garbage value from a buggy host would persist and corrupt
118
+ // cognition (it isn't recomputed away like blended affect is).
119
+ if( result.metrics )
120
+ for( const [ k, v ] of Object.entries( result.metrics ) ){
121
+ if( typeof v === 'number' && Number.isFinite( v ) )
122
+ instance.simulation.stateManager.setMetric( k, v )
123
+ else
124
+ logger.warn( `[effector] confirmExecution: dropped non-finite metric "${k}"=${String( v )} from host ack (${schema})` )
125
+ }
126
+
127
+ instance.sessionLogger?.write({
128
+ type: 'action.outcome',
129
+ tick,
130
+ actionType: schema,
131
+ success: result.success,
132
+ outcome: result.description.slice( 0, 300 ),
133
+ outcomeQuality: result.success ? 0.8 : 0.2,
134
+ confirmedExternally: true,
135
+ } as never)
136
+
137
+ logger.info( `[effector] ✓ reconciled ${result.success ? 'success' : 'failure'}: intent "${invocationId}" (${schema})` )
138
+ }
139
+ }
140
+
141
+ function num( v: unknown, fallback: number ): number {
142
+ return typeof v === 'number' && Number.isFinite( v ) ? v : fallback
143
+ }
@@ -0,0 +1,113 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/health.reporter.ts — per-Will cognitive-health reporter
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // HealthReporter owns the composite cognitive-health view extracted from
6
+ // WillStem (R5-f2): a pure, read-only computation over a running Will's
7
+ // belief quality, affect state, and goal state, collapsed into a single
8
+ // `overallScore` and a 'healthy' | 'drifting' | 'degraded' status band.
9
+ // Intended for developer dashboards and platform monitoring.
10
+ //
11
+ // WillStem.getCognitiveHealth delegates here. The op touches only
12
+ // WillInstance fields (no Will id needed), so this method takes the
13
+ // resolved instance directly; WillStem still validates existence via
14
+ // _get(id) before delegating.
15
+ //
16
+ // Behaviour is preserved verbatim from the original WillStem method;
17
+ // this is a pure extract-collaborator refactor.
18
+ // ─────────────────────────────────────────────────────────────
19
+
20
+ import type { CognitiveHealth, WillInstance } from '#stem/index'
21
+
22
+ /** Round to 3 decimals — keeps health payloads readable. */
23
+ const r3 = ( n: number ): number => Math.round( n * 1000 ) / 1000
24
+
25
+ export class HealthReporter {
26
+ /**
27
+ * Returns a composite health summary for a running Will.
28
+ * Intended for developer dashboards and platform monitoring.
29
+ *
30
+ * Status bands:
31
+ * healthy — normal operating range
32
+ * drifting — one or more indicators approaching problematic thresholds
33
+ * degraded — one or more indicators clearly outside healthy range
34
+ */
35
+ report( instance: WillInstance ): CognitiveHealth {
36
+ const
37
+ snap = instance.simulation.stateManager.snapshot(),
38
+ beliefs = instance.cognition.semanticIntegrator.getBeliefs(),
39
+ tick = instance.tickCount
40
+
41
+ // ── Belief quality ──────────────────────────────────────────
42
+ const totalBeliefs = beliefs.length
43
+ let
44
+ confidenceSum = 0,
45
+ highRiskCount = 0
46
+
47
+ for( const b of beliefs ){
48
+ confidenceSum += b.confidence
49
+ if( b.confidence > 0.85 && b.supportingEpisodes < 3 ) highRiskCount++
50
+ }
51
+
52
+ const
53
+ avgConfidence = totalBeliefs > 0 ? confidenceSum / totalBeliefs : 0,
54
+ // Confidence health peaks at ~0.62 and falls off linearly toward either
55
+ // extreme (over-confidence and chronic doubt are both unhealthy).
56
+ beliefScore = totalBeliefs === 0
57
+ ? 0.5 // no beliefs yet — neutral
58
+ : Math.max( 0,
59
+ 1.0
60
+ - Math.abs( avgConfidence - 0.62 ) * 1.5 // deviation from ideal
61
+ - ( highRiskCount / Math.max( totalBeliefs, 1 ) ) * 0.8
62
+ )
63
+
64
+ // ── Affect state ────────────────────────────────────────────
65
+ // Negative emotions AND sustained low valence both signal distress — the
66
+ // latter is exactly what the executive early-fires on, so health tracks it too.
67
+ const
68
+ valence = snap.metrics.get('affect.valence') ?? 0,
69
+ frustration = snap.metrics.get('emotion.frustration') ?? 0,
70
+ irritability = snap.metrics.get('emotion.irritability') ?? 0,
71
+ stress = snap.metrics.get('stress.load') ?? 0,
72
+ isElevated = irritability > 0.55 || frustration > 0.65 || stress > 0.7,
73
+ affectScore = Math.max( 0,
74
+ 1.0
75
+ - Math.max( 0, frustration - 0.3 ) * 0.8
76
+ - Math.max( 0, irritability - 0.3 ) * 0.8
77
+ - Math.max( 0, stress - 0.4 ) * 0.6
78
+ - Math.max( 0, -valence - 0.15 ) * 0.7 // sustained low valence drags health down
79
+ )
80
+
81
+ // ── Goal state ──────────────────────────────────────────────
82
+ // Use the O(1) type index rather than scanning every entity — health is polled.
83
+ let
84
+ activeGoals = 0,
85
+ totalGoals = 0
86
+
87
+ for( const entity of instance.simulation.stateManager.getEntitiesByType('goal') ){
88
+ totalGoals++
89
+ const s = entity.metadata?.status
90
+ if( s === 'active' || s === 'in_progress' ) activeGoals++
91
+ }
92
+
93
+ const goalScore = totalGoals === 0
94
+ ? 0.7 // no goals yet — mildly healthy
95
+ : Math.min( 1, 0.4 + ( activeGoals / totalGoals ) * 0.6 )
96
+
97
+ // ── Composite ───────────────────────────────────────────────
98
+ const
99
+ overallScore = ( beliefScore * 0.4 ) + ( affectScore * 0.4 ) + ( goalScore * 0.2 ),
100
+ status: CognitiveHealth['status'] =
101
+ overallScore >= 0.65 ? 'healthy' :
102
+ overallScore >= 0.40 ? 'drifting' : 'degraded'
103
+
104
+ return {
105
+ tick,
106
+ status,
107
+ overallScore: r3( overallScore ),
108
+ beliefs: { total: totalBeliefs, avgConfidence: r3( avgConfidence ), highRisk: highRiskCount },
109
+ affect: { valence: r3( valence ), frustration: r3( frustration ), irritability: r3( irritability ), stress: r3( stress ), isElevated },
110
+ goals: { total: totalGoals, active: activeGoals }
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,54 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/inbound.queue.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // InboundQueue — the determinism linchpin for external I/O.
6
+ //
7
+ // Every InboundEnvelope from the ExternalTransport (messages, percepts, acks)
8
+ // is enqueued here on arrival and applied LATER, at a fixed point in the tick
9
+ // loop, with a logicalTime (tick) stamp. This is what keeps a live, async,
10
+ // non-deterministic socket compatible with the replay-deterministic core:
11
+ // - arrival is decoupled from application
12
+ // - application happens on-tick, in FIFO order, stamped to that tick
13
+ // - the replay recorder logs each tick's drained batch; replay re-injects it
14
+ //
15
+ // The queue holds NO sockets and applies NOTHING itself — it is a buffer.
16
+ // ─────────────────────────────────────────────────────────────
17
+
18
+ import type { InboundEnvelope } from './transport/types'
19
+
20
+ /** An inbound envelope paired with the tick it was applied on (set at drain). */
21
+ export interface StampedInbound {
22
+ envelope: InboundEnvelope
23
+ /** Tick at which this was drained + applied. Assigned by drain(tick). */
24
+ appliedTick: number
25
+ }
26
+
27
+ export class InboundQueue {
28
+ private _pending: InboundEnvelope[] = []
29
+
30
+ /** Buffer an inbound envelope. Called from the transport handler — never applies. */
31
+ enqueue( env: InboundEnvelope ): void {
32
+ this._pending.push( env )
33
+ }
34
+
35
+ /** Number of envelopes waiting to be applied. */
36
+ get size(): number { return this._pending.length }
37
+
38
+ /**
39
+ * Remove and return all pending envelopes, stamped with the given tick.
40
+ * Called once per tick by the stem before simulation.step(). FIFO order is
41
+ * preserved so application is deterministic given the recorded batch.
42
+ */
43
+ drain( tick: number ): StampedInbound[] {
44
+ if( this._pending.length === 0 ) return []
45
+ const batch = this._pending
46
+ this._pending = []
47
+ return batch.map( envelope => ({ envelope, appliedTick: tick }) )
48
+ }
49
+
50
+ /** Discard everything without applying (teardown). */
51
+ clear(): void {
52
+ this._pending = []
53
+ }
54
+ }