@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,859 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/faculties/planning.engine/engine.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * PlanningEngine — plan executor with two-tier execution.
7
+ *
8
+ * Supervision modes (the cognitive dual-process axis, not a mechanism name):
9
+ *
10
+ * deliberate (System 2):
11
+ * Spawns a facet of the executive consciousness. Each step outcome is reported
12
+ * to the facet, which decides: continue / skip / abandon / replan / complete.
13
+ * The facet syncs bidirectionally with the master executive.
14
+ *
15
+ * automatic (System 1):
16
+ * Executes all steps without per-step supervision. Reports final status
17
+ * (completed or failed) to the master executive via salience-buffered events.
18
+ *
19
+ * A plan does NOT dispatch steps to an executor. It BIASES the one action
20
+ * competition: each tick it projects its ready frontier step(s) as `plan.prior`
21
+ * entities the AffordanceSynthesizer turns into competing affordances (top-down
22
+ * prior, never a bypass — see PLANNING_AS_PRIOR_TODO.md). The ordinary selector
23
+ * enacts the winner; the executor emits `action.outcome{planId,stepId}` (provenance
24
+ * carried through the affordance→intent chain), which this engine consumes to
25
+ * advance the frontier. Resolves the prerequisite DAG to choose the ready frontier.
26
+ *
27
+ * Structure (split along its seams — each module carries its own docs):
28
+ * planning/types.ts — shared types + dispositions + terminal set
29
+ * planning/plan.store.ts — canonical store, goal index, GC, persistence
30
+ * planning/plan.frontier.ts — ready-set DAG + `plan.prior` projection
31
+ * planning/plan.supervision.ts — emergent tier, facet judgment, directives
32
+ * (this file) — engine shell: bus events, react loop,
33
+ * ingestion, execution, lifecycle, activity API
34
+ */
35
+
36
+ import { logger } from '#core/logger'
37
+ import type {
38
+ Duration, Tick, SimulationContext,
39
+ ReadonlySimulationState, StateCommands,
40
+ } from '#core/types'
41
+ import type { SimulationEngine, EngineResult, CognitiveEngine } from '#cognition/types'
42
+ import type { GoalManager } from '#faculties/goal.manager'
43
+ import type { ExecutiveEngine } from '#faculties/executive.engine'
44
+ import type { CognitiveEventSchema } from '#cognition/schema.registry'
45
+ import type { CognitiveEvent, CognitiveBus } from '#cognition/bus'
46
+ import type { ExecutiveOutputFull } from '#faculties/executive.engine/types'
47
+ import type { SessionLogger } from '#stem/tracts/session.logger'
48
+ import { GenerativeModel } from '#cognition/generative.model'
49
+ import { readEffectiveParams } from '#cognition/persona.prior'
50
+
51
+ import {
52
+ TERMINAL_STATUSES,
53
+ type Plan, type PlanStep, type PlanContext, type PlanningDispositions,
54
+ type ActivityEvent, type ActivityEventHandler, type PlanningEngineConfig,
55
+ } from './types'
56
+ import { PlanStore } from './plan.store'
57
+ import { computeReadySet, projectFrontier } from './plan.frontier'
58
+ import { PlanSupervisor, type SupervisionHost } from './plan.supervision'
59
+
60
+ export type {
61
+ Plan, PlanStep, PlanContext,
62
+ ActivityEvent, ActivityEventHandler, PlanningEngineConfig,
63
+ }
64
+ export { PlanStore, PlanSupervisor }
65
+
66
+ // ── PlanningEngine ────────────────────────────────────────────
67
+
68
+ export class PlanningEngine implements SimulationEngine, CognitiveEngine {
69
+ readonly name = 'planning-engine'
70
+
71
+ private _planRetentionTicks: number
72
+
73
+ /**
74
+ * Trait-driven dispositions (Channel A) — ONE mutable object, refreshed from
75
+ * the persona-prior mirror each tick (_readConfigFromState) and read live by
76
+ * the supervisor + frontier projection. See PlanningDispositions.
77
+ */
78
+ private readonly _dispositions: PlanningDispositions
79
+
80
+ private _goalManager: GoalManager | null = null
81
+ private _executiveEngine: ExecutiveEngine | null = null
82
+
83
+ /** Canonical plan state — store, goal index, terminal bookkeeping, GC, persistence. */
84
+ private readonly _store = new PlanStore()
85
+
86
+ /** Deliberate-tier judgment — facet lifecycle, reports, directive dispatch. */
87
+ private readonly _supervisor: PlanSupervisor
88
+
89
+ /** Plan ids needing a recall descriptor emitted (created/revised this cycle). */
90
+ private _newPlanDescriptors: string[] = []
91
+
92
+ /**
93
+ * Last tick react() ran — used only to stamp session-log telemetry (never
94
+ * replay state) from off-tick callbacks like _activateStep / _onStepOutcome.
95
+ */
96
+ private _lastTick = 0
97
+
98
+ /**
99
+ * Monotonic suffix counter for activity-listener subscription ids. These ids
100
+ * are transient bus-subscription keys (HTTP/SSE-driven, never entering the
101
+ * event log or snapshot), so they must NOT draw from the seeded PRNG — that
102
+ * would consume sim-random draws and perturb determinism. A plain counter
103
+ * replaces Math.random() here (R2).
104
+ */
105
+ private _subCounter = 0
106
+
107
+ private _energyLevel: number = 100
108
+
109
+ private _bus: CognitiveBus | null = null
110
+ private _sessionLogger: SessionLogger | null = null
111
+
112
+ private readonly _model = new GenerativeModel()
113
+
114
+ // Tracks the last executive output object we ingested so we only process
115
+ // each executive cycle once — prevents re-logging on every non-executive tick.
116
+ private _lastIngestedOutput: ExecutiveOutputFull | null = null
117
+
118
+ constructor( config: PlanningEngineConfig = {} ){
119
+ this._bus = config.bus ?? null
120
+ this._planRetentionTicks = config.planRetentionTicks ?? 300
121
+
122
+ this._dispositions = {
123
+ deliberateGoalPriority: config.deliberateGoalPriority ?? 0.7,
124
+ lowPlanConfidence: config.lowPlanConfidence ?? 0.5,
125
+ surpriseOutcomeQuality: config.surpriseOutcomeQuality ?? 0.25,
126
+ maxStepRetries: config.maxStepRetries ?? 3,
127
+ planBiasGain: 1,
128
+ }
129
+
130
+ // The supervisor's effects flow back through this narrow host interface —
131
+ // the engine owns plan lifecycle, bus identity, and the execution pass.
132
+ const host: SupervisionHost = {
133
+ executePlans: () => this._executePlans(),
134
+ planCompleted: plan => this._onPlanCompleted( plan ),
135
+ planFailed: ( plan, reason ) => this._onPlanFailed( plan, reason ),
136
+ publish: event => this._bus?.publish( { ...event, sourceEngine: this.name } ),
137
+ }
138
+ this._supervisor = new PlanSupervisor( host, this._dispositions )
139
+ }
140
+
141
+ attachGoalManager( gm: GoalManager ): void {
142
+ this._goalManager = gm
143
+ this._supervisor.attachGoalManager( gm )
144
+ }
145
+ attachExecutiveEngine( oe: ExecutiveEngine ): void {
146
+ this._executiveEngine = oe
147
+ this._supervisor.attachExecutiveEngine( oe )
148
+ }
149
+ attachSessionLogger( logger: SessionLogger | null ): void { this._sessionLogger = logger }
150
+ /**
151
+ * Give the engine its CognitiveBus. Called by the orchestrator's addEngine()
152
+ * during assembly (every other faculty already exposes this). Without it the
153
+ * bus stayed null, so plan-lifecycle events (plan.started / plan.step.* /
154
+ * plan.completed) never published and addActivityListener no-op'd.
155
+ */
156
+ attachBus( bus: CognitiveBus ): void { this._bus = bus }
157
+
158
+ // ── CognitiveEngine interface ──────────────────────────────
159
+
160
+ subscribes(): string[] {
161
+ return [
162
+ 'energy.state.changed',
163
+ 'executive.prediction.formed',
164
+ 'action.outcome',
165
+ // Goal lifecycle — a terminal goal makes its plans moot; cancel them so they
166
+ // stop dispatching steps for a goal that's already done/dropped.
167
+ 'goal.achieved',
168
+ 'goal.abandoned',
169
+ ]
170
+ }
171
+
172
+ publishes(): CognitiveEventSchema[] {
173
+ return [
174
+ { type: 'plan.started', version: 1, validate: () => null },
175
+ { type: 'plan.step.activated', version: 1, validate: () => null },
176
+ { type: 'plan.step.outcome', version: 1, validate: () => null },
177
+ { type: 'plan.completed', version: 1, validate: () => null },
178
+ { type: 'plan.failed', version: 1, validate: () => null },
179
+ { type: 'plan.cancelled', version: 1, validate: () => null },
180
+ { type: 'plan.escalated', version: 1, validate: () => null },
181
+ { type: 'plan.replanned', version: 1, validate: () => null },
182
+ { type: 'planning.plan.created', version: 1, validate: () => null },
183
+ ]
184
+ }
185
+
186
+ onCognitiveEvent( e: CognitiveEvent ): StateCommands | void {
187
+ this._model.observe( e.type, e.salience )
188
+
189
+ switch( e.type ){
190
+ case 'energy.state.changed':
191
+ this._energyLevel = (e.payload as Record<string,number>)['level'] ?? this._energyLevel
192
+ break
193
+
194
+ case 'executive.prediction.formed': {
195
+ const p = e.payload as { predictedDomains: string[]; confidence: number }
196
+ if( p.predictedDomains.includes('planning') )
197
+ this._model.setPrecision( 'planning.plans', 1.0 + p.confidence * 0.5 )
198
+ break
199
+ }
200
+
201
+ case 'action.outcome': {
202
+ const p = e.payload as {
203
+ actionType: string; domain: string; success: boolean
204
+ outcomeQuality: number; description: string
205
+ planId?: string; stepId?: string
206
+ }
207
+
208
+ if( !p.planId || !p.stepId ) return
209
+ if( !this._store.has( p.planId ) ) return
210
+
211
+ this._onStepOutcome( p.planId, p.stepId, {
212
+ success: p.success,
213
+ description: p.description ?? ( p.success ? 'Completed' : 'Failed' ),
214
+ outcomeQuality: p.outcomeQuality,
215
+ } )
216
+ break
217
+ }
218
+
219
+ case 'goal.achieved':
220
+ this._cancelPlansForGoal( ( e.payload as { goalId?: string } ).goalId, 'goal achieved' )
221
+ break
222
+
223
+ case 'goal.abandoned':
224
+ this._cancelPlansForGoal( ( e.payload as { goalId?: string } ).goalId, 'goal abandoned' )
225
+ break
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Cancel every still-active plan for a goal that has reached a terminal state
231
+ * (achieved/abandoned). Marks them rejected (→ retention GC cleans the entity)
232
+ * and tears down any step-aware facet. A plan whose own completion triggered the
233
+ * goal is already terminal and is skipped, so this only reaps siblings still
234
+ * pursuing a goal that's now done/dropped. (planning↔goal sync)
235
+ */
236
+ private _cancelPlansForGoal( goalId: string | undefined, reason: string ): void {
237
+ if( !goalId ) return
238
+ for( const plan of this._store.plansForGoal( goalId ) ){
239
+ if( TERMINAL_STATUSES.includes( plan.status ) ) continue
240
+ plan.status = 'rejected'
241
+ this._store.markTerminal( plan.id, this._lastTick )
242
+ this._supervisor.cleanupFacet( plan.id )
243
+
244
+ // Terminal activity signal so SSE/socket watchers of this plan get explicit
245
+ // closure (the plan stopped because its goal ended, not via complete/fail).
246
+ this._bus?.publish( {
247
+ type: 'plan.cancelled', version: 1, sourceEngine: this.name,
248
+ salience: 0.6,
249
+ payload: {
250
+ planId: plan.id,
251
+ goalId: plan.goalId,
252
+ reason,
253
+ completedSteps: plan.steps.filter( s => s.status === 'completed' ).length,
254
+ totalSteps: plan.steps.length,
255
+ requestingEntityId: plan.requestingEntityId,
256
+ requestingThreadId: plan.requestingThreadId,
257
+ }
258
+ } )
259
+
260
+ logger.info( `[planning] plan ${plan.id} cancelled — ${reason} (goal ${goalId})` )
261
+ }
262
+ }
263
+
264
+ snapshot(): Record<string, unknown> {
265
+ return {
266
+ energyLevel: this._energyLevel,
267
+ totalPlans: this._store.size,
268
+ activeFacets: this._supervisor.activeFacetCount,
269
+ }
270
+ }
271
+
272
+ // ── Engine react ──────────────────────────────────────────
273
+
274
+ async react(
275
+ _delta: Duration,
276
+ tick: Tick,
277
+ state: ReadonlySimulationState,
278
+ context: SimulationContext
279
+ ): Promise<EngineResult> {
280
+ this._lastTick = tick as unknown as number
281
+ const commands: StateCommands = { set: [], delete: [], metrics: [] }
282
+
283
+ // 0. Channel A (subconscious): refresh trait-driven dispositions from the
284
+ // persona-prior mirror before acting on them this tick.
285
+ this._readConfigFromState( state )
286
+
287
+ // 1. Ingest new plans from executive output (the executive is the planner)
288
+ this._ingestExecutivePlans( tick )
289
+
290
+ // 2. Execute plans — advance the ready frontier (marks steps `active`)
291
+ this._executePlans()
292
+
293
+ // 2b. Project the active frontier as `plan.prior` entities — the top-down bias
294
+ // the AffordanceSynthesizer turns into competing affordances (no dispatch).
295
+ projectFrontier(
296
+ this._store.all(), commands, tick, state,
297
+ goalId => this._goalManager?.getGoal( goalId )?.priority ?? 0.5,
298
+ this._dispositions.planBiasGain,
299
+ )
300
+
301
+ // 3. Emit recall descriptors for new/revised plans (awareness Stage 2)
302
+ this._flushPlanDescriptors( commands )
303
+
304
+ // 5. GC long-terminal plans (bound accretion), then persist the rest
305
+ this._store.gcTerminal( tick, commands, this._planRetentionTicks, id => this._supervisor.cleanupFacet( id ) )
306
+ this._store.persist( commands, tick )
307
+
308
+ // 5. Metrics
309
+ const executingPlans = Array.from( this._store.all() )
310
+ .filter( p => p.status === 'executing' || p.status === 'ready' )
311
+
312
+ commands.metrics!.push(
313
+ [ 'planning.total_plans', this._store.size ],
314
+ [ 'planning.executing_plans', executingPlans.length ],
315
+ [ 'planning.active_facets', this._supervisor.activeFacetCount ],
316
+ // Supervision distribution — how the mind corrects course mid-execution. The
317
+ // raw signal the planning-quality eval harness reads (replan/abandon rates etc).
318
+ [ 'planning.supervision.replan', this._supervisor.supervisionCount( 'replan' ) ],
319
+ [ 'planning.supervision.retry', this._supervisor.supervisionCount( 'retry' ) ],
320
+ [ 'planning.supervision.skip', this._supervisor.supervisionCount( 'skip' ) ],
321
+ [ 'planning.supervision.escalate', this._supervisor.supervisionCount( 'escalate' ) ],
322
+ [ 'planning.supervision.abandon', this._supervisor.supervisionCount( 'abandon' ) ],
323
+ )
324
+
325
+ // Phase C + F: publish cognitive event — gated by prediction error
326
+ const _bus = this._bus
327
+ if( _bus && this._store.size > 0 ){
328
+ const predErr = this._model.observe( 'planning.plans', this._store.size )
329
+ if( !predErr.gated )
330
+ _bus.publish( {
331
+ type: 'planning.plan.created', version: 1, sourceEngine: this.name,
332
+ salience: Math.max( 0.3, predErr.salience ),
333
+ payload: { totalPlans: this._store.size },
334
+ } )
335
+ }
336
+
337
+ return { commands }
338
+ }
339
+
340
+ /**
341
+ * Channel A (subconscious disposition): refresh trait-driven supervision params
342
+ * from the persona-prior mirror (base `engine-config-planning` ⊕ metacog deltas).
343
+ * Demonstrated conscientiousness develops planning follow-through — it raises
344
+ * `maxStepRetries` (re-attempt a stuck step more before giving up) and
345
+ * `surpriseOutcomeQuality` (vigilance: escalate to deliberate supervision on
346
+ * smaller quality dips). Only present params override; absent config/prior leaves
347
+ * the constructor defaults standing. Pure + deterministic (R2): same state ⇒ same
348
+ * dispositions, no wall-clock, no RNG.
349
+ */
350
+ private _readConfigFromState( state: ReadonlySimulationState ): void {
351
+ const p = readEffectiveParams( state, 'engine-config-planning' )
352
+ if( typeof p.maxStepRetries === 'number' ) this._dispositions.maxStepRetries = p.maxStepRetries
353
+ if( typeof p.surpriseOutcomeQuality === 'number' ) this._dispositions.surpriseOutcomeQuality = p.surpriseOutcomeQuality
354
+ if( typeof p.planBiasGain === 'number' ) this._dispositions.planBiasGain = p.planBiasGain
355
+ }
356
+
357
+ // ── Plan ingestion ─────────────────────────────────────────
358
+
359
+ private _ingestExecutivePlans( tick: Tick ): void {
360
+ const executiveOutput = this._executiveEngine?.latestOutput
361
+
362
+ if( !executiveOutput?.plans || executiveOutput.plans.length === 0 ) return
363
+ if( !this._executiveEngine?.isFresh( tick ) ) return
364
+ // Guard: only ingest each executive cycle's output once.
365
+ // latestOutput is the same object reference until the next executive fires,
366
+ // so comparing by reference is sufficient to skip re-processing.
367
+ if( executiveOutput === this._lastIngestedOutput ) return
368
+ this._lastIngestedOutput = executiveOutput
369
+
370
+ for( const planData of executiveOutput.plans ){
371
+ // P4: an explicit planId targets a specific plan; otherwise the goal's
372
+ // active plan. draft (below) intentionally bypasses this to stack new plans.
373
+ const existingPlan = this._store.resolveIngestTarget( planData.goalId, planData.planId )
374
+
375
+ switch( planData.action ){
376
+ case 'draft': {
377
+ // P4: a draft creates a NEW plan rather than being skipped when one
378
+ // already exists — this is how multiple plans per goal are formed.
379
+ // Guard only against exact re-assertions: an active plan with the same
380
+ // (non-empty) expectedOutcome is the same candidate restated.
381
+ const expected = planData.expectedOutcome ?? ''
382
+ const isReassertion = expected.length > 0 && this._store.plansForGoal( planData.goalId ).some(
383
+ p => p.expectedOutcome === expected && !TERMINAL_STATUSES.includes( p.status )
384
+ )
385
+ if( isReassertion ){
386
+ logger.info(
387
+ `[planning] draft for goal ${planData.goalId} skipped — ` +
388
+ `matches active plan "${expected.slice( 0, 40 )}"`
389
+ )
390
+ break
391
+ }
392
+
393
+ this._createPlan( planData, tick, 'draft' )
394
+ break
395
+ }
396
+
397
+ case 'validate': {
398
+ if( !existingPlan ){
399
+ this._createPlan( planData, tick, 'validated' )
400
+ break
401
+ }
402
+
403
+ existingPlan.status = 'validated'
404
+ logger.info( `[planning] plan ${existingPlan.id} validated` )
405
+ break
406
+ }
407
+
408
+ case 'execute': {
409
+ if( !existingPlan ){
410
+ const newPlan = this._createPlan( planData, tick, 'approved' )
411
+ newPlan.status = 'ready'
412
+ // Infer supervision (emergent tier): important/uncertain plans start
413
+ // deliberate (a facet supervises from the first step); else automatic.
414
+ newPlan.executionTier = this._supervisor.inferInitialTier( newPlan )
415
+ if( newPlan.executionTier === 'deliberate' && this._executiveEngine )
416
+ this._supervisor.activateFacet( newPlan )
417
+ break
418
+ }
419
+
420
+ existingPlan.status = 'ready'
421
+ existingPlan.executionTier = this._supervisor.inferInitialTier( existingPlan )
422
+ existingPlan.expectedOutcome = planData.expectedOutcome ?? existingPlan.expectedOutcome
423
+
424
+ logger.info(
425
+ `[planning] plan ${existingPlan.id} approved for execution ` +
426
+ `(tier=${existingPlan.executionTier} — inferred)`
427
+ )
428
+
429
+ if( existingPlan.executionTier === 'deliberate' && this._executiveEngine ){
430
+ this._supervisor.activateFacet( existingPlan )
431
+ }
432
+ break
433
+ }
434
+
435
+ case 'revise': {
436
+ if( !existingPlan ){ this._createPlan( planData, tick, 'revised' ); break }
437
+
438
+ existingPlan.steps = planData.steps.map( ( s, i ) => ( {
439
+ id: `step-${i}`,
440
+ order: i,
441
+ action: s.action,
442
+ description: s.description,
443
+ expectedOutcome: s.expectedOutcome,
444
+ prerequisites: s.prerequisites ?? ( i > 0 ? [`step-${i - 1}`] : [] ),
445
+ estimatedDuration: s.estimatedDuration,
446
+ status: 'pending' as const,
447
+ } ) )
448
+ // Re-arm so the revised steps actually dispatch. An in-flight plan stays
449
+ // `executing` (new pending steps dispatch in place, like the facet replan
450
+ // path); an idle one returns to `ready` so _executePlans (re)starts it.
451
+ // Leaving it `revised` would strand it — _executePlans only runs
452
+ // ready/executing. (P3)
453
+ existingPlan.status = existingPlan.status === 'executing' ? 'executing' : 'ready'
454
+ existingPlan.expectedOutcome = planData.expectedOutcome ?? existingPlan.expectedOutcome
455
+
456
+ logger.info(
457
+ `[planning] plan ${existingPlan.id} revised ` +
458
+ `(${existingPlan.steps.length} steps, status=${existingPlan.status})`
459
+ )
460
+ this._newPlanDescriptors.push( existingPlan.id ) // re-describe (outcome may have changed)
461
+ break
462
+ }
463
+
464
+ case 'cancel': {
465
+ if( !existingPlan ) continue
466
+
467
+ existingPlan.status = 'rejected'
468
+ this._store.markTerminal( existingPlan.id, this._lastTick )
469
+ this._supervisor.cleanupFacet( existingPlan.id )
470
+
471
+ logger.info( `[planning] plan ${existingPlan.id} cancelled` )
472
+ break
473
+ }
474
+ }
475
+ }
476
+ }
477
+
478
+ private _createPlan(
479
+ planData: ExecutiveEngine extends { latestOutput: infer O }
480
+ ? O extends { plans?: Array<infer P> } ? P : never
481
+ : never,
482
+ tick: Tick,
483
+ status: Plan['status']
484
+ ): Plan {
485
+ const planId = this._store.nextId()
486
+
487
+ const steps: PlanStep[] = planData.steps.map( ( s, i ) => ( {
488
+ id: `step-${i}`,
489
+ order: i,
490
+ action: s.action,
491
+ description: s.description,
492
+ expectedOutcome: s.expectedOutcome,
493
+ prerequisites: s.prerequisites ?? ( i > 0 ? [`step-${i - 1}`] : [] ),
494
+ estimatedDuration: s.estimatedDuration,
495
+ status: 'pending' as const,
496
+ } ) )
497
+
498
+ // Copy causal link from the GoalState so all downstream bus events carry it.
499
+ const goalState = this._goalManager?.getGoal( planData.goalId )
500
+
501
+ const plan: Plan = {
502
+ id: planId,
503
+ goalId: planData.goalId,
504
+ steps,
505
+ estimatedCost: planData.estimatedCost,
506
+ confidence: planData.feasibility,
507
+ status,
508
+ executionTier: 'automatic', // engine-inferred at execute (emergent tier), not executive-set
509
+ expectedOutcome: planData.expectedOutcome ?? '',
510
+ createdAt: tick,
511
+ requestingEntityId: goalState?.requestingEntityId,
512
+ requestingThreadId: goalState?.requestingThreadId,
513
+ }
514
+
515
+ this._store.index( plan )
516
+ this._newPlanDescriptors.push( plan.id )
517
+
518
+ logger.info(
519
+ `[planning] plan created: ${planId} → goal ${plan.goalId} ` +
520
+ `(${steps.length} steps, status=${status}, tier=${plan.executionTier}` +
521
+ `${plan.requestingEntityId ? `, requester=${plan.requestingEntityId}` : ''})`
522
+ )
523
+
524
+ return plan
525
+ }
526
+
527
+ // ── Plan execution ─────────────────────────────────────────
528
+
529
+ private _executePlans(): void {
530
+ for( const plan of this._store.all() ){
531
+ if( plan.status !== 'ready' && plan.status !== 'executing' ) continue
532
+
533
+ if( plan.status === 'ready' ){
534
+ plan.status = 'executing'
535
+
536
+ // Emit plan.started so the activity stream knows execution has begun.
537
+ this._bus?.publish( {
538
+ type: 'plan.started', version: 1, sourceEngine: this.name,
539
+ salience: 0.65,
540
+ payload: {
541
+ planId: plan.id,
542
+ goalId: plan.goalId,
543
+ totalSteps: plan.steps.length,
544
+ executionTier: plan.executionTier,
545
+ requestingEntityId: plan.requestingEntityId,
546
+ requestingThreadId: plan.requestingThreadId,
547
+ }
548
+ } )
549
+ }
550
+
551
+ const readySteps = computeReadySet( plan )
552
+
553
+ if( readySteps.length > 0 )
554
+ for( const step of readySteps )
555
+ this._activateStep( plan, step )
556
+
557
+ // Check for plan completion
558
+ const allDone = plan.steps.every( s => s.status === 'completed' || s.status === 'skipped' )
559
+ const anyFailed = plan.steps.some( s => s.status === 'failed' )
560
+
561
+ if( allDone && !anyFailed )
562
+ this._onPlanCompleted( plan )
563
+
564
+ else if( anyFailed && plan.executionTier === 'automatic' )
565
+ this._onPlanFailed( plan, 'One or more steps failed' )
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Move a ready step onto the frontier: it starts biasing the competition (via
571
+ * `projectFrontier`) instead of being dispatched. Emits `plan.step.activated`
572
+ * for the activity stream — the awareness analog of the old `plan.step.dispatched`.
573
+ */
574
+ private _activateStep( plan: Plan, step: PlanStep ): void {
575
+ step.status = 'active'
576
+
577
+ this._bus?.publish( {
578
+ type: 'plan.step.activated', version: 1, sourceEngine: this.name,
579
+ salience: 0.6,
580
+ payload: {
581
+ planId: plan.id,
582
+ stepId: step.id,
583
+ action: step.action,
584
+ description: step.description,
585
+ expectedOutcome: step.expectedOutcome,
586
+ reasoning: `Pursuing plan step: ${step.description}`,
587
+ stepIndex: step.order,
588
+ requestingEntityId: plan.requestingEntityId,
589
+ requestingThreadId: plan.requestingThreadId,
590
+ }
591
+ } )
592
+
593
+ this._sessionLogger?.write({
594
+ type: 'plan.step.activated',
595
+ tick: this._lastTick,
596
+ planId: plan.id,
597
+ stepId: step.id,
598
+ action: step.action,
599
+ description: step.description,
600
+ stepIndex: step.order,
601
+ totalSteps: plan.steps.length,
602
+ } as any)
603
+
604
+ logger.info( `[planning] step active: ${plan.id}/${step.id}=${step.action} (biasing the field)` )
605
+ }
606
+
607
+ // ── Step outcome handling ──────────────────────────────────
608
+
609
+ private _onStepOutcome(
610
+ planId: string,
611
+ stepId: string,
612
+ outcome: { success: boolean; description: string; outcomeQuality: number },
613
+ ): void {
614
+ const plan = this._store.get( planId )
615
+ if( !plan ) return
616
+
617
+ // Late-outcome guard. A plan biases the field only while 'executing'; once it
618
+ // leaves that state — the executive's `complete` directive, a failure, abandon,
619
+ // pause/escalate, or a terminal goal — it stops projecting its frontier. But an
620
+ // `agency.intent` the competition had ALREADY committed from a frontier prior can
621
+ // still resolve a few ticks later (sync this tick, or an awaiting one timing out).
622
+ // That outcome carries this plan's provenance, so it lands here. We must NOT act on
623
+ // it: completion is the executive's call, not the late arrival of a step it has
624
+ // already moved past — and re-running supervision below could re-spawn the facet
625
+ // we just tore down. The enaction itself still happened (and still taught the
626
+ // repertoire via reafference); the plan simply no longer cares.
627
+ if( plan.status !== 'executing' ){
628
+ logger.info( `[planning] ignoring late step outcome ${planId}/${stepId} — plan is ${plan.status}, not executing` )
629
+ return
630
+ }
631
+
632
+ const step = plan.steps.find( s => s.id === stepId )
633
+ if( !step ) return
634
+
635
+ step.status = outcome.success ? 'completed' : 'failed'
636
+ step.outcome = outcome
637
+
638
+ logger.info(
639
+ `[planning] step outcome: ${planId}/${stepId} ` +
640
+ `${outcome.success ? '✓' : '✗'} (quality=${outcome.outcomeQuality.toFixed( 2 )})`
641
+ )
642
+
643
+ // Publish step outcome to the bus so the activity stream can forward it.
644
+ this._bus?.publish( {
645
+ type: 'plan.step.outcome', version: 1, sourceEngine: this.name,
646
+ salience: 0.6,
647
+ payload: {
648
+ planId,
649
+ stepId,
650
+ action: step.action,
651
+ success: outcome.success,
652
+ outcomeQuality: outcome.outcomeQuality,
653
+ description: outcome.description.slice( 0, 300 ),
654
+ completedSteps: plan.steps.filter( s => s.status === 'completed' || s.status === 'skipped' ).length,
655
+ totalSteps: plan.steps.length,
656
+ requestingEntityId: plan.requestingEntityId,
657
+ requestingThreadId: plan.requestingThreadId,
658
+ }
659
+ } )
660
+
661
+ this._sessionLogger?.write({
662
+ type: 'plan.step.outcome',
663
+ tick: this._lastTick,
664
+ planId,
665
+ stepId,
666
+ action: step.action,
667
+ success: outcome.success,
668
+ outcomeQuality: outcome.outcomeQuality,
669
+ description: outcome.description.slice( 0, 300 ),
670
+ completedSteps: plan.steps.filter( s => s.status === 'completed' || s.status === 'skipped' ).length,
671
+ totalSteps: plan.steps.length,
672
+ } as any)
673
+
674
+ // Supervision (emergent tier): a deliberate plan reports to its facet. An
675
+ // automatic plan ESCALATES to deliberate on a surprising outcome — attention
676
+ // recruited by prediction error — then reports. shouldEscalate is the
677
+ // extension point for more triggers.
678
+ if( plan.executionTier !== 'deliberate' && this._supervisor.shouldEscalate( plan, step, outcome ) ){
679
+ plan.executionTier = 'deliberate'
680
+ logger.info( `[planning] plan ${planId} escalated to deliberate (surprise on ${stepId})` )
681
+ this._supervisor.activateFacet( plan, false ) // lazy spawn; the step report below follows
682
+ }
683
+
684
+ if( plan.executionTier === 'deliberate' ){
685
+ const reported = this._supervisor.reportToFacet( plan, step, outcome )
686
+ if( !reported ){
687
+ // attention saturated / spawn failed → activateFacet downgraded to automatic
688
+ logger.warn( `[planning] no facet to supervise plan ${planId}; continuing automatically` )
689
+ this._executePlans()
690
+ }
691
+ }
692
+ }
693
+
694
+ // ── Plan completion / failure ──────────────────────────────
695
+
696
+ private _onPlanCompleted( plan: Plan ): void {
697
+ plan.status = 'completed'
698
+ this._store.markTerminal( plan.id, this._lastTick )
699
+
700
+ this._bus?.publish( {
701
+ type: 'plan.completed',
702
+ version: 1,
703
+ sourceEngine: this.name,
704
+ salience: 0.7,
705
+ payload: {
706
+ planId: plan.id,
707
+ goalId: plan.goalId,
708
+ completedSteps: plan.steps.filter( s => s.status === 'completed' ).length,
709
+ skippedSteps: plan.steps.filter( s => s.status === 'skipped' ).map( s => s.id ),
710
+ totalSteps: plan.steps.length,
711
+ requestingEntityId: plan.requestingEntityId,
712
+ requestingThreadId: plan.requestingThreadId,
713
+ }
714
+ } )
715
+
716
+ logger.info( `[planning] plan completed: ${plan.id} → goal ${plan.goalId}` )
717
+ }
718
+
719
+ private _onPlanFailed( plan: Plan, reason: string ): void {
720
+ plan.status = 'failed'
721
+ this._store.markTerminal( plan.id, this._lastTick )
722
+
723
+ this._bus?.publish( {
724
+ type: 'plan.failed',
725
+ version: 1,
726
+ sourceEngine: this.name,
727
+ salience: 0.7,
728
+ payload: {
729
+ planId: plan.id,
730
+ goalId: plan.goalId,
731
+ reason,
732
+ completedSteps: plan.steps.filter( s => s.status === 'completed' ).length,
733
+ skippedSteps: plan.steps.filter( s => s.status === 'skipped' ).map( s => s.id ),
734
+ totalSteps: plan.steps.length,
735
+ requestingEntityId: plan.requestingEntityId,
736
+ requestingThreadId: plan.requestingThreadId,
737
+ }
738
+ } )
739
+
740
+ logger.info( `[planning] plan failed: ${plan.id} — ${reason}` )
741
+ }
742
+
743
+ // ── Recall descriptors (awareness Stage 2) ─────────────────
744
+
745
+ /**
746
+ * Emit a stable, embeddable descriptor for newly created/revised plans as a
747
+ * `working_memory.item`, so the EpisodicConsolidator indexes it into vector
748
+ * memory and a later message can RECALL the plan (→ `context.relevantPlanIds` →
749
+ * the Active Plans awareness filter). Only the stable descriptor (goal +
750
+ * expectedOutcome) is embedded — never the live step state (which changes every
751
+ * tick) — so recall stays match-stable while the projector renders live state.
752
+ * The WorkingMemory faculty GCs the item after consolidation, so it doesn't
753
+ * accrete. Follows the established external-injection pattern (AuditionEngine).
754
+ */
755
+ private _flushPlanDescriptors( commands: StateCommands ): void {
756
+ for( const id of this._newPlanDescriptors ){
757
+ const plan = this._store.get( id )
758
+ if( !plan ) continue
759
+ commands.set!.push( {
760
+ id: `wm-plan-${plan.id}`,
761
+ type: 'working_memory.item',
762
+ createdAt: plan.createdAt,
763
+ updatedAt: this._lastTick,
764
+ metadata: {
765
+ wmType: 'plan',
766
+ content: {
767
+ summary: `Plan ${plan.id} for goal "${plan.goalId}": ${plan.expectedOutcome || 'achieve the goal'}`,
768
+ planId: plan.id,
769
+ goalId: plan.goalId,
770
+ },
771
+ activation: 0.8,
772
+ attendedCount: 1,
773
+ tags: [ 'plan', 'plan.descriptor' ],
774
+ tick: this._lastTick,
775
+ },
776
+ } )
777
+ }
778
+ this._newPlanDescriptors.length = 0
779
+ }
780
+
781
+ // ── Public API ─────────────────────────────────────────────
782
+
783
+ /** The goal's active plan, or its most-recent plan if all are terminal. */
784
+ getPlan( goalId: string ): Plan | undefined {
785
+ const active = this._store.activePlanForGoal( goalId )
786
+ if( active ) return active
787
+ const all = this._store.plansForGoal( goalId )
788
+ return all.length > 0 ? all[ all.length - 1 ] : undefined
789
+ }
790
+
791
+ /** All plans for a goal (any status), in creation order. (P4) */
792
+ getPlansForGoal( goalId: string ): Plan[] {
793
+ return this._store.plansForGoal( goalId )
794
+ }
795
+
796
+ /**
797
+ * Subscribe to plan activity events for a specific requesting entity.
798
+ *
799
+ * Subscribes to the internal CognitiveBus and forwards all plan-lifecycle
800
+ * events (`plan.started`, `plan.step.activated`, `plan.step.outcome`,
801
+ * `plan.completed`, `plan.failed`) that were triggered by `entityId` to
802
+ * the provided `handler`.
803
+ *
804
+ * Used by WillManager to back the `GET /wills/:id/activity` SSE stream.
805
+ *
806
+ * @returns Unsubscribe function — call it to remove the subscription.
807
+ */
808
+ addActivityListener(
809
+ entityId: string,
810
+ handler: ActivityEventHandler,
811
+ ): () => void {
812
+ if( !this._bus ){
813
+ logger.warn('[planning] addActivityListener called before bus is attached — listener is a no-op')
814
+ return () => {}
815
+ }
816
+
817
+ const subId = `activity-listener-${entityId}-${this._subCounter++}`
818
+
819
+ const PLAN_TOPICS = [
820
+ 'plan.started',
821
+ 'plan.step.activated',
822
+ 'plan.step.outcome',
823
+ 'plan.completed',
824
+ 'plan.failed',
825
+ 'plan.cancelled',
826
+ ]
827
+
828
+ const TYPE_MAP: Record<string, ActivityEvent['type']> = {
829
+ 'plan.started': 'plan_started',
830
+ 'plan.step.activated': 'step_activated',
831
+ 'plan.step.outcome': 'step_outcome',
832
+ 'plan.completed': 'plan_complete',
833
+ 'plan.failed': 'plan_failed',
834
+ 'plan.cancelled': 'plan_cancelled',
835
+ }
836
+
837
+ this._bus.subscribe( subId, PLAN_TOPICS, ev => {
838
+ const p = ev.payload as Record<string, unknown>
839
+
840
+ // Filter: only forward events that originated from this entity's request.
841
+ // `'*'` is a wildcard — forward all activity (used by the transport projection).
842
+ if( entityId !== '*' && p.requestingEntityId !== entityId ) return
843
+
844
+ const mapped = TYPE_MAP[ ev.type ]
845
+ if( !mapped ) return
846
+
847
+ handler({
848
+ type: mapped,
849
+ planId: p.planId as string,
850
+ goalId: p.goalId as string | undefined,
851
+ requestingEntityId: p.requestingEntityId as string | undefined,
852
+ requestingThreadId: p.requestingThreadId as string | undefined,
853
+ ...p,
854
+ })
855
+ })
856
+
857
+ return () => this._bus?.unsubscribe( subId )
858
+ }
859
+ }