Ariadne · Features

Realization & Living Suspects

A focused page within the Ariadne Features documentation. The full map and every sibling page live in the Features hub.

6sections12 minread1diagram

On this page

A player types a commission into the Bureau Case Board — "a 1947 arson in the warehouse district, hard, two suspects" — and a few minutes later walks a fog-lit crime scene, thumbs a coroner's report whose smudged time-of-death is the clue that breaks the case, and sits down across a table from a man who looks them in the eye and lies. That last part is the trick of it: he lies fairly. Somewhere in the evidence the player can already reach is the single fact that catches him, and when they slide it across the table he cracks — not because a dialogue tree had a "win" node, but because the lie was load-bearing and the evidence dissolved its foundation.

Getting from a proven case skeleton to that lived mystery is the work of the realization layer, and three named subsystems own it. Anansesɛm is the writers' room: it speaks the case — the opening briefing, scene prose, partner banter, a character sheet per suspect, and the interrogation scripts. Loom is the asset fabric: it gives the case a face and a sound — crime-scene environments, suspect portraits, evidence props and documents, a period score, and a voiced line for every spoken one. Ori is the runtime that makes a suspect a mind rather than a menu — memory, a stance that hardens under pressure, and the discipline never to tell a lie the player cannot refute. All three obey one inherited covenant, the spine of the whole product: the LLM may speak the case, but it may never invent it — no new suspect, no new room, no number, and no lie the proof did not already author. The fairness that case generation, proof & canon establish at authoring time is exactly what these three are forbidden to break at performance time. This is the realization half of the V8 feature set; the hub is ../V8_features.md.

What ships, honestly#

The realization layer is real and exercised end to end, with the generative pieces sitting behind typed seams. Three tiers, stated plainly:

  • Real, deterministic, and tested today. The Loom service runs the entire mint→publish pipeline with zero credentials: runPipeline (apps/v8/loom-service/src/pipeline.ts:127) threads Clew → verify → Anansesɛm → Loom assets → compile → eval → safety → release. The §12.4 acceptance suite mints a case, clears all eight ReleaseDecision gates, compiles a real V5 cold-cases pack through the build gate, and reproduces bit-for-bit from its seed5 passed (5) (apps/v8/loom-service/src/pipeline.test.ts, verified green here). With no LLM injected, Anansesɛm still emits a complete, grounded surface; Loom's planner, retrieval ranker, voice caster, and MetaHuman mapper are real algorithms (case-assets, 53 passed); Ori's memory stream, fair-lying guard, and offline voice are real and reproducible (case-suspects, 34 passed); the writers' room is 18 passed.
  • Provider-gated seams — fail-loud / fail-closed, not faked. The actual generation is injected, never fabricated. Anansesɛm's prose enricher is a CompletionFn — absent, the deterministic base text stands; present, a provider error propagates. Loom's minter wants an Isis JobRunner — 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_* credentials — absent, it reports { configured: false } and fails closed to a fully-playable offline interrogation.
  • Honest scope notes. The shipped §12.4 pack is text-only: the offline deps (apps/v8/loom-service/src/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: it is driven by the game host at play time, not by the offline mint, which bakes the static 12-beat trees Anansesɛm authored.

The architecture-side treatment of all three lives in Anansesɛm, Loom & Ori suspects; this page stays on the player-facing surface and dips into code where a claim needs backing.

Anansesɛm — prose that can't lie about the facts#

Anansesɛm (named for the Akan Anansi storytelling tradition) realizes everything the player reads or hears as words. It is a real multi-agent writers' room, not a single prompt: realizeCase (libs/yemaya/case-writers-room/src/dag.ts:244) compiles and invokes a StateGraph whose id is literally anansesem-writers-room (dag.ts:97). A Showrunner sets the brief — title, era voice, and the grounding vocabulary — and fans the work to five specialist writers: StoryDirector (the briefing), CharacterWriter (a sheet per cast member), DialogueWriter (an interrogation tree per suspect), Cinematographer (scene descriptions + shot notes), and SoundDesigner (partner banter). An Integrator assembles their disjoint slices into a RealizedCase. The graph is genuinely compile().invoke(state)-driven, not narrated.

The closed vocabulary — the room cannot name a stranger#

The hard feature guarantee — the writing never contradicts the proof — lives in grounding.ts. From the ground truth plus the canon slice, the Showrunner builds a closed GroundedVocabulary: the exact set of people (victim + suspects

  • recurring canon characters), location ids and display names, and the weapon. assertGrounded is a real proper-noun extractor, not a shape check — it walks every realized string, lifts runs of two-or-more capitalized, name-shaped words (extractProperNouns, the shape that could smuggle in an invented "Cornelius Blackwood"), strips genitives and function words, and reports any token that resolves to no grounded entity as a GroundingViolation. A player can never be sent to interview someone the case does not contain.

Twelve tells, one fair lie#

buildInterrogation (libs/yemaya/case-writers-room/src/interrogation.ts:123) aligns each suspect to V5's interrogation rig — twelve facial-tell beats (TELL_BEATS) and the Truth / Doubt / Lie mechanic the player reads off the suspect's face — with invariants that tie every line back to the skeleton:

  • Every tree fills 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 Lie whose refutedByClueId is the alibi-flaw clue (the single discoverable flaw); the builder throws if the session contains no such clue for the culprit (interrogation.ts:146), so a guilty party with an uncatchable alibi can never reach a player.
  • 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, so the rule "every Lie carries a refuting clue" always holds.

Stress tells escalate with veracity (a Lie under pressure plays gaze-aversion / swallow / jaw-clench; Truth plays calm), so what the player sees tracks what the suspect is doing — and that mapping is deterministic and grounded in the real victim name, weapon, and place, not free-associated.

The polish seam (fail-loud)#

Every spoken or written line passes through enrichLine (enrich.ts:32). With no CompletionFn it returns the deterministic base verbatim — the case is fully playable with no model at all. With one injected, it asks the model to rewrite the line in register and then re-grounds the result: withinBoundary rejects any rewrite that introduces a name-shaped token outside the allowed set, falling back to the deterministic base. A provider error, by contrast, propagates uncaught — the room never launders an outage into silently-degraded prose. The model can make the prose sing; it cannot invent a suspect, and it cannot fake success.

Loom — the case you can see and hear#

Loom binds a case to multimedia through Isis. Its entry, realizeCase (libs/yemaya/case-assets/src/realize.ts:62), runs the fabric end to end: plan → retrieve → degrade-to-budget → mint the surviving gaps → bind, returning a contract-valid AssetManifest keyed to the case.

A plan derived from the case, never a fixed list#

planAssets (libs/yemaya/case-assets/src/plan.ts:166) computes exactly what a specific case needs, every count read off its structure: one environment per distinct story location (de-duped by slug so "Gatehouse" and "gatehouse" are one scene), one portrait per suspect plus the victim, one 3D prop per physical/forensic clue and one document/photo per documentary/testimony clue, case music plus one sting per revelation or climax beat, one voice-over line per realized dialogue line, and optional flashback/CCTV video per timeline beat at the crime scene. Each PlannedAsset carries a boundTo target, so the produced media wires back into the exact V5 evidence node — the smudged report becomes this clue, this portrait pins to this suspect on the board.

Reuse before mint — the world is consistent because it remembers#

Before minting anything, retrieveThenGenerate asks an injected AssetLibrary whether the world already holds the right asset — reuse beats mint when a recurring district or NPC should look the same as last time. The default ranker is real: a specificity-weighted tag overlap (identity tags like location: / subject: / clue: weighted 4×, shared context tags like cell: / era: / theme: weighted 1×) blended 0.8 / 0.2 with a description-token Jaccard, against a DEFAULT_REUSE_THRESHOLD of 0.62 (retrieve.ts:61, :147). The weighting is the point: a wrong-location environment that shares all the case boilerplate still falls below threshold and gets minted bespoke. It is computed retrieval — never always-reuse, never always-mint.

Voices that stay the same, faces that match#

castVoice (voices.ts:85) assigns each character one stable, register-correct voice by reducing a SHA-256 of canonId::characterId over a partitioned pool of 12 distinct ElevenLabs-style voices, so a grizzled male suspect never lands a youthful voice and — crucially — the same character sounds the same in every case they appear in. assembleMetaHuman (metahuman.ts:87) maps a portrait plus persona tags onto MetaHuman Creator's real axes (an 18–80 age range, a 0–15 skin-tone ramp, hair, facial hair, body type, age-scaled wrinkle/scar overlays) deterministically from the character id, so the suspect you interrogate has a consistent, buildable face rather than a stock head.

Degrade, never fake#

degradeToBudget reserves asset-jobs from the per-case budget and, when the ceiling is hit, degrades in a real priority order — environments and portraits first, evidence next, optional flashback video dropped first — logging every drop so the pack is never silently truncated. And the mint step is where Loom refuses to fabricate: mintGaps (orchestrate.ts:208) builds a real JobRequest per gap with period-correct dimensions and an anachronism negative-prompt, and any runner error, job error, or job that returned no output file is raised as a MintingError (orchestrate.ts:67) — it never invents a result url. realizeCase throws outright if real gaps remain with no Isis runner wired (realize.ts:80). An absent backend is surfaced, not papered over with a plausible-looking asset.

Ori — suspects you actually interrogate#

Ori (Yoruba for the "inner head," the seat of identity) is the runtime that turns a suspect from a fixed dialogue tree into a questionable mind. Each suspect carries a SuspectOri (libs/yemaya/case-suspects/src/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; an innocent 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, a live interrogation 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) — and the player can feel it.

stateDiagram-v2 [*] --> cooperative cooperative --> guarded: pressure ≥ 0.25 guarded --> deflecting: pressure ≥ 0.5 & evasive guarded --> guarded: pressure ≥ 0.5 & not evasive deflecting --> rattled: pressure ≥ 0.8 (culprit) rattled --> cracking: load-bearing clue presented guarded --> cracking: load-bearing clue presented cracking --> [*]: grounded confession note right of cracking Only the culprit can crack, and only on the one clue that refutes their alibi (fair-play). end note

Memory keeps a suspect's story straight#

V5 lets the player re-interrogate a suspect across several passes, and a living mind must not contradict itself. MemoryStream (memory.ts:89) implements the Park et al. Generative Agents retrieval — every memory carries a poignancy, and retrieval scores candidates by recency · importance · relevance with an 8-turn recency half-life. So a suspect cannot say "I was home" and later "I was at the club": the prior statement is retrieved before the next answer is formed. 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 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: the cooperative → guarded → deflecting → rattled → cracking ladder the diagram above traces, driven entirely by accumulated pressure.

The fair-lying guard — the runtime half of the fairness covenant#

fair-lying.ts is the runtime mirror of the fair-play proof 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 (via each clue's metadata.groundTruthFactId), and a lie is permitted only if a reachable clue proves its refutingFactId. An Ori can never deliver an "authorial cheat" 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. This is the "lies you can catch with clues the game actually gives you" promise, enforced at every utterance.

Live or offline, always playable#

FailClosedBroker (runtime.ts:174) is the seam to a managed avatar service. It reads ACE/Inworld credentials once and reports an honest status() = { configured, mode }; it is "live" only when creds and a real live broker both exist. With neither, it fails closed to an OfflineInterrogation — the memory stream, the guard, and a deterministic OfflineDialogueBackend 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, which builds a grounded persona and propagates any provider error (fail-loud). The host treats both modes through one send surface, so the interrogation stays playable and fair-play-correct even with no cloud avatar — a pre-rendered face is an upgrade, never a prerequisite.

How it fits together#

The realization layer is the bridge between a proof and a performance. Upstream, the symbolic core hands it a case it has already solved; downstream, the compiler and director turn its output into something a player walks into.

  • Upstream: Case generation, proof & canon builds and proves the verified skeleton — the culprit Minos certified is the unique solution — that all three subsystems here are forbidden to contradict; Ori's fair-lying guard is the runtime echo of that authoring-time proof.
  • Downstream: Delivery, direction & operations compiles Anansesɛm's trees and Loom's manifest into the V5 cold-cases pack, play-tests it, and decides which generated mysteries reach which player, when — and the Loom service (apps/v8/loom-service) is the relay the UE5 client mints and fetches cases through.
  • Architecture: the Anansesɛm, Loom & Ori companion carries the full graph topology, the grounding extractor internals, the Isis job contracts, and the broker state machine behind these features.