@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,168 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/sensory.controller.ts — per-Will senses I/O subsystem
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // SensoryController owns the senses input + LLM chunk-streaming output
6
+ // boundary extracted from WillStem (R5-e):
7
+ // - input ingestion: ingestText / ingestSensory / injectEvent
8
+ // - real-time chunk streaming: addChunkListener / addSensoryChunkListener
9
+ // and their sync helpers (syncChunkBroadcaster + the addChunkCallback fan-out)
10
+ //
11
+ // WillStem delegates the matching public methods here, so the god-object no
12
+ // longer owns this concern. The ops touch only WillInstance fields (no Will
13
+ // id needed), so these methods take the resolved instance directly; WillStem
14
+ // still validates existence via _get(id) before delegating.
15
+ //
16
+ // Behaviour is preserved verbatim from the original WillStem methods;
17
+ // this is a pure extract-collaborator refactor.
18
+ // ─────────────────────────────────────────────────────────────
19
+
20
+ import type { TextMessage, SensoryInput } from '#senses/index'
21
+ import type { WillInstance } from '#stem/index'
22
+
23
+ export class SensoryController {
24
+ /** Instances whose SSE chunk fan-out has already been registered on the engine. */
25
+ private readonly _chunkWired = new WeakSet<WillInstance>()
26
+ /** Per-Will monotonic counter for deterministic injected-event ids (replay). */
27
+ private readonly _eventSeq = new WeakMap<WillInstance, number>()
28
+
29
+ // ── Input ingestion ──────────────────────────────────────────
30
+
31
+ /**
32
+ * Route an external text message through the Will's AuditionEngine.
33
+ * The senses-aware AuditionEngine handles salience, thread digests,
34
+ * LanguagePercept publication, conversation-facet spawn/reuse, and LLM
35
+ * reply generation. Reply delivery is async via the outbox / SSE channel.
36
+ */
37
+ async ingestText( instance: WillInstance, input: TextMessage ): Promise<void> {
38
+ await instance.cognition.auditionEngine.ingest( input )
39
+ }
40
+
41
+ /**
42
+ * Route a raw SensoryInput to the appropriate sense engine by domain.
43
+ * Used by the debug `POST /senses/:domain/ingest` route.
44
+ */
45
+ async ingestSensory( instance: WillInstance, domain: string, input: SensoryInput ): Promise<void> {
46
+ const cog = instance.cognition
47
+ const engineMap: Record<string, { ingest: (i: SensoryInput) => Promise<void> }> = {
48
+ audition: cog.auditionEngine,
49
+ vision: cog.visionEngine,
50
+ somatosensation: cog.somatosensationEngine,
51
+ olfaction: cog.olfactionEngine,
52
+ gustation: cog.gustationEngine,
53
+ }
54
+ const engine = engineMap[ domain ]
55
+ if( !engine ) throw Object.assign( new Error( `Unknown sense domain: ${domain}` ), { code: 400 } )
56
+ await engine.ingest( input )
57
+ }
58
+
59
+ /**
60
+ * Inject a percept or external event into the Will's world.
61
+ * The event is picked up by perceptual engines on the next tick.
62
+ */
63
+ injectEvent( instance: WillInstance, event: { type: string; payload: Record<string, unknown> } ): void {
64
+ // Deterministic id (tick + per-Will monotonic counter) so injected-event
65
+ // entities reproduce on replay — they're compared in ReplayComparison, and
66
+ // Date.now()+random made them diverge every run. createdAt/updatedAt are left
67
+ // to StateManager.setEntity, which stamps the (deterministic) sim clock time.
68
+ const seq = ( this._eventSeq.get( instance ) ?? 0 ) + 1
69
+ this._eventSeq.set( instance, seq )
70
+
71
+ instance.simulation.stateManager.setEntity({
72
+ id: `external-event-${instance.tickCount}-${seq}`,
73
+ type: event.type,
74
+ metadata: event.payload,
75
+ })
76
+ }
77
+
78
+ // ── Real-time LLM chunk streaming (F3) ───────────────────────
79
+
80
+ /**
81
+ * Subscribe to real-time LLM token chunks for this Will (Will-wide).
82
+ * The callback fires for each text token during executive LLM generation.
83
+ * Returns an unsubscribe function — call it when the SSE connection closes.
84
+ */
85
+ addChunkListener( instance: WillInstance, fn: ( chunk: string ) => void ): () => void {
86
+ instance.chunkListeners.add( fn )
87
+ this.syncChunkBroadcaster( instance )
88
+
89
+ return () => {
90
+ instance.chunkListeners.delete( fn )
91
+ this.syncChunkBroadcaster( instance )
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Subscribe to real-time LLM token chunks for a specific conversation entity.
97
+ *
98
+ * Unlike addChunkListener() (Will-wide), this fires only when the AuditionEngine
99
+ * conversation facet for `entityId` is actively streaming — isolating chunks to the
100
+ * relevant SSE/WS client connection.
101
+ *
102
+ * On the first listener for any entity, wires addChunkCallback() on the
103
+ * AuditionEngine to fan out chunks by entityId. Subsequent listeners reuse the same
104
+ * callback — no duplicate registration occurs.
105
+ *
106
+ * Returns an unsubscribe function — call it when the SSE/WS connection closes.
107
+ */
108
+ addSensoryChunkListener(
109
+ instance: WillInstance,
110
+ entityId: string,
111
+ fn: ( chunk: string ) => void,
112
+ ): () => void {
113
+ let listeners = instance.sensoryChunkListeners.get( entityId )
114
+ if( !listeners ){
115
+ listeners = new Set()
116
+ instance.sensoryChunkListeners.set( entityId, listeners )
117
+ }
118
+
119
+ listeners.add( fn )
120
+
121
+ // Register the SSE fan-out on the engine once per instance. It reads the live
122
+ // sensoryChunkListeners map, so it stays correct as listeners come and go, and
123
+ // coexists with other chunk subscribers (e.g. the transport emit) — the engine
124
+ // chunk callback is multi-subscriber.
125
+ this._ensureChunkFanout( instance )
126
+
127
+ return () => {
128
+ listeners!.delete( fn )
129
+ if( listeners!.size === 0 )
130
+ instance.sensoryChunkListeners.delete( entityId )
131
+ }
132
+ }
133
+
134
+ /** Register the per-instance SSE chunk fan-out on the AuditionEngine (idempotent). */
135
+ private _ensureChunkFanout( instance: WillInstance ): void {
136
+ if( this._chunkWired.has( instance ) ) return
137
+ const audition = instance.cognition.auditionEngine
138
+ if( typeof ( audition as { addChunkCallback?: unknown } ).addChunkCallback !== 'function' ) return
139
+
140
+ this._chunkWired.add( instance )
141
+ audition.addChunkCallback( ( entityId: string, _threadId: string, chunk: string ) => {
142
+ const fns = instance.sensoryChunkListeners.get( entityId )
143
+ if( !fns ) return
144
+ for( const fn of fns ){
145
+ try { fn( chunk ) } catch {}
146
+ }
147
+ })
148
+ }
149
+
150
+ syncChunkBroadcaster( instance: WillInstance ): void {
151
+ // Get the executive engine (typed loosely to avoid circular imports)
152
+ const exec = ( instance.cognition as unknown as Record<string, unknown> )?.executiveEngine as
153
+ { setChunkBroadcaster?: ( fn: (( c: string ) => void) | null ) => void } | undefined
154
+ if( !exec?.setChunkBroadcaster ) return
155
+
156
+ if( instance.chunkListeners.size === 0 ){
157
+ exec.setChunkBroadcaster( null )
158
+ }
159
+ else {
160
+ // Single broadcaster that fans out to all listeners
161
+ exec.setChunkBroadcaster(( chunk: string ) => {
162
+ for( const fn of instance.chunkListeners ){
163
+ try { fn( chunk ) } catch {}
164
+ }
165
+ })
166
+ }
167
+ }
168
+ }
@@ -0,0 +1,157 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/session.logger.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // Captures every significant event in a Will's cognitive cycle:
6
+ //
7
+ // tick — per-tick metrics snapshot + entity counts
8
+ // event — every semantic event from the EventBus
9
+ // executive.call — LLM call metadata (model, prompt size, path)
10
+ // executive.response — LLM response (tokens, latency, raw excerpt)
11
+ // executive.output — parsed structured output (actions, beliefs, ...)
12
+ // action.execute — effector execution result and feedback
13
+ // belief.integrate — belief written to store (pre/post confidence cap)
14
+ // conversation.in — inbound message from an external entity
15
+ // conversation.out — outbound message composed by the Will
16
+ // session.start — written once on logger creation
17
+ // session.end — written on close()
18
+ //
19
+ // Transport: every entry is forwarded (via `emit`/`attachEmit`) to the stem,
20
+ // which emits it as a `session_log` envelope on the Will's ExternalTransport —
21
+ // so the product wrapper / consumer owns persistence (Postgres, S3, Kafka,
22
+ // SSE …). The Will itself no longer uploads to S3.
23
+ //
24
+ // Files: in DEVELOPMENT only (see `fileLoggingEnabled()`), the same NDJSON is
25
+ // also written to data/wills/{willId}/sessions/{sessionId}.jsonl. One JSON
26
+ // object per line; parse with `jq`, Python, or any NDJSON reader.
27
+ // ─────────────────────────────────────────────────────────────
28
+
29
+ import { createWriteStream, mkdirSync } from 'node:fs'
30
+ import { join } from 'node:path'
31
+ import type { WriteStream } from 'node:fs'
32
+
33
+ /** Forwards each entry to the consumer (the stem bridges it onto the transport). */
34
+ export type SessionLogEmit = ( record: Record<string, unknown> ) => void
35
+
36
+ // ── Entry type union ──────────────────────────────────────────
37
+
38
+ export type LogEntryType =
39
+ | 'session.start'
40
+ | 'session.end'
41
+ | 'tick'
42
+ | 'event'
43
+ | 'executive.call'
44
+ | 'executive.response'
45
+ | 'executive.output'
46
+ | 'executive.facet.spawn'
47
+ | 'executive.facet.call'
48
+ | 'executive.facet.response'
49
+ | 'executive.facet.output'
50
+ | 'executive.facet.destroy'
51
+ | 'action.execute'
52
+ | 'action.error'
53
+ | 'action.outcome'
54
+ | 'belief.integrate'
55
+ | 'conversation.in'
56
+ | 'conversation.out'
57
+ | 'plan.step.activated'
58
+ | 'plan.step.outcome'
59
+ | 'goal.progress'
60
+ | 'goal.achieved'
61
+ | 'goal.blocked'
62
+ | 'goal.abandoned'
63
+ | 'outbox.push'
64
+ | 'outbox.expire'
65
+
66
+ interface BaseEntry {
67
+ type: LogEntryType
68
+ wallTime: number
69
+ tick?: number
70
+ }
71
+
72
+ export type LogEntry = BaseEntry & Record<string, unknown>
73
+
74
+ export interface SessionLoggerOptions {
75
+ /**
76
+ * Forward every entry to the consumer (typically the stem, which emits it as a
77
+ * `session_log` envelope on the transport). May also be set later via
78
+ * {@link SessionLogger.attachEmit} once the Will instance exists.
79
+ */
80
+ emit?: SessionLogEmit | null
81
+ /** Also write a local NDJSON file. Dev convenience only; off in production. */
82
+ fileLogging?: boolean
83
+ /** Override the data directory for the dev file (default `./data`). */
84
+ dataDir?: string
85
+ }
86
+
87
+ // ── SessionLogger ─────────────────────────────────────────────
88
+
89
+ export class SessionLogger {
90
+ readonly sessionId: string
91
+ /** Local file path — empty string when file logging is disabled. */
92
+ readonly filePath: string
93
+
94
+ private readonly _stream: WriteStream | null
95
+ private _emit: SessionLogEmit | null
96
+ private _closed = false
97
+ private _count = 0
98
+
99
+ constructor( willId: string, dataDir: string = './data', opts: SessionLoggerOptions = {} ) {
100
+ const ts = new Date().toISOString().replace( /[:.]/g, '-' ).slice( 0, 19 )
101
+ const rnd = Math.random().toString( 36 ).slice( 2, 8 )
102
+ this.sessionId = `${ts}-${rnd}`
103
+
104
+ this._emit = opts.emit ?? null
105
+
106
+ const fileLogging = opts.fileLogging ?? false
107
+ if( fileLogging ){
108
+ const dir = join( opts.dataDir ?? dataDir, 'wills', willId, 'sessions' )
109
+ mkdirSync( dir, { recursive: true } )
110
+ this.filePath = join( dir, `${this.sessionId}.jsonl` )
111
+ this._stream = createWriteStream( this.filePath, { flags: 'a' } )
112
+ } else {
113
+ this.filePath = ''
114
+ this._stream = null
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Late-bind the consumer forwarder (the stem sets this once the Will instance
120
+ * exists, so it can emit a `session_log` envelope on the instance's transport).
121
+ */
122
+ attachEmit( emit: SessionLogEmit ): void {
123
+ this._emit = emit
124
+ }
125
+
126
+ write( entry: Omit<LogEntry, 'wallTime'> ): void {
127
+ if( this._closed ) return
128
+ const record = { wallTime: Date.now(), ...entry }
129
+
130
+ // Forward first — this is the production transport; the consumer persists it.
131
+ try { this._emit?.( record ) }
132
+ catch { /* a listener fault must never block the tick loop */ }
133
+
134
+ // File is a dev-only mirror.
135
+ if( this._stream ){
136
+ try { this._stream.write( JSON.stringify( record ) + '\n' ) }
137
+ catch { /* never block the tick loop on a log failure */ }
138
+ }
139
+
140
+ this._count++
141
+ }
142
+
143
+ /**
144
+ * Emits `session.end` and closes the local file stream (if any). Persistence
145
+ * of the streamed entries is the consumer's responsibility — the Will no longer
146
+ * uploads anywhere itself.
147
+ */
148
+ close(): void {
149
+ if( this._closed ) return
150
+ this.write({ type: 'session.end', totalEntries: this._count + 1 })
151
+ this._closed = true
152
+ this._stream?.end()
153
+ }
154
+
155
+ get entryCount(): number { return this._count }
156
+ get isClosed(): boolean { return this._closed }
157
+ }
@@ -0,0 +1,38 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/transport/index.ts — ExternalTransport barrel
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ export type {
6
+ ExternalTransport,
7
+ Envelope,
8
+ OutboundEnvelope,
9
+ InboundEnvelope,
10
+ ReplyEnvelope,
11
+ ChunkEnvelope,
12
+ MessageEnvelope,
13
+ effectorInvocationEnvelope,
14
+ PerceptEnvelope,
15
+ ActivityEnvelope,
16
+ SessionLogEnvelope,
17
+ TokenReportEnvelope,
18
+ InboundMessageEnvelope,
19
+ InboundPerceptEnvelope,
20
+ AckEnvelope,
21
+ AckResult,
22
+ TransportStatus,
23
+ } from '#stem/tracts/transport/types'
24
+
25
+ export { LoopbackTransport, type AckPolicy } from '#stem/tracts/transport/loopback.transport'
26
+ export {
27
+ SocketIoTransport,
28
+ type SocketIoTransportOptions,
29
+ type SocketLike,
30
+ } from '#stem/tracts/transport/socketio.transport'
31
+
32
+
33
+ export {
34
+ type StreamChannel,
35
+ StreamTransport,
36
+ type OutboundListener,
37
+ fileLoggingEnabled,
38
+ } from '#stem/tracts/transport/stream.transport'
@@ -0,0 +1,86 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/transport/loopback.transport.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // LoopbackTransport — in-memory, deterministic ExternalTransport for tests,
6
+ // replay, and any configuration without a real peer.
7
+ //
8
+ // No sockets, no timers: emit() resolves its ack synchronously per a scripted
9
+ // policy, and inbound is driven by injectInbound(). Mirrors the role of
10
+ // InProcessCognitiveTransport for the cognitive bus.
11
+ // ─────────────────────────────────────────────────────────────
12
+
13
+ import type {
14
+ ExternalTransport,
15
+ OutboundEnvelope,
16
+ InboundEnvelope,
17
+ AckResult,
18
+ TransportStatus,
19
+ } from './types'
20
+
21
+ /** Decides how emit() resolves an ack for a given envelope. Default: acked via callback. */
22
+ export type AckPolicy = ( env: OutboundEnvelope ) => AckResult
23
+
24
+ const DEFAULT_ACK: AckPolicy = () => ({ acked: true, via: 'callback' })
25
+
26
+ export class LoopbackTransport implements ExternalTransport {
27
+ /** Every envelope passed to emit(), in order — inspect in tests. */
28
+ readonly sent: OutboundEnvelope[] = []
29
+
30
+ private _connected = true
31
+ private _ackPolicy: AckPolicy
32
+ private readonly _inboundHandlers = new Set<( env: InboundEnvelope ) => void>()
33
+ private readonly _statusHandlers = new Set<( s: TransportStatus ) => void>()
34
+
35
+ constructor( ackPolicy: AckPolicy = DEFAULT_ACK ){
36
+ this._ackPolicy = ackPolicy
37
+ }
38
+
39
+ get connected(): boolean { return this._connected }
40
+
41
+ async emit( env: OutboundEnvelope ): Promise<AckResult> {
42
+ this.sent.push( env )
43
+ return this._ackPolicy( env )
44
+ }
45
+
46
+ onInbound( handler: ( env: InboundEnvelope ) => void ): () => void {
47
+ this._inboundHandlers.add( handler )
48
+ return () => { this._inboundHandlers.delete( handler ) }
49
+ }
50
+
51
+ onStatus( handler: ( s: TransportStatus ) => void ): () => void {
52
+ this._statusHandlers.add( handler )
53
+ return () => { this._statusHandlers.delete( handler ) }
54
+ }
55
+
56
+ close(): void {
57
+ this._connected = false
58
+ this._inboundHandlers.clear()
59
+ this._statusHandlers.clear()
60
+ this.sent.length = 0
61
+ }
62
+
63
+ // ── Test / harness controls ──────────────────────────────────
64
+
65
+ /** Simulate the peer sending an inbound envelope to the Will. */
66
+ injectInbound( env: InboundEnvelope ): void {
67
+ for( const h of this._inboundHandlers ) h( env )
68
+ }
69
+
70
+ /** Flip connection state and notify status handlers. */
71
+ setConnected( connected: boolean ): void {
72
+ this._connected = connected
73
+ const status: TransportStatus = connected ? 'connected' : 'disconnected'
74
+ for( const h of this._statusHandlers ) h( status )
75
+ }
76
+
77
+ /** Swap the ack policy mid-test (e.g. to simulate timeouts then recovery). */
78
+ setAckPolicy( policy: AckPolicy ): void {
79
+ this._ackPolicy = policy
80
+ }
81
+
82
+ /** Convenience: only the envelopes on a given channel. */
83
+ sentOn<T extends OutboundEnvelope['channel']>( channel: T ): Extract<OutboundEnvelope, { channel: T }>[] {
84
+ return this.sent.filter( e => e.channel === channel ) as Extract<OutboundEnvelope, { channel: T }>[]
85
+ }
86
+ }
@@ -0,0 +1,200 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/transport/socketio.transport.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // SocketIoTransport — production ExternalTransport over socket.io-client.
6
+ //
7
+ // The peer (backend) hosts the socket.io server; this is Will's client side.
8
+ // It is constructed by the CALLER and injected via WillConfig.transport, so the
9
+ // `will` package never imports socket.io-client at module load — the dependency
10
+ // is OPTIONAL and loaded lazily via a dynamic import only when a transport is
11
+ // actually instantiated. The import specifier is indirected through a variable
12
+ // so the type checker does not require the module to be installed.
13
+ //
14
+ // Outbound: emit('envelope', env, ack) — socket.io ack callback gives the fast
15
+ // best-effort delivery/result ack (Section 3 reconciles with the
16
+ // durable discrete-event path).
17
+ // Inbound: 'envelope' events plus discrete 'message.delivered' /
18
+ // 'effector.invoked.ack' events (synthesized into ack envelopes), in
19
+ // case the peer emits them independently. All inbound is handed to
20
+ // onInbound() handlers — the stem enqueues them onto the tick queue.
21
+ // ─────────────────────────────────────────────────────────────
22
+
23
+ import { logger } from '#core/logger'
24
+ import { wallClock } from '#core/wall.clock'
25
+ import type {
26
+ ExternalTransport,
27
+ OutboundEnvelope,
28
+ InboundEnvelope,
29
+ AckEnvelope,
30
+ AckResult,
31
+ TransportStatus,
32
+ } from './types'
33
+
34
+ /** Minimal surface of a socket.io client we depend on — keeps us decoupled from the dep's types. */
35
+ export interface SocketLike {
36
+ connected: boolean
37
+ on( event: string, handler: ( ...args: any[] ) => void ): void
38
+ emit( event: string, payload: unknown, ack?: ( response: unknown ) => void ): void
39
+ disconnect(): void
40
+ }
41
+
42
+ export interface SocketIoTransportOptions {
43
+ /** socket.io server URL (e.g. wss://host or http://host:port). */
44
+ url: string
45
+ /** This Will's id — sent as auth + routing key. */
46
+ willId: string
47
+ /** Optional auth token sent in the socket.io handshake. */
48
+ token?: string
49
+ /** Default ack timeout for emit() in ms. Default 5000. */
50
+ ackTimeoutMs?: number
51
+ /**
52
+ * Override the socket factory — inject a fake in tests, or customize the
53
+ * socket.io connection. When omitted, `socket.io-client` is dynamically
54
+ * imported and `io(url, { auth })` is called.
55
+ */
56
+ socketFactory?: ( url: string, opts: { auth: { willId: string; token?: string } } ) => SocketLike | Promise<SocketLike>
57
+ }
58
+
59
+ const DEFAULT_ACK_TIMEOUT_MS = 5_000
60
+
61
+ export class SocketIoTransport implements ExternalTransport {
62
+ private _socket: SocketLike | null = null
63
+ private _connecting: Promise<SocketLike | null> | null = null
64
+ private _seq = 0
65
+
66
+ private readonly _inboundHandlers = new Set<( env: InboundEnvelope ) => void>()
67
+ private readonly _statusHandlers = new Set<( s: TransportStatus ) => void>()
68
+
69
+ constructor( private readonly _opts: SocketIoTransportOptions ){
70
+ // Begin connecting eagerly; emit()/inbound wiring await the same promise.
71
+ void this._ensureSocket()
72
+ }
73
+
74
+ get connected(): boolean { return this._socket?.connected ?? false }
75
+
76
+ // ── Outbound ─────────────────────────────────────────────────
77
+
78
+ async emit( env: OutboundEnvelope, opts?: { ackTimeoutMs?: number } ): Promise<AckResult> {
79
+ const socket = await this._ensureSocket()
80
+ if( !socket || !socket.connected )
81
+ // No connection — caller keeps the message buffered (outbox) and retries.
82
+ return { acked: false, via: 'timeout' }
83
+
84
+ const timeoutMs = opts?.ackTimeoutMs ?? this._opts.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS
85
+ const stamped: OutboundEnvelope = { ...env, seq: env.seq || ++this._seq }
86
+
87
+ return new Promise<AckResult>( resolve => {
88
+ let settled = false
89
+ const done = ( r: AckResult ) => { if( !settled ){ settled = true; clearTimeout( timer ); resolve( r ) } }
90
+
91
+ const timer = setTimeout( () => done({ acked: false, via: 'timeout' }), timeoutMs )
92
+
93
+ try {
94
+ socket.emit( 'envelope', stamped, ( response: unknown ) => done({ acked: true, via: 'callback', payload: response }) )
95
+ }
96
+ catch( err ){
97
+ logger.warn(`[socketio-transport] emit failed: ${( err as Error ).message}`)
98
+ done({ acked: false, via: 'timeout' })
99
+ }
100
+ } )
101
+ }
102
+
103
+ // ── Inbound + status subscriptions ───────────────────────────
104
+
105
+ onInbound( handler: ( env: InboundEnvelope ) => void ): () => void {
106
+ this._inboundHandlers.add( handler )
107
+ return () => { this._inboundHandlers.delete( handler ) }
108
+ }
109
+
110
+ onStatus( handler: ( s: TransportStatus ) => void ): () => void {
111
+ this._statusHandlers.add( handler )
112
+ return () => { this._statusHandlers.delete( handler ) }
113
+ }
114
+
115
+ close(): void {
116
+ try { this._socket?.disconnect() } catch { /* ignore */ }
117
+ this._socket = null
118
+ this._connecting = null
119
+ this._inboundHandlers.clear()
120
+ this._statusHandlers.clear()
121
+ }
122
+
123
+ // ── Internal ─────────────────────────────────────────────────
124
+
125
+ private async _ensureSocket(): Promise<SocketLike | null> {
126
+ if( this._socket ) return this._socket
127
+ if( !this._connecting ) this._connecting = this._connect()
128
+ return this._connecting
129
+ }
130
+
131
+ private async _connect(): Promise<SocketLike | null> {
132
+ try {
133
+ const socket = this._opts.socketFactory
134
+ ? await this._opts.socketFactory( this._opts.url, { auth: { willId: this._opts.willId, token: this._opts.token } } )
135
+ : await this._defaultFactory()
136
+
137
+ this._wire( socket )
138
+ this._socket = socket
139
+ return socket
140
+ }
141
+ catch( err ){
142
+ logger.error(`[socketio-transport] connect failed: ${( err as Error ).message}`)
143
+ this._connecting = null // allow a later retry
144
+ return null
145
+ }
146
+ }
147
+
148
+ private async _defaultFactory(): Promise<SocketLike> {
149
+ // Indirect the specifier so the type checker does not require the optional
150
+ // dependency to be installed for the core package to build.
151
+ const spec = 'socket.io-client'
152
+ const mod = await import( /* @vite-ignore */ spec ) as any
153
+ const io = mod.io ?? mod.default ?? mod
154
+ return io( this._opts.url, {
155
+ auth: { willId: this._opts.willId, token: this._opts.token },
156
+ transports: ['websocket'],
157
+ } ) as SocketLike
158
+ }
159
+
160
+ /** Attach socket.io event handlers that feed the inbound + status streams. */
161
+ private _wire( socket: SocketLike ): void {
162
+ socket.on( 'connect', () => this._notifyStatus( 'connected' ) )
163
+ socket.on( 'disconnect', () => this._notifyStatus( 'disconnected' ) )
164
+ socket.on( 'reconnect_attempt', () => this._notifyStatus( 'reconnecting' ) )
165
+
166
+ // Primary inbound channel — already-typed envelopes from the peer.
167
+ socket.on( 'envelope', ( env: InboundEnvelope ) => this._emitInbound( env ) )
168
+
169
+ // Discrete ack events (Section 2): in case the peer emits acks independently
170
+ // rather than via the emit() callback. Synthesized into ack envelopes; the
171
+ // AckReconciler (Section 3) dedups against the callback path by correlationId.
172
+ socket.on( 'message.delivered', ( m: { correlationId: string; delivered?: boolean } ) =>
173
+ this._emitInbound( this._deliveryAck( m.correlationId, m.delivered ?? true ) ) )
174
+
175
+ socket.on( 'effector.invoked.ack', ( m: { correlationId: string; result: AckEnvelope['result'] } ) =>
176
+ this._emitInbound( this._resultAck( m.correlationId, m.result ) ) )
177
+ }
178
+
179
+ private _emitInbound( env: InboundEnvelope ): void {
180
+ for( const h of this._inboundHandlers ){
181
+ try { h( env ) }
182
+ catch( err ){ logger.error(`[socketio-transport] inbound handler error: ${( err as Error ).message}`) }
183
+ }
184
+ }
185
+
186
+ private _notifyStatus( status: TransportStatus ): void {
187
+ for( const h of this._statusHandlers ){
188
+ try { h( status ) }
189
+ catch { /* ignore */ }
190
+ }
191
+ }
192
+
193
+ private _deliveryAck( correlationId: string, delivered: boolean ): AckEnvelope {
194
+ return { channel: 'ack', ackKind: 'delivery', delivered, willId: this._opts.willId, correlationId, seq: 0, wallTime: wallClock() }
195
+ }
196
+
197
+ private _resultAck( correlationId: string, result: AckEnvelope['result'] ): AckEnvelope {
198
+ return { channel: 'ack', ackKind: 'result', result, willId: this._opts.willId, correlationId, seq: 0, wallTime: wallClock() }
199
+ }
200
+ }