@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,462 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/agency/action.selector.ts — the biased, gated competition
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // The ActionSelector runs every tick with no LLM. It reads the affordance field
6
+ // the synthesizer wrote, assembles the bias signals the affective / drive /
7
+ // inhibition engines already expose, and runs a soft winner-take-all (see
8
+ // scoring.ts). The winner is committed as an `agency.intent` for the executor.
9
+ //
10
+ // System 1 is the default: the substrate selects and commits on its own. The
11
+ // executive (System 2, the LLM) is recruited only when the choice is genuinely
12
+ // uncertain or consequential — `deliberate` is set when competition entropy or
13
+ // stakes exceed threshold. Per design, an overlearned schema both wins more
14
+ // cheaply (additive habit bonus in scoring) AND raises its own deliberation
15
+ // thresholds (habit relief below), so proceduralized actions skip the LLM —
16
+ // the instrumental→habitual gradient cashing out as falling inference spend.
17
+ //
18
+ // The committed intent always carries the `deliberate` flag, so a tier without
19
+ // an executive still acts on the substrate's best guess; when an executive is
20
+ // present it may revise a deliberate intent before the executor runs next tick.
21
+ // ─────────────────────────────────────────────────────────────
22
+
23
+ import { logger } from '#core/logger'
24
+ import type {
25
+ Duration, Tick, SimulationContext,
26
+ ReadonlySimulationState, StateCommands, EntityInput,
27
+ } from '#core/types'
28
+ import type { CognitiveBus } from '#cognition/bus'
29
+ import type { CognitiveEngine, EngineResult } from '#cognition/types'
30
+ import type { CognitiveEventSchema } from '#cognition/schema.registry'
31
+ import type { Affordance, AffordanceSource, ScoredAffordance } from '#agency/types'
32
+ import {
33
+ scoreAffordance, competitionEntropy, stakes, collectGoalTargets,
34
+ DEFAULT_WEIGHTS,
35
+ type BiasContext, type ScoreWeights,
36
+ } from '#agency/selection.scoring'
37
+ import { readEffectiveParams, readPersonaPrior } from '#cognition/persona.prior'
38
+
39
+ /**
40
+ * Activation margin (winner − runner-up) below which the choice is "contested" —
41
+ * a genuine contender sits next to the winner, so System 2 is worth recruiting.
42
+ * Uses the top-2 margin, NOT full-field entropy: a long tail of low-activation
43
+ * options (e.g. the ever-present innate floor) must not by itself trigger
44
+ * deliberation — only a real rival near the top does.
45
+ */
46
+ const MARGIN_THRESHOLD = 0.06
47
+ /** Base stakes above which the choice is consequential enough to recruit the LLM. */
48
+ const BASE_STAKES_THRESHOLD = 0.60
49
+ /** How much a fully-proceduralized winner relaxes both gates (habit relief). */
50
+ const HABIT_RELIEF = 0.25
51
+
52
+ // ── Shared deliberativeness (R1) ───────────────────────────────
53
+ // The selector's deliberation gate and the executive's EffortGate are the two consumers
54
+ // of ONE deliberativeness disposition: the persona-prior on the executive's
55
+ // `deliberateThreshold` (analytical ↓ → deliberate more; decisiveness ↑ → less). Reading
56
+ // that same signal here keeps a Will from being impulsive in reasoning yet over-deliberate
57
+ // in action (or vice-versa). The signal is the deviation from baseline (the prior delta
58
+ // itself), so it is 0 — and these terms vanish — until a disposition actually develops.
59
+ // Each gate lives at its own native scale, so the deviation is mapped through a per-gate
60
+ // sensitivity (mirrors R2's "one disposition, two owners, native scales").
61
+ /** Margin-gate widening per unit deliberativeness (more deliberative ⇒ contest on larger margins). */
62
+ const DELIB_MARGIN_SENS = 0.15
63
+ /** Stakes-gate lowering per unit deliberativeness (more deliberative ⇒ deliberate at lower stakes). */
64
+ const DELIB_STAKES_SENS = 0.40
65
+
66
+ // ── Preemption (the "smarter serializer") ──────────────────────
67
+ /**
68
+ * Switch-cost hysteresis: while an action is *awaiting*, the Will re-competes
69
+ * every tick, but a challenger must beat the incumbent's activation by at least
70
+ * this to interrupt it — preventing thrashing over marginally-better options.
71
+ * The cost is scaled DOWN by the challenger's stakes, so a salient/urgent event
72
+ * (a threat) faces almost no hysteresis and overrides immediately. This mirrors
73
+ * basal-ganglia action selection: continuous, with a maintenance bias on the
74
+ * ongoing action that salient interrupts can override.
75
+ *
76
+ * This is the *fallback* base only. The live base comes from
77
+ * `engine-config-action-selector.switchCost` (base ⊕ persona-prior), so a
78
+ * conscientious Will develops a higher switch resistance — the SAME disposition
79
+ * the TaskSwitcher develops in attention space (R2: one switch-resistance trait,
80
+ * two owners at their native scales). See `effectiveSwitchCost` in react().
81
+ */
82
+ const BASE_SWITCH_COST = 0.15
83
+ /**
84
+ * Per-focus-tick growth of switch resistance: the longer the Will has been
85
+ * committed (shared `task_switch.current_focus_ticks`), the costlier to be pulled
86
+ * off. Mirrors the TaskSwitcher's own internal `baseSwitchCost·(1+focusTicks·0.01)`
87
+ * so both owners harden with focus identically — mechanism timing, not a
88
+ * disposition, so it stays a module constant (not a persona-tunable param).
89
+ */
90
+ const FOCUS_GAIN = 0.01
91
+ /** Ticks over which an awaiting action goes fully stale (matches the executor's
92
+ * await timeout) — a stale incumbent weakens and yields more readily. */
93
+ const AWAIT_STALE_TICKS = 15
94
+ /** How much a fully-stale awaiting incumbent's strength decays (0..1). */
95
+ const STALE_DECAY = 0.5
96
+
97
+ export class ActionSelector implements CognitiveEngine {
98
+ readonly name = 'action-selector'
99
+
100
+ private _bus: CognitiveBus | null = null
101
+ private _lastEntropy = 0
102
+ private _lastDeliberate = false
103
+
104
+ attachBus( bus: CognitiveBus ): void { this._bus = bus }
105
+
106
+ publishes(): CognitiveEventSchema[] {
107
+ return [
108
+ { type: 'agency.selection.made', version: 1, validate: () => null },
109
+ { type: 'agency.selection.ambiguous', version: 1, validate: () => null },
110
+ { type: 'agency.action.preempted', version: 1, validate: () => null },
111
+ ]
112
+ }
113
+ subscribes(): string[] { return [] }
114
+ onCognitiveEvent(): void { /* pull model — reads the field from frozen state */ }
115
+ snapshot(): Record<string, unknown> {
116
+ return { lastEntropy: this._lastEntropy, lastDeliberate: this._lastDeliberate }
117
+ }
118
+
119
+ async react(
120
+ _delta: Duration,
121
+ tick: Tick,
122
+ state: ReadonlySimulationState,
123
+ _context: SimulationContext,
124
+ ): Promise<EngineResult> {
125
+ // ── Gather the field + classify the in-flight action ──────────
126
+ // The Will acts serially (one body). But it is not frozen while busy: an
127
+ // action merely *awaiting* a slow host/delivery can be PREEMPTED by a
128
+ // sufficiently stronger / higher-stakes want (re-compete each tick, below). A
129
+ // short-lived 'selected' (one tick) or a mid-composite 'expanding' is left to
130
+ // finish — they resolve within a tick or carry their own progress.
131
+ const eligible: Affordance[] = []
132
+ const intents: Array<{ id: string; st: string; parentIntentId?: string; activation: number; schema: string; target: string; dispatchedAt: number }> = []
133
+ const expandingParents = new Set<string>()
134
+ for( const [ id, e ] of state.entities ){
135
+ if( e.type === 'agency.intent' ){
136
+ const m = ( e.metadata ?? {} ) as Record<string, unknown>
137
+ const st = str( m['status'] ) ?? ''
138
+ if( st === 'expanding' ) expandingParents.add( id )
139
+ intents.push({
140
+ id, st,
141
+ parentIntentId: str( m['parentIntentId'] ),
142
+ activation: num( m['activation'], 0 ),
143
+ schema: str( m['schema'] ) ?? '',
144
+ target: str( m['targetEntityId'] ) ?? '',
145
+ dispatchedAt: num( m['dispatchedAt'], tick ),
146
+ })
147
+ continue
148
+ }
149
+ if( e.type !== 'affordance' ) continue
150
+ const a = readAffordance( id, e.metadata )
151
+ if( a.available ) eligible.push( a )
152
+ }
153
+
154
+ // Classify in-flight: 'awaiting' and an 'expanding' composite are PREEMPTIBLE;
155
+ // a standalone 'selected', an orphan macro sub (its parent already cancelled), and
156
+ // 'deliberating' BLOCK — let them finish (race-safe).
157
+ let blocking = false
158
+ let awaiting: { id: string; activation: number; schema: string; target: string; dispatchedAt: number } | null = null
159
+ let composite: { id: string; activation: number; schema: string } | null = null
160
+ for( const it of intents ){
161
+ if( it.st === 'deliberating' ) blocking = true
162
+ else if( it.st === 'expanding' ) composite = { id: it.id, activation: it.activation, schema: it.schema }
163
+ else if( it.st === 'awaiting' ) awaiting = { id: it.id, activation: it.activation, schema: it.schema, target: it.target, dispatchedAt: it.dispatchedAt }
164
+ else if( it.st === 'selected' ){
165
+ const activeMacroSub = it.parentIntentId !== undefined && expandingParents.has( it.parentIntentId )
166
+ if( !activeMacroSub ) blocking = true // standalone or orphan sub → finish it
167
+ }
168
+ }
169
+
170
+ const busy = ( n: number ): EngineResult => ({ commands: { metrics: [
171
+ [ 'agency.field.eligible', n ], [ 'agency.selection.busy', 1 ],
172
+ ] } })
173
+
174
+ if( blocking ) return busy( eligible.length )
175
+
176
+ // Nothing afforded → idle (if an action is in flight, keep waiting).
177
+ if( eligible.length === 0 )
178
+ return { commands: { metrics: [ [ 'agency.field.eligible', 0 ], [ 'agency.selection.busy', ( awaiting || composite ) ? 1 : 0 ] ] } }
179
+
180
+ // ── Run the competition over the current field ────────────────
181
+ const bias = buildBias( state )
182
+ // Switch resistance = f(focus duration, conscientiousness): the persona-developed
183
+ // base (engine-config-action-selector.switchCost ⊕ prior) hardened by how long the
184
+ // Will has been committed (shared task_switch.current_focus_ticks). Computed once;
185
+ // both preemption paths scale it DOWN by the challenger's stakes.
186
+ const effSwitchCost = effectiveSwitchCost( state )
187
+ // Competition weights (base ⊕ prior): a steadier Will weighs risk less (bolder), an
188
+ // open Will weighs novelty more (curiosity pulls toward the unpracticed). Other weights
189
+ // stay at DEFAULT_WEIGHTS — only the two with a clean trait owner are developable.
190
+ const weights = effectiveWeights( state )
191
+ const scored: ScoredAffordance[] = eligible
192
+ .map( a => ({ affordance: a, activation: scoreAffordance( a, bias, weights ) }) )
193
+ .sort( ( x, y ) => y.activation - x.activation )
194
+
195
+ const winner = scored[0]
196
+ if( !winner ) return busy( eligible.length )
197
+
198
+ // ── Preempt a mid-composite routine (cancel-only) ─────────────
199
+ // A strong/high-stakes challenger CANCELS the routine: delete the parent so the
200
+ // executor's `_advance` guard (`if(!parent) return`) stops queueing sub-steps. We
201
+ // do NOT commit the challenger this tick — that would race the executor's in-tick
202
+ // macro-advance and double-enact. The current sub drains, then the next free tick
203
+ // selects. (Immediate-switch composite preemption needs deferred macro-advance.)
204
+ if( composite ){
205
+ const different = winner.affordance.schema !== composite.schema
206
+ const switchCost = effSwitchCost * ( 1 - stakes( winner.affordance, bias ) )
207
+ if( different && winner.activation > composite.activation + switchCost ){
208
+ if( this._bus ) try {
209
+ this._bus.publish({ type: 'agency.action.preempted', version: 1, sourceEngine: this.name,
210
+ salience: 0.8, payload: { from: composite.schema, to: winner.affordance.schema, activation: winner.activation, tick } })
211
+ } catch( err ){ logger.warn( `[selector] preempt publish failed: ${ err instanceof Error ? err.message : String( err ) }` ) }
212
+ return { commands: { delete: [ composite.id ], metrics: [
213
+ [ 'agency.field.eligible', eligible.length ],
214
+ [ 'agency.selection.busy', 1 ],
215
+ [ 'agency.selection.preempted', 1 ],
216
+ ] } }
217
+ }
218
+ return busy( eligible.length ) // routine continues
219
+ }
220
+
221
+ // ── Preempt an awaiting action (commit the challenger) ────────
222
+ // The incumbent carries a switch-cost bonus (hysteresis); a high-stakes challenger
223
+ // faces almost none, so salient events interrupt. Deleting an 'awaiting' intent is
224
+ // race-free: the executor never enacts it (only times it out at 15 ticks).
225
+ let preemptDelete: string | undefined
226
+ let preemptedFrom: string | undefined
227
+ if( awaiting ){
228
+ const sameAction = winner.affordance.schema === awaiting.schema
229
+ && ( winner.affordance.targetEntityId ?? '' ) === awaiting.target
230
+ if( sameAction ) return busy( eligible.length ) // field still favours what we await
231
+
232
+ const staleness = Math.min( 1, ( tick - awaiting.dispatchedAt ) / AWAIT_STALE_TICKS )
233
+ const incumbentStrength = awaiting.activation * ( 1 - staleness * STALE_DECAY )
234
+ const switchCost = effSwitchCost * ( 1 - stakes( winner.affordance, bias ) )
235
+
236
+ if( winner.activation <= incumbentStrength + switchCost )
237
+ return busy( eligible.length ) // not worth interrupting — keep waiting
238
+
239
+ preemptDelete = awaiting.id // PREEMPT — fall through and commit the challenger
240
+ preemptedFrom = awaiting.schema
241
+ }
242
+
243
+ // ── Commit the winner (fresh selection or preempting challenger) ──
244
+ const entropy = competitionEntropy( scored.map( s => s.activation ) ) // telemetry only
245
+ const second = scored[1]?.activation
246
+ const margin = second !== undefined ? winner.activation - second : 1
247
+ const stk = stakes( winner.affordance, bias )
248
+ const habit = winner.affordance.habitStrength
249
+ const relief = Math.min( 1, habit ) * HABIT_RELIEF
250
+
251
+ // Recruit System 2 when the top choice is genuinely contested (close runner-up)
252
+ // or consequential. A strong habit relaxes both gates (the gradient); the shared
253
+ // deliberativeness disposition (R1) shifts both gates the OTHER way for an analytical
254
+ // Will — wider margin gate (contest sooner) and lower stakes gate (deliberate at lower
255
+ // stakes). The two forces compose additively, each independent and bounded.
256
+ const deliberativeness = -( readPersonaPrior( state, 'engine-config-executive' )['deliberateThreshold'] ?? 0 )
257
+ const marginGate = Math.max( 0, MARGIN_THRESHOLD - relief + deliberativeness * DELIB_MARGIN_SENS )
258
+ const stakesGate = clamp01( BASE_STAKES_THRESHOLD + relief - deliberativeness * DELIB_STAKES_SENS )
259
+ const deliberate = margin < marginGate || stk > stakesGate
260
+
261
+ this._lastEntropy = entropy
262
+ this._lastDeliberate = deliberate
263
+
264
+ const intent: EntityInput = {
265
+ id: `agency-intent-${ tick }`,
266
+ type: 'agency.intent',
267
+ metadata: {
268
+ schema: winner.affordance.schema,
269
+ affordanceId: winner.affordance.id,
270
+ targetEntityId: winner.affordance.targetEntityId,
271
+ parameters: winner.affordance.parameters,
272
+ source: winner.affordance.source,
273
+ // Plan provenance (when a plan's frontier-step prior won the competition) —
274
+ // flows through the executor's action.outcome so the PlanningEngine advances.
275
+ ...( winner.affordance.planId ? { planId: winner.affordance.planId } : {} ),
276
+ ...( winner.affordance.stepId ? { stepId: winner.affordance.stepId } : {} ),
277
+ // forward-model priors carried so the executor can emit an efference copy
278
+ // without depending on the (now-cleared) transient affordance entity.
279
+ expectedReward: winner.affordance.expectedReward,
280
+ expectedValence: winner.affordance.expectedValence,
281
+ habitStrength: winner.affordance.habitStrength,
282
+ activation: winner.activation,
283
+ entropy,
284
+ stakes: stk,
285
+ deliberate,
286
+ // Ambiguous/high-stakes → hand to the Deliberator (System 2), which resolves
287
+ // it back to 'selected'. The executor ignores 'deliberating' intents. The
288
+ // candidate set is carried so the Deliberator can choose without the field.
289
+ status: deliberate ? 'deliberating' : 'selected',
290
+ candidates: deliberate
291
+ ? scored.slice( 0, 3 ).map( s => ({
292
+ schema: s.affordance.schema,
293
+ targetEntityId: s.affordance.targetEntityId,
294
+ parameters: s.affordance.parameters,
295
+ activation: s.activation,
296
+ // Channel B: flag a candidate that is an active plan's frontier step, so
297
+ // the deliberation facet can own "this is my plan's next step" in-character.
298
+ ...( s.affordance.source === 'plan' ? { fromPlan: true } : {} ),
299
+ }) )
300
+ : undefined,
301
+ // Channel B: a preempting challenger that's ALSO deliberating carries what it
302
+ // interrupted, so the deliberation facet can own the interruption in-character
303
+ // ("I was about to X, but this pulled me away"). Only meaningful while deliberating.
304
+ ...( preemptedFrom ? { preemptedFrom } : {} ),
305
+ tick,
306
+ },
307
+ }
308
+
309
+ if( this._bus ){
310
+ try {
311
+ this._bus.publish({
312
+ type: 'agency.selection.made',
313
+ version: 1,
314
+ sourceEngine: this.name,
315
+ salience: 0.6,
316
+ payload: { schema: winner.affordance.schema, activation: winner.activation, entropy, tick },
317
+ })
318
+ if( deliberate )
319
+ this._bus.publish({
320
+ type: 'agency.selection.ambiguous',
321
+ version: 1,
322
+ sourceEngine: this.name,
323
+ salience: 0.75,
324
+ payload: {
325
+ topSchemas: scored.slice( 0, 3 ).map( s => s.affordance.schema ),
326
+ entropy, stakes: stk, tick,
327
+ },
328
+ })
329
+ if( preemptedFrom )
330
+ this._bus.publish({
331
+ type: 'agency.action.preempted',
332
+ version: 1,
333
+ sourceEngine: this.name,
334
+ salience: 0.8,
335
+ payload: { from: preemptedFrom, to: winner.affordance.schema, activation: winner.activation, tick },
336
+ })
337
+ }
338
+ catch( err ){
339
+ logger.warn( `[selector] bus publish failed: ${ err instanceof Error ? err.message : String( err ) }` )
340
+ }
341
+ }
342
+
343
+ const commands: StateCommands = {
344
+ set: [ intent ],
345
+ ...( preemptDelete ? { delete: [ preemptDelete ] } : {} ),
346
+ metrics: [
347
+ [ 'agency.field.eligible', eligible.length ],
348
+ [ 'agency.selection.busy', 0 ],
349
+ [ 'agency.selection.entropy', entropy ],
350
+ [ 'agency.selection.margin', margin ],
351
+ [ 'agency.selection.deliberate', deliberate ? 1 : 0 ],
352
+ [ 'agency.selection.preempted', preemptedFrom ? 1 : 0 ],
353
+ [ 'agency.selection.activation', winner.activation ],
354
+ ],
355
+ }
356
+ return { commands }
357
+ }
358
+ }
359
+
360
+ // ─── switch resistance ───────────────────────────────────────────────────────
361
+
362
+ /**
363
+ * Effective preemption hysteresis = persona-developed base ⊕ focus hardening.
364
+ *
365
+ * R2 reconciliation: the selector and the TaskSwitcher are the two owners of one
366
+ * "switch resistance" disposition. They live at different scales (activation vs.
367
+ * goal-priority), so the selector keeps its own `switchCost` base — but develops
368
+ * it from the SAME conscientiousness driver and hardens it with the SAME focus
369
+ * signal (`task_switch.current_focus_ticks`) using the SAME formula shape the
370
+ * TaskSwitcher uses internally. One disposition, two owners, native scales.
371
+ */
372
+ function effectiveSwitchCost( state: ReadonlySimulationState ): number {
373
+ const params = readEffectiveParams( state, 'engine-config-action-selector' )
374
+ const base = num( params['switchCost'], BASE_SWITCH_COST )
375
+ const focusTicks = metric( state, 'task_switch.current_focus_ticks', 0 )
376
+ return base * ( 1 + focusTicks * FOCUS_GAIN )
377
+ }
378
+
379
+ /**
380
+ * Competition weights = DEFAULT_WEIGHTS with the two trait-owned ones (risk, novelty)
381
+ * overridden by their developed values (base ⊕ prior). Negative weights are clamped to
382
+ * 0 (a prior can soften a weight to indifference but never invert its sign). The other
383
+ * weights have no clean single-trait owner, so they stay fixed (see the TODO catalogue).
384
+ */
385
+ function effectiveWeights( state: ReadonlySimulationState ): ScoreWeights {
386
+ const p = readEffectiveParams( state, 'engine-config-action-selector' )
387
+ return {
388
+ ...DEFAULT_WEIGHTS,
389
+ risk: Math.max( 0, num( p['riskWeight'], DEFAULT_WEIGHTS.risk ) ),
390
+ novelty: Math.max( 0, num( p['noveltyWeight'], DEFAULT_WEIGHTS.novelty ) ),
391
+ }
392
+ }
393
+
394
+ // ─── bias assembly ───────────────────────────────────────────────────────────
395
+
396
+ function buildBias( state: ReadonlySimulationState ): BiasContext {
397
+ // Entity links recognized from `targetEntityId`/`requestingEntityId` metadata AND
398
+ // `keid:` tags — shared with the synthesizer via collectGoalTargets so both stages
399
+ // see the same goal→entity links (incl. KnownEntityTracker's curiosity goals).
400
+ const goalTargets = new Set<string>( collectGoalTargets( state ).keys() )
401
+ let maxGoalPriority = 0
402
+
403
+ for( const e of state.entities.values() ){
404
+ if( e.type !== 'goal' ) continue
405
+ const m = e.metadata
406
+ const status = str( m?.['status'] )
407
+ if( status !== 'active' && status !== 'in_progress' ) continue
408
+ maxGoalPriority = Math.max( maxGoalPriority, num( m?.['priority'], 0 ) )
409
+ }
410
+
411
+ return {
412
+ goalTargets,
413
+ maxGoalPriority: clamp01( maxGoalPriority ),
414
+ drives: {
415
+ energy: clamp01( ( 100 - metric( state, 'energy.level', 100 ) ) / 100 ),
416
+ sleep: clamp01( metric( state, 'sleep.pressure', 0 ) / 100 ),
417
+ stress: clamp01( metric( state, 'stress.load', 0 ) / 100 ),
418
+ social: clamp01( metric( state, 'drive.social', 0 ) ),
419
+ },
420
+ threat: clamp01( metric( state, 'threat.level', 0 ) ),
421
+ inhibition: clamp01( metric( state, 'inhibition.level', 0 ) ),
422
+ }
423
+ }
424
+
425
+ // ─── entity decoding ─────────────────────────────────────────────────────────
426
+
427
+ function readAffordance( id: string, m: ReadonlyMap<string, unknown> | Record<string, unknown> | undefined ): Affordance {
428
+ const meta = ( m ?? {} ) as Record<string, unknown>
429
+ return {
430
+ id,
431
+ schema: str( meta['schema'] ) ?? '',
432
+ source: ( str( meta['source'] ) as AffordanceSource ) ?? 'innate',
433
+ parameters: ( meta['parameters'] as Record<string, unknown> ) ?? {},
434
+ targetEntityId: str( meta['targetEntityId'] ),
435
+ evokedBy: str( meta['evokedBy'] ),
436
+ expectedValence: num( meta['expectedValence'], 0 ),
437
+ expectedReward: num( meta['expectedReward'], 0 ),
438
+ cost: num( meta['cost'], 0 ),
439
+ habitStrength: num( meta['habitStrength'], 0 ),
440
+ available: meta['available'] === true,
441
+ tags: Array.isArray( meta['tags'] ) ? ( meta['tags'] as unknown[] ).filter( ( t ): t is string => typeof t === 'string' ) : [],
442
+ planBias: typeof meta['planBias'] === 'number' ? ( meta['planBias'] as number ) : undefined,
443
+ planId: str( meta['planId'] ),
444
+ stepId: str( meta['stepId'] ),
445
+ tick: num( meta['tick'], 0 ),
446
+ }
447
+ }
448
+
449
+ // ─── helpers ─────────────────────────────────────────────────────────────────
450
+
451
+ function metric( state: ReadonlySimulationState, key: string, fallback: number ): number {
452
+ return state.metrics.get( key ) ?? fallback
453
+ }
454
+ function num( v: unknown, fallback: number ): number {
455
+ return typeof v === 'number' && Number.isFinite( v ) ? v : fallback
456
+ }
457
+ function str( v: unknown ): string | undefined {
458
+ return typeof v === 'string' ? v : undefined
459
+ }
460
+ function clamp01( n: number ): number {
461
+ return n < 0 ? 0 : n > 1 ? 1 : n
462
+ }