@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,605 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/faculties/executive.engine/context.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import type { ReadonlySimulationState } from '#core/types'
6
+ import type { GoalManager } from '#faculties/goal.manager'
7
+ import type { WorkingMemory } from '#faculties/working.memory'
8
+ import type { ExecutiveContext } from '#faculties/executive.engine/types'
9
+ import type { EpisodicConsolidator } from '#faculties/episodic.consolidator'
10
+ import type { SemanticIntegrator } from '#faculties/semantic.engine/integrator'
11
+ import { readEffectiveParams, summarizePersonaPrior } from '#cognition/persona.prior'
12
+
13
+ export interface ContextDependencies {
14
+ workingMemory: WorkingMemory | null
15
+ goalManager: GoalManager | null
16
+ episodicConsolidator: EpisodicConsolidator | null
17
+ semanticIntegrator: SemanticIntegrator | null
18
+ }
19
+
20
+ export async function buildExecutiveContext(
21
+ state: ReadonlySimulationState,
22
+ deps: ContextDependencies,
23
+ /**
24
+ * Optional override for the episodic-recall query. When a focus supplies it
25
+ * (e.g. a conversation facet passes the current message), it drives the single
26
+ * "## Relevant Memories" section instead of the general state+goals query —
27
+ * so conversation recall is message-relevant AND there is exactly one recall
28
+ * surface (no separate per-focus recall block).
29
+ */
30
+ recallQuery?: string,
31
+ ): Promise<ExecutiveContext> {
32
+ // Identity — read directly from the identity-self entity.
33
+ const identityEntity = state.entities.get('identity-self')
34
+ // name is the canonical persona name from WillConfig.name (e.g. "Aria", "Guard-1").
35
+ // Falls back to 'Will' for snapshots created before this field was added.
36
+ const identityName = (identityEntity?.metadata?.name as string) ?? 'Will'
37
+ const identityPrompt = (identityEntity?.metadata?.prompt as string) ?? 'I am a self-aware mind.'
38
+ const identityValues = (identityEntity?.metadata?.values as string[]) ?? []
39
+ const identityTraits = (identityEntity?.metadata?.traits as Record<string, number>) ?? {}
40
+ // Per-trait baseline + recency (graded salience B/C) — present once the self-model has evaluated.
41
+ const identityTraitStats = identityEntity?.metadata?.traitStats as
42
+ Record<string, { mean: number; shiftDir: number; shiftTick: number }> | undefined
43
+ const identityStyle = (identityEntity?.metadata?.style as string) ?? 'natural and authentic'
44
+
45
+ // Working memory — getItems() returns WMItem[] sorted by activation descending.
46
+ // Filter types that are already rendered elsewhere in the prompt:
47
+ // conversation.exchange → shown in Recent Action Outcomes
48
+ // goal → shown in Active Goals
49
+ const WM_FILTER_TYPES = new Set([ 'conversation.exchange', 'goal' ])
50
+ const wmItems = ( deps.workingMemory?.getItems() ?? [] )
51
+ .filter( item => !WM_FILTER_TYPES.has( item.type ) )
52
+ const workingMemory = wmItems.map( item => ({
53
+ type: item.type,
54
+ summary: extractSummary( item.content ),
55
+ activation: item.activation
56
+ }))
57
+
58
+ // Episodic memory — semantic query for relevant memories based on current goals
59
+ // and affective state. Falls back to recent query if semantic not available.
60
+ let memories: ExecutiveContext['memories'] = []
61
+ // Plan ids surfaced by THIS recall (descriptors carry planId) — feeds the
62
+ // relevance filter on the Active Plans awareness section (recall-scoped awareness).
63
+ let relevantPlanIds: string[] = []
64
+
65
+ if( deps.episodicConsolidator ){
66
+ // Focus-supplied query (e.g. the live conversation message) takes precedence;
67
+ // otherwise build a general query from current state + goals.
68
+ const semanticQuery = recallQuery && recallQuery.trim().length > 0
69
+ ? recallQuery
70
+ : buildSemanticQuery( state, deps.goalManager )
71
+
72
+ try {
73
+ // Try semantic search first (if vector memory configured).
74
+ // Omit minSimilarity → defers to the adapter's configured default (0.35,
75
+ // tuned for real sentence embeddings). Hardcoding 0.65 here starved recall.
76
+ // affectiveBias adds gentle mood-congruent recall: memories encoded in a
77
+ // mood like the current one surface a little more readily (weight 0.15 so
78
+ // similarity still dominates).
79
+ const moodValence = state.metrics.get('affect.valence') ?? 0
80
+ const semanticResults = await deps.episodicConsolidator.semanticQuery(
81
+ semanticQuery,
82
+ { limit: 10, affectiveBias: { valence: moodValence, weight: 0.15 } }
83
+ )
84
+
85
+ // Augment with recent memories for freshness
86
+ const recentResults = deps.episodicConsolidator.query({ limit: 5 })
87
+
88
+ // Deduplicate by id and combine
89
+ const seen = new Set<string>()
90
+ const combined: typeof semanticResults = []
91
+
92
+ for( const ep of semanticResults ){
93
+ if( !seen.has( ep.id ) ){
94
+ seen.add( ep.id )
95
+ combined.push( ep )
96
+ }
97
+ }
98
+ for( const ep of recentResults ){
99
+ if( !seen.has( ep.id ) ){
100
+ seen.add( ep.id )
101
+ combined.push( ep )
102
+ }
103
+ }
104
+
105
+ // Filter goal-type episodes — they duplicate the Active Goals section
106
+ const recalled = combined.filter( ep => ep.sourceType !== 'goal' ).slice( 0, 8 )
107
+ relevantPlanIds = collectPlanIds( recalled )
108
+ memories = recalled.map( mapEpisodeToMemory )
109
+ // Recall reinforces retention: marking each surfaced episode as retrieved
110
+ // increments its retrievalCount, which unlocks the ForgettingCurve's
111
+ // retrievalBoost so actively-recalled memories decay slower than unused ones.
112
+ for( const ep of recalled ) deps.episodicConsolidator?.markRetrieved( ep.id, state.tick )
113
+ }
114
+ catch( err ){
115
+ // Fallback to recent query if semantic fails (no vector memory configured).
116
+ // Sort by createdAt descending so the most recent memories surface first —
117
+ // the default query order may not be chronological when using PMA replay.
118
+ const fallbackResults = deps.episodicConsolidator.query({ limit: 20 })
119
+ const recalled = fallbackResults
120
+ .filter( ep => ep.sourceType !== 'goal' )
121
+ .slice()
122
+ .sort( ( a, b ) => ( ( b.createdAt as unknown as number ) ?? 0 ) - ( ( a.createdAt as unknown as number ) ?? 0 ) )
123
+ .slice( 0, 8 )
124
+ relevantPlanIds = collectPlanIds( recalled )
125
+ memories = recalled.map( mapEpisodeToMemory )
126
+ for( const ep of recalled ) deps.episodicConsolidator?.markRetrieved( ep.id, state.tick )
127
+ }
128
+ }
129
+
130
+ // Goals — built before beliefs so their tags can inform belief relevance scoring.
131
+ const goalsList = deps.goalManager?.getActiveGoals() ?? []
132
+ const activeGoalTags = new Set( goalsList.flatMap( g => g.tags ) )
133
+ const goals = goalsList.map( g => ({
134
+ id: g.id,
135
+ description: g.description,
136
+ priority: g.priority,
137
+ progress: g.progress,
138
+ status: g.status,
139
+ deadline: g.deadline as number | undefined,
140
+ lastActionAttemptTick: g.lastActionAttemptTick,
141
+ lastActionType: g.lastActionType,
142
+ }))
143
+
144
+ // Beliefs — deduplicated, scored by relevance, and capped to prevent attention dilution.
145
+ //
146
+ // Pipeline:
147
+ // 1. Dedup: within the same category, drop beliefs with >DEDUP_THRESHOLD Jaccard
148
+ // similarity to an already-accepted belief (keep higher-confidence version).
149
+ // 2. Score: confidence × recency × goal-tag-match.
150
+ // 3. Cap: at most PER_CATEGORY_CAP per category, then global BELIEF_PROMPT_LIMIT.
151
+ //
152
+ // Constants:
153
+ // BELIEF_PROMPT_LIMIT — max beliefs in the prompt (was 50, reduced to 30)
154
+ // PER_CATEGORY_CAP — no single category dominates attention
155
+ // DEDUP_THRESHOLD — Jaccard similarity above which a belief is a duplicate
156
+ // RECENCY_HALF_LIFE — ticks after which a belief's recency score halves
157
+ // GOAL_TAG_BOOST — multiplier when a belief shares tags with active goals
158
+ // RECENCY_FLOOR — minimum recency so old high-value beliefs aren't buried
159
+ const BELIEF_PROMPT_LIMIT = 30
160
+ const PER_CATEGORY_CAP = 8
161
+ const DEDUP_THRESHOLD = 0.70
162
+ const RECENCY_HALF_LIFE = 200
163
+ const GOAL_TAG_BOOST = 1.4
164
+ const RECENCY_FLOOR = 0.4
165
+
166
+ const allBeliefs = deps.semanticIntegrator?.getBeliefs() ?? []
167
+ const currentTick = state.tick as unknown as number
168
+
169
+ // ── Stage 1: Deduplication ────────────────────────────────
170
+ const _tokenize = ( s: string ): Set<string> =>
171
+ new Set( s.toLowerCase().replace( /[^a-z0-9 ]/g, '' ).split( /\s+/ ).filter( Boolean ) )
172
+
173
+ const _jaccard = ( a: Set<string>, b: Set<string> ): number => {
174
+ let intersection = 0
175
+ for( const t of a ) if( b.has( t ) ) intersection++
176
+ return intersection / ( a.size + b.size - intersection )
177
+ }
178
+
179
+ // Sort highest-confidence first so the winner of each duplicate cluster is kept.
180
+ const sortedForDedup = [ ...allBeliefs ].sort( ( a, b ) => b.confidence - a.confidence )
181
+ const acceptedBeliefs: Array<(typeof allBeliefs)[number]> = []
182
+ const acceptedTokens: Array<{ tokens: Set<string>; category: string }> = []
183
+
184
+ for( const belief of sortedForDedup ){
185
+ const tokens = _tokenize( belief.statement )
186
+ const isDuplicate = acceptedTokens.some( ({ tokens: ut, category: uc }) =>
187
+ uc === belief.category && _jaccard( tokens, ut ) > DEDUP_THRESHOLD
188
+ )
189
+ if( !isDuplicate ){
190
+ acceptedBeliefs.push( belief )
191
+ acceptedTokens.push({ tokens, category: belief.category })
192
+ }
193
+ }
194
+
195
+ const beliefsOmittedByDedup = allBeliefs.length - acceptedBeliefs.length
196
+
197
+ // ── Stage 2: Relevance scoring ────────────────────────────
198
+ const scoredBeliefs = acceptedBeliefs
199
+ .map( b => {
200
+ const ticksSince = Math.max( 0, currentTick - ( b.lastUpdatedAt as unknown as number ) )
201
+ const recency = Math.max( RECENCY_FLOOR, 1 / ( 1 + ticksSince / RECENCY_HALF_LIFE ) )
202
+ const goalMatch = b.tags.some( t => activeGoalTags.has( t ) ) ? GOAL_TAG_BOOST : 1.0
203
+ return { b, score: b.confidence * recency * goalMatch }
204
+ } )
205
+ .sort( ( a, b ) => b.score - a.score )
206
+
207
+ // ── Stage 3: Per-category cap + global cap ────────────────
208
+ const categoryCount = new Map<string, number>()
209
+ const cappedBeliefs: typeof scoredBeliefs = []
210
+ for( const item of scoredBeliefs ){
211
+ const cat = item.b.category
212
+ const count = categoryCount.get( cat ) ?? 0
213
+ if( count >= PER_CATEGORY_CAP ) continue
214
+ categoryCount.set( cat, count + 1 )
215
+ cappedBeliefs.push( item )
216
+ if( cappedBeliefs.length >= BELIEF_PROMPT_LIMIT ) break
217
+ }
218
+
219
+ const beliefs = cappedBeliefs
220
+ .map( ( { b } ) => ({ statement: b.statement, category: b.category, confidence: b.confidence }))
221
+
222
+ const beliefsOmitted = Math.max( 0, allBeliefs.length - beliefs.length )
223
+ void beliefsOmittedByDedup // surfaced via beliefsOmitted total
224
+
225
+ // Percepts — extract from state entities, sort by salience, take top 10.
226
+ const percepts = extractPercepts( state )
227
+
228
+ // Affect — read from state entities and computed metrics.
229
+ // dominantEmotion and blends are strings/arrays stored on affect-related entities,
230
+ // not in the numeric metrics map.
231
+ const affect = extractAffect( state )
232
+
233
+ // Behavioral disposition — PMA seeds risk/exploration/impulsivity onto
234
+ // engine-config-executive at session start, and the persona-prior mirror then
235
+ // *develops* it (base ⊕ prior): exploration grows with openness/creativity,
236
+ // impulsivity falls as conscientiousness is demonstrated. So it is no longer static
237
+ // per session. `readEffectiveParams` only layers a prior onto a key the base already
238
+ // has, so absent a PMA seed this degrades to base ⇒ undefined disposition.
239
+ const execParams = readEffectiveParams( state, 'engine-config-executive' )
240
+ const behavioralDisposition = (
241
+ execParams.riskTolerance !== undefined ||
242
+ execParams.explorationRate !== undefined ||
243
+ execParams.impulsivity !== undefined
244
+ ) ? {
245
+ riskTolerance: execParams.riskTolerance ?? 0.5,
246
+ explorationRate: execParams.explorationRate ?? 0.3,
247
+ impulsivity: execParams.impulsivity ?? 0.3,
248
+ } : undefined
249
+
250
+ // Recent action outcomes — scan decision.record entities that have been processed
251
+ // (actionStatus is set) and return the last 5 most recent, newest first.
252
+ // This closes the Act → Confirm → Perceive loop: the executive can see what it
253
+ // tried, whether it landed, and whether an external dispatch went unanswered.
254
+ const recentActions: Array<{
255
+ type: string; status: 'completed' | 'failed' | 'awaiting_host' | 'timed_out'
256
+ tick: number; outcome: string; planId?: string
257
+ }> = []
258
+
259
+ for( const entity of state.entities.values() ){
260
+ if( entity.type !== 'decision.record' ) continue
261
+ const actionStatus = entity.metadata?.actionStatus as string | undefined
262
+ if( !actionStatus ) continue
263
+ if( !( [ 'completed', 'failed', 'awaiting_host', 'timed_out' ] as string[] ).includes( actionStatus ) ) continue
264
+
265
+ recentActions.push({
266
+ type: ( entity.metadata?.actionType as string ) ?? 'unknown',
267
+ status: actionStatus as 'completed' | 'failed' | 'awaiting_host' | 'timed_out',
268
+ tick: ( entity.metadata?.executionTick as number ) ?? ( entity.metadata?.dispatchedAt as number ) ?? 0,
269
+ outcome: String( entity.metadata?.outcome ?? '' ).slice( 0, 120 ),
270
+ planId: entity.metadata?.planId as string | undefined,
271
+ })
272
+ }
273
+
274
+ // Sort newest-first, cap at 5
275
+ recentActions.sort( ( a, b ) => b.tick - a.tick )
276
+ const recentActionsCapped = recentActions.slice( 0, 5 )
277
+
278
+ // Active/known plans — read persisted `plan` entities so the executive has
279
+ // execution awareness: which plans exist per goal, their status + step
280
+ // progress, enabling it to target a specific plan by id when managing several. (P4)
281
+ const plans: ExecutiveContext['plans'] = []
282
+ for( const entity of state.entities.values() ){
283
+ if( entity.type !== 'plan' ) continue
284
+ const m = entity.metadata ?? {}
285
+ const steps = ( m['steps'] as Array<{ status?: string }> | undefined ) ?? []
286
+ plans.push({
287
+ id: entity.id,
288
+ goalId: ( m['goalId'] as string ) ?? '',
289
+ status: ( m['status'] as string ) ?? 'unknown',
290
+ executionTier: ( m['executionTier'] as string ) ?? 'automatic',
291
+ totalSteps: steps.length,
292
+ completedSteps: steps.filter( s => s.status === 'completed' || s.status === 'skipped' ).length,
293
+ expectedOutcome: ( m['expectedOutcome'] as string ) ?? '',
294
+ requestingEntityId: m['requestingEntityId'] as string | undefined,
295
+ })
296
+ }
297
+
298
+ // How the Will has self-tuned its own faculties (metacognition persona-prior).
299
+ const selfTuningList = summarizePersonaPrior( state ).map( a => a.description )
300
+ const selfTuning = selfTuningList.length > 0 ? selfTuningList : undefined
301
+
302
+ return {
303
+ identity: {
304
+ name: identityName,
305
+ prompt: identityPrompt,
306
+ values: identityValues,
307
+ traits: identityTraits,
308
+ traitStats: identityTraitStats,
309
+ style: identityStyle
310
+ },
311
+ worldState: {
312
+ energyLevel: state.metrics.get('energy.level') ?? 100,
313
+ sleepPressure: state.metrics.get('sleep.pressure') ?? 0,
314
+ stressLoad: state.metrics.get('stress.load') ?? 0,
315
+ circadianPhase: state.metrics.get('circadian.phase') ?? 0,
316
+ timeOfDay: state.metrics.get('time.of_day') ?? 12,
317
+ // Tonic threat representation — survives event habituation (guardrail).
318
+ threatLevel: state.metrics.get('threat.level') ?? 0
319
+ },
320
+ affect,
321
+ goals,
322
+ plans,
323
+ relevantPlanIds,
324
+ percepts,
325
+ workingMemory,
326
+ memories,
327
+ beliefs,
328
+ beliefsOmitted,
329
+ recentActions: recentActionsCapped,
330
+ behavioralDisposition,
331
+ selfTuning,
332
+ knownEntities: extractKnownEntities( state ),
333
+ currentFocus: extractCurrentFocus( state, goals )
334
+ }
335
+ }
336
+
337
+ /**
338
+ * What the Will is currently focused on (the `task-switch-focus` entity from task.switcher)
339
+ * plus the effort cost of switching away (the `task_switch.switch_cost` metric = effective
340
+ * baseSwitchCost). Surfaces task-persistence into the executive's reasoning — previously
341
+ * task.switcher computed this but it reached nothing. Undefined when nothing is in focus.
342
+ */
343
+ export function extractCurrentFocus(
344
+ state: ReadonlySimulationState,
345
+ goals: ExecutiveContext['goals']
346
+ ): ExecutiveContext['currentFocus'] {
347
+ const m = state.entities.get( 'task-switch-focus' )?.metadata as Record<string, unknown> | undefined
348
+ if( !m?.goalId ) return undefined
349
+ return {
350
+ goalId: m.goalId as string,
351
+ goalDescription: goals.find( g => g.id === m.goalId )?.description,
352
+ focusTicks: ( m.focusTicks as number ) ?? 0,
353
+ switchCost: state.metrics.get( 'task_switch.switch_cost' ) ?? 0,
354
+ }
355
+ }
356
+
357
+ /**
358
+ * The Will's social models — its understanding of other agents. Joins the social-cognition
359
+ * stack by keid: theory-of-mind (`tom-*`: what they want/feel), reputation
360
+ * (`reputation-*`: trust/cooperativeness), and attachment (`bond-*`: closeness). Sorted by
361
+ * interaction recency and capped, so the executive can reason about *whom it is dealing
362
+ * with* without flooding the prompt. Undefined when the Will knows no one.
363
+ */
364
+ export function extractKnownEntities( state: ReadonlySimulationState ): ExecutiveContext['knownEntities'] {
365
+ type Acc = NonNullable<ExecutiveContext['knownEntities']>[number] & { _recency: number }
366
+ const byKeid = new Map<string, Acc>()
367
+
368
+ // Recognition (Phase 5): redirect a fused alias keid to its canonical referent, so the
369
+ // triple / dossier / beliefs under the old keid all aggregate under the one someone.
370
+ const alias = new Map<string, string>()
371
+ for( const e of state.entities.values() )
372
+ if( e.type === 'known-entity-alias' ){
373
+ const a = e.metadata?.aliasKeid as string | undefined
374
+ const c = e.metadata?.canonicalKeid as string | undefined
375
+ if( a && c ) alias.set( a, c )
376
+ }
377
+ const get = ( id: string ): Acc => {
378
+ id = alias.get( id ) ?? id
379
+ let a = byKeid.get( id )
380
+ // kind is 'sentient' for the whole social triple; 'thing' dossiers arrive with the
381
+ // generalised store. name stays undefined until the Will actually learns it.
382
+ if( !a ){ a = { keid: id, kind: 'sentient', _recency: 0 }; byKeid.set( id, a ) }
383
+ return a
384
+ }
385
+
386
+ for( const e of state.entities.values() ){
387
+ const m = ( e.metadata ?? {} ) as Record<string, unknown>
388
+ if( e.type === 'theory_of_mind' && m.keid ){
389
+ const a = get( m.keid as string )
390
+ a.intention = ( m.dominantIntention as string | null ) ?? undefined
391
+ a.emotion = ( m.estimatedEmotion as string | null ) ?? undefined
392
+ a.confidence = m.modelConfidence as number
393
+ }
394
+ else if( e.type === 'reputation' && m.keid ){
395
+ const a = get( m.keid as string )
396
+ if( typeof m.name === 'string' ) a.name = m.name
397
+ a.trust = m.trustworthiness as number
398
+ a.cooperativeness = m.cooperativeness as number
399
+ a.reliability ??= m.reliability as number // sentient social reliability — the dossier's wins if present
400
+ a._recency = Math.max( a._recency, ( m.lastInteractionTick as number ) ?? 0 )
401
+ }
402
+ else if( e.type === 'known-entity' && m.keid ){
403
+ // The perceptual dossier (known.entity.tracker): a learned name, the kind, recency,
404
+ // and the *general* track-record reliability (decision #3 — authoritative over the
405
+ // sentient social one). (familiarity is intentionally NOT surfaced — it decays each
406
+ // tick and would churn the cached prompt; it stays internal to the tracker.)
407
+ const a = get( m.keid as string )
408
+ if( typeof m.name === 'string' ) a.name = m.name
409
+ if( m.kind === 'thing' || m.kind === 'sentient' ) a.kind = m.kind as 'thing' | 'sentient'
410
+ if( typeof m.reliability === 'number' ) a.reliability = m.reliability as number
411
+ a._recency = Math.max( a._recency, ( m.lastSeenTick as number ) ?? 0 )
412
+ }
413
+ else if( e.type === 'attachment.bond' ){
414
+ const keid = ( m.keid as string ) ?? e.id.replace( /^bond-/, '' )
415
+ get( keid ).closeness = m.attachmentStrength as number
416
+ }
417
+ }
418
+
419
+ const entities = [ ...byKeid.values() ]
420
+ .sort( ( a, b ) => b._recency - a._recency || ( b.closeness ?? 0 ) - ( a.closeness ?? 0 ) )
421
+ .slice( 0, 6 )
422
+ .map( ( { _recency, ...rest } ) => rest )
423
+
424
+ return entities.length > 0 ? entities : undefined
425
+ }
426
+
427
+ /**
428
+ * Build a semantic query string from current cognitive state.
429
+ * Combines active goals, current affect, and recent percepts.
430
+ */
431
+ function buildSemanticQuery(
432
+ state: ReadonlySimulationState,
433
+ goalManager: GoalManager | null
434
+ ): string {
435
+ const parts: string[] = []
436
+
437
+ // Active goals (highest priority first)
438
+ const goals = goalManager?.getActiveGoals().slice( 0, 3 ) ?? []
439
+ if( goals.length > 0 ){
440
+ parts.push( `Current goals: ${goals.map( g => g.description ).join( '; ' )}` )
441
+ }
442
+
443
+ // Dominant emotion if intense
444
+ const valence = state.metrics.get('affect.valence') ?? 0
445
+ const dominantEmotion = valence > 0.3 ? 'positive' : valence < -0.3 ? 'negative' : 'neutral'
446
+ if( dominantEmotion !== 'neutral' ) parts.push( `Feeling ${dominantEmotion}` )
447
+
448
+ // Active conversation entities — pulls memories about current interlocutors
449
+ const senderNames = new Set<string>()
450
+ for( const entity of state.entities.values() ){
451
+ if( entity.type !== 'communication' ) continue
452
+ const msgTick = ( entity.metadata?.tick as number ) ?? 0
453
+ if( state.tick - msgTick > 30 ) continue
454
+ if( entity.metadata?.processedByExecutive ) continue
455
+ const name = entity.metadata?.agentName as string | undefined
456
+ if( name && name !== 'unknown' ) senderNames.add( name )
457
+ }
458
+ if( senderNames.size > 0 ) parts.push( `Talking with: ${[ ...senderNames ].join( ', ' )}` )
459
+
460
+ // Recent percepts (top 3 by salience)
461
+ const percepts = extractPercepts( state ).slice( 0, 3 )
462
+ if( percepts.length > 0 ){
463
+ parts.push( `Recently observed: ${percepts.map( p => p.summary ).join( '; ' )}` )
464
+ }
465
+
466
+ return parts.length > 0 ? parts.join( '. ' ) : 'Current situation'
467
+ }
468
+
469
+ /** Pull planIds from recalled plan-descriptor episodes (their content carries planId). */
470
+ function collectPlanIds( episodes: Array<{ content: unknown }> ): string[] {
471
+ const ids: string[] = []
472
+ for( const ep of episodes ){
473
+ const c = ep.content as Record<string, any> | null | undefined
474
+ const pid = c?.[ 'planId' ] ?? c?.[ 'content' ]?.[ 'planId' ]
475
+ if( typeof pid === 'string' ) ids.push( pid )
476
+ }
477
+ return [ ...new Set( ids ) ]
478
+ }
479
+
480
+ function _extractEpisodeContent( raw: unknown ): string {
481
+ if( typeof raw === 'string' ) return raw
482
+ if( typeof raw !== 'object' || raw === null ) return String( raw )
483
+ const c = raw as Record<string, unknown>
484
+
485
+ // WMItem wrapper: { wmType, content: { description | summary | ... } }
486
+ // Check wmType first so we correctly unwrap structured WM items.
487
+ if( typeof c['wmType'] === 'string' ){
488
+ const inner = c['content']
489
+ if( typeof inner === 'string' ) return inner
490
+ if( typeof inner === 'object' && inner !== null ){
491
+ const ic = inner as Record<string, unknown>
492
+ if( typeof ic['description'] === 'string' ) return ic['description']
493
+ if( typeof ic['summary'] === 'string' ) return ic['summary']
494
+ if( typeof ic['userMessage'] === 'string' ){
495
+ const reply = typeof ic['willReply'] === 'string' ? ` → "${ic['willReply']}"` : ''
496
+ return `"${ic['userMessage']}"${reply}`
497
+ }
498
+ }
499
+ }
500
+
501
+ // Plain percept WMItem: { summary } or nested { content: { summary } }
502
+ if( typeof c['summary'] === 'string' ) return c['summary']
503
+ const inner = c['content']
504
+ if( typeof inner === 'string' ) return inner
505
+ if( typeof inner === 'object' && inner !== null ){
506
+ const ic = inner as Record<string, unknown>
507
+ if( typeof ic['summary'] === 'string' ) return ic['summary']
508
+ if( typeof ic['description'] === 'string' ) return ic['description']
509
+ }
510
+ // Belief: { statement }
511
+ if( typeof c['statement'] === 'string' ) return c['statement']
512
+ // Conversation turn: { userMessage, willReply }
513
+ if( typeof c['userMessage'] === 'string' ){
514
+ const reply = typeof c['willReply'] === 'string' ? ` → "${c['willReply']}"` : ''
515
+ return `"${c['userMessage']}"${reply}`
516
+ }
517
+ // Goal: { description }
518
+ if( typeof c['description'] === 'string' ) return c['description']
519
+ return JSON.stringify( raw ).slice( 0, 200 )
520
+ }
521
+
522
+ function mapEpisodeToMemory( ep: {
523
+ content: unknown
524
+ emotionalTags?: Record<string, number>
525
+ activationStrength: number
526
+ timestamp?: unknown
527
+ } ): {
528
+ content: string
529
+ relevance: number
530
+ emotionalContext: string
531
+ tick?: number
532
+ } {
533
+ const dominantEmotion = Object.entries( ep.emotionalTags ?? {} )
534
+ .sort( ( [, a], [, b] ) => b - a )[0]?.[0] ?? 'neutral'
535
+
536
+ return {
537
+ content: _extractEpisodeContent( ep.content ),
538
+ relevance: ep.activationStrength,
539
+ emotionalContext: dominantEmotion,
540
+ tick: typeof ep.timestamp === 'number' ? ep.timestamp : undefined
541
+ }
542
+ }
543
+
544
+ function extractSummary( content: unknown ): string {
545
+ if( typeof content === 'string' )
546
+ return content.slice( 0, 120 )
547
+
548
+ if( content && typeof content === 'object' ){
549
+ const obj = content as Record<string, unknown>
550
+
551
+ return (obj.summary as string)
552
+ ?? (obj.description as string)
553
+ ?? JSON.stringify( content ).slice( 0, 120 )
554
+ }
555
+
556
+ return String( content ?? '' ).slice( 0, 120 )
557
+ }
558
+
559
+ function extractPercepts( state: ReadonlySimulationState ): Array<{ category: string; summary: string; salience: number }> {
560
+ const percepts: Array<{ category: string; summary: string; salience: number }> = []
561
+
562
+ for( const entity of state.entities.values() ){
563
+ if( entity.type !== 'percept' && entity.type !== 'percept.social') continue
564
+
565
+ const summary = (entity.metadata?.summary as string)
566
+ ?? (entity.metadata?.content as string)
567
+ ?? ''
568
+ if( !summary || summary.startsWith('New percept:') ) continue
569
+
570
+ percepts.push({
571
+ category: (entity.metadata?.category as string) ?? 'general',
572
+ summary,
573
+ salience: (entity.metadata?.salience as number) ?? 0
574
+ })
575
+ }
576
+
577
+ return percepts.sort( ( a, b ) => b.salience - a.salience ).slice( 0, 10 )
578
+ }
579
+
580
+ /**
581
+ * Extract affective state from the simulation.
582
+ *
583
+ * Valence, arousal, and dominance are numeric metrics stored in state.metrics
584
+ * (Map<string, number>). dominantEmotion and blends are non-numeric — they come
585
+ * from the affective state entity written by AffectiveBlender each tick.
586
+ */
587
+ function extractAffect( state: ReadonlySimulationState ): ExecutiveContext['affect'] {
588
+ // Numeric core affect dimensions — these ARE in the metrics map.
589
+ const valence = state.metrics.get('affect.valence') ?? 0
590
+ const arousal = state.metrics.get('affect.arousal') ?? 0.5
591
+ const dominance = state.metrics.get('affect.dominance') ?? 0.5
592
+
593
+ // Non-numeric affect data — read from the affective-state entity.
594
+ // AffectiveBlender writes this entity each tick with dominantEmotion and blends.
595
+ let dominantEmotion = 'neutral'
596
+ let blends: string[] = []
597
+
598
+ const affectEntity = state.entities.get('affective-state')
599
+ if( affectEntity ){
600
+ dominantEmotion = (affectEntity.metadata?.dominantEmotion as string) ?? 'neutral'
601
+ blends = (affectEntity.metadata?.blends as string[]) ?? []
602
+ }
603
+
604
+ return { dominantEmotion, valence, arousal, dominance, blends }
605
+ }