Metis · Features

Chiron and Hephaestus — the Embodied Teacher and Explorables

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

5sections14 minread2diagrams3tables

On this page

Once Prometheus has forged a gated V9LessonArtifact, two subsystems turn that verified data structure into something a learner actually experiences. Chiron is the teacher who delivers it — a persona, an integrity policy, and a delivery tier, never a chat box that freestyles. Hephaestus is the workshop that makes the idea manipulable — a real Nyx sky or a real Kalika orbit the learner can perturb, not an animation. The two are deliberately small, composable libraries (libs/v9/chiron/src, @oshun/v9-chiron; libs/v9/hephaestus/src, @oshun/v9-hephaestus) that sit on top of the same verified skeleton: Chiron reads the artifact's narrative.socraticTurns and assessment.retrievalCheck; Hephaestus produces the explorables[] that the artifact's definition-of-done requires. Both refuse to operate on an ungated lesson — planLessonDelivery throws UngatedLessonError and assertComputedExplorableDoD throws when no real computed explorable reached its success state — so neither can present something the seven gates did not clear.

The reason they work the way they do is the V9 thesis applied to delivery: an LLM proposes, a verifier disposes, and nothing reaches a learner that was not demonstrated. A teacher persona is a re-skin over immutable gated truth (the mentor ref is regenerable surface; groundTruth is not), so the same lesson can be voiced by a warm generalist or a grounded historical figure without re-verifying a single claim. An explorable's successState.reachable is set true only after a kernel actually produced the result — the Sun's right ascension really did sweep ≈360° across the slider, the symplectic integrator really did conserve energy below tolerance. This page is the features-side companion for the delivery half of V9; the hub for the full set is ../V9_features.md, and the upstream forge that hands these two their input is documented in Prometheus — the Lesson Forge.

What ships, honestly#

The policy and projection logic is real and tested. Chiron's three modules — integrity-mode resolution (integrity-modes.ts), the avatar→voice→text delivery fallback (delivery.ts), and the persona-fronted delivery plan (lesson-delivery.ts) — are exercised by chiron.spec.ts (11 cases, green in this audit). Hephaestus's two computed explorables and the G4 definition-of-done (explorable-runtime.ts) are exercised by hephaestus.spec.ts (7 cases, green), and they call the real substrates, not mocks: the Nyx sky explorable invokes calculateSunPosition from @nyx/ephemeris, and the Kalika orbit explorable integrates a Kepler Hamiltonian through @kalika/symplectic's velocity-Verlet.

Three honest boundaries deserve naming up front:

  • The live runtimes are injected, not in this code. @oshun/v9-chiron depends on @oshun/contracts and nothing else — there is no avatar renderer, no speech runtime, no Psyche emotion model wired in. The integrity modes and the delivery-fallback shape Chiron mirrors are genuinely shipped, but in the Metis Python backend (services/metis/src/metis/schemas/tutoring.py), which already defines TutoringIntegrityMode, TutoringDeliveryMode, TutoringDeliveryFallback*, and supports_barge_in / supports_turn_taking. Chiron-lite is the TypeScript policy that produces the plan those runtimes execute; the embodied face and barge-in voice loop are the boundary, not a fabricated capability.
  • planLessonDelivery is not yet wired into the player. It is implemented and tested, but the consumer-facing lesson player (libs/v9/experience/src/lesson-player.ts) projects the artifact independently rather than calling it. Both read the same gated artifact; they are parallel projections, and the delivery plan is not currently on the path the experience layer renders. This page describes what Chiron computes, and is explicit where that output is not yet consumed.
  • Generative and game-bridge explorables are spec/seam, not shipped kernels. Hephaestus ships exactly two computed-kernel builders (Nyx sky, Kalika orbit). The generative-widget path lives in @oshun/v9-theia (generative-widgets.ts) and fails loud without a HeadlessWidgetRunner; the game-bridge kind exists in the contract enum but no V9 builder constructs one today. Where the source monolith promises "learn-by-playing," the explorable runtime backs the contract for it, not an implementation.

The embodied teacher (Chiron)#

Integrity modes — the academic-integrity floor#

integrity-modes.ts encodes the rule that keeps a tutor honest: it may support the learner's work but never substitute for it. The four modes are the same ones the Metis live-voice tutor exposes:

ts
type IntegrityMode = 'teach' | 'hint' | 'practice' | 'do-not-complete-for-me';

resolveTutorAction(mode, request) is the gate. A TutorRequest carries a kind (explain / full-answer / hint / check-my-work) and an isGradedWork flag, and the function returns a TutorActionVerdict ({ allowed, action, reason }) where action is one of explain | hint | socratic | check | refuse. The hard rule sits before the per-mode switch: a request for a full answer to the learner's graded work under do-not-complete-for-me or practice is refused outright and redirected to a hint (integrity-modes.ts:48). The rest of the policy:

Mode explain full-answer (graded) full-answer (ungraded) check-my-work
teach explain explain explain check
hint hint hint hint check
practice socratic refuse (→ hint) socratic socratic
do-not-complete-for-me hint refuse (not allowed) hint check

The defaults matter: practice is Socratic for everything (the learner does the work), and do-not-complete-for-me will still check work or hint, but allowed is false the moment a full answer is requested. The spec asserts the floor directly — resolveTutorAction('do-not-complete-for-me', { kind: 'full-answer', isGradedWork: true }) returns allowed: false, action: 'refuse', and the same holds in practice mode.

Delivery fallback — richest embodiment, disclosed downgrade#

A lesson is delivered through the richest embodiment available and degrades gracefully, but never silently. delivery.ts defines the order richest-first (DELIVERY_FALLBACK_ORDER = ['live_avatar', 'live_voice', 'text']), with text as the guaranteed floor (DeliveryCapabilities.text?: true). resolveDelivery walks the order from the requested tier, picks the first available mode, and — critically — pushes a disclosure string for every tier it had to skip:

flowchart TD R["requested: live_avatar"] --> A{avatar available<br/>& in budget?} A -- yes --> AM["active: live_avatar<br/>state: native"] A -- no --> D1["disclose: 'live_avatar unavailable; falling back'"] D1 --> V{voice available?} V -- yes --> VM["active: live_voice<br/>state: fallback"] V -- no --> D2["disclose: 'live_voice unavailable; falling back'"] D2 --> TM["active: text<br/>state: fallback (the floor)"]

The returned DeliveryResolution carries requestedMode, the resolved activeMode, a state of native or fallback, the fallbackChain that was considered, a headline reason (avatar-runtime-unavailable / voice-runtime-unavailable / native), and the per-tier disclosures[]. So a learner who asked for a face and got a voice is told the avatar was unavailable; one who fell all the way to text sees both downgrades. The spec covers all three landings: native when the avatar is up, live_voice (with a non-empty disclosure list) when only voice is up, and text when neither is. This is the contract-level expression of the source's promise that "a learner is never silently given a lesser experience."

One disclosed synthetic teacher#

persona.ts ships exactly one P1 persona, and discloses that it is synthetic. ChironPersona binds a stable id (the value stamped into V9LessonArtifact.mentor), a ChironPersonality of five Hathor facets in [0,1] (warmth, curiosity, patience, rigor, humor), a Psyche voicePackRef, a literal synthetic: true, a disclosureLabel, and an integrityDefault. The shipped WARM_GENERALIST_PERSONA is:

Field Value
id chiron:warm-generalist
personality warmth 0.9, curiosity 0.95, patience 0.9, rigor 0.8, humor 0.5
voicePackRef psyche:voice/chiron-warm-en
synthetic true
disclosureLabel "AI teacher (synthetic voice). Reconstructions are labeled."
integrityDefault teach

CHIRON_PERSONAS holds exactly this one entry, and getChironPersona(id) resolves it. The disclosure label is not decoration — it is the Sekhmet/Metis representation rule made structural: a synthetic teacher must announce that it is synthetic. Multiple faces, a domain specialist, and a grounded historical figure are P2 (see below); P1 ships one honest voice.

Binding a gated lesson to a session#

lesson-delivery.ts is where the persona, the integrity mode, and the delivery tier combine over a forged artifact. planLessonDelivery(input) does five things in order:

  1. Refuse the ungated. If isV9LessonPublishable(artifact) is false (any of the seven gates failed), it throws UngatedLessonError — nothing ungated reaches a learner. The spec proves this by flipping G2 to pass: false and asserting the throw.
  2. Resolve the persona. input.persona ?? getChironPersona(artifact.mentor) ?? WARM_GENERALIST_PERSONA — the artifact's stamped mentor wins, with the warm generalist as the floor.
  3. Resolve the integrity mode. input.integrityMode ?? persona.integrityDefault.
  4. Resolve delivery. resolveDelivery(input.requestedDelivery, input.capabilities).
  5. Project the turns. Each narrative.socraticTurns entry becomes a DeliveryTurn { role, text, claimRefs }, preserving claimRefs so every spoken line still traces back to a grounded claim in groundTruth.

A worked call from the spec: deliver the gated Olbers'-paradox lesson, requesting live_voice with { avatar: false, voice: true }. The plan comes back with persona.id = 'chiron:warm-generalist', integrityMode = 'teach' (the persona default), delivery.activeMode = 'live_voice', a single turn whose claimRefs is [0], a retrievalPrompt of "Why is the sky dark?" pulled straight from assessment.retrievalCheck.prompt, and the synthetic-teacher disclosure. The plan is the thing the live tutoring/avatar runtime would then execute.

One honest subtlety worth stating: planLessonDelivery computes a per-turn resolveTutorAction verdict (a mentor turn is treated as an explain request, which under practice resolves to socratic), but in P1 the turn text is passed through unchanged either way — the "Socratic mentor keeps the question form" because the forged narrative is already Socratic, not because this function rewrites it. The integrity enforcement with teeth is the live-Q&A path (resolveTutorAction applied to a learner's real request) and the ungated-lesson refusal — not a post-hoc rewrite of forged turns. The code is honest about this; the comment says exactly what the line does.

Where the live runtime is — and Chiron-full (P2)#

The embodied face, the barge-in/turn-taking voice loop, and pacing adapted to detected engagement are the injected boundary. The Metis backend already implements the voice-runtime surface (supports_barge_in, supports_turn_taking, VAD config in tutoring.py), and Psyche ships emotion models (libs/psyche/emotion-recognition, libs/psyche/emotion-engine) — but neither is wired into @oshun/v9-chiron, so engagement-adaptive pacing is spec/planned at the V9 layer.

The two richer persona promises live in @oshun/v9-theia (chiron-full.ts, P2), with real logic and the runtime as the gated remainder:

  • Cross-session memory. buildSessionMemory(traces) projects a learner's Mnemosyne MASTERY_LEVELS concept traces into struggledConcepts (≤ beginner) and masteredConcepts (≥ advanced), and chironOpeningLine(memory) opens a returning session on what was tricky last time — "Last time, X was tricky — want to revisit it?" See Threads and the Mastery Loop.
  • Grounded historical personas. buildHistoricalPersona({ figure, sources, isLivingVoice?, consentRecordId? }) requires ≥1 Nisaba source pin or throws UngroundedPersonaError (the figure may say nothing the sources don't ground), stamps a reconstructionLabel, and refuses a living voice without a consent record (the V3 voice-clone registry / Sekhmet gate). This is the "Stoic from Epictetus' actual words" promise as enforceable code, not an invitation to invent personal claims. See Governance and Boundaries.

The workshop (Hephaestus) — explorables you can manipulate#

The Explorable contract#

Every explorable is a manipulable surface bound to a concept node, defined by V9ExplorableSchema (libs/contracts/src/v9/explorable.ts). Three kinds in increasing generative risk:

Kind Trust Grounding required Status in V9
computed-kernel highest a kernelRef (enforced) shipped (Nyx sky, Kalika orbit)
generative-widget verified a Sophia/grounding pin seam (theia, fails loud)
game-bridge bridged a pin into a V2–V8 substrate contract only (no builder)

Two pieces of the schema carry the weight. params[] are the canvas knobs (name, optional label, min, max, default, optional step/unit); a superRefine rejects min > max or a default outside the range, so a slider cannot be born outside its verified regime. successState is the reachable target: a description, a machine-checkable predicate, a boolean reachable set true only after the state was demonstrated, and an optional evidence string. The top-level superRefine enforces that a computed-kernel explorable must be grounded by a kernelRef, not a pin — a computed surface cannot pretend its truth came from a citation. isV9ExplorableShippable(ex) is simply ex.successState.reachable, and that boolean is the entire basis of the G4 gate.

The Nyx sky explorable — real ephemeris, not animation#

nyx-sky-explorable.ts turns a cosmology concept into a "time-travel" sky. buildNyxSkyExplorable({ conceptId, startUnixMs, days?, samples? }) steps a date slider across days (default 365) in samples points (default 366) and, at each step, computes the Sun's true apparent position with the real kernel: dateToJd(...) then calculateSunPosition(jd) from @nyx/ephemeris, which places the Sun at the antipode of Earth's VSOP-style heliocentric ecliptic position and converts ecliptic→equatorial with an obliquity correction (generator.ts:842). The explorable's success state is a measured fact, not an assertion: the total forward sweep of the Sun's right ascension should be ≈360 · days / 365.24219 (one tropical year). reachable is true only when every sample is finite and the measured sweep lands within 10% of that expectation (nyx-sky-explorable.ts:81). The evidence string records the actual numbers ("swept 359.x° (expected 360.0°)"), and the provenance.contentHash is a sha256 of the rounded (dayOffset, raDeg, decDeg) series — a determinism binding. The spec runs this from J2000 and asserts the sweep lands between 350° and 370° over a year, and that a 30-day window sweeps ≈30° and still reaches its scaled success state — the predicate scales with the span, so a short window is honest about being a short window.

The Kalika orbit explorable — real symplectic integrator#

kalika-orbit-explorable.ts turns a physics concept into "a planet you can perturb." It wraps buildOrbitExplorable from @oshun/v9-lesson-explorables, which builds the 2-D Kepler separable Hamiltonian H = p²/2 − GM/|q|, hands it to integrateCanonical(..., { method: 'velocity-verlet' }) from @kalika/symplectic, and reads back the trajectory plus analyzeEnergyBehavior's backward-error energy diagnostic. The initial state is perihelion (r₀, 0) with the vis-viva tangential speed v₀ = √(GM(1+e)/r₀), and the period comes from the semi-major axis a = r₀/(1−e) via Kepler's third law. The success state is the measured fact that a symplectic integrator conserves energy: reachable is true only when the orbit was actually computed (orbit.computed), the relative energy drift is finite, and |drift| < maxEnergyDrift (default 1e-3). The explorable binds two real knobs — an eccentricity slider (00.89) and a GM slider (0.110) — so changing a parameter recomputes the actual solution rather than scrubbing an animation. The spec confirms a circular orbit conserves energy below 1e-3 and that an eccentricity: 0.5 orbit binds its slider default to 0.5.

The definition-of-done and the G4 gate#

explorable-runtime.ts is the rule that every P1 lesson ships ≥1 computed explorable whose success state was actually reached. evaluateExplorableDoD(explorables) filters to computed-kernel, counts how many pass isV9ExplorableShippable, and returns an ExplorableDoDReport ({ ok, total, computedKernel, reachableComputedKernel, reasons }) where ok is true iff at least one reachable computed-kernel explorable exists. The reasons[] are specific and human: "no computed-kernel explorable (a P1 lesson needs ≥1 Nyx/Kalika computed explorable)" or explorable "<title>" never reached its success state: <description>. assertComputedExplorableDoD throws on failure (a build-time block); explorableDoDToGateVerdict maps the report to a V9GateVerdict.

This report is the lesson's G4 verdict. In the Prometheus forge, composeGates (libs/v9/prometheus/src/gates.ts:133) calls evaluateExplorableDoD(input.explorables) and folds the result into the seven-gate struct as G4, alongside Aletheia's G1/G2/G6, the pedagogy G3, the quality G5, and the provenance G7. So Hephaestus does not merely describe completeness — it decides it, and a lesson with a generated-but-unverified explorable cannot clear the gate. The mechanics of the full gate suite are in Subsystem Map and Gates, and the kernels that back the values these explorables compute are catalogued in Atlas — Wonder Resolution.

Generative and game-bridge explorables — the honest seam#

For ideas without a pre-built kernel, the design calls for a generated, verified-not-just-plausible widget. That verifier exists — verifyGenerativeWidget(spec, runner?) in @oshun/v9-theia's generative-widgets.ts — and it is a fail-loud seam: with no HeadlessWidgetRunner wired it throws WidgetRunnerNotConfiguredError, and even with a runner it returns { shipped: false, reason } unless the widget both compiled and reached its success state under the headless check. Only then does it mint a generative-widget V9Explorable. This is the correct shape for a real-but-absent integration: refuse to fabricate rather than ship a plausible widget. The game-bridge kind — the V2 racing mode as an applied-physics lab — is present in V9ExplorableKindSchema but has no builder in libs/v9; treat "learn-by-playing" as a contracted-but-unimplemented promise.

How the two halves connect#

Chiron and Hephaestus bracket the lesson: Hephaestus's output is gated into the artifact upstream, and Chiron reads that gated artifact out to the learner.

sequenceDiagram participant Forge as Prometheus forge participant Heph as Hephaestus participant Art as V9LessonArtifact participant Chiron as Chiron (planLessonDelivery) participant Player as Lesson player / live runtime Forge->>Heph: buildNyxSky / buildKalikaOrbit (real kernels) Heph-->>Forge: V9Explorable (successState.reachable computed) Forge->>Heph: evaluateExplorableDoD(explorables) → G4 Note over Forge: composeGates folds G4 into G1–G7 Forge->>Art: emit gated artifact (mentor ref stamped) Chiron->>Art: isV9LessonPublishable? (else UngatedLessonError) Chiron->>Chiron: resolve persona + integrity + delivery tier Chiron-->>Player: LessonDeliveryPlan (turns w/ claimRefs, retrievalPrompt, disclosure)

The seam is the V9LessonArtifact itself, and the skeleton/surface split is what lets the two stay decoupled: Hephaestus contributes G4-verified explorables[] to the gated, immutable-post-gate artifact, while Chiron's mentor ref is regenerable surface (grouped with narrative/media in the contract) — re-skinning the teacher never touches the gated explorable or the grounded claims in groundTruth. That is the whole point of the two-hash design (skeletonHash vs surfaceHash) described in Overview: a popular lesson is gated once, then delivered by any persona.