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-chirondepends on@oshun/contractsand 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 definesTutoringIntegrityMode,TutoringDeliveryMode,TutoringDeliveryFallback*, andsupports_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. planLessonDeliveryis 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-widgetpath lives in@oshun/v9-theia(generative-widgets.ts) and fails loud without aHeadlessWidgetRunner; thegame-bridgekind 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:
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:
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:
- Refuse the ungated. If
isV9LessonPublishable(artifact)is false (any of the seven gates failed), it throwsUngatedLessonError— nothing ungated reaches a learner. The spec proves this by flipping G2 topass: falseand asserting the throw. - Resolve the persona.
input.persona ?? getChironPersona(artifact.mentor) ?? WARM_GENERALIST_PERSONA— the artifact's stamped mentor wins, with the warm generalist as the floor. - Resolve the integrity mode.
input.integrityMode ?? persona.integrityDefault. - Resolve delivery.
resolveDelivery(input.requestedDelivery, input.capabilities). - Project the turns. Each
narrative.socraticTurnsentry becomes aDeliveryTurn { role, text, claimRefs }, preservingclaimRefsso every spoken line still traces back to a grounded claim ingroundTruth.
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 MnemosyneMASTERY_LEVELSconcept traces intostruggledConcepts(≤ beginner) andmasteredConcepts(≥ advanced), andchironOpeningLine(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 throwsUngroundedPersonaError(the figure may say nothing the sources don't ground), stamps areconstructionLabel, 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 (0–0.89) and a GM slider (0.1–10) — 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.
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.
Related#
- Overview — the product, the artifact, and the skeleton/surface split
- Subsystem Map and Gates — where G4 sits in the
seven-gate contract and how
composeGatesfolds it in - Prometheus — the Lesson Forge — the upstream pipeline that builds and gates the artifact Chiron delivers
- Atlas — Wonder Resolution — the concept graph and the kernels that compute the values an explorable manipulates
- Threads and the Mastery Loop — Mnemosyne traces, cross-session memory, and the retrieval checkpoint Chiron ends on
- The Braid, the Commons, and Films — the Agora creator surface and the Manim explainer-film path (sibling Theia subsystems)
- Governance and Boundaries — synthetic-teacher disclosure, grounded-persona consent gating, and the injected-runtime seams
- The hub: ../V9_features.md