@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.
- package/LICENSE +202 -0
- package/README.md +777 -0
- package/dist/index.d.ts +8144 -0
- package/dist/index.js +26231 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/src/cognition/agency/access.grants.ts +59 -0
- package/src/cognition/agency/competence.codec.ts +89 -0
- package/src/cognition/agency/engines/action.selector.ts +462 -0
- package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
- package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
- package/src/cognition/agency/engines/instruction.intake.ts +54 -0
- package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
- package/src/cognition/agency/engines/reafference.engine.ts +241 -0
- package/src/cognition/agency/execution.primitives.ts +118 -0
- package/src/cognition/agency/index.ts +58 -0
- package/src/cognition/agency/proactive.communicator.ts +274 -0
- package/src/cognition/agency/reconcile.learning.ts +82 -0
- package/src/cognition/agency/schemas/external.ts +59 -0
- package/src/cognition/agency/schemas/innate.ts +111 -0
- package/src/cognition/agency/schemas/repertoire.ts +246 -0
- package/src/cognition/agency/selection.scoring.ts +191 -0
- package/src/cognition/agency/types.ts +165 -0
- package/src/cognition/bus.ts +391 -0
- package/src/cognition/completion.inbox.ts +86 -0
- package/src/cognition/config.mirror.entities.ts +475 -0
- package/src/cognition/conversation.memory.ts +81 -0
- package/src/cognition/event.log.ts +116 -0
- package/src/cognition/event.schemas.ts +568 -0
- package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
- package/src/cognition/faculties/affective.blender.ts +562 -0
- package/src/cognition/faculties/attachment.evaluator.ts +432 -0
- package/src/cognition/faculties/attention.allocator.ts +422 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
- package/src/cognition/faculties/bias.detector.ts +363 -0
- package/src/cognition/faculties/circadian.oscillator.ts +294 -0
- package/src/cognition/faculties/confidence.calibrator.ts +324 -0
- package/src/cognition/faculties/dream.simulator.ts +251 -0
- package/src/cognition/faculties/empathy.simulator.ts +253 -0
- package/src/cognition/faculties/energy.regulator.ts +320 -0
- package/src/cognition/faculties/episodic.consolidator.ts +728 -0
- package/src/cognition/faculties/executive.engine/commands.ts +459 -0
- package/src/cognition/faculties/executive.engine/config.ts +46 -0
- package/src/cognition/faculties/executive.engine/context.ts +605 -0
- package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
- package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
- package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
- package/src/cognition/faculties/executive.engine/facet.ts +628 -0
- package/src/cognition/faculties/executive.engine/gating.ts +222 -0
- package/src/cognition/faculties/executive.engine/index.ts +6 -0
- package/src/cognition/faculties/executive.engine/messages.ts +102 -0
- package/src/cognition/faculties/executive.engine/parser.ts +380 -0
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
- package/src/cognition/faculties/executive.engine/types.ts +323 -0
- package/src/cognition/faculties/exteroception.ts +338 -0
- package/src/cognition/faculties/forgetting.curve.ts +202 -0
- package/src/cognition/faculties/frustration.evaluator.ts +280 -0
- package/src/cognition/faculties/goal.manager.ts +1008 -0
- package/src/cognition/faculties/inhibition.controller.ts +351 -0
- package/src/cognition/faculties/interoception.ts +417 -0
- package/src/cognition/faculties/introspection.engine.ts +246 -0
- package/src/cognition/faculties/known.entity.tracker.ts +439 -0
- package/src/cognition/faculties/loss.evaluator.ts +224 -0
- package/src/cognition/faculties/moral.evaluator.ts +437 -0
- package/src/cognition/faculties/novelty.detector.ts +254 -0
- package/src/cognition/faculties/persona.consolidator.ts +845 -0
- package/src/cognition/faculties/planning.engine/engine.ts +859 -0
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
- package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
- package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
- package/src/cognition/faculties/planning.engine/types.ts +143 -0
- package/src/cognition/faculties/reputation.tracker.ts +294 -0
- package/src/cognition/faculties/reward.evaluator.ts +304 -0
- package/src/cognition/faculties/self.model.updater.ts +677 -0
- package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
- package/src/cognition/faculties/semantic.engine/index.ts +6 -0
- package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
- package/src/cognition/faculties/semantic.engine/types.ts +53 -0
- package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
- package/src/cognition/faculties/social.perception.ts +333 -0
- package/src/cognition/faculties/spaced.repetition.ts +605 -0
- package/src/cognition/faculties/stress.regulator.ts +375 -0
- package/src/cognition/faculties/task.switcher.ts +239 -0
- package/src/cognition/faculties/theory.of.mind.ts +333 -0
- package/src/cognition/faculties/threat.evaluator.ts +312 -0
- package/src/cognition/faculties/working.memory.ts +406 -0
- package/src/cognition/generative.model.ts +344 -0
- package/src/cognition/heartbeat.ts +70 -0
- package/src/cognition/index.ts +291 -0
- package/src/cognition/instruction.handler.ts +146 -0
- package/src/cognition/memory/index.ts +35 -0
- package/src/cognition/memory/vector.adapter.ts +316 -0
- package/src/cognition/memory/vector.content.ts +47 -0
- package/src/cognition/memory/vector.embedder.ts +212 -0
- package/src/cognition/memory/vector.index.ts +459 -0
- package/src/cognition/memory/vector.types.ts +65 -0
- package/src/cognition/orchestrator.ts +142 -0
- package/src/cognition/persona.prior.ts +265 -0
- package/src/cognition/schema.registry.ts +145 -0
- package/src/cognition/senses/audition.engine/engine.ts +976 -0
- package/src/cognition/senses/audition.engine/salience.ts +59 -0
- package/src/cognition/senses/base.sense.engine.ts +117 -0
- package/src/cognition/senses/gustation.engine.ts +24 -0
- package/src/cognition/senses/index.ts +172 -0
- package/src/cognition/senses/olfaction.engine.ts +24 -0
- package/src/cognition/senses/somatosensation.engine.ts +23 -0
- package/src/cognition/senses/vision.engine.ts +23 -0
- package/src/cognition/types.ts +63 -0
- package/src/cognition/utilities/token.tracker.ts +492 -0
- package/src/core/abstracts.ts +60 -0
- package/src/core/async.engine.ts +539 -0
- package/src/core/clock.ts +196 -0
- package/src/core/completion.recorder.ts +122 -0
- package/src/core/conflict.detector.ts +140 -0
- package/src/core/distributed.ts +626 -0
- package/src/core/event.bus.ts +292 -0
- package/src/core/inbound.recorder.ts +100 -0
- package/src/core/index.ts +167 -0
- package/src/core/logger.ts +85 -0
- package/src/core/metrics.ts +107 -0
- package/src/core/orchestrator.ts +625 -0
- package/src/core/replay.ts +589 -0
- package/src/core/scenario.ts +120 -0
- package/src/core/serialization.ts +448 -0
- package/src/core/simulation.ts +197 -0
- package/src/core/snapshot.manager.ts +293 -0
- package/src/core/state.manager.ts +283 -0
- package/src/core/types.ts +274 -0
- package/src/core/utils.ts +82 -0
- package/src/core/wall.clock.ts +23 -0
- package/src/extensions/livestream.ext.ts +570 -0
- package/src/extensions/time.ext.ts +339 -0
- package/src/index.ts +91 -0
- package/src/llm/gate.ts +140 -0
- package/src/llm/index.ts +716 -0
- package/src/llm/summarizer.ts +170 -0
- package/src/llm/wire.contracts.ts +67 -0
- package/src/pma/eval.ts +651 -0
- package/src/pma/index.ts +1082 -0
- package/src/profiles/built-in.ts +7 -0
- package/src/profiles/companion.ts +28 -0
- package/src/profiles/company-brain.ts +54 -0
- package/src/profiles/customer-service.ts +30 -0
- package/src/profiles/game-npc.ts +20 -0
- package/src/profiles/index.ts +42 -0
- package/src/profiles/smart-home.ts +34 -0
- package/src/runners/coherence.runner.ts +49 -0
- package/src/runners/outreach.runner.ts +158 -0
- package/src/runners/social.runner.ts +182 -0
- package/src/runners/thin-shim.runner.ts +195 -0
- package/src/sdk/will.ts +334 -0
- package/src/stem/assembly.audit.ts +88 -0
- package/src/stem/distribution.ts +203 -0
- package/src/stem/guards/identity.coherence.ts +152 -0
- package/src/stem/guards/identity.guard.ts +227 -0
- package/src/stem/index.ts +1145 -0
- package/src/stem/mind.ts +1090 -0
- package/src/stem/tracts/ack.reconciler.ts +55 -0
- package/src/stem/tracts/biography.writer.ts +190 -0
- package/src/stem/tracts/effector.controller.ts +143 -0
- package/src/stem/tracts/health.reporter.ts +113 -0
- package/src/stem/tracts/inbound.queue.ts +54 -0
- package/src/stem/tracts/outbox.controller.ts +167 -0
- package/src/stem/tracts/outbox.writer.ts +133 -0
- package/src/stem/tracts/pma.controller.ts +109 -0
- package/src/stem/tracts/replay.controller.ts +177 -0
- package/src/stem/tracts/sensory.controller.ts +168 -0
- package/src/stem/tracts/session.logger.ts +157 -0
- package/src/stem/tracts/transport/index.ts +38 -0
- package/src/stem/tracts/transport/loopback.transport.ts +86 -0
- package/src/stem/tracts/transport/socketio.transport.ts +200 -0
- package/src/stem/tracts/transport/stream.transport.ts +152 -0
- package/src/stem/tracts/transport/types.ts +193 -0
- package/src/stem/tracts/transport.controller.ts +380 -0
- package/src/types.ts +116 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/core/replay.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Deterministic replay system for debugging and analysis.
|
|
7
|
+
* Records all inputs/events and allows frame-accurate replay.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SimulationContext, SimulationEvent, Tick, Timestamp } from '#core/types'
|
|
11
|
+
import { BunStorageAdapter, type StorageAdapter } from '#core/abstracts'
|
|
12
|
+
import { DeltaEncoder } from '#core/serialization'
|
|
13
|
+
import type { DeltaSnapshot } from '#core/serialization'
|
|
14
|
+
import type { LLMCompletionRecord, LLMCompletionSink } from '#core/completion.recorder'
|
|
15
|
+
import type { InboundRecord, InboundSink } from '#core/inbound.recorder'
|
|
16
|
+
import { wallClock } from '#core/wall.clock'
|
|
17
|
+
|
|
18
|
+
export interface ReplayRecord {
|
|
19
|
+
tick: Tick
|
|
20
|
+
timestamp: Timestamp
|
|
21
|
+
events: SimulationEvent[]
|
|
22
|
+
delta?: DeltaSnapshot
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ReplayMetadata {
|
|
26
|
+
simulationId: string
|
|
27
|
+
runId: string
|
|
28
|
+
startTime: Timestamp
|
|
29
|
+
endTime: Timestamp
|
|
30
|
+
totalTicks: Tick
|
|
31
|
+
totalEvents: number
|
|
32
|
+
totalCompletions: number
|
|
33
|
+
/** Count of recorded external inbound envelopes (messages/percepts/acks). */
|
|
34
|
+
totalInbound?: number
|
|
35
|
+
randomSeed: number
|
|
36
|
+
version: string
|
|
37
|
+
tags: Record<string, string>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ReplaySession {
|
|
41
|
+
readonly metadata: ReplayMetadata
|
|
42
|
+
readonly currentTick: Tick
|
|
43
|
+
readonly isPlaying: boolean
|
|
44
|
+
readonly isPaused: boolean
|
|
45
|
+
readonly speed: number
|
|
46
|
+
play(): void
|
|
47
|
+
pause(): void
|
|
48
|
+
stop(): void
|
|
49
|
+
seekToTick( tick: Tick ): Promise<void>
|
|
50
|
+
seekToTime( time: Timestamp ): Promise<void>
|
|
51
|
+
stepForward( count?: number ): Promise<SimulationEvent[]>
|
|
52
|
+
stepBackward( count?: number ): Promise<SimulationEvent[]>
|
|
53
|
+
setSpeed( speed: number ): void
|
|
54
|
+
export(): Promise<Uint8Array>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ReplayRecorder extends LLMCompletionSink, InboundSink {
|
|
58
|
+
recordEvent( event: SimulationEvent, context: SimulationContext ): void
|
|
59
|
+
recordTick( tick: Tick, events: SimulationEvent[] ): void
|
|
60
|
+
recordCompletion( record: LLMCompletionRecord ): void
|
|
61
|
+
recordInbound( record: InboundRecord ): void
|
|
62
|
+
flush(): Promise<void>
|
|
63
|
+
save( path: string ): Promise<void>
|
|
64
|
+
getMetadata(): ReplayMetadata
|
|
65
|
+
close(): void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** On-disk shape of a flush segment: a batch of tick records + completions. */
|
|
69
|
+
interface ReplaySegment {
|
|
70
|
+
records: ReplayRecord[]
|
|
71
|
+
completions: LLMCompletionRecord[]
|
|
72
|
+
inbound?: InboundRecord[]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ReplayConfig {
|
|
76
|
+
bufferSize?: number
|
|
77
|
+
flushIntervalMs?: number
|
|
78
|
+
compress?: boolean
|
|
79
|
+
recordDeltas?: boolean
|
|
80
|
+
maxEventsPerTick?: number
|
|
81
|
+
/**
|
|
82
|
+
* Base path for incremental flush segments. When set, flush() persists the
|
|
83
|
+
* buffered records to `${flushBasePath}.partN.json` and clears the buffer;
|
|
84
|
+
* save() consolidates the segments back into the final file. When unset,
|
|
85
|
+
* flush() retains records in memory (still no data loss) until save().
|
|
86
|
+
*/
|
|
87
|
+
flushBasePath?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Recorder ─────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
export class DefaultReplayRecorder implements ReplayRecorder {
|
|
93
|
+
private _records: ReplayRecord[] = []
|
|
94
|
+
private _completions: LLMCompletionRecord[] = []
|
|
95
|
+
private _inbound: InboundRecord[] = []
|
|
96
|
+
private _metadata: ReplayMetadata
|
|
97
|
+
private _config: ReplayConfig
|
|
98
|
+
private _bufferSize: number
|
|
99
|
+
private _flushInterval: NodeJS.Timeout | null = null
|
|
100
|
+
private _deltaEncoder: DeltaEncoder
|
|
101
|
+
private _storage: StorageAdapter
|
|
102
|
+
private _flushBasePath: string | undefined
|
|
103
|
+
private _segments: string[] = []
|
|
104
|
+
private _segmentCount = 0
|
|
105
|
+
|
|
106
|
+
constructor(
|
|
107
|
+
simulationId: string,
|
|
108
|
+
runId: string,
|
|
109
|
+
randomSeed: number = wallClock(),
|
|
110
|
+
config: ReplayConfig = {},
|
|
111
|
+
storage: StorageAdapter = new BunStorageAdapter()
|
|
112
|
+
){
|
|
113
|
+
this._config = config
|
|
114
|
+
this._bufferSize = config.bufferSize ?? 1000
|
|
115
|
+
this._deltaEncoder = new DeltaEncoder()
|
|
116
|
+
this._storage = storage
|
|
117
|
+
this._flushBasePath = config.flushBasePath
|
|
118
|
+
|
|
119
|
+
this._metadata = {
|
|
120
|
+
simulationId,
|
|
121
|
+
runId,
|
|
122
|
+
startTime: wallClock(),
|
|
123
|
+
endTime: 0,
|
|
124
|
+
totalTicks: 0,
|
|
125
|
+
totalEvents: 0,
|
|
126
|
+
totalCompletions: 0,
|
|
127
|
+
totalInbound: 0,
|
|
128
|
+
randomSeed,
|
|
129
|
+
version: '1.0.0',
|
|
130
|
+
tags: {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if( config.flushIntervalMs && config.flushIntervalMs > 0 )
|
|
134
|
+
this._flushInterval = setInterval( () => this.flush(), config.flushIntervalMs )
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
recordEvent( event: SimulationEvent, _context: SimulationContext ): void {
|
|
138
|
+
let record = this._records.find( r => r.tick === event.tick )
|
|
139
|
+
if( !record ){
|
|
140
|
+
record = { tick: event.tick, timestamp: event.timestamp, events: [] }
|
|
141
|
+
this._records.push( record )
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
record.events.push( event )
|
|
145
|
+
this._metadata.totalEvents++
|
|
146
|
+
|
|
147
|
+
this._records.length >= this._bufferSize && this.flush().catch( () => {} )
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
recordTick( tick: Tick, events: SimulationEvent[] ): void {
|
|
151
|
+
this._records.push({ tick, timestamp: wallClock(), events: [ ...events ] })
|
|
152
|
+
this._metadata.totalEvents += events.length
|
|
153
|
+
this._metadata.totalTicks = tick
|
|
154
|
+
|
|
155
|
+
this._records.length >= this._bufferSize && this.flush().catch( () => {} )
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Record an LLM completion (input + output + model/params) into the replay
|
|
160
|
+
* stream. The LLM is the system's non-deterministic oracle; capturing each
|
|
161
|
+
* completion is the prerequisite for deterministic re-execution (REORIENT R2,
|
|
162
|
+
* deferred) and is the gap FN3 calls out. Completions ride along with tick
|
|
163
|
+
* records through the same flush/segment/save pipeline.
|
|
164
|
+
*/
|
|
165
|
+
recordCompletion( record: LLMCompletionRecord ): void {
|
|
166
|
+
this._completions.push( record )
|
|
167
|
+
this._metadata.totalCompletions++
|
|
168
|
+
|
|
169
|
+
this._records.length >= this._bufferSize && this.flush().catch( () => {} )
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Record an external inbound envelope (message/percept/ack) at the tick it was
|
|
174
|
+
* applied. The transport is a non-deterministic input — capturing each envelope
|
|
175
|
+
* is the prerequisite for re-injecting them on a deterministic re-execution
|
|
176
|
+
* replay. Rides the same flush/segment/save pipeline as completions.
|
|
177
|
+
*/
|
|
178
|
+
recordInbound( record: InboundRecord ): void {
|
|
179
|
+
this._inbound.push( record )
|
|
180
|
+
this._metadata.totalInbound = ( this._metadata.totalInbound ?? 0 ) + 1
|
|
181
|
+
|
|
182
|
+
this._records.length >= this._bufferSize && this.flush().catch( () => {} )
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async flush(): Promise<void> {
|
|
186
|
+
if( this._records.length === 0 && this._completions.length === 0 ) return
|
|
187
|
+
|
|
188
|
+
// Without a persistence target, retain records in memory so they survive
|
|
189
|
+
// until save() — never discard (the previous behaviour silently lost all
|
|
190
|
+
// history once the buffer rolled over at bufferSize).
|
|
191
|
+
if( !this._flushBasePath ) return
|
|
192
|
+
|
|
193
|
+
// Swap the buffers out and reserve the segment name synchronously before
|
|
194
|
+
// any await: recordTick fires flush() without awaiting and keeps pushing,
|
|
195
|
+
// so concurrent flushes must each get a fresh array and a unique segment
|
|
196
|
+
// name (reading _segmentCount after the await would collide and overwrite).
|
|
197
|
+
const batch: ReplaySegment = { records: this._records, completions: this._completions, inbound: this._inbound }
|
|
198
|
+
this._records = []
|
|
199
|
+
this._completions = []
|
|
200
|
+
this._inbound = []
|
|
201
|
+
const segPath = `${this._flushBasePath}.part${this._segmentCount}.json`
|
|
202
|
+
this._segmentCount++
|
|
203
|
+
this._segments.push( segPath )
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
await this._storage.write( segPath, JSON.stringify( batch ) )
|
|
207
|
+
}
|
|
208
|
+
catch( err ){
|
|
209
|
+
// Persisting failed — drop the reservation and fold the batch back in
|
|
210
|
+
// (ahead of newer records) so save() still captures it from memory.
|
|
211
|
+
this._segments = this._segments.filter( s => s !== segPath )
|
|
212
|
+
this._records = batch.records.concat( this._records )
|
|
213
|
+
this._completions = batch.completions.concat( this._completions )
|
|
214
|
+
this._inbound = ( batch.inbound ?? [] ).concat( this._inbound )
|
|
215
|
+
throw err
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async save( path: string ): Promise<void> {
|
|
220
|
+
// Consolidate any flushed segments with the live buffers so nothing is lost
|
|
221
|
+
// across buffer rollovers.
|
|
222
|
+
const records: ReplayRecord[] = []
|
|
223
|
+
const completions: LLMCompletionRecord[] = []
|
|
224
|
+
const inbound: InboundRecord[] = []
|
|
225
|
+
for( const seg of this._segments ){
|
|
226
|
+
try {
|
|
227
|
+
const parsed = JSON.parse( await this._storage.read( seg ) ) as ReplaySegment
|
|
228
|
+
records.push( ...( parsed.records ?? [] ) )
|
|
229
|
+
completions.push( ...( parsed.completions ?? [] ) )
|
|
230
|
+
inbound.push( ...( parsed.inbound ?? [] ) )
|
|
231
|
+
} catch { /* segment unreadable — skip rather than abort the save */ }
|
|
232
|
+
}
|
|
233
|
+
records.push( ...this._records )
|
|
234
|
+
completions.push( ...this._completions )
|
|
235
|
+
inbound.push( ...this._inbound )
|
|
236
|
+
|
|
237
|
+
const output = {
|
|
238
|
+
metadata: { ...this._metadata, endTime: wallClock() },
|
|
239
|
+
config: this._config,
|
|
240
|
+
records,
|
|
241
|
+
completions,
|
|
242
|
+
inbound,
|
|
243
|
+
}
|
|
244
|
+
await this._storage.write( path, JSON.stringify( output, null, 2 ) )
|
|
245
|
+
|
|
246
|
+
// Clean up consolidated segment files.
|
|
247
|
+
const del = this._storage.delete
|
|
248
|
+
if( del )
|
|
249
|
+
for( const seg of this._segments )
|
|
250
|
+
await del.call( this._storage, seg ).catch( () => {} )
|
|
251
|
+
|
|
252
|
+
this._segments = []
|
|
253
|
+
this._segmentCount = 0
|
|
254
|
+
this._records = []
|
|
255
|
+
this._completions = []
|
|
256
|
+
this._inbound = []
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
getMetadata(): ReplayMetadata {
|
|
260
|
+
return { ...this._metadata }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
close(): void {
|
|
264
|
+
this._flushInterval && clearInterval( this._flushInterval )
|
|
265
|
+
this._metadata.endTime = wallClock()
|
|
266
|
+
this.flush().catch( () => {} )
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Session ──────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
export class DefaultReplaySession implements ReplaySession {
|
|
273
|
+
private _records: ReplayRecord[]
|
|
274
|
+
private _metadata: ReplayMetadata
|
|
275
|
+
private _currentIndex = 0
|
|
276
|
+
private _isPlaying = false
|
|
277
|
+
private _isPaused = false
|
|
278
|
+
private _speed = 1.0
|
|
279
|
+
private _playInterval: NodeJS.Timeout | null = null
|
|
280
|
+
private _eventHandlers: Array<( event: SimulationEvent ) => void> = []
|
|
281
|
+
|
|
282
|
+
constructor( records: ReplayRecord[], metadata: ReplayMetadata ){
|
|
283
|
+
this._records = records.sort( ( a, b ) => a.tick - b.tick )
|
|
284
|
+
this._metadata = metadata
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
get metadata(): ReplayMetadata { return { ...this._metadata } }
|
|
288
|
+
get currentTick(): Tick { return this._records[this._currentIndex]?.tick ?? 0 }
|
|
289
|
+
get isPlaying(): boolean { return this._isPlaying }
|
|
290
|
+
get isPaused(): boolean { return this._isPaused }
|
|
291
|
+
get speed(): number { return this._speed }
|
|
292
|
+
|
|
293
|
+
// ── Playback ─────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
play(): void {
|
|
296
|
+
if( this._isPlaying ) return
|
|
297
|
+
|
|
298
|
+
this._isPlaying = true
|
|
299
|
+
this._isPaused = false
|
|
300
|
+
|
|
301
|
+
this._playInterval = setInterval( () => {
|
|
302
|
+
if( !this._isPlaying || this._isPaused ) return
|
|
303
|
+
if( this._currentIndex >= this._records.length - 1 ){
|
|
304
|
+
this.stop()
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this._currentIndex++
|
|
309
|
+
this._emitEventsForCurrentTick()
|
|
310
|
+
}, 16 / this._speed ) // ~60fps base
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
pause(): void {
|
|
314
|
+
this._isPaused = true
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
stop(): void {
|
|
318
|
+
this._isPlaying = false
|
|
319
|
+
this._isPaused = false
|
|
320
|
+
this._playInterval && clearInterval( this._playInterval )
|
|
321
|
+
this._playInterval = null
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async seekToTick( targetTick: Tick ): Promise<void> {
|
|
325
|
+
const wasPlaying = this._isPlaying
|
|
326
|
+
if( wasPlaying ) this.pause()
|
|
327
|
+
|
|
328
|
+
let index = this._records.findIndex( r => r.tick >= targetTick )
|
|
329
|
+
if( index === -1 ) index = this._records.length - 1
|
|
330
|
+
if( index < 0 ) index = 0
|
|
331
|
+
|
|
332
|
+
this._currentIndex = index
|
|
333
|
+
await this._emitEventsForCurrentTick()
|
|
334
|
+
|
|
335
|
+
wasPlaying && this.play()
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async seekToTime( targetTime: Timestamp ): Promise<void> {
|
|
339
|
+
const wasPlaying = this._isPlaying
|
|
340
|
+
wasPlaying && this.pause()
|
|
341
|
+
|
|
342
|
+
let index = this._records.findIndex( r => r.timestamp >= targetTime )
|
|
343
|
+
if( index === -1 ) index = this._records.length - 1
|
|
344
|
+
if( index < 0 ) index = 0
|
|
345
|
+
|
|
346
|
+
this._currentIndex = index
|
|
347
|
+
await this._emitEventsForCurrentTick()
|
|
348
|
+
|
|
349
|
+
wasPlaying && this.play()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async stepForward( count: number = 1 ): Promise<SimulationEvent[]> {
|
|
353
|
+
const events: SimulationEvent[] = []
|
|
354
|
+
|
|
355
|
+
for( let i = 0; i < count; i++ ){
|
|
356
|
+
if( this._currentIndex >= this._records.length - 1 ) break
|
|
357
|
+
|
|
358
|
+
this._currentIndex++
|
|
359
|
+
const tickEvents = await this._emitEventsForCurrentTick()
|
|
360
|
+
events.push( ...tickEvents )
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return events
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async stepBackward( count: number = 1 ): Promise<SimulationEvent[]> {
|
|
367
|
+
const events: SimulationEvent[] = []
|
|
368
|
+
|
|
369
|
+
for( let i = 0; i < count; i++ ){
|
|
370
|
+
if( this._currentIndex <= 0 ) break
|
|
371
|
+
|
|
372
|
+
this._currentIndex--
|
|
373
|
+
const tickEvents = await this._emitEventsForCurrentTick()
|
|
374
|
+
events.push( ...tickEvents )
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return events
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
setSpeed( speed: number ): void {
|
|
381
|
+
this._speed = Math.max( 0.1, Math.min( 10, speed ) )
|
|
382
|
+
|
|
383
|
+
if( this._isPlaying && !this._isPaused ){
|
|
384
|
+
this.stop()
|
|
385
|
+
this.play()
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async export(): Promise<Uint8Array> {
|
|
390
|
+
const encoder = new TextEncoder()
|
|
391
|
+
return encoder.encode( JSON.stringify({ metadata: this._metadata, records: this._records }) )
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
onEvent( handler: ( event: SimulationEvent ) => void ): () => void {
|
|
395
|
+
this._eventHandlers.push( handler )
|
|
396
|
+
return () => {
|
|
397
|
+
const index = this._eventHandlers.indexOf( handler )
|
|
398
|
+
index !== -1 && this._eventHandlers.splice( index, 1 )
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Binary search for a record at a specific tick.
|
|
404
|
+
* O(log n) — records are sorted ascending by tick in the constructor.
|
|
405
|
+
* Used by ReplayManager.compare() to avoid seekToTick side effects
|
|
406
|
+
* and (session as any) type escapes.
|
|
407
|
+
*/
|
|
408
|
+
getRecordAtTick( tick: Tick ): ReplayRecord | undefined {
|
|
409
|
+
let lo = 0, hi = this._records.length - 1
|
|
410
|
+
|
|
411
|
+
while( lo <= hi ){
|
|
412
|
+
const mid = ( lo + hi ) >>> 1
|
|
413
|
+
const t = this._records[mid]!.tick
|
|
414
|
+
|
|
415
|
+
if( t === tick ) return this._records[mid]
|
|
416
|
+
t < tick ? lo = mid + 1 : hi = mid - 1
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return undefined
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private async _emitEventsForCurrentTick(): Promise<SimulationEvent[]> {
|
|
423
|
+
const record = this._records[this._currentIndex]
|
|
424
|
+
if( !record ) return []
|
|
425
|
+
|
|
426
|
+
for( const handler of this._eventHandlers )
|
|
427
|
+
for( const event of record.events )
|
|
428
|
+
handler( event )
|
|
429
|
+
|
|
430
|
+
return record.events
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Manager ──────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
export class ReplayManager {
|
|
437
|
+
private _recorders: Map<string, DefaultReplayRecorder> = new Map()
|
|
438
|
+
private _sessions: Map<string, DefaultReplaySession> = new Map()
|
|
439
|
+
private _storage: StorageAdapter
|
|
440
|
+
|
|
441
|
+
constructor( storage: StorageAdapter = new BunStorageAdapter() ){
|
|
442
|
+
this._storage = storage
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
createRecorder(
|
|
446
|
+
simulationId: string,
|
|
447
|
+
runId: string,
|
|
448
|
+
randomSeed?: number,
|
|
449
|
+
config: ReplayConfig = {}
|
|
450
|
+
): DefaultReplayRecorder {
|
|
451
|
+
const
|
|
452
|
+
key = `${simulationId}:${runId}`,
|
|
453
|
+
recorder = new DefaultReplayRecorder( simulationId, runId, randomSeed, config, this._storage )
|
|
454
|
+
|
|
455
|
+
this._recorders.set( key, recorder )
|
|
456
|
+
return recorder
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
getRecorder( simulationId: string, runId: string ): DefaultReplayRecorder | undefined {
|
|
460
|
+
return this._recorders.get(`${simulationId}:${runId}`)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async loadReplay( path: string ): Promise<DefaultReplaySession> {
|
|
464
|
+
if( !( await this._storage.exists( path ) ) )
|
|
465
|
+
throw new Error(`Replay file not found: ${path}`)
|
|
466
|
+
|
|
467
|
+
const content = await this._storage.read( path )
|
|
468
|
+
const parsed = JSON.parse( content )
|
|
469
|
+
|
|
470
|
+
if( !parsed.records || !parsed.metadata )
|
|
471
|
+
throw new Error('Invalid replay file format')
|
|
472
|
+
|
|
473
|
+
const
|
|
474
|
+
session = new DefaultReplaySession( parsed.records, parsed.metadata ),
|
|
475
|
+
key = `${parsed.metadata.simulationId}:${parsed.metadata.runId}`
|
|
476
|
+
|
|
477
|
+
this._sessions.set( key, session )
|
|
478
|
+
return session
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
getSession( simulationId: string, runId: string ): DefaultReplaySession | undefined {
|
|
482
|
+
return this._sessions.get(`${simulationId}:${runId}`)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Compare two replay files tick-by-tick.
|
|
487
|
+
* Uses getRecordAtTick() for O(log n) lookup per tick — no seek side
|
|
488
|
+
* effects, no type escapes into private session fields.
|
|
489
|
+
*/
|
|
490
|
+
async compare( replay1Path: string, replay2Path: string ): Promise<ReplayComparison> {
|
|
491
|
+
const
|
|
492
|
+
session1 = await this.loadReplay( replay1Path ),
|
|
493
|
+
session2 = await this.loadReplay( replay2Path ),
|
|
494
|
+
differences: ReplayDifference[] = [],
|
|
495
|
+
maxTicks = Math.max( session1.metadata.totalTicks, session2.metadata.totalTicks )
|
|
496
|
+
|
|
497
|
+
for( let tick = 0; tick <= maxTicks; tick++ ){
|
|
498
|
+
const
|
|
499
|
+
events1 = session1.getRecordAtTick( tick )?.events ?? [],
|
|
500
|
+
events2 = session2.getRecordAtTick( tick )?.events ?? []
|
|
501
|
+
|
|
502
|
+
// Compare event counts
|
|
503
|
+
if( events1.length !== events2.length )
|
|
504
|
+
differences.push({
|
|
505
|
+
tick,
|
|
506
|
+
type: 'event-mismatch',
|
|
507
|
+
expected: events1.length,
|
|
508
|
+
actual: events2.length,
|
|
509
|
+
path: `tick-${tick}.events.length`,
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
// Compare event types present at this tick
|
|
513
|
+
const
|
|
514
|
+
types1 = new Set( events1.map( e => e.type ) ),
|
|
515
|
+
types2 = new Set( events2.map( e => e.type ) )
|
|
516
|
+
|
|
517
|
+
for( const type of types1 )
|
|
518
|
+
!types2.has( type ) && differences.push({
|
|
519
|
+
tick,
|
|
520
|
+
type: 'event-mismatch',
|
|
521
|
+
expected: type,
|
|
522
|
+
actual: '(missing)',
|
|
523
|
+
path: `tick-${tick}.events.type.${type}`,
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
for( const type of types2 )
|
|
527
|
+
!types1.has( type ) && differences.push({
|
|
528
|
+
tick,
|
|
529
|
+
type: 'event-mismatch',
|
|
530
|
+
expected: '(missing)',
|
|
531
|
+
actual: type,
|
|
532
|
+
path: `tick-${tick}.events.type.${type}`,
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
// Compare individual event payloads in order
|
|
536
|
+
const maxEvents = Math.max( events1.length, events2.length )
|
|
537
|
+
for( let i = 0; i < maxEvents; i++ ){
|
|
538
|
+
const
|
|
539
|
+
e1 = events1[i],
|
|
540
|
+
e2 = events2[i]
|
|
541
|
+
|
|
542
|
+
if( !e1 || !e2 ){
|
|
543
|
+
differences.push({
|
|
544
|
+
tick,
|
|
545
|
+
type: 'event-mismatch',
|
|
546
|
+
expected: e1 ? `event[${i}]: ${e1.type}` : '(missing)',
|
|
547
|
+
actual: e2 ? `event[${i}]: ${e2.type}` : '(missing)',
|
|
548
|
+
path: `tick-${tick}.events[${i}]`,
|
|
549
|
+
})
|
|
550
|
+
continue
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const
|
|
554
|
+
payload1 = JSON.stringify( e1.payload ),
|
|
555
|
+
payload2 = JSON.stringify( e2.payload )
|
|
556
|
+
|
|
557
|
+
payload1 !== payload2 && differences.push({
|
|
558
|
+
tick,
|
|
559
|
+
type: 'event-mismatch',
|
|
560
|
+
expected: e1.payload,
|
|
561
|
+
actual: e2.payload,
|
|
562
|
+
path: `tick-${tick}.events[${i}].payload`,
|
|
563
|
+
})
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
areIdentical: differences.length === 0,
|
|
569
|
+
differences,
|
|
570
|
+
totalTicksCompared: maxTicks,
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ── Supporting types ─────────────────────────────────────────
|
|
576
|
+
|
|
577
|
+
export interface ReplayComparison {
|
|
578
|
+
areIdentical: boolean
|
|
579
|
+
differences: ReplayDifference[]
|
|
580
|
+
totalTicksCompared: number
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export interface ReplayDifference {
|
|
584
|
+
tick: Tick
|
|
585
|
+
type: 'entity-mismatch' | 'metric-mismatch' | 'event-mismatch'
|
|
586
|
+
expected: unknown
|
|
587
|
+
actual: unknown
|
|
588
|
+
path?: string
|
|
589
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/core/scenario.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import type { SimulationState, SimulationContext, SimulationEntity } from '#core/types'
|
|
6
|
+
import type { StateManager } from '#core/state.manager'
|
|
7
|
+
|
|
8
|
+
export interface Scenario {
|
|
9
|
+
readonly id: string
|
|
10
|
+
readonly name: string
|
|
11
|
+
readonly description?: string
|
|
12
|
+
readonly version: string
|
|
13
|
+
readonly tags: string[]
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Populate the live StateManager with initial entities and metrics.
|
|
17
|
+
* Receives the actual StateManager so writes propagate — unlike a
|
|
18
|
+
* snapshot copy which would be silently discarded.
|
|
19
|
+
*/
|
|
20
|
+
initialize( state: StateManager, context: SimulationContext ): Promise<void>
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns a plain SimulationState snapshot for inspection or seeding
|
|
24
|
+
* before a SimulationContext is available.
|
|
25
|
+
*/
|
|
26
|
+
getInitialState(): SimulationState
|
|
27
|
+
|
|
28
|
+
validate(): ScenarioValidationResult
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ScenarioValidationResult {
|
|
32
|
+
isValid: boolean
|
|
33
|
+
errors: string[]
|
|
34
|
+
warnings: string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ScenarioConfig {
|
|
38
|
+
id: string
|
|
39
|
+
name: string
|
|
40
|
+
version?: string
|
|
41
|
+
tags?: string[]
|
|
42
|
+
initialEntities?: SimulationEntity[]
|
|
43
|
+
initialMetrics?: Record<string, number>
|
|
44
|
+
parameters?: Record<string, unknown>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class DefaultScenario implements Scenario {
|
|
48
|
+
readonly id: string
|
|
49
|
+
readonly name: string
|
|
50
|
+
readonly description?: string
|
|
51
|
+
readonly version: string
|
|
52
|
+
readonly tags: string[]
|
|
53
|
+
readonly parameters: Record<string, unknown>
|
|
54
|
+
|
|
55
|
+
private _initialEntities: SimulationEntity[]
|
|
56
|
+
private _initialMetrics: Map<string, number>
|
|
57
|
+
|
|
58
|
+
constructor( config: ScenarioConfig ){
|
|
59
|
+
this.id = config.id
|
|
60
|
+
this.name = config.name
|
|
61
|
+
this.version = config.version ?? '1.0.0'
|
|
62
|
+
this.tags = config.tags ?? []
|
|
63
|
+
this.parameters = config.parameters ?? {}
|
|
64
|
+
this._initialEntities = config.initialEntities ?? []
|
|
65
|
+
this._initialMetrics = new Map( Object.entries( config.initialMetrics ?? {} ) )
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async initialize( state: StateManager, _context: SimulationContext ): Promise<void> {
|
|
69
|
+
state.clear()
|
|
70
|
+
|
|
71
|
+
// Initial entities are stamped from the sim clock, not wall time, so a
|
|
72
|
+
// scenario loaded under a fixed clock yields identical entity timestamps on
|
|
73
|
+
// replay (R2). After clear() the StateManager sits at sim-time 0, and
|
|
74
|
+
// setEntity re-stamps createdAt/updatedAt from that sim time.
|
|
75
|
+
const now = state.currentTime
|
|
76
|
+
for( const entity of this._initialEntities )
|
|
77
|
+
state.setEntity({ ...entity, createdAt: now, updatedAt: now })
|
|
78
|
+
|
|
79
|
+
for( const [ key, value ] of this._initialMetrics )
|
|
80
|
+
state.setMetric( key, value )
|
|
81
|
+
|
|
82
|
+
state.updateClock( 0, now )
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getInitialState(): SimulationState {
|
|
86
|
+
const entities = new Map<string, SimulationEntity>()
|
|
87
|
+
for( const entity of this._initialEntities )
|
|
88
|
+
entities.set( entity.id, { ...entity })
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
tick: 0,
|
|
92
|
+
// Deterministic sim-time 0 to match tick 0 — never wall time (R2).
|
|
93
|
+
time: 0,
|
|
94
|
+
entities,
|
|
95
|
+
metrics: new Map( this._initialMetrics ),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
validate(): ScenarioValidationResult {
|
|
100
|
+
const
|
|
101
|
+
errors: string[] = [],
|
|
102
|
+
warnings: string[] = []
|
|
103
|
+
|
|
104
|
+
if( !this.id ) errors.push('Scenario id is required')
|
|
105
|
+
if( !this.name ) errors.push('Scenario name is required')
|
|
106
|
+
|
|
107
|
+
const ids = new Set<string>()
|
|
108
|
+
for( const entity of this._initialEntities ){
|
|
109
|
+
if( ids.has( entity.id ) ){
|
|
110
|
+
errors.push(`Duplicate entity id: ${entity.id}`)
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ids.add( entity.id )
|
|
115
|
+
!entity.type && warnings.push(`Entity ${entity.id} has no type`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { isValid: errors.length === 0, errors, warnings }
|
|
119
|
+
}
|
|
120
|
+
}
|