@mindot/will 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +777 -0
- package/dist/index.d.ts +8144 -0
- package/dist/index.js +26231 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/src/cognition/agency/access.grants.ts +59 -0
- package/src/cognition/agency/competence.codec.ts +89 -0
- package/src/cognition/agency/engines/action.selector.ts +462 -0
- package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
- package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
- package/src/cognition/agency/engines/instruction.intake.ts +54 -0
- package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
- package/src/cognition/agency/engines/reafference.engine.ts +241 -0
- package/src/cognition/agency/execution.primitives.ts +118 -0
- package/src/cognition/agency/index.ts +58 -0
- package/src/cognition/agency/proactive.communicator.ts +274 -0
- package/src/cognition/agency/reconcile.learning.ts +82 -0
- package/src/cognition/agency/schemas/external.ts +59 -0
- package/src/cognition/agency/schemas/innate.ts +111 -0
- package/src/cognition/agency/schemas/repertoire.ts +246 -0
- package/src/cognition/agency/selection.scoring.ts +191 -0
- package/src/cognition/agency/types.ts +165 -0
- package/src/cognition/bus.ts +391 -0
- package/src/cognition/completion.inbox.ts +86 -0
- package/src/cognition/config.mirror.entities.ts +475 -0
- package/src/cognition/conversation.memory.ts +81 -0
- package/src/cognition/event.log.ts +116 -0
- package/src/cognition/event.schemas.ts +568 -0
- package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
- package/src/cognition/faculties/affective.blender.ts +562 -0
- package/src/cognition/faculties/attachment.evaluator.ts +432 -0
- package/src/cognition/faculties/attention.allocator.ts +422 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
- package/src/cognition/faculties/bias.detector.ts +363 -0
- package/src/cognition/faculties/circadian.oscillator.ts +294 -0
- package/src/cognition/faculties/confidence.calibrator.ts +324 -0
- package/src/cognition/faculties/dream.simulator.ts +251 -0
- package/src/cognition/faculties/empathy.simulator.ts +253 -0
- package/src/cognition/faculties/energy.regulator.ts +320 -0
- package/src/cognition/faculties/episodic.consolidator.ts +728 -0
- package/src/cognition/faculties/executive.engine/commands.ts +459 -0
- package/src/cognition/faculties/executive.engine/config.ts +46 -0
- package/src/cognition/faculties/executive.engine/context.ts +605 -0
- package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
- package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
- package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
- package/src/cognition/faculties/executive.engine/facet.ts +628 -0
- package/src/cognition/faculties/executive.engine/gating.ts +222 -0
- package/src/cognition/faculties/executive.engine/index.ts +6 -0
- package/src/cognition/faculties/executive.engine/messages.ts +102 -0
- package/src/cognition/faculties/executive.engine/parser.ts +380 -0
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
- package/src/cognition/faculties/executive.engine/types.ts +323 -0
- package/src/cognition/faculties/exteroception.ts +338 -0
- package/src/cognition/faculties/forgetting.curve.ts +202 -0
- package/src/cognition/faculties/frustration.evaluator.ts +280 -0
- package/src/cognition/faculties/goal.manager.ts +1008 -0
- package/src/cognition/faculties/inhibition.controller.ts +351 -0
- package/src/cognition/faculties/interoception.ts +417 -0
- package/src/cognition/faculties/introspection.engine.ts +246 -0
- package/src/cognition/faculties/known.entity.tracker.ts +439 -0
- package/src/cognition/faculties/loss.evaluator.ts +224 -0
- package/src/cognition/faculties/moral.evaluator.ts +437 -0
- package/src/cognition/faculties/novelty.detector.ts +254 -0
- package/src/cognition/faculties/persona.consolidator.ts +845 -0
- package/src/cognition/faculties/planning.engine/engine.ts +859 -0
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
- package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
- package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
- package/src/cognition/faculties/planning.engine/types.ts +143 -0
- package/src/cognition/faculties/reputation.tracker.ts +294 -0
- package/src/cognition/faculties/reward.evaluator.ts +304 -0
- package/src/cognition/faculties/self.model.updater.ts +677 -0
- package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
- package/src/cognition/faculties/semantic.engine/index.ts +6 -0
- package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
- package/src/cognition/faculties/semantic.engine/types.ts +53 -0
- package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
- package/src/cognition/faculties/social.perception.ts +333 -0
- package/src/cognition/faculties/spaced.repetition.ts +605 -0
- package/src/cognition/faculties/stress.regulator.ts +375 -0
- package/src/cognition/faculties/task.switcher.ts +239 -0
- package/src/cognition/faculties/theory.of.mind.ts +333 -0
- package/src/cognition/faculties/threat.evaluator.ts +312 -0
- package/src/cognition/faculties/working.memory.ts +406 -0
- package/src/cognition/generative.model.ts +344 -0
- package/src/cognition/heartbeat.ts +70 -0
- package/src/cognition/index.ts +291 -0
- package/src/cognition/instruction.handler.ts +146 -0
- package/src/cognition/memory/index.ts +35 -0
- package/src/cognition/memory/vector.adapter.ts +316 -0
- package/src/cognition/memory/vector.content.ts +47 -0
- package/src/cognition/memory/vector.embedder.ts +212 -0
- package/src/cognition/memory/vector.index.ts +459 -0
- package/src/cognition/memory/vector.types.ts +65 -0
- package/src/cognition/orchestrator.ts +142 -0
- package/src/cognition/persona.prior.ts +265 -0
- package/src/cognition/schema.registry.ts +145 -0
- package/src/cognition/senses/audition.engine/engine.ts +976 -0
- package/src/cognition/senses/audition.engine/salience.ts +59 -0
- package/src/cognition/senses/base.sense.engine.ts +117 -0
- package/src/cognition/senses/gustation.engine.ts +24 -0
- package/src/cognition/senses/index.ts +172 -0
- package/src/cognition/senses/olfaction.engine.ts +24 -0
- package/src/cognition/senses/somatosensation.engine.ts +23 -0
- package/src/cognition/senses/vision.engine.ts +23 -0
- package/src/cognition/types.ts +63 -0
- package/src/cognition/utilities/token.tracker.ts +492 -0
- package/src/core/abstracts.ts +60 -0
- package/src/core/async.engine.ts +539 -0
- package/src/core/clock.ts +196 -0
- package/src/core/completion.recorder.ts +122 -0
- package/src/core/conflict.detector.ts +140 -0
- package/src/core/distributed.ts +626 -0
- package/src/core/event.bus.ts +292 -0
- package/src/core/inbound.recorder.ts +100 -0
- package/src/core/index.ts +167 -0
- package/src/core/logger.ts +85 -0
- package/src/core/metrics.ts +107 -0
- package/src/core/orchestrator.ts +625 -0
- package/src/core/replay.ts +589 -0
- package/src/core/scenario.ts +120 -0
- package/src/core/serialization.ts +448 -0
- package/src/core/simulation.ts +197 -0
- package/src/core/snapshot.manager.ts +293 -0
- package/src/core/state.manager.ts +283 -0
- package/src/core/types.ts +274 -0
- package/src/core/utils.ts +82 -0
- package/src/core/wall.clock.ts +23 -0
- package/src/extensions/livestream.ext.ts +570 -0
- package/src/extensions/time.ext.ts +339 -0
- package/src/index.ts +91 -0
- package/src/llm/gate.ts +140 -0
- package/src/llm/index.ts +716 -0
- package/src/llm/summarizer.ts +170 -0
- package/src/llm/wire.contracts.ts +67 -0
- package/src/pma/eval.ts +651 -0
- package/src/pma/index.ts +1082 -0
- package/src/profiles/built-in.ts +7 -0
- package/src/profiles/companion.ts +28 -0
- package/src/profiles/company-brain.ts +54 -0
- package/src/profiles/customer-service.ts +30 -0
- package/src/profiles/game-npc.ts +20 -0
- package/src/profiles/index.ts +42 -0
- package/src/profiles/smart-home.ts +34 -0
- package/src/runners/coherence.runner.ts +49 -0
- package/src/runners/outreach.runner.ts +158 -0
- package/src/runners/social.runner.ts +182 -0
- package/src/runners/thin-shim.runner.ts +195 -0
- package/src/sdk/will.ts +334 -0
- package/src/stem/assembly.audit.ts +88 -0
- package/src/stem/distribution.ts +203 -0
- package/src/stem/guards/identity.coherence.ts +152 -0
- package/src/stem/guards/identity.guard.ts +227 -0
- package/src/stem/index.ts +1145 -0
- package/src/stem/mind.ts +1090 -0
- package/src/stem/tracts/ack.reconciler.ts +55 -0
- package/src/stem/tracts/biography.writer.ts +190 -0
- package/src/stem/tracts/effector.controller.ts +143 -0
- package/src/stem/tracts/health.reporter.ts +113 -0
- package/src/stem/tracts/inbound.queue.ts +54 -0
- package/src/stem/tracts/outbox.controller.ts +167 -0
- package/src/stem/tracts/outbox.writer.ts +133 -0
- package/src/stem/tracts/pma.controller.ts +109 -0
- package/src/stem/tracts/replay.controller.ts +177 -0
- package/src/stem/tracts/sensory.controller.ts +168 -0
- package/src/stem/tracts/session.logger.ts +157 -0
- package/src/stem/tracts/transport/index.ts +38 -0
- package/src/stem/tracts/transport/loopback.transport.ts +86 -0
- package/src/stem/tracts/transport/socketio.transport.ts +200 -0
- package/src/stem/tracts/transport/stream.transport.ts +152 -0
- package/src/stem/tracts/transport/types.ts +193 -0
- package/src/stem/tracts/transport.controller.ts +380 -0
- package/src/types.ts +116 -0
|
@@ -0,0 +1,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
|
+
}
|