Corrects a common misconception: the 60s heartbeat must not call any LLM.
It runs the mood decay equation -- microseconds of float math. Giving it a
model is an architecture error, not a resource problem.
Real token spend lands in two places:
- per-turn input context (ontology + state + dyad + memory)
- background task frequency
So route by scene, not by time:
none rule/math tick, reach-out decision, fillers, avatar drive
low local small life events, memory gist, fact extract
standard mid + short speculative-response second segment
high flagship main dialogue, proactive message, crisis
Hard constraints:
- dialogue floor is LOW, never NONE (can get dumber, cannot lose lines)
- crisis never degrades (product constraint, not perf tuning)
- degradation is predictable: one rung per request, floor only at
the hard budget threshold
Cost levers, by actual payoff:
1. prompt caching (fixed prefix) -- by far the largest
2. upstream compression (small model condenses 20 memories to 3)
3. batching (one batch per 30min, not 20 single calls)
4. swapping to a smaller model -- smallest payoff, hurts persona
Also: cost degradation is narratable. 'Net error -> I zoned out' already
exists in the projection table, so being throttled is in-character and true.
- config.py: tiers / routing / budget, replacing llm+llm_fast+llm_background
- think/: scenes, budget, router, wiring
- models.yaml restructured around tiers
- SPEC gains 9.5
- 20 new tests; 78 total pass; ruff clean
Contracts (the shared language):
- percept / state / context / utterance / render / audio / capabilities
- Capability model instead of vendor schemas (see below)
Providers (capability-based abstraction):
- LLM + Embedding: OpenAI-compatible shape IS the standard, adopted directly
- ASR: internal contract is an event stream (partial -> final), because
/v1/audio/transcriptions has no partial and dialogue needs it for
endpointing. OpenAI adapter degrades to supports_partial=False.
- TTS: internal contract carries tags/emotion/style, because
/v1/audio/speech has no paralinguistics and [laughter]/[breath] are
the core of feeling alive. Unsupported providers are stripped AND
reported via Degradation -- silent degradation is forbidden.
- Avatar: no de-facto standard, custom RenderCmd protocol driven by State.
Core:
- mood: dynamical system, slow variables, negativity bias (grudge),
numbing after prolonged negativity
- bazi: four pillars -> numeric vector; 'ming' is data, not prose
- projection: phi, system event -> in-world statement; uncovered events
are counted as projection gaps
Storage:
- SQLite schema, three memory layers, dyad ledger
- accessibility floor 0.03 (not 0): forgettable but still there
Config split:
- models.yaml is the ONLY place model versions live
- character.yaml holds world rules / emotion params, no version numbers
58 tests pass; ruff clean.