V8's architectural thesis splits a mystery into two layers: a symbolic
skeleton (ground truth, clue logic, a uniqueness proof) that must be
machine-verifiable, and an experienced surface (prose, dialogue, art, voice,
living suspects) that agents realize from that skeleton. The symbolic core —
Clew, Minos, Palimpsest — hands this layer a case it has already solved: a
MysterySession IR whose culprit Minos proved is the unique model of the
player-available clues, plus the hidden CaseGroundTruth it was derived from.
This page is about the second layer — turning that proven skeleton into a
playable, interrogatable mystery without ever letting the surface contradict
the proof.
Three subsystems do the realization, and all three obey one inherited rule: the
LLM may speak the case but never invent it — no new suspect, no new
location, no number, and no lie the proof did not author. Anansesem
(libs/yemaya/case-writers-room) is the writers' room that realizes scenes,
briefings, character sheets, partner banter, and the static interrogation trees.
Loom (libs/yemaya/case-assets) is the asset fabric that binds the case to
generated environments, portraits, evidence props, music, and voice-over.
Ori (libs/yemaya/case-suspects) is the runtime layer that turns each
suspect into a memory-bearing, interrogatable mind. The orchestration and
contracts are real and deterministic; the generation — LLM prose polish, Isis
media, ACE/Inworld avatars — is provider-gated behind honest fail-loud /
fail-closed seams. This is the surface companion to the
symbolic-core page; the section hub
is ../V8_ARCHITECTURE.md.
What ships, honestly#
The realization layer is real and exercised end-to-end, with the generative pieces sitting behind typed seams. Three tiers:
Implemented, deterministic, and tested (real today). The Loom service runs
the whole mint→publish pipeline with zero external credentials:
runPipeline (apps/v8/loom-service/src/pipeline.ts:127) threads Clew → verify
→ Anansesem → assets → compile → eval → safety → release, and the §12.4
acceptance suite mints a case, passes all eight ReleaseDecision gates,
compiles a real V5 pack through the validate-cold-cases.py build gate, and
reproduces bit-for-bit from the seed
(apps/v8/loom-service/src/pipeline.test.ts, 5/5 green). With no LLM injected,
Anansesem emits a complete, grounded, deterministic surface; Loom's planner,
retrieval ranker, budget-degrader, voice caster, and MetaHuman mapper are real
algorithms with their own unit tests (libs/yemaya/case-assets/src/*.test.ts);
and Ori's memory stream, fair-lying guard, and offline voice are real and
reproducible (libs/yemaya/case-suspects/src/*.test.ts).
Provider-gated injected seams (fail-loud / fail-closed, not faked). The
actual generation is injected, never fabricated. Anansesem's prose enricher is a
CompletionFn (types.ts:30) — absent, the deterministic base text stands;
present, a provider error propagates. Loom's minter wants an Isis JobRunner
(orchestrate.ts:52) — absent, with real gaps to fill, realizeCase throws
rather than ship a half-empty pack. Ori's live avatar broker reads
OSHUN_INWORLD_* / OSHUN_NVIDIA_ACE_* creds (runtime.ts:49) — absent, it
reports { configured: false } and fails closed to the deterministic
offline interrogation, which is itself fully playable and fair-play-correct.
Honest scope notes. The shipped §12.4 acceptance pack is text-only: the
offline deps (deps.ts) wire no Isis runner, so no media is minted — Loom's
fabric is real and unit-tested but not exercised by that run. And Ori is the
runtime layer (Stage 9–10): it is driven by the relay/host, not by the offline
mint, which bakes the static 12-beat trees Anansesem authored.
The realization boundary#
Anansesem and Loom both read the same verified skeleton and write disjoint slices of the pack; Ori re-projects the skeleton at runtime so a live interrogation can never drift out of solvability.
Anansesem — the writers' room#
Anansesɛm (an Akan word for the Anansi storytelling tradition) realizes the
narrative surface. It "only speaks the case" — solvability is owned by Clew
and proven by Minos, and nothing here re-derives mystery logic
(case-writers-room/src/types.ts:8). The public entry is realizeCase
(dag.ts:244): it takes a WritersRoomInput (the session, the ground truth,
the canon slice) and an optional CompletionFn, and returns a RealizedCase.
The realization DAG#
The room is a genuine directed graph built on an in-house StateGraph (V8 ships
its own rather than importing the broken agents-orchestration barrel —
dag.ts:2). A Showrunner sets the brief (title, era voice, grounding
vocabulary) and fans the work out to five specialist writers — StoryDirector
(the briefing), CharacterWriter (a sheet per cast member),
DialogueWriter (an interrogation tree per suspect), Cinematographer
(scene descriptions + shot notes), SoundDesigner (partner banter + audio
cues) — whose disjoint slices an Integrator assembles (dag.ts:96). Because
the five slices never read one another, order is immaterial; the graph is
compiled and actually invoked (graph.compile().invoke(state)), not narrated.
The graph id is literally anansesem-writers-room (dag.ts:97).
Grounding: the closed vocabulary#
The hard guarantee is in grounding.ts. From the ground truth + canon, the
Showrunner builds a GroundedVocabulary (grounding.ts:58): the closed set of
people (victim + suspects + canon characters), location ids and display names,
and the weapon. assertGrounded (grounding.ts:241) is a real proper-noun
extractor, not a shape check: it walks every realized string, lifts runs of two
or more consecutive capitalized, name-shaped words (extractProperNouns,
grounding.ts:182) — the shape that could smuggle in an invented "Cornelius
Blackwood" — strips genitives, excludes function words and sentence-initial
verbs, and reports any token resolving to no grounded entity as a
GroundingViolation. The room cannot name a suspect or place the skeleton
didn't author.
Interrogation: twelve tells, one fair lie#
buildInterrogation (interrogation.ts:123) aligns each suspect to V5's
twelve facial-tell beats (TELL_BEATS, interrogation.ts:26) and the Truth
/ Doubt / Lie mechanic, with invariants that tie the surface back to the proof:
- Every tree has exactly 12 beats; beat 7 is the alibi beat, beat 9 the pressure beat.
- The culprit lies exactly once — their alibi (beat 7) is a
LiewhoserefutedByClueIdis the alibi-flaw clue (alibiFlawClue,interrogation.ts:67); the builder throws if the culprit has no flaw clue in the session (interrogation.ts:146). - An innocent's alibi is Truth (verified, with grounded corroborator names)
or Doubt (unverifiable) — never a Lie about the alibi. A red-herring
innocent may tell one deflatable Lie tied to a fair herring clue
(
herringClueFor), so the "every Lie carries a refuting clue" invariant (InterrogationLine.refutedByClueId,types.ts:139) always holds.
Stress tells escalate with veracity (a Lie under pressure plays gaze-aversion
/ swallow / jaw-clench; Truth plays calm — tellFor,
interrogation.ts:98), and the prose for non-decisive beats is deterministic
and grounded in the real victim name, weapon, and place.
The enrichment seam (fail-loud)#
Every node's prose passes through enrichLine (enrich.ts:32). With no
CompletionFn it returns the deterministic base unchanged. With one, it asks
the model to rewrite the line in register — and then re-grounds the result:
withinBoundary (enrich.ts:69) rejects any rewrite that introduces a
name-shaped token outside the allowed proper-noun set, falling back to the
deterministic base (fail-safe on content). A provider error, by contrast,
propagates uncaught (enrich.ts:39) — the room never swallows an outage into
silently-degraded prose. The model can polish; it cannot invent a suspect, and
it cannot fake success.
Loom — the asset-realization fabric#
Loom binds a case to multimedia through the existing Isis client. Its entry,
realizeCase (case-assets/src/realize.ts:62), runs the fabric end to end:
plan → retrieve → degrade-to-budget → mint the surviving gaps → bind + assemble,
returning a contract-valid AssetManifest.
Planning: counts derived, never literal#
planAssets (plan.ts:166) computes exactly which assets a case needs from
its structure — every count derived, none a fixed literal: one environment
per distinct story location (de-duped by slug), one portrait per suspect
plus the victim, one 3D prop per physical/forensic clue and one
document/photo per documentary/testimony clue (classifyClueModality,
plan.ts:138), case music plus one sting per revelation/climax plot
point, one voice-over line per realized dialogue line (or a
VO-minute-budget-derived set, deriveDialogueBudget), and optional
flashback/CCTV video per timeline beat at the crime scene. Each
PlannedAsset carries semantic tags (cell, era, subject, clue type) and a
boundTo target so the binder can wire the produced url back into the exact V5
evidence node.
Retrieve-then-generate#
Before minting anything, retrieveThenGenerate (retrieve.ts:67) asks an
injected AssetLibrary whether an existing asset already satisfies a plan entry
— reuse beats mint when the world already has the right thing. The default
InMemoryTagAssetLibrary is a real ranker (retrieve.ts:123): a
specificity-weighted tag overlap (identity tags like location:/subject:/
clue: weighted 4×, shared context tags like cell:/era:/theme: weighted
1×, retrieve.ts:175) blended 0.8/0.2 with a description-token Jaccard. The
weighting is the point: a wrong-location environment that shares all the case
boilerplate still falls below the 0.62 reuse threshold
(DEFAULT_REUSE_THRESHOLD) and gets minted bespoke. It is computed retrieval,
never always-reuse and never always-mint; the same surface can be backed by
Isis's CLIP semantic index through the isis-asset-library.ts adapter.
Budget, voice, and MetaHuman#
degradeToBudget (budget-degrade.ts:87) reserves asset-jobs from the
CaseBudget and, when the ceiling is hit, degrades rather than fails in a
real priority order — environments and portraits first, evidence next, optional
flashback video dropped first (mintPriority, budget-degrade.ts:46) — logging
every drop so the plan is never silently truncated. castVoice (voices.ts:85)
assigns each character one stable ElevenLabs-style voice id by reducing a
SHA-256 of the key over a register-partitioned pool (DEFAULT_VOICE_POOL of 12
distinct voices), so the same character sounds the same across every case they
appear in. assembleMetaHuman (metahuman.ts:87) maps a portrait + persona
tags to MetaHuman Creator's real axes (skin-tone ramp, age 18–80, hair, facial
hair, body type, age-scaled detail overlays) deterministically from the
character id.
Minting through Isis (fail-loud)#
The mint step is where Loom refuses to fabricate. mintGaps
(orchestrate.ts:208) builds a real JobRequest per gap — the right generation
type, a prompt assembled from the case with period-correct dimensions and an
anachronism negative-prompt, and an idempotency key derived from a per-asset
seed — and submits it through a JobRunner structurally satisfied by
@isis/client. Any runner error, a job-level error, or a job that returned no
output file is raised as a MintingError (orchestrate.ts:67); it never
invents a result url. And realizeCase throws if real gaps remain with no
runner wired (realize.ts:80) — an absent backend is surfaced, not papered
over.
Ori — the living suspects#
Ori (Yoruba for the "inner head," the seat of identity) is the runtime that
makes a suspect interrogatable rather than a fixed dialogue tree. Each suspect
carries a SuspectOri (ori.ts:106) seeded deterministically from
caseId + suspectId: a total projection of the hidden ground truth — what
they KNOW (their movements, who they saw, their alibi), what they HIDE (the
culprit conceals the deed behind a flawed alibi; innocents may hide an
unrelated, harmless secret), and the set of fair lies they may tell. Because
the Ori invents nothing the symbolic layer didn't author, the runtime can never
drift out of solvability. Persona axes are derived, not random: the culprit is
measurably more evasive and more stressed under questioning than a cleared
innocent (ori.ts:216).
The memory stream (Generative Agents)#
MemoryStream (memory.ts:89) implements the Park et al. Generative Agents
retrieval: every memory carries an importance/poignancy, and retrieval scores
candidates by recency · importance · relevance (an 8-turn recency half-life, a
Jaccard relevance term, memory.ts:149). This is what keeps re-interrogation
consistent across V5's multiple passes — the suspect cannot say "I was home"
then "I was at the club," because the prior statement is retrieved before the
next answer. Once accumulated poignancy crosses a threshold (2.0, tuned so two
firm accusations plus a presented clue trip it but idle small-talk does not),
maybeReflect (memory.ts:174) synthesizes a higher-level belief ("the
detective has hard evidence and is closing in"), and plan (memory.ts:207)
chooses the stance to hold next turn — cooperative → guarded → deflecting →
rattled → cracking — from accumulated pressure.
The fair-lying guard#
fair-lying.ts is the runtime half of the fair-play covenant Minos enforces at
authoring time. canLie (fair-lying.ts:105) returns false for any lie that
no player-reachable clue can refute: it indexes the session's clues by the
ground-truth fact each establishes (indexClues, via each clue's
metadata.groundTruthFactId), and a lie is permitted only if a reachable clue
proves its refutingFactId. An Ori may never assert an "authorial cheat"
delivered through dialogue. crackUnderEvidence (fair-lying.ts:149) is its
mirror: the culprit cracks only when the player presents the specific clue
that establishes the fact refuting their load-bearing alibi lie — an
irrelevant clue raises pressure but does not break them, and an innocent (who
told the truth) never cracks. The confession is grounded in the suspect's own
ground truth, not improvised.
The fail-closed broker#
FailClosedBroker (runtime.ts:174) is the §6.5 seam to a managed avatar
service. It reads ACE/Inworld credentials once (credentialsFromEnv,
runtime.ts:49) and reports an honest status() of { configured, mode }; it
is "live" only when creds and a real live broker implementation both exist
(runtime.ts:197). With neither, it fails closed to an OfflineInterrogation —
the memory stream, the guard, and the deterministic OfflineDialogueBackend
(dialogue-backend.ts:104), whose persona-driven voice is a pure function of
the Ori, the question, the stance, and whether a lie was authorized. A live LLM
is adapted in through hathorBackendFrom (hathor-adapter.ts:70), which builds
a grounded persona and propagates any provider error (fail-loud). The host
treats both modes through one send surface, so the game stays playable and
fair-play-correct even with no cloud avatar — the same
fail-loud-when-unconfigured discipline the platform's
domain-orchestration layer uses
everywhere.
Where this connects#
- Upstream: Clew, Minos & Palimpsest — the symbolic core produces and proves the verified skeleton this layer realizes; the fair-lying guard is the runtime mirror of Minos's authoring-time fair-play proof.
- Downstream:
Daedalus & Theseus — compile and eval
compiles Anansesem's trees + Loom's manifest into the V5 pack and play-tests
it end to end; the Loom service (
apps/v8/loom-service) is the relay the UE5 client mints and fetches cases through. - Platform: the domain-orchestration layer owns the Isis (media), Hathor (NPC dialogue), and ACE/Inworld substrates these three subsystems compose behind their injected seams.