MeridiansMeridians

The two systems

Source path: knowledge-base/knowledge/conventions/ai-models/00-two-systems.md

# The two systems

Every model call in the engine is one of two kinds of thinking, and the boundary it crosses declares
which. The split is the first question to ask of any new call — before category, before tier.

| | **System 1 — decide** | **System 2 — generate** |
|---|---|---|
| What it produces | A typed, calibrated answer: a **choice** among named options, a **score** on ordered levels, or a **noul** (probability a statement is true) — each with a probability distribution and confidence. A Score's `score` is the probability-weighted expected level index and may be fractional. | Text and structured artifacts: prose, plans, graphs, JSON payloads, chat turns. |
| Boundary | `callDecide(state, questions, caller, decisionModelFor(category))` — `src/lib/engine/ai/infra/decide.ts` | `callGenerate*(prompt, system, maxTokens, caller, modelFor(category), reasoningBudget, …)` — `src/lib/engine/ai/infra/api.ts` |
| Provider | OpenRouter Decisions (`/api/alpha/decisions`), model `DECISION_MODEL` (`~typesafe/jev-latest`), one model for the whole system. | OpenRouter chat completions on the tiered fleet (`TIER_MODEL`, allocated per category by the active profile). |
| Shape of a call | One `state` string + a **batch** of questions, evaluated together in one round trip. No output tokens, no reasoning budget, no temperature. | One prompt, one generated response (optionally streamed), sized by `MAX_TOKENS_*`, with an explicit reasoning budget. |
| Latency / cost | Sub-second; input-only pricing (`$0.042`/M, output free). | Seconds to minutes; input + output token pricing per tier. |
| Failure mode | Schema is guaranteed by the provider; the boundary still rejects an envelope that is incomplete, mistyped, or out of range — never a partial answer set. | Free text may be malformed or truncated; parse once, validate, fail loudly. |
| Authority | Proposes only. Every answer passes a legality/authority gate (`legalizeAgentPlays`, a reducer, a Director) before anything canonical moves. | Proposes only. Same gates apply. |

## The rule for choosing

Ask what the output **is**:

- If the answer is one of a known set, a level on a known ladder, or a probability that something
  holds — and the prose around it is not the product — it is **System 1**. Route it through `callDecide`.
  Bounded action selection, routing, classification, eligibility, feasibility, conflict existence,
  closure, and distributions over known outcomes are all System 1.
- If the words are the product — narration, scene prose, plans whose content is read, chat, names,
  descriptions, queries, explanations the user reads — it is **System 2**.
- A call that would need both is **two calls**, never one: decide first, generate only what the decision
  says is needed (and only if the prose is actually wanted). Mixing a decision into a prose prompt dilutes
  both — the decision becomes an invented number inside a story and the prose becomes a rationale for it.
  The agent seat is the reference case: cards, stake, concealment, and travel are one `callDecide`; table
  talk is a separate post-reveal generation (`generateTableReactions`), not a field on the decision.

Why the split matters beyond speed and cost: System 1 answers carry **calibration**. A probability
vector over known outcomes can be checked against what later happened (settled Scenario ledgers, resolved
positions, extracted records), which makes the decision layer measurable and improvable in a way prose
never is. That is the honest reading of "calibrated": vendor-claimed until Meridians has checked it on its
own resolved outcomes.

### Context budget

Jev has a 32k-token context window. `callDecide` preflights each request with
`assertModelContextFits`; complete evidence enters the boundary, and if it exceeds the window a single
System 2 compaction call condenses prose while preserving every item and id, with the reduction logged.
Batching remains preferred; truncation by callers is never allowed. Jev uses a calibrated estimate of
2.5 characters per token rather than the generic four: Alice Run 7 reported 51,618 provider prompt tokens
for a decision locally estimated at 31,603 with the generic ratio, a 1.63× underestimate.

State construction is complete evidence: callers never slice or summarise it to fit. Judgment lives in
the question, instructions never sit in the state, and subjects are quoted in full. The boundary may
compact prose when necessary, but the only caller-side guard is the loud `assertModelContextFits`
failure plus the 80% headroom warning; batching remains preferred for states that do not fit.

## How the two systems meet the three layers

The system is **orthogonal** to tier, category, and profile:

- **Category** still names the work (`action`, `predict`, `extract`, …) and every call — either system —
  states one. A category may carry calls of both systems (`action` today: the agent seat decides via
  System 1; other live-move surfaces still generate). Do not create categories per system.
- **Tier / profile** allocate capability to **System 2 only**. System 1 has one model today
  (`decisionModelFor(category)` returns `DECISION_MODEL` for every category); the category argument is kept
  so attribution, the generated call ledger, and a later per-category decision allocation need no call-site
  change.
- **Reasoning budget** is a System 2 lever. System 1 has none — a decision that needs deliberation is
  either mis-shaped (split it) or needs a richer `state`, not more thinking tokens.

## Where the seam lives

```text
System 1                                        System 2
core/ai/decision.ts        pure types, parse    —
engine/ai/infra/decide.ts  callDecide           engine/ai/infra/api.ts      callGenerate*
engine/ai/decide-service   executeDecide        engine/ai/generate-service  executeGenerate*
engine/ai/infra/openrouter openrouterDecide     engine/ai/infra/openrouter  openrouterChat
app/api/ai/decide          route                app/api/ai/generate         route
AiRuntime.decide           headless seam        AiRuntime.generate          headless seam
```

Both boundaries stamp the same `ApiLogEntry`, price through `MODEL_PRICING`, classify into the `llm` spend
lane, and appear in the generated [model-call ledger](../../../generated/MODEL_CALLS.md), which lists every
call by system and rejects a `decisionModelFor` or `modelFor` lookup that does not reach its boundary.

## Migration ledger

System 1 is adopted call by call, each as its own non-mixed change. The audit of decision-shaped calls
and the order they are taken in:

| Order | Surface | Primitives | Status |
|---|---|---|---|
| 1 | Scenario agent seat (`decideAgentPlays`) | choice · score · noul · choice | **System 1** |
| 2 | Fate scoring (`scoreThreadsWithAI`: realized probabilities, seat drive) | choice-as-distribution · score | **System 1**; explanation is a separate System 2 generation |
| 3 | Program watch checks (`scoreProjectionArticleCheck`, `…WindowCheck`) | score · choice | **System 1**; log line is a separate System 2 generation |
| 4 | Fate-search priors (`priceFutures` / `pricePresent` over generated coordinations) | score | **System 1**; candidates remain System 2 generation |
| 5 | Survey closed cells | choice · score · noul | **System 1**; open and estimate cells remain System 2 generation |
| 6 | Conflict settlement outcome / closure (`resolveConflictRealism`, `detectConflicts`) | choice · noul | **System 1**; telling stays System 2 prose |
| 7 | Chat / constellation routing, coordination preset, map-scale classification, attachment relevance | choice · score · noul | **System 1**; `suggestTableCast` stays System 2: unbounded roster |
| 8 | Streams (`instantiateStream`, `generateSeatStream`, `scoreStreamPrior`) | choice · score · noul | **System 1** pricing for landing distributions, log type, horizon, action availability, travel, and evidence; outcome/question prose stays System 2 |
| 9 | Topic filing, location parents, findings routing, prose / plan quality verdicts | choice · noul | **System 1**; new-topic proposals, observations, critiques, overall analysis, and patterns stay System 2. `reviewBranch` stays System 2 because merge/insert/move carry targets and new content. |
| 10 | System invocations, detected paradigm, scene count, research loop/reminder shape | noul · choice · score | **System 1**; attribution edges, pattern prose, scene directions, names/sites/topics/labels/prompts stay System 2. |
| 11 | Extraction reconciliation (`reconcileEntities`, `reconcileSemantic`, `reconcileThemeTypes`) | choice | **System 1** for entity identity, open-thread identity, repeated System rules/facts, repeated Theme propositions, and Theme Type territory. System 2 still proposes the source entities and propositions; System 1 verifies bounded folds. |
| 12 | Causality flag verification (`verifyCausality`) | noul | **System 1** verifies turning-point admission and claimed Fate settlement after System 2 cites candidate flags. The fixed scene-to-scene wuxing ring prior was rejected and removed; coupling is measured per record, not presumed. |

Calls whose parsed output is prose or open structure (`generateDomain`, `expandWorld`, `generateScenes`,
scene writers, `chatCompact`, chat turns, newsletter, notes, suggestions, `generateTableReactions`) stay
System 2 by definition; a JSON return shape alone does not make a call a decision.

Seat-stream `idleReason` remains a speculative System 2 field and is surfaced only when the typed
`canAct` decision says the seat sits out; a separate third call is not worth the latency and cost.
Canonical feasible travel options are passed as structured choices from the scenario advance callers.
Standalone stream operations lack room/economy context, so they omit the travel question rather than
extracting destination ids from rendered spatial prose.

The following remain System 2 because the product is open structure or text, or because a bounded field
determines the shape of the same generation and splitting it would require another generation: `generateSurveyProposal`
and `generateSurveyBatch` (question type determines the choice options or likert scale the call must also
write); `refineArticle.tags` (open vocabulary); `reorganizeCurriculum`, `treeSkeleton`,
`suggestConstellationGraph`, `suggestOnboardingArchitecture`, `suggestPublicResearchFlow`,
`groupScenesIntoArcs`, extraction structure phases, `fileConversion`, all `plan`-category graph and
scene-plan generators, `generateSceneGameAnalysis`, `generateRoundGameTheoryAnalysis`, and
`craftEditorial` (the structure or text is the product and option sets are unbounded).

The generated model-call ledger is the current executable inventory; this table records the migration
principle and intentionally retained System 2 boundaries.
Open on GitHub

Raw Markdown source