@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,492 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/generics/token.tracker.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * TokenTracker — monitors LLM token consumption across all engines.
7
+ *
8
+ * Hooks into the LLM calls to record:
9
+ * - Prompt tokens (input)
10
+ * - Completion tokens (output)
11
+ * - Total tokens
12
+ * - Cost (based on model pricing)
13
+ * - Per-engine breakdowns
14
+ * - Per-agent breakdowns
15
+ *
16
+ * Exposes as metrics so the orchestrator and runner can log costs,
17
+ * and the ParameterOptimizer can factor cost into optimization decisions.
18
+ */
19
+ import type {
20
+ Duration,
21
+ Tick,
22
+ SimulationContext,
23
+ ReadonlySimulationState,
24
+ StateCommands,
25
+ SimulationEvent,
26
+ } from '#core/types'
27
+ import type { SimulationEngine, EngineResult } from '#cognition/types'
28
+ import { appendFileSync, mkdirSync } from 'node:fs'
29
+ import { wallClock } from '#core/wall.clock'
30
+ // NOTE: no transport import here. cognition/ must not depend on the stem
31
+ // transport layer (determinism contract). The tracker exposes a neutral
32
+ // onRecord() sink; the stem bridges records onto the transport.
33
+
34
+ /** One attributed ledger record (5-axis attribution + tokens + cost). */
35
+ export type TokenLedgerRecord = Record<string, unknown>
36
+ export type TokenRecordListener = ( record: TokenLedgerRecord ) => void
37
+
38
+ // ── Pricing table (USD per 1M tokens) ─────────────────────
39
+
40
+ const MODEL_PRICING: Record<string, { input: number; output: number }> = {
41
+ // OpenAI
42
+ 'openai/gpt-4o': { input: 2.50, output: 10.00 },
43
+ 'openai/gpt-4o-mini': { input: 0.15, output: 0.60 },
44
+ 'openai/gpt-4-turbo': { input: 10.00, output: 30.00 },
45
+ 'openai/gpt-3.5-turbo': { input: 0.50, output: 1.50 },
46
+
47
+ // Anthropic (Claude 4.x family — prices per 1M tokens)
48
+ 'anthropic/claude-haiku-4-5': { input: 1.00, output: 5.00 },
49
+ 'anthropic/claude-sonnet-4-5': { input: 3.00, output: 15.00 },
50
+ 'anthropic/claude-sonnet-4-6': { input: 3.00, output: 15.00 },
51
+ 'anthropic/claude-opus-4-7': { input: 5.00, output: 25.00 },
52
+ // Legacy aliases kept for backward compat
53
+ 'anthropic/claude-haiku-4': { input: 1.00, output: 5.00 },
54
+ 'anthropic/claude-opus-4': { input: 5.00, output: 25.00 },
55
+
56
+ // Google
57
+ 'google/gemini-2.0-flash': { input: 0.10, output: 0.40 },
58
+ 'google/gemini-2.0-pro': { input: 1.25, output: 5.00 },
59
+
60
+ // Meta (via Groq/Replicate)
61
+ 'meta/llama-3.3-70b': { input: 0.59, output: 0.79 },
62
+ 'meta/llama-4-maverick': { input: 0.20, output: 0.60 },
63
+
64
+ // DeepSeek
65
+ 'deepseek/deepseek-v3': { input: 0.27, output: 1.10 },
66
+ 'deepseek/deepseek-r1': { input: 0.55, output: 2.19 },
67
+
68
+ // Embeddings (input-only — no completion tokens). Priced per 1M input tokens.
69
+ 'openai/text-embedding-3-small': { input: 0.02, output: 0 },
70
+ 'openai/text-embedding-3-large': { input: 0.13, output: 0 },
71
+ 'google/text-embedding-004': { input: 0, output: 0 }, // free tier
72
+ 'google/gemini-embedding-001': { input: 0, output: 0 }, // free tier
73
+
74
+ // Fallback for unknown models
75
+ '__default__': { input: 3.00, output: 15.00 },
76
+ }
77
+
78
+ // ── Prompt-cache pricing (Anthropic) ──────────────────────
79
+ // `input_tokens` in the API usage already EXCLUDES cached tokens, so the full
80
+ // input cost is: fresh input ×1 + cache reads ×0.1 + cache writes ×1.25.
81
+ const CACHE_READ_MULT = 0.1
82
+ const CACHE_WRITE_MULT = 1.25
83
+
84
+ /**
85
+ * Normalize a model id to its bare, dateless form so a raw provider model string
86
+ * ("claude-sonnet-4-5-20250929", "anthropic/claude-haiku-4-5") resolves to the
87
+ * right pricing row. Without this, every non-default id missed the `provider/model`
88
+ * keys and silently fell through to __default__ ($3/$15) — pricing Haiku ~3× and
89
+ * DeepSeek ~11× too high, and breaking the self-hosting margin telemetry entirely.
90
+ */
91
+ function normalizeModelKey( model: string ): string {
92
+ let m = model.toLowerCase().trim()
93
+ const slash = m.lastIndexOf( '/' )
94
+ if( slash >= 0 ) m = m.slice( slash + 1 ) // drop "provider/" prefix
95
+ return m.replace( /[-@]\d{6,8}$/, '' ) // drop trailing -YYYYMMDD date stamp
96
+ }
97
+
98
+ // Pre-index the pricing table by normalized model name for O(1), date-insensitive
99
+ // lookup. Built once at module load.
100
+ const PRICING_BY_NORM: Record<string, { input: number; output: number }> = ( () => {
101
+ const out: Record<string, { input: number; output: number }> = {}
102
+ for( const [ key, price ] of Object.entries( MODEL_PRICING ) ){
103
+ if( key === '__default__' ) continue
104
+ out[ normalizeModelKey( key ) ] = price
105
+ }
106
+ return out
107
+ } )()
108
+
109
+ /** Resolve the pricing row for any model id (exact, normalized, then default). */
110
+ export function resolvePricing( model: string ): { input: number; output: number } {
111
+ return PRICING_BY_NORM[ normalizeModelKey( model ) ]
112
+ ?? MODEL_PRICING[ model ]
113
+ ?? MODEL_PRICING['__default__']!
114
+ }
115
+
116
+ export interface TokenUsage {
117
+ /** Model identifier (e.g., 'openai/gpt-4o') */
118
+ model: string
119
+ /** Input/prompt tokens consumed */
120
+ promptTokens: number
121
+ /** Output/completion tokens consumed */
122
+ completionTokens: number
123
+ /** Total tokens */
124
+ totalTokens: number
125
+ /** Anthropic prompt-cache read tokens (billed at 0.1× input). Optional. */
126
+ cacheReadTokens?: number
127
+ /** Anthropic prompt-cache write tokens (billed at 1.25× input). Optional. */
128
+ cacheWriteTokens?: number
129
+ /** Estimated cost in USD */
130
+ estimatedCostUsd: number
131
+
132
+ // ── 5-axis cost attribution ──────────────────────────────
133
+ /** Top-level cost bucket: 'executive' | 'summarizer' | 'embedding' | 'identity-guard' | … */
134
+ category: string
135
+ /** Actor/subsystem doing the work: 'master' | 'facet' | 'memory' | 'guard' | … */
136
+ attribute: string
137
+ /** Cognitive function: 'decision' | 'ideation' | 'conversation' | 'planning' | 'deliberation' | 'outreach' | 'consolidation' | 'recall' | 'index' | 'identity-coherence' | … */
138
+ function: string
139
+ /** Optional specific id or namespace: facet id, entity id, model name. */
140
+ scope?: string
141
+ /** Human-readable label — auto-composed from the axes when the caller omits it. */
142
+ label: string
143
+
144
+ /** Optional pre-cache prompt size estimate (chars/4) — for cache-savings analysis. */
145
+ estPromptTokens?: number
146
+
147
+ /** Tick when the call completed */
148
+ tick: Tick
149
+ /** Latency in milliseconds */
150
+ latencyMs: number
151
+ }
152
+
153
+ /** What callers pass to {@link TokenTracker.recordUsage} — cost and label are derived. */
154
+ export type RecordUsageInput = Omit<TokenUsage, 'estimatedCostUsd' | 'label'> & { label?: string }
155
+
156
+ /** Compose a stable, readable label from the attribution axes. */
157
+ function composeLabel( m: { category: string; attribute: string; function: string; scope?: string } ): string {
158
+ const base = `${m.category}/${m.attribute}/${m.function}`
159
+ return m.scope ? `${base}#${m.scope}` : base
160
+ }
161
+
162
+ export interface TokenTrackerConfig {
163
+ /** Whether to emit cost events */
164
+ emitCostEvents?: boolean
165
+ /** Cost threshold for warning events */
166
+ costWarningThresholdUsd?: number
167
+ /**
168
+ * When set together with `writeLedger`, every recorded call is appended (with
169
+ * full 5-axis attribution + cost) to `./data/wills/<willId>/debug/token-report.jsonl`
170
+ * — the complete, billable per-Will usage ledger (master, facets, summarizer,
171
+ * embedding, guard — not just the master path).
172
+ */
173
+ willId?: string
174
+ /** Append the attributed ledger to disk (dev convenience). Off in prod/tests/replay. */
175
+ writeLedger?: boolean
176
+ }
177
+
178
+ export class TokenTracker implements SimulationEngine {
179
+ readonly name = 'token-tracker'
180
+
181
+ private _emitCostEvents: boolean
182
+ private _costWarningThreshold: number
183
+
184
+ // All recorded usage for the simulation run
185
+ private _usageLog: TokenUsage[] = []
186
+
187
+ // Running totals
188
+ private _totalPromptTokens = 0
189
+ private _totalCompletionTokens = 0
190
+ private _totalCost = 0
191
+
192
+ // Per-axis breakdowns (cost + tokens), keyed by category and by function.
193
+ private _categoryCosts = new Map<string, number>()
194
+ private _categoryTokens = new Map<string, { prompt: number; completion: number }>()
195
+ private _functionCosts = new Map<string, number>()
196
+ private _functionTokens = new Map<string, { prompt: number; completion: number }>()
197
+
198
+ // Per-tick costs (for spike detection)
199
+ private _tickCosts: number[] = []
200
+ private _maxTickCostSamples = 1000
201
+
202
+ // Track last cost warning tick to avoid spam
203
+ private _lastCostWarningTick = 0
204
+ private _lastProcessedIndex = 0
205
+
206
+ // Attributed-ledger transports. Listeners are the production transport (the
207
+ // stem bridges them onto the ExternalTransport); the file (`_ledgerPath`) is a
208
+ // dev-only mirror, null when disabled.
209
+ private _recordListeners = new Set<TokenRecordListener>()
210
+ private _ledgerPath: string | null
211
+ private _ledgerDirReady = false
212
+
213
+ constructor( config: TokenTrackerConfig = {} ){
214
+ this._emitCostEvents = config.emitCostEvents ?? true
215
+ this._costWarningThreshold = config.costWarningThresholdUsd ?? 0.05
216
+ this._ledgerPath = ( config.writeLedger && config.willId )
217
+ ? `./data/wills/${config.willId}/debug/token-report.jsonl`
218
+ : null
219
+ }
220
+
221
+ // ── Public API: record usage ──────────────────────────────
222
+
223
+ /**
224
+ * Record a completed LLM call.
225
+ * Called by LLMDirector.call after each completion (src/llm/index.ts).
226
+ */
227
+ recordUsage( usage: RecordUsageInput ): void {
228
+ const pricing = resolvePricing( usage.model )
229
+ const cacheRead = usage.cacheReadTokens ?? 0
230
+ const cacheWrite = usage.cacheWriteTokens ?? 0
231
+ const costUsd =
232
+ ( usage.promptTokens / 1_000_000 ) * pricing.input +
233
+ ( usage.completionTokens / 1_000_000 ) * pricing.output +
234
+ ( cacheRead / 1_000_000 ) * pricing.input * CACHE_READ_MULT +
235
+ ( cacheWrite / 1_000_000 ) * pricing.input * CACHE_WRITE_MULT
236
+
237
+ const full: TokenUsage = {
238
+ ...usage,
239
+ label: usage.label ?? composeLabel( usage ),
240
+ estimatedCostUsd: Math.round( costUsd * 1_000_000 ) / 1_000_000, // round to micro-dollars
241
+ }
242
+
243
+ this._usageLog.push( full )
244
+
245
+ // Running totals
246
+ this._totalPromptTokens += full.promptTokens
247
+ this._totalCompletionTokens += full.completionTokens
248
+ this._totalCost += full.estimatedCostUsd
249
+
250
+ // Per-axis breakdowns — the repartition surface (category × function).
251
+ this._accumulate( this._categoryCosts, this._categoryTokens, full.category, full )
252
+ this._accumulate( this._functionCosts, this._functionTokens, full.function, full )
253
+
254
+ // Complete attributed ledger record (every call, all axes + cost): notify
255
+ // record listeners (the stem forwards them onto the transport) and mirror to
256
+ // a file in dev. Accounting must never break a completion → both best-effort.
257
+ this._emitLedger( full )
258
+ }
259
+
260
+ /**
261
+ * Subscribe to every attributed ledger record (5-axis attribution + tokens +
262
+ * cost). Neutral sink — the stem bridges these onto the ExternalTransport so
263
+ * cognition/ stays free of any transport dependency. Returns an unsubscribe fn.
264
+ */
265
+ onRecord( listener: TokenRecordListener ): () => void {
266
+ this._recordListeners.add( listener )
267
+ return () => { this._recordListeners.delete( listener ) }
268
+ }
269
+
270
+ private _emitLedger( full: TokenUsage ): void {
271
+ if( this._recordListeners.size === 0 && !this._ledgerPath ) return
272
+
273
+ const record: TokenLedgerRecord = {
274
+ tick: full.tick,
275
+ ts: new Date( wallClock() ).toISOString(), // determinism-ok: ledger timestamp is telemetry, never replay state
276
+ model: full.model,
277
+ category: full.category,
278
+ attribute: full.attribute,
279
+ function: full.function,
280
+ scope: full.scope,
281
+ label: full.label,
282
+ inputTok: full.promptTokens,
283
+ outputTok: full.completionTokens,
284
+ cacheReadTok: full.cacheReadTokens ?? 0,
285
+ cacheWriteTok: full.cacheWriteTokens ?? 0,
286
+ estPromptTok: full.estPromptTokens,
287
+ costUsd: full.estimatedCostUsd,
288
+ latencyMs: full.latencyMs,
289
+ }
290
+
291
+ // Production transport — listeners (bridged onto the transport by the stem)
292
+ // persist / re-bill from this.
293
+ for( const fn of this._recordListeners ){
294
+ try { fn( record ) }
295
+ catch { /* listener fault must never break a completion */ }
296
+ }
297
+
298
+ // Dev-only file mirror.
299
+ if( this._ledgerPath ){
300
+ try {
301
+ if( !this._ledgerDirReady ){
302
+ mkdirSync( this._ledgerPath.slice( 0, this._ledgerPath.lastIndexOf( '/' ) ), { recursive: true } )
303
+ this._ledgerDirReady = true
304
+ }
305
+ appendFileSync( this._ledgerPath, JSON.stringify( record ) + '\n' )
306
+ }
307
+ catch { /* ledger file is best-effort */ }
308
+ }
309
+ }
310
+
311
+ /** Fold one call's cost + tokens into a (cost, tokens) breakdown pair under `key`. */
312
+ private _accumulate(
313
+ costMap: Map<string, number>,
314
+ tokenMap: Map<string, { prompt: number; completion: number }>,
315
+ key: string,
316
+ full: TokenUsage,
317
+ ): void {
318
+ costMap.set( key, ( costMap.get( key ) ?? 0 ) + full.estimatedCostUsd )
319
+ const prev = tokenMap.get( key ) ?? { prompt: 0, completion: 0 }
320
+ tokenMap.set( key, {
321
+ prompt: prev.prompt + full.promptTokens,
322
+ completion: prev.completion + full.completionTokens,
323
+ })
324
+ }
325
+
326
+ // ── Engine interface ─────────────────────────────────────
327
+
328
+ async react(
329
+ _delta: Duration,
330
+ tick: Tick,
331
+ _state: ReadonlySimulationState,
332
+ context: SimulationContext
333
+ ): Promise<EngineResult> {
334
+ const
335
+ events: Array<Omit<SimulationEvent, 'id' | 'timestamp' | 'tick'>> = [],
336
+ commands: StateCommands = { metrics: [] }
337
+
338
+ // Compute this tick's cost (usage recorded since last tick)
339
+ const tickCost = this._computeTickCost()
340
+ this._tickCosts.push( tickCost )
341
+ if( this._tickCosts.length > this._maxTickCostSamples )
342
+ this._tickCosts.shift()
343
+
344
+ // Metrics
345
+ commands.metrics!.push(
346
+ [ 'llm.prompt_tokens_total', this._totalPromptTokens ],
347
+ [ 'llm.completion_tokens_total', this._totalCompletionTokens ],
348
+ [ 'llm.cost_total_usd', this._totalCost ],
349
+ [ 'llm.cost_this_tick_usd', tickCost ],
350
+ [ 'llm.cost_avg_per_tick_usd', this._averageTickCost() ],
351
+ [ 'llm.total_calls', this._usageLog.length ],
352
+ )
353
+
354
+ // Per-axis cost + token breakdown — the transparency surface for
355
+ // "how much goes into conversation vs executive vs embedding" (by category)
356
+ // and "decision vs ideation vs conversation vs planning…" (by function).
357
+ for( const [ cat, cost ] of this._categoryCosts ){
358
+ commands.metrics!.push([ `llm.cost.${cat}`, cost ])
359
+ }
360
+ for( const [ cat, tok ] of this._categoryTokens ){
361
+ commands.metrics!.push(
362
+ [ `llm.prompt_tokens.${cat}`, tok.prompt ],
363
+ [ `llm.completion_tokens.${cat}`, tok.completion ],
364
+ )
365
+ }
366
+ for( const [ fn, cost ] of this._functionCosts ){
367
+ commands.metrics!.push([ `llm.cost.fn.${fn}`, cost ])
368
+ }
369
+
370
+ // Cost warning event
371
+ if( tickCost > this._costWarningThreshold
372
+ && this._emitCostEvents
373
+ && tick - this._lastCostWarningTick > 50 ){
374
+ this._lastCostWarningTick = tick
375
+ events.push({
376
+ type: 'llm.cost_spike',
377
+ source: this.name,
378
+ payload: {
379
+ tickCost,
380
+ totalCost: this._totalCost,
381
+ categoryBreakdown: Object.fromEntries( this._categoryCosts ),
382
+ functionBreakdown: Object.fromEntries( this._functionCosts ),
383
+ },
384
+ })
385
+ }
386
+
387
+
388
+ return { events: events.length > 0 ? events : undefined, commands }
389
+ }
390
+
391
+ // ── Public query methods ─────────────────────────────────
392
+
393
+ /** Total cost since simulation start */
394
+ get totalCostUsd(): number { return this._totalCost }
395
+
396
+ /** Total tokens consumed */
397
+ get totalTokens(): { prompt: number; completion: number } {
398
+ return {
399
+ prompt: this._totalPromptTokens,
400
+ completion: this._totalCompletionTokens,
401
+ }
402
+ }
403
+
404
+ /** Cost broken down by category ('executive' | 'summarizer' | 'embedding' | …). */
405
+ get categoryBreakdown(): ReadonlyMap<string, number> {
406
+ return this._categoryCosts
407
+ }
408
+
409
+ /** Token counts (prompt + completion) broken down by category. */
410
+ get categoryTokenBreakdown(): ReadonlyMap<string, { prompt: number; completion: number }> {
411
+ return this._categoryTokens
412
+ }
413
+
414
+ /** Cost broken down by function ('decision' | 'ideation' | 'conversation' | 'planning' | …). */
415
+ get functionBreakdown(): ReadonlyMap<string, number> {
416
+ return this._functionCosts
417
+ }
418
+
419
+ /** Token counts (prompt + completion) broken down by function. */
420
+ get functionTokenBreakdown(): ReadonlyMap<string, { prompt: number; completion: number }> {
421
+ return this._functionTokens
422
+ }
423
+
424
+ /** Cost per call average */
425
+ get averageCostPerCall(): number {
426
+ if( this._usageLog.length === 0 ) return 0
427
+ return this._totalCost / this._usageLog.length
428
+ }
429
+
430
+ /** Cost per tick average */
431
+ get averageCostPerTick(): number {
432
+ return this._averageTickCost()
433
+ }
434
+
435
+ /** Estimated cost per hour at current rate */
436
+ estimateHourlyCost(): number {
437
+ if( this._tickCosts.length === 0 ) return 0
438
+ const avgTickCost = this._averageTickCost()
439
+ // Assume 1 tick/second (or read actual tick rate from config)
440
+ return avgTickCost * 3600
441
+ }
442
+
443
+ /** Full usage log for export/analysis */
444
+ getUsageLog(): ReadonlyArray<TokenUsage> {
445
+ return this._usageLog
446
+ }
447
+
448
+ /** Reset all counters (for new simulation run) */
449
+ reset(): void {
450
+ this._usageLog = []
451
+ this._totalPromptTokens = 0
452
+ this._totalCompletionTokens = 0
453
+ this._totalCost = 0
454
+ this._categoryCosts.clear()
455
+ this._categoryTokens.clear()
456
+ this._functionCosts.clear()
457
+ this._functionTokens.clear()
458
+ this._tickCosts = []
459
+ }
460
+
461
+ // ── Internal ─────────────────────────────────────────────
462
+
463
+ private _computeTickCost(): number {
464
+ // Sum costs of calls that completed since last tick
465
+ // In practice, track the index of last-processed call
466
+ // For simplicity: return average (engine.react is called once/tick)
467
+ // A more precise approach uses a last-processed index
468
+ if( this._usageLog.length === 0 ) return 0
469
+
470
+ // Return the cost of the most recent call(s) as a simple heuristic
471
+ // A production version would track a cursor
472
+ // Sum all calls recorded since last tick
473
+ let total = 0
474
+ for( let i = this._lastProcessedIndex; i < this._usageLog.length; i++ )
475
+ total += this._usageLog[i]!.estimatedCostUsd
476
+
477
+ this._lastProcessedIndex = this._usageLog.length
478
+
479
+ return total
480
+ }
481
+
482
+ private _averageTickCost(): number {
483
+ if( this._tickCosts.length === 0 ) return 0
484
+ return this._tickCosts.reduce( ( s, c ) => s + c, 0 ) / this._tickCosts.length
485
+ }
486
+ }
487
+
488
+ // NOTE (R4): the former process-global `_tracker` + getTokenTracker/
489
+ // setTokenTracker accessors were removed. The TokenTracker is now a per-Will
490
+ // instance — created in stem/mind.ts, registered as an engine, and injected
491
+ // into the executive's LLMDirector via attachTokenTracker(). This keeps
492
+ // usage/cost from conflating across Wills and lets parallel runs stay isolated.
@@ -0,0 +1,60 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/core/abstracts.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * Independent abstractions that decouple the framework from
7
+ * runtime-specific implementations. New cross-cutting abstractions
8
+ * belong here alongside existing ones.
9
+ */
10
+
11
+ // ── Storage ─────────────────────────────────────────────────
12
+
13
+ export interface StorageAdapter {
14
+ write( path: string, content: string | Uint8Array ): Promise<void>
15
+ read( path: string ): Promise<string>
16
+ readBytes( path: string ): Promise<Uint8Array>
17
+ exists( path: string ): Promise<boolean>
18
+ delete?( path: string ): Promise<void>
19
+ ensureDir?( path: string ): Promise<void>
20
+ }
21
+
22
+ /**
23
+ * Bun-native storage adapter.
24
+ * Default for all framework components that perform file I/O.
25
+ */
26
+ export class BunStorageAdapter implements StorageAdapter {
27
+ async write( path: string, content: string | Uint8Array ): Promise<void> {
28
+ await Bun.write( path, content )
29
+ }
30
+
31
+ async read( path: string ): Promise<string> {
32
+ const file = Bun.file( path )
33
+ if( !( await file.exists() ) )
34
+ throw new Error(`File not found: ${path}`)
35
+
36
+ return file.text()
37
+ }
38
+
39
+ async readBytes( path: string ): Promise<Uint8Array> {
40
+ const file = Bun.file( path )
41
+ if( !( await file.exists() ) )
42
+ throw new Error(`File not found: ${path}`)
43
+
44
+ return new Uint8Array( await file.arrayBuffer() )
45
+ }
46
+
47
+ async exists( path: string ): Promise<boolean> {
48
+ return Bun.file( path ).exists()
49
+ }
50
+
51
+ async delete( path: string ): Promise<void> {
52
+ const file = Bun.file( path )
53
+ await file.exists() && await file.delete()
54
+ }
55
+
56
+ async ensureDir( path: string ): Promise<void> {
57
+ const { mkdirSync } = await import('node:fs')
58
+ mkdirSync( path, { recursive: true } )
59
+ }
60
+ }