@mindot/will 0.7.0 → 0.9.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 (88) hide show
  1. package/README.md +87 -22
  2. package/dist/channels/discord.d.ts +67 -6
  3. package/dist/channels/discord.js +112 -6
  4. package/dist/channels/discord.js.map +1 -1
  5. package/dist/channels/whatsapp.d.ts +1 -1
  6. package/dist/channels/whatsapp.js +4 -1
  7. package/dist/channels/whatsapp.js.map +1 -1
  8. package/dist/cli.js +13733 -11057
  9. package/dist/cli.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +3729 -1199
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/effectors.d.ts +1 -1
  14. package/dist/{will-DAW0l-lY.d.ts → will-DbDj_TEH.d.ts} +1214 -93
  15. package/package.json +1 -1
  16. package/src/channels/discord.ts +189 -11
  17. package/src/channels/types.ts +90 -0
  18. package/src/channels/whatsapp.ts +13 -4
  19. package/src/cli.ts +9 -4
  20. package/src/cognition/agency/consequence.ts +122 -1
  21. package/src/cognition/agency/conversation.aim.ts +260 -0
  22. package/src/cognition/agency/engines/action.selector.ts +85 -3
  23. package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
  24. package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
  25. package/src/cognition/agency/engines/reafference.engine.ts +129 -2
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +16 -2
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/repertoire.ts +12 -5
  30. package/src/cognition/agency/selection.scoring.ts +33 -0
  31. package/src/cognition/agency/types.ts +35 -0
  32. package/src/cognition/cache/composition.ts +232 -0
  33. package/src/cognition/cache/deliberation.cache.ts +219 -0
  34. package/src/cognition/cache/fingerprint.ts +120 -0
  35. package/src/cognition/cache/types.ts +105 -0
  36. package/src/cognition/config.mirror.entities.ts +109 -1
  37. package/src/cognition/event.schemas.ts +22 -0
  38. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  39. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  40. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  41. package/src/cognition/faculties/executive.engine/context.ts +67 -13
  42. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  43. package/src/cognition/faculties/executive.engine/engine.ts +686 -187
  44. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
  45. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
  46. package/src/cognition/faculties/executive.engine/facet.ts +89 -26
  47. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  48. package/src/cognition/faculties/executive.engine/parser.ts +21 -1
  49. package/src/cognition/faculties/executive.engine/prompt.factory.ts +169 -20
  50. package/src/cognition/faculties/executive.engine/types.ts +69 -0
  51. package/src/cognition/faculties/goal.manager.ts +94 -14
  52. package/src/cognition/faculties/known.entity.tracker.ts +267 -28
  53. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  54. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  55. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  56. package/src/cognition/faculties/self.model.updater.ts +19 -12
  57. package/src/cognition/faculties/social.perception.ts +47 -3
  58. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  59. package/src/cognition/faculties/working.memory.ts +10 -20
  60. package/src/cognition/identity.entity.ts +205 -0
  61. package/src/cognition/index.ts +11 -0
  62. package/src/cognition/memory/vector.adapter.ts +12 -3
  63. package/src/cognition/memory/vector.embedder.ts +54 -7
  64. package/src/cognition/persona.prior.ts +6 -0
  65. package/src/cognition/senses/audition.engine/engine.ts +404 -46
  66. package/src/cognition/senses/base.sense.engine.ts +1 -1
  67. package/src/cognition/senses/index.ts +12 -0
  68. package/src/cognition/social.identity.ts +273 -0
  69. package/src/cognition/utilities/token.tracker.ts +246 -98
  70. package/src/core/orchestrator.ts +38 -0
  71. package/src/host/boot.ts +78 -22
  72. package/src/index.ts +35 -0
  73. package/src/llm/index.ts +415 -97
  74. package/src/llm/routing.ts +204 -0
  75. package/src/llm/summarizer.ts +5 -1
  76. package/src/llm/wire.contracts.ts +19 -0
  77. package/src/pma/index.ts +67 -53
  78. package/src/runners/thin-shim.runner.ts +18 -6
  79. package/src/sdk/will.ts +121 -22
  80. package/src/stem/assembly.audit.ts +1 -0
  81. package/src/stem/guards/identity.coherence.ts +17 -6
  82. package/src/stem/index.ts +82 -5
  83. package/src/stem/mind.ts +327 -79
  84. package/src/stem/policy/arbiter.ts +49 -14
  85. package/src/stem/policy/rule.table.ts +2 -2
  86. package/src/stem/tracts/effector.controller.ts +56 -9
  87. package/src/stem/tracts/outbox.writer.ts +40 -2
  88. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
@@ -16,6 +16,7 @@
16
16
  * Exposes as metrics so the orchestrator and runner can log costs,
17
17
  * and the ParameterOptimizer can factor cost into optimization decisions.
18
18
  */
19
+ import { logger } from '#core/logger'
19
20
  import type {
20
21
  Duration,
21
22
  Tick,
@@ -31,56 +32,51 @@ import { wallClock } from '#core/wall.clock'
31
32
  // transport layer (determinism contract). The tracker exposes a neutral
32
33
  // onRecord() sink; the stem bridges records onto the transport.
33
34
 
35
+ // ── Attribution axes ──────────────────────────────────────
36
+ //
37
+ // Typed rather than free strings so a deviation is caught at the call site
38
+ // instead of surfacing as a silently-unmatched routing rule or a cost bucket
39
+ // nobody notices is empty. These live here (not in #llm) because cognition is
40
+ // the lower layer — #llm already imports from this module, and the reverse
41
+ // would be circular.
42
+
43
+ /** Top-level cost bucket for an LLM call. */
44
+ export type LLMCallCategory =
45
+ | 'executive' // the master consciousness and its facets
46
+ | 'summarizer' // rolling memory consolidation
47
+ | 'embedding' // semantic-memory vectorisation
48
+ | 'identity-guard' // creation-time persona review
49
+
50
+ /** The actor/subsystem doing the work. */
51
+ export type LLMCallAttribute =
52
+ | 'master' // the executive itself
53
+ | 'facet' // a spawned focus (conversation, planning, outreach, supervision)
54
+ | 'memory' // consolidation / embedding
55
+ | 'guard' // a safety reviewer
56
+
57
+ /** The specific cognitive process being paid for. */
58
+ export type LLMCallProcess =
59
+ | 'cog' // background cogs' call
60
+ | 'decision' // the master's fused decision call
61
+ | 'ideation' // the deliberate path's propose pass
62
+
63
+ /** The specific cognitive function being paid for. */
64
+ export type LLMCallFunction =
65
+ | '-' // no-specific function
66
+ | 'deliberation' // action choice under contest
67
+ | 'conversation' // a live reply
68
+ | 'outreach' // an unprompted message
69
+ | 'planning' // plan formation / revision
70
+ | 'supervision' // plan-step supervision
71
+ | 'consolidation' // rolling summary
72
+ | 'recall' // embedding a query
73
+ | 'index' // embedding a write
74
+ | 'identity-coherence' // persona review
75
+
34
76
  /** One attributed ledger record (5-axis attribution + tokens + cost). */
35
77
  export type TokenLedgerRecord = Record<string, unknown>
36
78
  export type TokenRecordListener = ( record: TokenLedgerRecord ) => void
37
79
 
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
- // Z.ai (GLM-5 family). `glm-5.2[1m]` is the same model asking for its 1M
57
- // context window — same rate, so it gets its own row rather than relying on
58
- // the normalizer (a future long-context tier would price differently).
59
- 'glm/glm-5.2': { input: 1.40, output: 4.40 },
60
- 'glm/glm-5.2[1m]': { input: 1.40, output: 4.40 },
61
-
62
- // Google
63
- 'google/gemini-2.0-flash': { input: 0.10, output: 0.40 },
64
- 'google/gemini-2.0-pro': { input: 1.25, output: 5.00 },
65
-
66
- // Meta (via Groq/Replicate)
67
- 'meta/llama-3.3-70b': { input: 0.59, output: 0.79 },
68
- 'meta/llama-4-maverick': { input: 0.20, output: 0.60 },
69
-
70
- // DeepSeek
71
- 'deepseek/deepseek-v3': { input: 0.27, output: 1.10 },
72
- 'deepseek/deepseek-r1': { input: 0.55, output: 2.19 },
73
-
74
- // Embeddings (input-only — no completion tokens). Priced per 1M input tokens.
75
- 'openai/text-embedding-3-small': { input: 0.02, output: 0 },
76
- 'openai/text-embedding-3-large': { input: 0.13, output: 0 },
77
- 'google/text-embedding-004': { input: 0, output: 0 }, // free tier
78
- 'google/gemini-embedding-001': { input: 0, output: 0 }, // free tier
79
-
80
- // Fallback for unknown models
81
- '__default__': { input: 3.00, output: 15.00 },
82
- }
83
-
84
80
  // ── Prompt-cache pricing (Anthropic) ──────────────────────
85
81
  // `input_tokens` in the API usage already EXCLUDES cached tokens, so the full
86
82
  // input cost is: fresh input ×1 + cache reads ×0.1 + cache writes ×1.25.
@@ -88,40 +84,83 @@ const CACHE_READ_MULT = 0.1
88
84
  const CACHE_WRITE_MULT = 1.25
89
85
 
90
86
  /**
91
- * Normalize a model id to its bare, dateless form so a raw provider model string
92
- * ("claude-sonnet-4-5-20250929", "anthropic/claude-haiku-4-5") resolves to the
93
- * right pricing row. Without this, every non-default id missed the `provider/model`
94
- * keys and silently fell through to __default__ ($3/$15) pricing Haiku ~3× and
95
- * DeepSeek ~11× too high, and breaking the self-hosting margin telemetry entirely.
87
+ * Normalize a model id to its bare form so a raw provider string
88
+ * ("claude-sonnet-5-20260114", "anthropic/claude-haiku-4-5", "glm-5.2[1m]")
89
+ * matches a host price keyed plainly and vice versa. Exact keys are tried
90
+ * first, so a host that prices a long-context variant differently just lists it
91
+ * verbatim and that wins.
96
92
  */
97
93
  function normalizeModelKey( model: string ): string {
98
94
  let m = model.toLowerCase().trim()
99
95
  const slash = m.lastIndexOf('/')
100
- if( slash >= 0 ) m = m.slice( slash + 1 ) // drop "provider/" prefix
101
- return m.replace( /[-@]\d{6,8}$/, '') // drop trailing -YYYYMMDD date stamp
96
+ if( slash >= 0 ) m = m.slice( slash + 1 ) // drop "provider/" prefix
97
+ m = m.replace( /\[[^\]]*\]$/, '') // drop a trailing qualifier, e.g. "[1m]"
98
+ return m.replace( /[-@]\d{6,8}$/, '') // drop trailing -YYYYMMDD date stamp
102
99
  }
100
+ /** USD per 1M tokens for one model. */
101
+ export interface ModelPrice { input: number; output: number }
102
+
103
+ /**
104
+ * Host-supplied prices, keyed by model id. Matching is exact first, then
105
+ * normalized (provider prefix, date stamp and context qualifier stripped), so
106
+ * `claude-sonnet-5` matches `claude-sonnet-5-20260114`.
107
+ *
108
+ * Prices belong to the host: they change on a vendor's schedule, differ per
109
+ * account, and are ~0 for a self-hosted model. The engine ships none.
110
+ */
111
+ export type PriceTable = Record<string, ModelPrice>
112
+
113
+ /** Models already warned about as unpriced — one line each, not one per call. */
114
+ const _unpricedWarned = new Set<string>()
103
115
 
104
- // Pre-index the pricing table by normalized model name for O(1), date-insensitive
105
- // lookup. Built once at module load.
106
- const PRICING_BY_NORM: Record<string, { input: number; output: number }> = ( () => {
107
- const out: Record<string, { input: number; output: number }> = {}
108
- for( const [ key, price ] of Object.entries( MODEL_PRICING ) ){
109
- if( key === '__default__') continue
110
- out[ normalizeModelKey( key ) ] = price
116
+ /**
117
+ * Resolve the price for a model id from the host's table.
118
+ *
119
+ * The engine ships no prices at all. A table baked into a release is wrong the
120
+ * week a vendor changes a rate, differs per account, and is meaningless for a
121
+ * self-hosted model — and a *partial* table is worse than none, because some
122
+ * models then report plausible-but-stale numbers while others honestly report
123
+ * nothing. Prices live with the host, next to the routing policy they inform.
124
+ *
125
+ * `null` does NOT mean free — it means *unknown*, and the caller reports zero
126
+ * cost with `priced: false` so the gap stays visible rather than confidently
127
+ * wrong. (The removed built-in default priced every unrecognised model at
128
+ * Sonnet's rate, overstating a budget model's output by ~54×.)
129
+ */
130
+ export function resolvePricing( model: string, hostPrices?: PriceTable ): ModelPrice | null {
131
+ if( !hostPrices ) return null
132
+
133
+ const exact = hostPrices[ model ]
134
+ if( exact ) return exact
135
+
136
+ // A host table keyed by bare ids still matches a dated / provider-prefixed
137
+ // model id, and vice versa.
138
+ const norm = normalizeModelKey( model )
139
+ if( hostPrices[ norm ] ) return hostPrices[ norm ]
140
+
141
+ for( const [ key, price ] of Object.entries( hostPrices ) ){
142
+ if( normalizeModelKey( key ) === norm ) return price
111
143
  }
112
- return out
113
- } )()
114
-
115
- /** Resolve the pricing row for any model id (exact, normalized, then default). */
116
- export function resolvePricing( model: string ): { input: number; output: number } {
117
- return PRICING_BY_NORM[ normalizeModelKey( model ) ]
118
- ?? MODEL_PRICING[ model ]
119
- ?? MODEL_PRICING['__default__']!
144
+
145
+ return null
120
146
  }
121
147
 
122
148
  export interface TokenUsage {
123
149
  /** Model identifier (e.g., 'openai/gpt-4o') */
124
150
  model: string
151
+ /**
152
+ * The provider that actually served this call.
153
+ *
154
+ * Not derivable from `model`: routing is what makes the same model id
155
+ * reachable from several places — `deepseek-v3` direct, through a gateway, or
156
+ * self-hosted — at prices that differ by orders of magnitude. Without this a
157
+ * host billing across a multi-vendor routing table can attribute spend to a
158
+ * model but never to the vendor it actually paid.
159
+ *
160
+ * Optional because a caller recording usage directly (outside the LLM
161
+ * director) may not know it; absent means unattributed, not "the default".
162
+ */
163
+ provider?: string
125
164
  /** Input/prompt tokens consumed */
126
165
  promptTokens: number
127
166
  /** Output/completion tokens consumed */
@@ -132,16 +171,36 @@ export interface TokenUsage {
132
171
  cacheReadTokens?: number
133
172
  /** Anthropic prompt-cache write tokens (billed at 1.25× input). Optional. */
134
173
  cacheWriteTokens?: number
135
- /** Estimated cost in USD */
174
+ /** Estimated cost in USD. Zero when `priced` is false — unknown, not free. */
136
175
  estimatedCostUsd: number
176
+ /**
177
+ * Whether a price was found for this model. False ⇒ `estimatedCostUsd` is 0
178
+ * because nothing priced it, NOT because the call was free. A consumer
179
+ * summing costs should surface unpriced calls rather than fold them in as
180
+ * zero.
181
+ */
182
+ priced: boolean
183
+
184
+ /**
185
+ * How much this call demanded, 0..1 — the cognitive measure the router saw.
186
+ *
187
+ * Recorded so routing can be ANSWERED rather than argued. Every call computes
188
+ * this, routes on it, and until now threw it away — which left questions like
189
+ * "is deliberation being rated by the tick's mood rather than the stakes of
190
+ * its own choice?" with no dataset at all.
191
+ *
192
+ * Absent means UNMEASURED, never zero. It must stay nullable all the way to
193
+ * storage: a call that never reported demand and a call that reported 0.0 are
194
+ * different facts, and collapsing them would put a floor of invented
195
+ * confidence under exactly the analysis this exists to enable.
196
+ */
197
+ demand?: number
137
198
 
138
199
  // ── 5-axis cost attribution ──────────────────────────────
139
- /** Top-level cost bucket: 'executive' | 'summarizer' | 'embedding' | 'identity-guard' | … */
140
- category: string
141
- /** Actor/subsystem doing the work: 'master' | 'facet' | 'memory' | 'guard' | … */
142
- attribute: string
143
- /** Cognitive function: 'decision' | 'ideation' | 'conversation' | 'planning' | 'deliberation' | 'outreach' | 'consolidation' | 'recall' | 'index' | 'identity-coherence' | … */
144
- function: string
200
+ category: LLMCallCategory
201
+ attribute: LLMCallAttribute
202
+ process: LLMCallProcess
203
+ function: LLMCallFunction
145
204
  /** Optional specific id or namespace: facet id, entity id, model name. */
146
205
  scope?: string
147
206
  /** Human-readable label — auto-composed from the axes when the caller omits it. */
@@ -157,15 +216,29 @@ export interface TokenUsage {
157
216
  }
158
217
 
159
218
  /** What callers pass to {@link TokenTracker.recordUsage} — cost and label are derived. */
160
- export type RecordUsageInput = Omit<TokenUsage, 'estimatedCostUsd' | 'label'> & { label?: string }
219
+ export type RecordUsageInput = Omit<TokenUsage, 'estimatedCostUsd' | 'label' | 'priced'> & { label?: string }
161
220
 
162
- /** Compose a stable, readable label from the attribution axes. */
163
- function composeLabel( m: { category: string; attribute: string; function: string; scope?: string } ): string {
164
- const base = `${m.category}/${m.attribute}/${m.function}`
221
+ /**
222
+ * Compose a stable, readable label from the attribution axes.
223
+ *
224
+ * `function` carries '-' when the call has no specific one (the master's own
225
+ * decision/ideation passes), so it is dropped rather than rendered — a label of
226
+ * `executive/master/-` names nothing. Process always shows: it is what
227
+ * distinguishes the propose pass from the decision it feeds.
228
+ */
229
+ function composeLabel( m: { category: LLMCallCategory; attribute: LLMCallAttribute; process: LLMCallProcess; function: LLMCallFunction; scope?: string } ): string {
230
+ const base = [ m.category, m.attribute, m.process, m.function === '-' ? '' : m.function ]
231
+ .filter( Boolean ).join('/')
165
232
  return m.scope ? `${base}#${m.scope}` : base
166
233
  }
167
234
 
168
235
  export interface TokenTrackerConfig {
236
+ /**
237
+ * Host-supplied model prices (USD per 1M tokens), merged from the per-provider
238
+ * `prices` maps in `WillLLMConfig.providers`. These win over the built-in
239
+ * fallback table. Omitted ⇒ fallback only.
240
+ */
241
+ prices?: PriceTable
169
242
  /** Whether to emit cost events */
170
243
  emitCostEvents?: boolean
171
244
  /** Cost threshold for warning events */
@@ -186,6 +259,7 @@ export class TokenTracker implements SimulationEngine {
186
259
 
187
260
  private _emitCostEvents: boolean
188
261
  private _costWarningThreshold: number
262
+ private _prices: PriceTable | undefined
189
263
 
190
264
  // All recorded usage for the simulation run
191
265
  private _usageLog: TokenUsage[] = []
@@ -200,6 +274,17 @@ export class TokenTracker implements SimulationEngine {
200
274
  private _categoryTokens = new Map<string, { prompt: number; completion: number }>()
201
275
  private _functionCosts = new Map<string, number>()
202
276
  private _functionTokens = new Map<string, { prompt: number; completion: number }>()
277
+ // Split out of the function axis: `decision` vs `ideation` is which PROCESS ran,
278
+ // not which function it served. Without its own bucket the deliberate path's
279
+ // propose pass became invisible in cost reporting — every master call landing in
280
+ // the '-' function bucket regardless of whether it deliberated.
281
+ private _processCosts = new Map<string, number>()
282
+ private _processTokens = new Map<string, { prompt: number; completion: number }>()
283
+ // Per-provider spend. The axis a host actually reconciles against invoices —
284
+ // "which vendor did we pay?" is not answerable from the model id once routing
285
+ // can reach one model through several of them.
286
+ private _providerCosts = new Map<string, number>()
287
+ private _providerTokens = new Map<string, { prompt: number; completion: number }>()
203
288
 
204
289
  // Per-tick costs (for spike detection)
205
290
  private _tickCosts: number[] = []
@@ -219,6 +304,7 @@ export class TokenTracker implements SimulationEngine {
219
304
  constructor( config: TokenTrackerConfig = {} ){
220
305
  this._emitCostEvents = config.emitCostEvents ?? true
221
306
  this._costWarningThreshold = config.costWarningThresholdUsd ?? 0.05
307
+ this._prices = config.prices
222
308
  this._ledgerPath = ( config.writeLedger && config.willId )
223
309
  ? `./data/wills/${config.willId}/debug/token-report.jsonl`
224
310
  : null
@@ -231,18 +317,31 @@ export class TokenTracker implements SimulationEngine {
231
317
  * Called by LLMDirector.call after each completion (src/llm/index.ts).
232
318
  */
233
319
  recordUsage( usage: RecordUsageInput ): void {
234
- const pricing = resolvePricing( usage.model )
320
+ const pricing = resolvePricing( usage.model, this._prices )
235
321
  const cacheRead = usage.cacheReadTokens ?? 0
236
322
  const cacheWrite = usage.cacheWriteTokens ?? 0
237
- const costUsd =
238
- ( usage.promptTokens / 1_000_000 ) * pricing.input +
239
- ( usage.completionTokens / 1_000_000 ) * pricing.output +
240
- ( cacheRead / 1_000_000 ) * pricing.input * CACHE_READ_MULT +
241
- ( cacheWrite / 1_000_000 ) * pricing.input * CACHE_WRITE_MULT
323
+
324
+ // No price cost 0 and `priced: false`. Warn once per model id so an
325
+ // unconfigured provider is visible without flooding the log.
326
+ if( !pricing && !_unpricedWarned.has( usage.model ) ){
327
+ _unpricedWarned.add( usage.model )
328
+ logger.warn(
329
+ `[tokens] no price for "${usage.model}" — reporting cost 0 for it. ` +
330
+ `Supply one via llm.providers.<provider>.prices to get cost telemetry.`
331
+ )
332
+ }
333
+
334
+ const costUsd = pricing
335
+ ? ( usage.promptTokens / 1_000_000 ) * pricing.input +
336
+ ( usage.completionTokens / 1_000_000 ) * pricing.output +
337
+ ( cacheRead / 1_000_000 ) * pricing.input * CACHE_READ_MULT +
338
+ ( cacheWrite / 1_000_000 ) * pricing.input * CACHE_WRITE_MULT
339
+ : 0
242
340
 
243
341
  const full: TokenUsage = {
244
342
  ...usage,
245
343
  label: usage.label ?? composeLabel( usage ),
344
+ priced: pricing !== null,
246
345
  estimatedCostUsd: Math.round( costUsd * 1_000_000 ) / 1_000_000, // round to micro-dollars
247
346
  }
248
347
 
@@ -256,6 +355,10 @@ export class TokenTracker implements SimulationEngine {
256
355
  // Per-axis breakdowns — the repartition surface (category × function).
257
356
  this._accumulate( this._categoryCosts, this._categoryTokens, full.category, full )
258
357
  this._accumulate( this._functionCosts, this._functionTokens, full.function, full )
358
+ this._accumulate( this._processCosts, this._processTokens, full.process, full )
359
+ // Unattributed rather than guessed: a caller that did not say which
360
+ // provider served the call must not be silently folded into the default.
361
+ this._accumulate( this._providerCosts, this._providerTokens, full.provider ?? 'unattributed', full )
259
362
 
260
363
  // Complete attributed ledger record (every call, all axes + cost): notify
261
364
  // record listeners (the stem forwards them onto the transport) and mirror to
@@ -280,8 +383,10 @@ export class TokenTracker implements SimulationEngine {
280
383
  tick: full.tick,
281
384
  ts: new Date( wallClock() ).toISOString(), // determinism-ok: ledger timestamp is telemetry, never replay state
282
385
  model: full.model,
386
+ provider: full.provider,
283
387
  category: full.category,
284
388
  attribute: full.attribute,
389
+ process: full.process,
285
390
  function: full.function,
286
391
  scope: full.scope,
287
392
  label: full.label,
@@ -291,6 +396,13 @@ export class TokenTracker implements SimulationEngine {
291
396
  cacheWriteTok: full.cacheWriteTokens ?? 0,
292
397
  estPromptTok: full.estPromptTokens,
293
398
  costUsd: full.estimatedCostUsd,
399
+ // Whether costUsd came from a real price. False ⇒ 0 because nothing
400
+ // priced this model, NOT because the call was free — a consumer summing
401
+ // spend must not fold unpriced calls in as zero.
402
+ priced: full.priced,
403
+ // Undefined stays undefined — see TokenUsage.demand. A consumer that
404
+ // coerces this to 0 has silently invented a measurement.
405
+ demand: full.demand,
294
406
  latencyMs: full.latencyMs,
295
407
  }
296
408
 
@@ -347,31 +459,34 @@ export class TokenTracker implements SimulationEngine {
347
459
  if( this._tickCosts.length > this._maxTickCostSamples )
348
460
  this._tickCosts.shift()
349
461
 
350
- // Metrics
462
+ // Metrics — TOKENS ONLY (W8c).
463
+ //
464
+ // Token counts are a physical, deterministic fact of a call and belong in
465
+ // state. Dollars are the host's accounting over that fact: prices differ per
466
+ // account, change on a vendor's schedule, and are ~0 self-hosted. Nothing in
467
+ // cognition ever read them (the sole consumer was a console runner), yet
468
+ // while they sat in state a host editing its price table changed state bytes
469
+ // and broke replay-equivalence over a number that influenced nothing.
470
+ //
471
+ // Cost still reaches the host every call on the ledger path
472
+ // (`onRecord` → the stem's transport bridge), which is where it was already
473
+ // being consumed. See `totalCostUsd` / `costBreakdown()` for in-process reads.
351
474
  commands.metrics!.push(
352
475
  [ 'llm.prompt_tokens_total', this._totalPromptTokens ],
353
476
  [ 'llm.completion_tokens_total', this._totalCompletionTokens ],
354
- [ 'llm.cost_total_usd', this._totalCost ],
355
- [ 'llm.cost_this_tick_usd', tickCost ],
356
- [ 'llm.cost_avg_per_tick_usd', this._averageTickCost() ],
357
477
  [ 'llm.total_calls', this._usageLog.length ],
358
478
  )
359
479
 
360
- // Per-axis cost + token breakdown — the transparency surface for
361
- // "how much goes into conversation vs executive vs embedding" (by category)
362
- // and "decision vs ideation vs conversation vs planning…" (by function).
363
- for( const [ cat, cost ] of this._categoryCosts ){
364
- commands.metrics!.push([ `llm.cost.${cat}`, cost ])
365
- }
480
+ // Per-axis TOKEN breakdown — the transparency surface for "how much goes
481
+ // into conversation vs executive vs embedding". The matching cost
482
+ // breakdown is host-side now (W8c); `costBreakdown()` still exposes it
483
+ // in-process for anyone holding the tracker.
366
484
  for( const [ cat, tok ] of this._categoryTokens ){
367
485
  commands.metrics!.push(
368
486
  [ `llm.prompt_tokens.${cat}`, tok.prompt ],
369
487
  [ `llm.completion_tokens.${cat}`, tok.completion ],
370
488
  )
371
489
  }
372
- for( const [ fn, cost ] of this._functionCosts ){
373
- commands.metrics!.push([ `llm.cost.fn.${fn}`, cost ])
374
- }
375
490
 
376
491
  // Cost warning event
377
492
  if( tickCost > this._costWarningThreshold
@@ -386,6 +501,7 @@ export class TokenTracker implements SimulationEngine {
386
501
  totalCost: this._totalCost,
387
502
  categoryBreakdown: Object.fromEntries( this._categoryCosts ),
388
503
  functionBreakdown: Object.fromEntries( this._functionCosts ),
504
+ processBreakdown: Object.fromEntries( this._processCosts ),
389
505
  },
390
506
  })
391
507
  }
@@ -427,6 +543,34 @@ export class TokenTracker implements SimulationEngine {
427
543
  return this._functionTokens
428
544
  }
429
545
 
546
+ /** Cost broken down by process ('decision' | 'ideation' | 'cog'). */
547
+ get processBreakdown(): ReadonlyMap<string, number> {
548
+ return this._processCosts
549
+ }
550
+
551
+ /** Token counts (prompt + completion) broken down by process. */
552
+ get processTokenBreakdown(): ReadonlyMap<string, { prompt: number; completion: number }> {
553
+ return this._processTokens
554
+ }
555
+
556
+ /**
557
+ * Cost broken down by provider ('anthropic' | 'glm' | 'moonshot' | …), plus
558
+ * an `unattributed` bucket for usage recorded without one.
559
+ *
560
+ * This is the axis a host reconciles against vendor invoices. Calls whose
561
+ * model went unpriced contribute 0 here, so compare against
562
+ * `getUsageLog()`'s `priced` flag before treating a small number as a small
563
+ * bill.
564
+ */
565
+ get providerBreakdown(): ReadonlyMap<string, number> {
566
+ return this._providerCosts
567
+ }
568
+
569
+ /** Token counts (prompt + completion) broken down by provider. */
570
+ get providerTokenBreakdown(): ReadonlyMap<string, { prompt: number; completion: number }> {
571
+ return this._providerTokens
572
+ }
573
+
430
574
  /** Cost per call average */
431
575
  get averageCostPerCall(): number {
432
576
  if( this._usageLog.length === 0 ) return 0
@@ -461,6 +605,10 @@ export class TokenTracker implements SimulationEngine {
461
605
  this._categoryTokens.clear()
462
606
  this._functionCosts.clear()
463
607
  this._functionTokens.clear()
608
+ this._processCosts.clear()
609
+ this._processTokens.clear()
610
+ this._providerCosts.clear()
611
+ this._providerTokens.clear()
464
612
  this._tickCosts = []
465
613
  }
466
614
 
@@ -62,6 +62,13 @@ export interface SimulationEngine {
62
62
  }
63
63
 
64
64
 
65
+ /**
66
+ * Fraction of one tick interval a single non-async engine may consume before the
67
+ * orchestrator says so. Half a tick is generous for work that is meant to be
68
+ * in-memory, and low enough to fire long before tick-denominated deadlines drift.
69
+ */
70
+ const SLOW_ENGINE_FRACTION = 0.5
71
+
65
72
  // ── Middleware ───────────────────────────────────────────────
66
73
 
67
74
  export type TickMiddleware = (
@@ -384,6 +391,35 @@ export class DefaultOrchestrator implements Orchestrator {
384
391
  * calls _executeTick() which handles pause/stop checks internally.
385
392
  * The orchestrator is the sole driver of ticks.
386
393
  */
394
+ /**
395
+ * A plain SimulationEngine's `react()` is implicitly "finish inside the tick" —
396
+ * only an AsyncEngine is allowed to span ticks, and it does so by LAUNCHING work
397
+ * and landing it later (see async.engine.ts: "react() never awaits LLM calls").
398
+ * Nothing enforces that on everyone else, and the failure is silent and severe:
399
+ * every agency deadline is denominated in TICKS, so an engine that awaits network
400
+ * I/O does not merely run slowly, it rescales time for the whole mind.
401
+ *
402
+ * Measured: one rate-limited embedding call awaited inside EpisodicConsolidator
403
+ * made two consecutive ticks take 64.9s and 63.5s. `AWAIT_TIMEOUT` — 15 ticks,
404
+ * normally ~15s — silently became 15 minutes, so a communicate intent sat
405
+ * 'awaiting' forever and the serial selector never chose anything again. 45
406
+ * executive decisions produced one intent and zero delivered messages, with no
407
+ * error anywhere. This turns that into a line in the log the first time it happens.
408
+ */
409
+ private _warnIfSlow( engine: SimulationEngine, elapsedMs: number ): void {
410
+ // AsyncEngines are exempt by design — spanning ticks is their contract.
411
+ if( ( engine as { hasPendingWork?: unknown } ).hasPendingWork !== undefined ) return
412
+
413
+ const budget = ( this._config.tickIntervalMs ?? 1000 ) * SLOW_ENGINE_FRACTION
414
+ if( elapsedMs <= budget ) return
415
+
416
+ logger.warn(
417
+ `[Orchestrator] Engine "${ engine.name }" held the tick for ${ Math.round( elapsedMs ) }ms ` +
418
+ `at tick ${ this._currentTick } (budget ${ Math.round( budget ) }ms). A non-async engine must not ` +
419
+ `await network I/O — tick-denominated deadlines elsewhere are being stretched by this.`
420
+ )
421
+ }
422
+
387
423
  private _runLoop( context: SimulationContext ): void {
388
424
  if( this._tickTimer ) return
389
425
 
@@ -478,6 +514,7 @@ export class DefaultOrchestrator implements Orchestrator {
478
514
  const allResults: EngineResult[] = []
479
515
 
480
516
  const runEngine = async ( engine: SimulationEngine ): Promise<void> => {
517
+ const startedAt = wallClock()
481
518
  try {
482
519
  const result = await engine.react?.( this._clock.delta, this._currentTick, snapshot, engineContext )
483
520
  if( result ) allResults.push( result )
@@ -494,6 +531,7 @@ export class DefaultOrchestrator implements Orchestrator {
494
531
  )
495
532
  }
496
533
  }
534
+ finally { this._warnIfSlow( engine, wallClock() - startedAt ) }
497
535
  }
498
536
 
499
537
  for( const engine of this._enginesTick() )