@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,570 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/extensions/live.ext.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * Live streaming extension for dashboards, monitoring, and observability.
7
+ * Layers on top of core — not part of the framework itself.
8
+ *
9
+ * Provides WebSocket (full-duplex) and Server-Sent Events (unidirectional)
10
+ * transports for real-time simulation state, metrics, and event streaming.
11
+ */
12
+
13
+ import { logger } from '#core/logger'
14
+ import type { SimulationClock } from '#core/clock'
15
+ import type { EventBus } from '#core/event.bus'
16
+ import type { MetricCollector } from '#core/metrics'
17
+ import type { Orchestrator } from '#core/orchestrator'
18
+ import type { StateManager } from '#core/state.manager'
19
+ import type {
20
+ SimulationContext,
21
+ SimulationEvent,
22
+ ReadonlySimulationState,
23
+ Tick,
24
+ Timestamp,
25
+ } from '../core/types'
26
+
27
+ // ── Transport-agnostic stream message ──────────────────────
28
+
29
+ export interface StreamMessage {
30
+ type: 'snapshot' | 'tick-summary' | 'event' | 'metric-point' | 'log'
31
+ timestamp: Timestamp
32
+ tick: Tick
33
+ payload: unknown
34
+ }
35
+
36
+ export interface TickSummary {
37
+ tick: Tick
38
+ simTime: Timestamp
39
+ wallDuration: number // ms this tick took
40
+ entityCount: number
41
+ metricCount: number
42
+ pendingEvents: number
43
+ }
44
+
45
+ // ── Stream transports ─────────────────────────────────────
46
+
47
+ /**
48
+ * Pluggable transport for live data.
49
+ * Implementations: WebSocketStream, SSEStream, ConsoleStream, NoopStream
50
+ */
51
+ export interface LiveStreamTransport {
52
+ /** Push a single message to connected clients. */
53
+ send( message: StreamMessage ): void
54
+
55
+ /** Close all connections gracefully. */
56
+ close(): void
57
+
58
+ /** Called when the transport is registered with the extension. */
59
+ onRegister?(): void
60
+ }
61
+
62
+ // ── Configuration ─────────────────────────────────────────
63
+
64
+ export interface LiveStreamConfig {
65
+ /** Emit full state snapshot every N ticks (0 = never) */
66
+ snapshotIntervalTicks?: number
67
+ /** Emit tick summary (entity count, latency) every N ticks (1 = every tick) */
68
+ tickSummaryIntervalTicks?: number
69
+ /** Forward all events from EventBus */
70
+ streamEvents?: boolean
71
+ /** Forward metric points */
72
+ streamMetrics?: boolean
73
+ /** Event types to exclude from streaming (e.g., verbose debug events) */
74
+ excludeEventTypes?: string[]
75
+ /** Maximum message queue depth before dropping (backpressure) */
76
+ maxQueueDepth?: number
77
+ }
78
+
79
+ // ── Extension implementation ──────────────────────────────
80
+
81
+ /**
82
+ * FIX 1: Instead of implementing TickMiddleware (which has a call-signature
83
+ * constraint), we expose the handler as a property that matches the signature.
84
+ * The class no longer `implements TickMiddleware` — it provides an `onTick`
85
+ * arrow function that is structurally compatible and can be assigned to
86
+ * orchestratorConfig.onAfterTick.
87
+ */
88
+ export class LiveStreamExtension {
89
+ private _transport: LiveStreamTransport
90
+ private _config: LiveStreamConfig
91
+ private _eventBus: EventBus
92
+ private _stateManager: StateManager
93
+ private _metricCollector?: MetricCollector
94
+ private _clock: SimulationClock
95
+ /** FIX 2: Use the concrete DefaultOrchestrator type which has tickLatencies. */
96
+ private _orchestrator: Orchestrator
97
+ private _getTickLatency: () => number
98
+
99
+ private _unsubscribeEventBus?: () => void
100
+ private _messageQueue: StreamMessage[] = []
101
+ private _maxQueueDepth: number
102
+ private _flushInterval: ReturnType<typeof setInterval> | null = null
103
+
104
+ constructor(
105
+ transport: LiveStreamTransport,
106
+ eventBus: EventBus,
107
+ stateManager: StateManager,
108
+ orchestrator: Orchestrator,
109
+ clock: SimulationClock,
110
+ config: LiveStreamConfig = {},
111
+ metricCollector?: MetricCollector
112
+ ){
113
+ this._transport = transport
114
+ this._eventBus = eventBus
115
+ this._stateManager = stateManager
116
+ this._orchestrator = orchestrator
117
+ this._clock = clock
118
+ this._metricCollector = metricCollector
119
+ this._config = {
120
+ snapshotIntervalTicks: config.snapshotIntervalTicks ?? 0,
121
+ tickSummaryIntervalTicks: config.tickSummaryIntervalTicks ?? 1,
122
+ streamEvents: config.streamEvents ?? true,
123
+ streamMetrics: config.streamMetrics ?? false,
124
+ excludeEventTypes: config.excludeEventTypes ?? [],
125
+ maxQueueDepth: config.maxQueueDepth ?? 1000,
126
+ }
127
+ this._maxQueueDepth = this._config.maxQueueDepth!
128
+
129
+ /**
130
+ * FIX 2: tickLatencies is only available on DefaultOrchestrator, not the
131
+ * Orchestrator interface. We probe for it and provide a fallback so the
132
+ * extension works with any orchestrator implementation.
133
+ */
134
+ const orchestratorAny = orchestrator as any
135
+ if( Array.isArray( orchestratorAny.tickLatencies ) ){
136
+ this._getTickLatency = () => {
137
+ const latencies = orchestratorAny.tickLatencies as number[]
138
+ return latencies.length > 0 ? ( latencies.at( -1 ) ?? 0 ) : 0
139
+ }
140
+ }
141
+ else {
142
+ this._getTickLatency = () => 0
143
+ }
144
+
145
+ // Wire transport lifecycle
146
+ transport.onRegister?.()
147
+ }
148
+
149
+ // ── Tick handler (assignable to OrchestratorConfig.onAfterTick) ──
150
+
151
+ /**
152
+ * Arrow function property — structurally matches the TickMiddleware
153
+ * call signature `(tick, time, state, context) => void | Promise<void>`.
154
+ * Assign this to orchestratorConfig.onAfterTick.
155
+ */
156
+ readonly onTick = (
157
+ tick: Tick,
158
+ _time: Timestamp,
159
+ state: ReadonlySimulationState,
160
+ _context: SimulationContext
161
+ ): void => {
162
+ // Tick summary
163
+ if(
164
+ this._config.tickSummaryIntervalTicks! > 0
165
+ && tick % this._config.tickSummaryIntervalTicks! === 0
166
+ ){
167
+ this._enqueue({
168
+ type: 'tick-summary',
169
+ timestamp: Date.now(),
170
+ tick,
171
+ payload: {
172
+ tick,
173
+ simTime: this._clock.now,
174
+ wallDuration: this._getTickLatency(),
175
+ entityCount: state.entities.size,
176
+ metricCount: state.metrics.size,
177
+ pendingEvents: this._eventBus.getPendingCount(),
178
+ } satisfies TickSummary,
179
+ })
180
+ }
181
+
182
+ // Full snapshot
183
+ if(
184
+ this._config.snapshotIntervalTicks! > 0
185
+ && tick % this._config.snapshotIntervalTicks! === 0
186
+ ){
187
+ this._enqueue({
188
+ type: 'snapshot',
189
+ timestamp: Date.now(),
190
+ tick,
191
+ payload: this._stateManager.snapshot(),
192
+ })
193
+ }
194
+ }
195
+
196
+ // ── Lifecycle ───────────────────────────────────────────
197
+
198
+ /** Start streaming. Must be called after construction. */
199
+ start(): void {
200
+ // Subscribe to event bus
201
+ if( this._config.streamEvents ){
202
+ this._unsubscribeEventBus = this._eventBus.subscribeAll( ( event, _ctx ) => {
203
+ if( this._config.excludeEventTypes?.includes( event.type ) ) return
204
+ this._enqueue({
205
+ type: 'event',
206
+ timestamp: Date.now(),
207
+ tick: event.tick,
208
+ payload: event,
209
+ })
210
+ })
211
+ }
212
+
213
+ // Periodic flush (handles metric streaming + drain)
214
+ this._flushInterval = setInterval( () => {
215
+ // Metric streaming can be wired here if MetricCollector exposes a
216
+ // per-point callback. Currently metrics flow through tick summaries.
217
+ this._drainQueue()
218
+ }, 50 ) // 20 fps flush rate
219
+ }
220
+
221
+ /** Stop streaming and close transport. */
222
+ stop(): void {
223
+ this._unsubscribeEventBus?.()
224
+ if( this._flushInterval ) clearInterval( this._flushInterval )
225
+ this._drainQueue()
226
+ this._transport.close()
227
+ }
228
+
229
+ // ── Backpressure handling ───────────────────────────────
230
+
231
+ private _enqueue( message: StreamMessage ): void {
232
+ if( this._messageQueue.length >= this._maxQueueDepth ){
233
+ // Drop oldest message (circular buffer semantics)
234
+ this._messageQueue.shift()
235
+ logger.warn(`[LiveStream] Queue full (${this._maxQueueDepth}), dropping oldest message`)
236
+ }
237
+
238
+ this._messageQueue.push( message )
239
+ }
240
+
241
+ private _drainQueue(): void {
242
+ while( this._messageQueue.length > 0 ){
243
+ const message = this._messageQueue.shift()!
244
+ try {
245
+ this._transport.send( message )
246
+ }
247
+ catch( error ){
248
+ logger.error('[LiveStream] Failed to send message, transport may be closed:', error )
249
+ // Stop trying — transport is dead
250
+ this._messageQueue.length = 0
251
+ break
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ // ── WebSocket transport (Bun-native) ──────────────────────
258
+
259
+ export interface WebSocketConfig {
260
+ port: number
261
+ path?: string // e.g., '/live'
262
+ maxConnections?: number
263
+ }
264
+
265
+ /**
266
+ * FIX 3: Bun.serve().upgrade() requires an options object with `data`.
267
+ * The third argument is not rest parameters — it's a single options bag.
268
+ */
269
+ export class WebSocketTransport implements LiveStreamTransport {
270
+ private _server: ReturnType<typeof Bun.serve> | null = null
271
+ private _clients: Set<{ send: ( data: string ) => void }> = new Set()
272
+ private _config: WebSocketConfig
273
+ private _maxConnections: number
274
+
275
+ constructor( config: WebSocketConfig ){
276
+ this._config = config
277
+ this._maxConnections = config.maxConnections ?? 100
278
+ }
279
+
280
+ send( message: StreamMessage ): void {
281
+ const data = JSON.stringify( message )
282
+
283
+ for( const client of this._clients )
284
+ client.send( data )
285
+ }
286
+
287
+ close(): void {
288
+ this._server?.stop()
289
+ this._clients.clear()
290
+ }
291
+
292
+ onRegister(): void {
293
+ this._server = Bun.serve({
294
+ port: this._config.port,
295
+ fetch: ( req, server ) => {
296
+ // Only upgrade on the configured path
297
+ const url = new URL( req.url )
298
+ if( this._config.path && url.pathname !== this._config.path )
299
+ return new Response('Not found', { status: 404 } )
300
+
301
+ // Connection limit
302
+ if( this._clients.size >= this._maxConnections )
303
+ return new Response('Too many connections', { status: 503 } )
304
+
305
+ // FIX 3: upgrade() requires { data } as second argument per Bun types
306
+ const upgraded = server.upgrade( req, { data: {} } )
307
+ if( !upgraded )
308
+ return new Response('WebSocket upgrade failed', { status: 400 } )
309
+
310
+ return undefined // Bun handles the upgrade response
311
+ },
312
+ websocket: {
313
+ open: ( ws ) => {
314
+ this._clients.add( ws )
315
+ logger.info(`[WebSocket] Client connected (${this._clients.size} total)`)
316
+ },
317
+ close: ( ws ) => {
318
+ this._clients.delete( ws )
319
+ logger.info(`[WebSocket] Client disconnected (${this._clients.size} total)`)
320
+ },
321
+ message: ( _ws, _message ) => {
322
+ // Clients can send commands here (pause, resume, set speed, etc.)
323
+ // For now, read-only streaming
324
+ },
325
+ },
326
+ })
327
+
328
+ logger.info(`[WebSocket] Server listening on ws://localhost:${this._config.port}${this._config.path ?? '/'}`)
329
+ }
330
+ }
331
+
332
+ // ── Server-Sent Events transport ──────────────────────────
333
+
334
+ export interface SSEConfig {
335
+ port: number
336
+ path?: string
337
+ }
338
+
339
+ /**
340
+ * FIX 4 & 5: ReadableStreamDefaultController (from Bun.serve fetch) is not
341
+ * a WritableStreamDefaultWriter. SSE uses a ReadableStream to push events
342
+ * to the client. We need to store the controller and use controller.enqueue()
343
+ * instead of writer.write().
344
+ */
345
+ export class SSETransport implements LiveStreamTransport {
346
+ private _server: ReturnType<typeof Bun.serve> | null = null
347
+ private _clients: Set<ReadableStreamDefaultController<Uint8Array>> = new Set()
348
+ private _config: SSEConfig
349
+ private _encoder = new TextEncoder()
350
+
351
+ constructor( config: SSEConfig ){
352
+ this._config = config
353
+ }
354
+
355
+ send( message: StreamMessage ): void {
356
+ const
357
+ eventLine = `event: ${message.type}\n`,
358
+ dataLine = `data: ${JSON.stringify( message )}\n\n`,
359
+ encoded = this._encoder.encode( eventLine + dataLine )
360
+
361
+ for( const controller of this._clients )
362
+ controller.enqueue( encoded )
363
+ }
364
+
365
+ close(): void {
366
+ this._server?.stop()
367
+ for( const controller of this._clients ){
368
+ try { controller.close() } catch {}
369
+ }
370
+ this._clients.clear()
371
+ }
372
+
373
+ onRegister(): void {
374
+ this._server = Bun.serve({
375
+ port: this._config.port,
376
+ fetch: async ( req ) => {
377
+ const url = new URL( req.url )
378
+ if( this._config.path && url.pathname !== this._config.path )
379
+ return new Response('Not found', { status: 404 } )
380
+
381
+ /**
382
+ * FIX 4 & 5: ReadableStream with start(controller) gives us a
383
+ * ReadableStreamDefaultController. We store the controller directly
384
+ * and use controller.enqueue() to push SSE data. The client's
385
+ * abort signal handles disconnection cleanup.
386
+ */
387
+ let streamController: ReadableStreamDefaultController<Uint8Array> | null = null
388
+
389
+ const stream = new ReadableStream<Uint8Array>({
390
+ start: ( controller ) => {
391
+ streamController = controller
392
+ this._clients.add( controller )
393
+
394
+ // Send initial comment to establish connection
395
+ controller.enqueue( this._encoder.encode(': connected\n\n') )
396
+
397
+ req.signal.addEventListener('abort', () => {
398
+ if( streamController ){
399
+ this._clients.delete( streamController )
400
+ logger.info(`[SSE] Client disconnected (${this._clients.size} total)`)
401
+ streamController = null
402
+ }
403
+ })
404
+ },
405
+ cancel: () => {
406
+ if( streamController ){
407
+ this._clients.delete( streamController )
408
+ streamController = null
409
+ }
410
+ },
411
+ })
412
+
413
+ return new Response( stream, {
414
+ headers: {
415
+ 'Content-Type': 'text/event-stream',
416
+ 'Cache-Control': 'no-cache',
417
+ 'Connection': 'keep-alive',
418
+ },
419
+ })
420
+ },
421
+ })
422
+
423
+ logger.info(`[SSE] Server listening on http://localhost:${this._config.port}${this._config.path ?? '/'}`)
424
+ }
425
+ }
426
+
427
+ // ── Console transport (for development) ───────────────────
428
+
429
+ export class ConsoleTransport implements LiveStreamTransport {
430
+ private _verbose: boolean
431
+
432
+ constructor( verbose: boolean = false ){
433
+ this._verbose = verbose
434
+ }
435
+
436
+ send( message: StreamMessage ): void {
437
+ if( !this._verbose && message.type === 'tick-summary'){
438
+ // Only log every 100 ticks for summary
439
+ const summary = message.payload as TickSummary
440
+ if( summary.tick % 100 === 0 ){
441
+ logger.info(
442
+ `[Live] Tick ${summary.tick}: ${summary.entityCount} entities, ${summary.wallDuration}ms`
443
+ )
444
+ }
445
+ return
446
+ }
447
+
448
+ logger.info(`[Live] ${message.type}:`, message.payload )
449
+ }
450
+
451
+ close(): void {
452
+ // No-op for console
453
+ }
454
+ }
455
+
456
+ // ── Factory helpers ───────────────────────────────────────
457
+
458
+ export class LiveStreamFactory {
459
+ /**
460
+ * Create a WebSocket-backed live stream wired into a running simulation.
461
+ * Wire `stream.onTick` as `orchestratorConfig.onAfterTick`.
462
+ */
463
+ static createWebSocket(
464
+ sim: {
465
+ eventBus: EventBus
466
+ stateManager: StateManager
467
+ orchestrator: Orchestrator
468
+ clock: SimulationClock
469
+ metrics: MetricCollector
470
+ },
471
+ config: LiveStreamConfig & WebSocketConfig
472
+ ): LiveStreamExtension {
473
+ const transport = new WebSocketTransport({
474
+ port: config.port,
475
+ path: config.path,
476
+ maxConnections: config.maxConnections,
477
+ })
478
+
479
+ return new LiveStreamExtension(
480
+ transport,
481
+ sim.eventBus,
482
+ sim.stateManager,
483
+ sim.orchestrator,
484
+ sim.clock,
485
+ config,
486
+ sim.metrics
487
+ )
488
+ }
489
+
490
+ /**
491
+ * Create an SSE-backed live stream.
492
+ */
493
+ static createSSE(
494
+ sim: {
495
+ eventBus: EventBus
496
+ stateManager: StateManager
497
+ orchestrator: Orchestrator
498
+ clock: SimulationClock
499
+ metrics: MetricCollector
500
+ },
501
+ config: LiveStreamConfig & SSEConfig
502
+ ): LiveStreamExtension {
503
+ const transport = new SSETransport({
504
+ port: config.port,
505
+ path: config.path,
506
+ })
507
+
508
+ return new LiveStreamExtension(
509
+ transport,
510
+ sim.eventBus,
511
+ sim.stateManager,
512
+ sim.orchestrator,
513
+ sim.clock,
514
+ config,
515
+ sim.metrics
516
+ )
517
+ }
518
+
519
+ /**
520
+ * Quick console logger for development.
521
+ */
522
+ static createConsole(
523
+ sim: {
524
+ eventBus: EventBus
525
+ stateManager: StateManager
526
+ orchestrator: Orchestrator
527
+ clock: SimulationClock
528
+ metrics: MetricCollector
529
+ },
530
+ config: LiveStreamConfig = {}
531
+ ): LiveStreamExtension {
532
+ const transport = new ConsoleTransport()
533
+
534
+ return new LiveStreamExtension(
535
+ transport,
536
+ sim.eventBus,
537
+ sim.stateManager,
538
+ sim.orchestrator,
539
+ sim.clock,
540
+ config,
541
+ sim.metrics
542
+ )
543
+ }
544
+ }
545
+
546
+ // ── Usage example ─────────────────────────────────────────
547
+
548
+ // import { DefaultSimulation } from '../core/simulation'
549
+ // import { LiveStreamFactory } from './live.ext'
550
+ //
551
+ // const sim = new DefaultSimulation()
552
+ // // ... load scenario, add engines ...
553
+ //
554
+ // const liveStream = LiveStreamFactory.createWebSocket( sim, {
555
+ // port: 8080,
556
+ // path: '/simulation-live',
557
+ // tickSummaryIntervalTicks: 1,
558
+ // streamEvents: true,
559
+ // snapshotIntervalTicks: 100,
560
+ // })
561
+ //
562
+ // // Wire into orchestrator using updateConfig — works post-construction
563
+ // const unsubLive = sim.orchestrator.onAfterTick( liveStream.onTick )
564
+ //
565
+ // // Later, remove one without affecting the other
566
+ // unsubLive()
567
+ //
568
+ // liveStream.start()
569
+ // await sim.run()
570
+ // liveStream.stop()