Egbe Companions · Architecture

Clio: The Story Engine

A focused page within the Egbe Companions Architecture documentation. The full map and every sibling page live in the Architecture hub.

10sections11 minread1diagram1table

On this page

V6 — Egbe, the agentic-companion universe — runs on a wager that an agent's life is worth reading. The Moirai kernel keeps thousands of Ori thinking off-screen, the Ori service records every consequential thing each of them does as a durable event, and then a player logs off for a week. When they come back, the world has moved without them: a bond formed, a feud sharpened, an agent reached a Crossroads, one transcended. Someone has to turn that pile of machine-recorded life-events into something a human wants to read — a recap, a biography, a gentle nudge toward a story still forming — and to do it without ever lying about what happened. That someone is Clio, named for the muse of history. Clio is the story engine: it ranks the significance of life-events, assembles the returning-player Chronicle as voiced narrative beats inside a few-second budget, surfaces emergent arcs before they conclude, writes the long-form Book of the Ori that becomes a memorial at death or transcendence, and reconciles unorderable concurrency conflicts into a coherent life. Its one inviolable rule is inherited from the Ori service it reads: Clio never invents events — every word of narration is a read over the authoritative log. This page is the deep companion to the "The Clio Story Engine" section of ../V6_ARCHITECTURE.md.

What ships, honestly#

The story engine is real, deterministic, and tested as a TypeScript library; the model that polishes prose is an injected, fail-loud seam; and the Rust "service" is currently a thin capability shell, not the orchestrator the monolith sketches. Three honest layers:

  • Real and tested — the engine is the TS library. Every algorithm lives in libs/v6/clio-story/src/index.ts (1,701 lines, zero external dependencies): significance ranking (rankChronicleEventsBySignificance, :445), the Chronicle (createReturningPlayerChronicle, :487), emergent-arc surfacing (surfaceEmergentArcPrompts, :683), the Book of the Ori (generateBookOfTheOri, :722), narrative reconciliation (reconcileOriConflictNarrative, :764), and Tier-2 density backfill (createClioDensityBackfillSummary, :819). These are pure functions over typed event inputs with domain-specific scoring formulas and stable ordering, exercised by 12 Vitest cases (index.spec.ts, 9; clio-narration.spec.ts, 3) that assert computed outcomes — rank order, a ≤ 3,500 ms budget, newest-significant-first streaming, chapter titles, keepsake render refs — not mere shape.
  • The generation seam is injected and fail-loud, never faked. The base Chronicle produces deterministic, grounded template prose. The model-narrated path (createReturningPlayerChronicleNarrated, :636) takes an injected ClioNarrativeWriter (:605) — the V6 mount supplies Iris — and replaces each beat's prose with the model's. It then re-checks the model against the log: a writer that cites an event outside its beat's Ori-log slice throws ClioNarrationFabricationError (:614), and an empty narrative throws too (:665). The LLM may speak the life; it cannot invent one, and it cannot fake success. With no writer injected, the deterministic prose stands.
  • The Rust service is a thin shell — honestly less than the monolith implies. The monolith bills apps/v6/egbe-clio-service/ as a "Rust orchestrator + batched LLM summarization." What is committed is a 68-line HTTP health endpoint (src/lib.rs) that binds port 46106, answers GET /health with a capability list, and 404s everything else; its Cargo.toml depends only on egbe-protocol, so it does not — and cannot — call the TypeScript engine. The orchestration and summarization logic genuinely lives in @oshun/clio-story; the Rust binary is today a service-discovery/health surface advertising the Clio capabilities, not the compute path.

One engine, two faces#

Clio is split the way most V6 subsystems are: a Rust process boundary for the fleet to discover and health-check, and a TypeScript library where the real work happens. The split is honest about where intelligence lives.

The Rust shell (apps/v6/egbe-clio-service/src/lib.rs:6) is a ServiceDescriptorname: "egbe-clio-service", port: 46106, and a capabilities array (chronicle-generation, story-beat-ranking, batched-narrative-beat-generation, book-of-the-ori, narrative-reconciliation, …). health_document() renders it through the shared health_json formatter (libs/v6/egbe-protocol/rust/src/lib.rs:1241), exactly as every Egbe service does, so a fleet supervisor sees Clio as {"service": "egbe-clio-service","status":"ok","port":46106,...}. That is the entire Rust surface, and its one unit test only asserts the health document names the service.

The library carries a V6PackageDescriptor (index.ts:22) — authority 'story' — enumerating 20 capabilities from significance-ranking through tier2-density-summary-backfill, each backed by an exported function and a test. clioStoryHasCapability (:441) is the typed predicate the verifiers and the package's own descriptor test exercise. Everything below is in that one file.

Significance ranking: the rubric that decides what matters#

Nothing in Clio is interesting until it can answer "of everything that happened, what is worth telling first?" That is rankChronicleEventsBySignificance (index.ts:445). It scores each ClioSignificanceCandidate — a projection of one Ori event — against a curated, versioned rubric (v6.clio.significance-rubric.1) whose five weights are real constants (:449):

Factor Weight
Event type 0.30
Emotional weight 0.22
Arc relevance 0.20
Relationship impact 0.16
Steward relevance 0.12

Event type is not a free input: eventTypeSignificance (:1581) maps the 17-value life-event taxonomy to a fixed scale, and the ranking is exactly the V6 ending hierarchy made numeric — Died 100, Transcended 96, Departed 92, Crossroads 88, RelationshipChanged 82, … down to Discovered 44 and Born 40. The other four factors are clamped 0–100, the weighted sum is rounded to a totalScore, and compareRankedChronicleEvents (:917) breaks ties first by recency, then by eventRef for total determinism. The top N (default 3) become chronicleLeadEventRefs. This single function is the engine's spine: the Chronicle leads with it, the stream prioritizes by it, and emergent-arc surfacing borrows its clamping discipline. The test at index.spec.ts:41 pins the produced order and the lead/non-lead split against known inputs, so a regression to random or constant scoring would fail.

How narrative emerges from agent lives#

The returning-player Chronicle (createReturningPlayerChronicle, index.ts:487) is where ranked events become a reading. The pipeline is mechanical and auditable:

flowchart TB subgraph ori["Ori service — the MEMORY (upstream)"] LOG[("event-sourced agent biographies<br/>durable life-events")] end subgraph clio["Clio — @oshun/clio-story (the engine)"] RANK["rankChronicleEventsBySignificance<br/><sub>curated 5-factor rubric</sub>"] GROUP["groupRankedEventsForChronicle<br/><sub>by (agent, arc-thread), ≤6/batch</sub>"] BATCH["createChronicleBatch<br/><sub>estimatedGenerationMs · promptContract</sub>"] BUDGET{"isLongChronicleAbsence?<br/><sub>>7 game-days · >18 events · over 3.5s</sub>"} READY["status: ready<br/>parallel batched beats"] STREAM["status: streaming<br/>newest-significant-first chunks"] BEAT["createChronicleBeat → ClioChronicleBeat<br/><sub>narrative in agent voice</sub>"] end WRITER(["ClioNarrativeWriter — Iris (injected)"]) -.->|"narrate(beat); cites outside log ⇒ throw"| BEAT LOG -->|"ClioSignificanceCandidate[]"| RANK RANK --> GROUP --> BATCH --> BUDGET BUDGET -->|"typical absence"| READY BUDGET -->|"long absence"| STREAM READY --> BEAT STREAM --> BEAT BEAT --> READER[/"steward client — Chronicle recap"/]

Grouping by life-thread. groupRankedEventsForChronicle (:938) keys events by (agentRef, primary arcThreadRef) so a beat is always one agent carrying one thread — never a jumble across agents — and chunks each group at CHRONICLE_MAX_EVENTS_PER_BATCH = 6 (:56). Each ClioChronicleBatch records a promptContract: 'read-over-ori-log-no-invention' (:1017): the contract is in the data, not just the prose.

The few-second budget. Clio's summarization is the one latency-sensitive thing it does, so it models its own cost. Each batch's estimatedGenerationMs is CHRONICLE_MODEL_BATCH_BASE_MS (420) + events × CHRONICLE_EVENT_SYNTHESIS_MS (24) (:1014), and because batches generate in parallel, estimateReadyMs (:1137) is CHRONICLE_BATCH_OVERHEAD_MS (220) + max(batch ms), compared against CHRONICLE_READY_BUDGET_MS = 3_500 (:50). isLongChronicleAbsence (:1118) flips the Chronicle from ready to streaming when the absence exceeds 7 game-days, the event count exceeds 18, or the estimate blows the budget. The budget test (index.spec.ts:154) asserts estimatedReadyMs ≤ 3500; the streaming test (:294) asserts long absences come back status: 'streaming' with the stream ordered newest-significant-first and the first chunk readable within budget. This is the same cost discipline Moirai applies fleet-wide, made local to one read.

Beats in the agent's voice. createChronicleBeat (:1021) emits a ClioChronicleBeat whose narrative weaves the grouped event summaries with the agent's display name and arc-thread title, marks the lead beats, and carries a voiceContext derived from the agent's voice summary — so the recap reads as this agent's story, not a log dump.

The no-invention covenant#

The base beat prose is deterministic template assembly — safe, but flat. The real prose pass is createReturningPlayerChronicleNarrated (:636). It keeps ranking, budget, batching, and streaming exactly as produced, and replaces only each beat's narrative with output from the injected ClioNarrativeWriter (:605), handing the writer only that beat's Ori-log slice (logEvents, built from sourceEventRefs, :647). Then it audits the model: every citedEventRef the writer returns must be in the allowed set, or ClioNarrationFabricationError (:614) is thrown naming the fabricated refs (:662); an all-whitespace narrative throws as well (:665). The evidence tags swap from read-over-ori-log-no-invention to model-narrated-grounded-over-ori-log + fabrication-checked-against-ori-log (:675). clio-narration.spec.ts proves all three branches: a faithful writer is woven in, a writer that plants event:planted:not-in-log is rejected (:89), and an empty writer fails loud (:108). This is V6's local copy of the fair-play covenant V8's writers' room enforces against its mystery proof — the model speaks, it does not author.

Citation membership is only the first check. The required ClioNarrativeEntailmentVerifier extracts every factual claim, binds its receipt to the SHA-256 of the exact trimmed narrative, anchors every extracted claim as a verbatim span of that narrative, and assesses it against bounded Ori-log passages. Missing or stale hashes, incomplete extraction, unanchored claims, unknown passages, or any non-entailing claim throw ClioNarrationEntailmentError; released beats carry semantic-claim-entailment-checked-over-ori-log and their complete semantic proof bundle.

Emergent-arc surfacing: stories worth a glance#

The Chronicle is retrospective; surfaceEmergentArcPrompts (index.ts:683) is prospective. It reads the live event stream for ClioEmergentArcSignals of four kinds — escalating-feud, forming-romance, struggling-agent, fitting-wild-agent — and raises the worthwhile ones as gentle prompts before they conclude. scoreEmergentArcSignal (:1465) is genuinely kind-specific, not one formula renamed: a feud weights relationship tension 0.32 and conclusion-risk 0.20; a romance weights affinity 0.36 and inverts tension ((100 - tension) × 0.10); a struggling agent weights welfare risk 0.42; a wild-agent fit weights household fit 0.40. Signals below minScore (default 65) or already concluded are dropped (:1433); the rest are sorted, capped (default 3), and rendered. Every prompt is typed tone: 'gentle', requiresImmediateAction: false, beforeConclusion: true (:271), and the copy is non-demanding by construction (gentleEmergentArcPromptText, :1521 — "A quiet check-in could help if you choose"). Suppressed signals are returned by ref so the surfacing is auditable. The test (index.spec.ts:632) asserts the kinds that surface, the priority order, and that every prompt is gentle — surfacing a demanding or post-conclusion prompt would fail.

The Book of the Ori and the legacy edition#

generateBookOfTheOri (index.ts:722) is the long-form biography — a continuous read over an agent's whole event log. Events are sorted ascending, chunked into chapters (default 4 events each), and titled by bookOfOriChapterTitle (:1211): a chapter containing a Died/Transcended event is "Legacy", an opening chapter with Born/Discovered is "Beginnings", otherwise the dominant arc-thread names it. Coverage is explicit — coverage.complete is true only when every covered ref equals every input ref (:744) — so the biography provably omits nothing. The test (:363) asserts the chapter sequence "Beginnings" → … → "Legacy", that the prose contains specific events, and that coverage is total.

When a life ends, createBookOfOriKeepsake (:1266) detects the terminal event and emits a ClioBookOfOriYemayaKeepsakeservice: 'Yemaya', editionFormat: 'book-of-ori-keepsake-edition', trigger of 'transcendence' or 'death', an idempotency key, and status: 'requested'. Note the honest seam: Clio does not render the keepsake — it emits a render request to Yemaya (clio --> yemaya in the monolith topology). The library produces the significance-ranked biography and the request; the media pipeline fulfills it. The test (:509) pins the death-trigger keepsake's shape and render ref.

Narrative reconciliation: two truths, one coherent life#

V6's event store accepts commutative concurrent writes, but some pairs are genuinely unorderable — two accounts of the same moment that both land. The Ori service hands those to Clio, and reconcileOriConflictNarrative (index.ts:764) resolves them narratively rather than by deleting either. It writes a small connective beat (createNarrativeReconciliationBeat, :1295) — itself an event of type 'Reflected', with promptContract: 'small-connective-beat-no-invention' — that bridges the conflicting accounts ("connects concurrent accounts … so the biography keeps both events readable without replacing either", :1332). It folds that beat plus both conflicting events into a merged Book of the Ori, records an auditable ClioNarrativeReconciliationLogEntry (:1392, auditable: true), and computes a coherence struct (:1409) that is coherent only if the connective beat is included, both conflicting events are covered, and the merged biography's coverage is complete. The reconciliation is the one place Clio writes a new event — and it logs that it did. The test (:517) asserts the connective beat's timestamp falls just after the conflict, both events survive in the merged biography, and the bridge is logged.

Tier-2 density backfill: keeping culled agents in the story#

When a constrained device hits its embodiment cap, the world server stops rendering distant agents — but they must not vanish from the narrative. createClioDensityBackfillSummary (index.ts:819) takes the culled set and emits a tier2-density-summary-backfill: it orders agents by cognition tier (tierWeight, :1631 — Clotho 3 > Lachesis 2 > Atropos 1) then distance, names the three representatives, and writes a headline plus beats that preserve goal and relationship counts "behind the cap" so a later rehydration is believable. It is a real summarization with a stable ordering and an empty-set branch, not a placeholder string.

Verification and the readiness gate#

Clio carries five dedicated CI verifiers — verify:v6 clio-significance, clio-chronicle, clio-emergent-arcs, clio-book-of-ori, and clio-narrative-reconciliation (scripts/v6/verify-v6-clio-*.mjs). Read honestly, these are source-presence and substring gates: e.g. verify-v6-clio-chronicle.mjs asserts the engine source contains createReturningPlayerChronicle, CHRONICLE_READY_BUDGET_MS = 3_500, the streaming evidence tags, and the named test cases — they guard against the symbols and contracts being deleted or renamed, while the behavioral correctness is owned by the Vitest specs they require to exist. The five roll up into the launch-blocking aggregate verify:v6 fate-legacy-clio-readiness, backed by V6/release/fate-legacy-clio-readiness.v6release.json (status: green, requiredBeforeLaunch: true, covering TODO sections 24–26). Treat the verifiers as drift gates and the specs as the proof of behavior.

Where this connects#

  • Upstream — the memory Clio reads. Clio narrates over the Ori service's event-sourced biographies (clio -->|reads event log| ori); the ClioSignificanceEventType taxonomy and the unorderable-conflict handoff both originate there. See Ori: the biography service.
  • Sibling — how the agents speak. The Chronicle's voiced beats and the gentle arc prompts reach the steward through the conversation surface; the intent/voice pipeline is Vac: the communication pipeline.
  • Sibling — where the lives end. Death and transcendence trigger Clio's Book of the Ori and the Yemaya keepsake; the ending lifecycle and the governed foundry that begins lives are The Foundry: endings and legacy.
  • Cost — what the summarization is allowed to spend. Clio's batched beat generation runs as lower-tier model work under the same cost discipline its own 3.5 s budget mirrors; the fleet-wide scheduler is The Moirai kernel and cost tiering.