@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.
- package/README.md +87 -22
- package/dist/channels/discord.d.ts +67 -6
- package/dist/channels/discord.js +112 -6
- package/dist/channels/discord.js.map +1 -1
- package/dist/channels/whatsapp.d.ts +1 -1
- package/dist/channels/whatsapp.js +4 -1
- package/dist/channels/whatsapp.js.map +1 -1
- package/dist/cli.js +13733 -11057
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3729 -1199
- package/dist/index.js.map +1 -1
- package/dist/mcp/effectors.d.ts +1 -1
- package/dist/{will-DAW0l-lY.d.ts → will-DbDj_TEH.d.ts} +1214 -93
- package/package.json +1 -1
- package/src/channels/discord.ts +189 -11
- package/src/channels/types.ts +90 -0
- package/src/channels/whatsapp.ts +13 -4
- package/src/cli.ts +9 -4
- package/src/cognition/agency/consequence.ts +122 -1
- package/src/cognition/agency/conversation.aim.ts +260 -0
- package/src/cognition/agency/engines/action.selector.ts +85 -3
- package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
- package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
- package/src/cognition/agency/engines/reafference.engine.ts +129 -2
- package/src/cognition/agency/proactive.communicator.ts +19 -3
- package/src/cognition/agency/reconcile.learning.ts +16 -2
- package/src/cognition/agency/restart.ts +66 -0
- package/src/cognition/agency/schemas/repertoire.ts +12 -5
- package/src/cognition/agency/selection.scoring.ts +33 -0
- package/src/cognition/agency/types.ts +35 -0
- package/src/cognition/cache/composition.ts +232 -0
- package/src/cognition/cache/deliberation.cache.ts +219 -0
- package/src/cognition/cache/fingerprint.ts +120 -0
- package/src/cognition/cache/types.ts +105 -0
- package/src/cognition/config.mirror.entities.ts +109 -1
- package/src/cognition/event.schemas.ts +22 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
- package/src/cognition/faculties/episodic.consolidator.ts +59 -3
- package/src/cognition/faculties/executive.engine/commands.ts +189 -14
- package/src/cognition/faculties/executive.engine/context.ts +67 -13
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
- package/src/cognition/faculties/executive.engine/engine.ts +686 -187
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
- package/src/cognition/faculties/executive.engine/facet.ts +89 -26
- package/src/cognition/faculties/executive.engine/gating.ts +14 -14
- package/src/cognition/faculties/executive.engine/parser.ts +21 -1
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +169 -20
- package/src/cognition/faculties/executive.engine/types.ts +69 -0
- package/src/cognition/faculties/goal.manager.ts +94 -14
- package/src/cognition/faculties/known.entity.tracker.ts +267 -28
- package/src/cognition/faculties/moral.evaluator.ts +8 -3
- package/src/cognition/faculties/persona.consolidator.ts +141 -0
- package/src/cognition/faculties/reputation.tracker.ts +66 -2
- package/src/cognition/faculties/self.model.updater.ts +19 -12
- package/src/cognition/faculties/social.perception.ts +47 -3
- package/src/cognition/faculties/threat.evaluator.ts +7 -0
- package/src/cognition/faculties/working.memory.ts +10 -20
- package/src/cognition/identity.entity.ts +205 -0
- package/src/cognition/index.ts +11 -0
- package/src/cognition/memory/vector.adapter.ts +12 -3
- package/src/cognition/memory/vector.embedder.ts +54 -7
- package/src/cognition/persona.prior.ts +6 -0
- package/src/cognition/senses/audition.engine/engine.ts +404 -46
- package/src/cognition/senses/base.sense.engine.ts +1 -1
- package/src/cognition/senses/index.ts +12 -0
- package/src/cognition/social.identity.ts +273 -0
- package/src/cognition/utilities/token.tracker.ts +246 -98
- package/src/core/orchestrator.ts +38 -0
- package/src/host/boot.ts +78 -22
- package/src/index.ts +35 -0
- package/src/llm/index.ts +415 -97
- package/src/llm/routing.ts +204 -0
- package/src/llm/summarizer.ts +5 -1
- package/src/llm/wire.contracts.ts +19 -0
- package/src/pma/index.ts +67 -53
- package/src/runners/thin-shim.runner.ts +18 -6
- package/src/sdk/will.ts +121 -22
- package/src/stem/assembly.audit.ts +1 -0
- package/src/stem/guards/identity.coherence.ts +17 -6
- package/src/stem/index.ts +82 -5
- package/src/stem/mind.ts +327 -79
- package/src/stem/policy/arbiter.ts +49 -14
- package/src/stem/policy/rule.table.ts +2 -2
- package/src/stem/tracts/effector.controller.ts +56 -9
- package/src/stem/tracts/outbox.writer.ts +40 -2
- 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
|
|
92
|
-
* ("claude-sonnet-
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
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 )
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
(
|
|
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
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
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
|
|
package/src/core/orchestrator.ts
CHANGED
|
@@ -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() )
|