Psyche is the V1 real-time embodiment substrate: it owns the contract for a live text / voice / avatar session — the session envelope, server-mediated turn-taking, interruption and barge-in, transcript synchronization, lip-sync and expression coherence, latency budgeting, provider failover, and continuity under reconnect or a safety crisis. It serves every surface where a member talks with an assistant in real time rather than reading a response (the live assistant shell, the embodied teacher personas, conferencing, and the Living Scenes frame stream). It sits among the platform-substrate deep-dives hubbed at ../ARCHITECTURE.md, alongside Sophia, Iris, Lilith, Isis, and Aje.
A deliberate split runs through this page, and it is the single most important
thing to understand about Psyche's maturity. The contract-and-logic layer —
the pure data shapes and pure functions that define what a correct session looks
like — is real, rich, and exhaustively tested in
libs/oshun/embodiment-psyche. The live wire/transport binding and the actual
provider integration (real ASR/TTS/avatar vendors, the WebSocket data plane,
the operator review lane) are spec-only / partially built: the adapter
library carries no IO, no rendering, and no transport by design. The end-to-end
"tutor live session to graded record" walkthrough is rated partial in the
completeness audit, with the live-voice envelope, library write, and operator
review lane noted as unbuilt surfaces. Where this page describes a function or
constant, it exists; where it describes a deployed live session, it is honestly
labeled planned or gated.
Canonical home (§13).
Psycheis a cross-product substrate, so its canonical reference home is the domain spacedocs/domains/psycheand its code-linked entity catalog atsystems/psyche. This page is V1's view — how the V1 platform composesPsyche; the substrate itself is documented in full at its canonical home, which this page references rather than duplicates.
Where Psyche lives in the codebase#
@oshun/embodiment-psyche (libs/oshun/embodiment-psyche, v0.1.0) is the
canonical contract library. It is type: module, and its main/types point
at ./src/index.ts — source, not a built dist — so consumers import the
contract directly. src/index.ts re-exports 23 modules, which is the real
scope of the substrate; the older architecture note that named only
adapter.ts, avatar-sync.ts, backpressure.ts, and crisis-frame.ts
materially undersells it.
| Module | Responsibility |
|---|---|
canonical-adapter.ts |
Canonical factory createCanonicalPsycheEmbodimentAdapter({ apiAdapter }) — wraps an injected low-level adapter |
adapter.ts |
Contract descriptor, availability/health, session-plan builder — no direct IO (no fetch/ws/http/prisma) |
embodiment-model.ts |
Persona → embodiment profile, capabilities, live-session-state assembly |
session-envelope.ts |
The validated session record, lifecycle graph, modality negotiation, persona-switch planning, fingerprinting |
session-events.ts |
The event taxonomy, gap-free stream, and the server turn-state reducer |
voice-orchestration.ts |
Voice pipeline orchestration contracts |
reconnect-behavior.ts |
Reconnect/resume/handoff continuity |
transcript-sync.ts |
Partial/final transcript reconciliation |
multimodal-state.ts |
Cross-modality coherence state |
latency-dashboard.ts |
Latency targets, sample builders, budget assessment |
backpressure.ts |
Admission control, shed planning, provider backpressure policy |
quality-thresholds.ts |
Quality gates that can trigger fallback |
fallback-routing.ts |
Avatar → voice → text → unavailable cascade |
provider-failover.ts |
Provider-failure routing |
session-audit.ts / session-diagnostics.ts |
Audit trail and diagnostics |
avatar-sync.ts |
Viseme table, pose interpolation, alignment/drift |
emotion-modulation.ts |
Expression/emotion modulation contracts |
screen-context.ts |
Screen-share / screen-context attachment |
crisis-frame.ts |
Safety-boundary crisis-frame entry |
events/ |
Living-Scenes scene events, frame stream, cue-plan replay |
The service backing — the live runtime that consumes these contracts —
exists with substantive content but is the less-mature half: services/psyche/*
(real subdirectories include orchestrator, avatar-engine, behavior-engine,
voice-engine, conferencing/video-conferencing, tavus-integration,
perception-engine, tool-framework, persona-service, …),
infrastructure/psyche/{docker,kubernetes,terraform}, contracts under
libs/contracts/psyche/src, and apps/psyche/admin (only the admin app is
present). Separately, libs/psyche is a much larger substrate tree (~139
subdirectories, including tavus-*, avatar-*, memory-*, viseme-generator,
voice-synthesis, speech-recognition) — this is not the same thing as the
embodiment-psyche adapter contract; do not conflate them.
The canonical adapter — pure logic over an injected port#
createCanonicalPsycheEmbodimentAdapter({ apiAdapter }) is the canonical entry
point. It takes a PsycheEmbodimentApiAdapter — the low-level port that does
the IO — and returns a PsycheEmbodimentAdapter that layers the canonical
contract logic on top: getContractDescriptor, getMetadata, getAvailability
(derived from the injected getHealth), getSessionCapabilities,
resolveEmbodimentProfile, getLiveSessionState, planSession,
startEmbodiedSession, and the pause/resume/terminate lifecycle calls.
The split is the whole point. adapter.ts contains zero fetch,
WebSocket, http, or prisma calls — verified by grep.
startEmbodiedSession illustrates the discipline: it resolves the embodiment
profile, builds a session plan via buildPsycheSessionPlan, and refuses to
proceed if the plan is not approved (if (!plan.approved) throw …). Only then
does it hand the granted modalities and session kind to the injected
apiAdapter.createSession. Policy and shape are decided in pure code; the
network call is delegated. This is why the library is real and rich while a
deployed live session is still partial: the rules are implemented; a running
session requires the transport and provider bindings that live downstream in
services/psyche/*.
The session envelope#
The session envelope (session-envelope.ts, PsycheSessionEnvelope at
line 393) is a single validated record that both endpoints of a live session
agree on. It is far richer than a connection descriptor — it carries identity,
residency, capability grants, entitlement, policy binding, transport, latency
budget, continuity, governance, and lifecycle in one fingerprinted object.
Key fields:
sessionId,version,mode(PsycheSessionMode, an alias ofPsycheSessionKind).assistantIdentityId+assistantIdentityFingerprint— the Iris assistant identity driving the session, with a fingerprint so both ends can confirm they are bound to the same identity.locale,region(residency/routing),timezone,deviceClass(web/mobile/wearable/desktop/unknown),platformShell,consumer.capabilities— the wire/runtime capabilities the server may emit (PSYCHE_SESSION_CAPABILITIES, 21 entries, includinglip-sync,emotional-modulation,screen-context, andtranslation).entitlementClass—PSYCHE_SESSION_ENTITLEMENTS=free/premium/enterprise/operator-admin/operator-studio.lilithPolicyVersion— the Lilith policy pack bound at session open.embodiment,transport,latencyBudget,governance, andcontinuity.continuity.memoryScope—PSYCHE_SESSION_MEMORY_SCOPES=off/session/profile— plusmemoryConsentGranted,groundingMode(none/recommended/required),activeDomain,conversationHistoryId, andcarryOverContextId, which is how an Iris conversation carries into a live session.lifecycleState, timestamps,reconnectAttempts,lastError, and a deterministicfingerprint.
The envelope is not a passive bag of fields — the module ships the operations that keep it correct:
- Lifecycle as a transition graph.
PSYCHE_SESSION_LIFECYCLE_TRANSITIONSencodes the legal moves between states (pending → ready → connecting → connected, withdegradedreachable fromconnecting/connectedand recoverable back toconnected, and the terminal setclosed/failed/timed-out).isPsycheSessionTransitionAllowedandisPsycheSessionTerminalStateenforce it. A session cannot skip frompendingstraight toconnected. - Fingerprinting.
computePsycheSessionEnvelopeFingerprintderives a deterministic hash over the identity-relevant fields, so two endpoints holding matching fingerprints know they are looking at the same envelope without reshipping the whole record. - Modality negotiation.
negotiatePsycheSessionModalitiesreconciles requested modalities against grants and re-fingerprints the result. - Persona-switch planning.
planPsycheSessionPersonaSwitchplans a mid-session persona change while preserving continuity.
The event taxonomy and the gap-free stream#
Psyche ships a dual event taxonomy, and the architecture note that only listed the 15 canonical V1 wire types under-describes it.
Canonical V1 wire events — PSYCHE_V1_SESSION_EVENT_TYPES
(session-events.ts:115), 15 entries that match the published list exactly:
text-token-stream, asr-partial, asr-final, tts-chunk, avatar-frame,
viseme-stream, expression-update, turn-complete, interruption,
reconnect, transcript-sync, error, kill-switch, fallback-engaged,
policy-intervention.
Server turn events — PSYCHE_SERVER_TURN_EVENT_KINDS = turn-start,
turn-end. These are emitted by the server-side turn reducer (below) and are
not in the 15-entry V1 list, which is a documentation gap rather than a
contradiction.
Legacy dotted protocol — PSYCHE_LEGACY_SESSION_EVENT_KINDS is the older
namespaced family that the union still carries:
- Turn-taking:
turn.claim,turn.grant,turn.yield,turn.interrupt,turn.barge-in,turn.timeout - Streaming:
stream.audio.frame,stream.transcript.partial,stream.transcript.final,stream.avatar.frame,stream.tool.call,stream.tool.result - Reconnect:
reconnect.attempt,reconnect.success,reconnect.failed,reconnect.handoff - Lifecycle:
session.state-changed,session.error,session.heartbeat
The discriminated union PsycheSessionEvent (and the combined
PSYCHE_SESSION_EVENT_KINDS) covers all three families.
Every event flows through a gap-free, monotonic stream.
buildPsycheSessionEventStream / appendPsycheSessionEvent enforce that
sequence === nextSequence (no gaps, no reordering) and that emittedAt is
non-decreasing (monotonic time). Each event is fully traceable, carrying an
eventId, sessionId, traceId, timing (ingress/egress plus provider
attribution and retryAttempts), the sequence, and an actor. This is what
makes the stream auditable and replayable: a kill-switch or
policy-intervention event has a stable position and a provable provenance.
Server-mediated turn-taking — a real state machine#
Turn-taking is not narrative hand-waving; it is an implemented reducer.
createPsycheServerTurnState builds the initial state and
applyPsycheServerTurnCommand advances it. The phases are
PsycheServerTurnPhase = idle | active | interrupted | complete, and the
commands are turn-start, turn-end, interruption, and
system-initiated-barge-in:
| Command | Effect |
|---|---|
turn-start |
Opens a turn (turnId, speakerId, turnCeilingMs); emits a turn-start event |
turn-end |
Closes a turn (reason, whether the final transcript is available, final TTS chunk index) |
interruption |
A caller barges in (interruptedBy, policyApproved); fades partial TTS |
system-initiated-barge-in |
The runtime itself interrupts (e.g. for a policy or safety reason) |
The server is the authority on whose turn it is — barge-in and interruption are
mediated through this reducer rather than negotiated peer-to-peer, which is what
keeps a multi-party live session deterministic. The interruption commands fade
out any in-flight TTS over PSYCHE_DEFAULT_PARTIAL_TTS_FADE_OUT_MS = 180
(session-events.ts:768) by default — the configurable barge-in fade-out that
lets the synthetic voice tail off cleanly instead of cutting dead mid-word.
Latency budgets and the "thinking indicator" deadline#
Live embodiment lives or dies on latency, so the targets are codified, not
aspirational prose (latency-dashboard.ts:68-84):
| Metric | p50 | p95 | p99 |
|---|---|---|---|
Voice first-audio (PSYCHE_VOICE_FIRST_AUDIO_LATENCY_TARGETS) |
500 ms | 900 ms | 1500 ms |
First token (PSYCHE_FIRST_TOKEN_LATENCY_TARGETS) |
350 ms | 700 ms | — |
Avatar frame (PSYCHE_AVATAR_FRAME_LATENCY_TARGETS) |
80 ms | — | — |
ASR final text (PSYCHE_ASR_FINAL_TEXT_LATENCY_TARGETS) |
150 ms | — | — |
Sample builders (e.g. buildPsycheVoiceFirstAudioLatencySample,
buildPsycheAvatarFrameLatencySample) compute each value from raw timestamps
and validate ordering (first-audio cannot precede speech-end), and
assessPsycheLatencyAgainstBudget compares observed percentiles to the
envelope's budget, returning PsycheLatencyBudgetStatus
(PSYCHE_LATENCY_BUDGET_STATUSES = ok / warning / exceeded) for the round
trip, the pipeline, and overall. A warning/exceeded status is the input that
feeds quality-driven fallback.
When a provider lags, the runtime must not leave the member staring at silence.
The provider-backpressure policy PSYCHE_PROVIDER_BACKPRESSURE_DEFAULT_POLICY
sets thinkingIndicatorDeadlineMs: 200 (backpressure.ts:322), clamped to ≤
200 ms — so a "thinking" indicator must surface within 200 ms of a stall, which
matches the published "within 200 ms" requirement.
Backpressure: admission control and load shedding#
Beyond the thinking-indicator deadline, backpressure.ts implements real
load-management mechanics:
- Admission control.
decidePsycheSessionAdmissionreturns a typed decision (PSYCHE_SESSION_ADMISSION_DECISIONS) weighingPSYCHE_SESSION_ADMISSION_PRIORITIES— i.e. whether a new live session may start under current load. - Shed planning.
planPsycheBackpressureShedpicks running sessions to relieve, choosing amongPSYCHE_BACKPRESSURE_SHED_ACTIONS=downgrade-to-text,graceful-disconnect,queue-handoff. - Provider backpressure response.
planPsycheProviderBackpressureResponseproduces a throttle / thinking-indicator / fallback plan from the default policy (the ASR-throttle ratio, model-streaming-throttle ratio, andfallbackToTextAfterMsall live in that policy). - Status rollup.
computePsycheBackpressureStatusreduces the picture toPSYCHE_BACKPRESSURE_STATUSES=healthy/elevated/critical.
The fallback cascade#
When quality, latency, a provider failure, a kill-switch, a policy, or an
accessibility need degrades a modality, Psyche steps down a fixed cascade
rather than dropping the session. PSYCHE_FALLBACK_MODES
(fallback-routing.ts:55) = avatar → voice → text → unavailable, and the
triggers are PSYCHE_FALLBACK_TRIGGERS = provider-failure, quality,
latency, kill-switch, policy, accessibility.
resolvePsycheFallbackMode ranks the requested mode against current capability
and resolves the highest mode that can actually be served.
buildPsycheFallbackCascadePlan and planPsycheFallbackRenegotiation produce
the step-down plan and the user-facing decline message, and a fallback-engaged
event is emitted onto the stream so the degradation is visible and audited. The
kill switch has its own granularity: PSYCHE_KILL_SWITCH_SCOPES = session
/ tenant / region / global, with PSYCHE_KILL_SWITCH_FALLBACK_MODES =
voice / text / closed — so an operator can collapse all avatar synthesis
in a single region to text-only without taking text away.
Avatar lip-sync and expression coherence#
avatar-sync.ts makes "viseme alignment" concrete. PSYCHE_AVATAR_VISEMES is
an 11-viseme set (rest, ah, ee, oh, oo, mb, fv, ss, th, eh,
ay), and PSYCHE_AVATAR_VISEME_TABLE maps 39 ARPABET phonemes onto those
visemes (resolvePsycheAvatarViseme upper-cases the phoneme id and looks it
up). The pipeline is:
buildPsycheAvatarPhonemeTimelineorders phonemes into a timed sequence.interpolatePsycheAvatarPoselinearly interpolates between viseme poses so the mouth transitions smoothly rather than snapping.assessPsycheAvatarAlignmentmeasures the gap between the avatar's actual pose and the audio it should track, returningPSYCHE_AVATAR_ALIGNMENT_STATUSES=aligned/drifting/desynced— drift detection so the runtime knows when lip-sync has come unstuck.
Expression coherence is handled alongside in emotion-modulation.ts, and
screen-context.ts carries screen-share context for sessions that share a
screen.
Crisis-frame continuity — a first-class safety boundary#
crisis-frame.ts is a fully implemented module, not narrative. A crisis-frame
entry is a safety boundary, not a normal persona transition (its own
fileoverview says so): when Lilith policy takes over
for a member in distress, the runtime must break persona, stop synthetic output,
and suspend memory writes at the same timestamp.
enterPsycheCrisisFrame produces a new envelope and the audit/event trail in
one deterministic step:
- Sets
continuity.memoryScope → 'off',embodiment.syntheticVoice → falseandsyntheticAvatar → false, clears the voice/avatar pack ids, strips the synthesis capabilities, and moveslifecycleState → 'degraded'. - Emits a
policy-interventionevent onto the stream by actor{ kind: 'system', id: 'lilith' }. - Writes a
persona.breakaudit event and alilith.interventionaudit event, both stamping the triple actionPSYCHE_CRISIS_FRAME_AUDIT_ACTIONS = ['break-persona', 'halt-synthesis', 'suspend-memory-writes']. - Produces a synthesis-halt plan (the
tts/avatarpipeline services andvoice/avatar/videomodalities are removed) and re-validates the new envelope before returning.
The user-visible disclosure is fixed and honest: "I need to pause the persona voice and switch to direct safety support for this moment." The point is that a crisis cannot half-happen — persona break, synthesis halt, and the memory-write suspension are computed together, so Iris never records anything said inside a crisis frame.
Living Scenes integration#
The events/ subtree wires Psyche into Living Scenes:
events/scene-events.ts carries scene events, events/cue-plan-replay.ts
handles cue-plan replay, and events/frame-stream.ts is a real backpressure
channel for the render frame stream. frame-stream.ts ships a
DEFAULT_FRAME_STREAM_CONFIG, an evaluateBackpressure step, and
stepFrameStream/consumeFrame, with a high-water policy that drops
non-keyframes first when the channel saturates — which is how render envelopes
are kept inside Living-Scenes latency budgets without tearing the scene.
Maturity, honestly#
To restate the split so nothing is oversold:
- Real and rich (contract + logic): the 23-module adapter library — session
envelope and its lifecycle graph, the full event taxonomy and gap-free stream,
the server turn-state reducer, latency budgets and assessment, admission /
shed / provider backpressure, the fallback cascade and kill switch, the viseme
table and alignment drift detection, and crisis-frame continuity. Every claim
on this page maps to an exported symbol or constant, and the modules ship with
comprehensive
*.test.tssuites. - Spec-only / partially built (runtime): the live WebSocket transport
binding and the real ASR/TTS/avatar provider integrations (deliberately absent
from the pure-logic library), and the end-to-end "tutor live session to graded
record" flow — whose live-voice envelope, library write, and operator review
lane are noted as unbuilt surfaces. The service trees under
services/psyche/*,infrastructure/psyche/*, andapps/psyche/adminexist with substantive content but are the less-mature half. See the backlog (§11) for the live-session and operator-lane work that closes this gap.
Related#
- Lilith — Contemplative Policy Substrate
- Iris — Assistant Memory Substrate
- Living Scenes
- Sophia — Grounding Substrate
- Persona, Avatar, and Voice Packs
- Observability, Design System, Testing, and Performance
- Trust, Safety, and Privacy
- Subsystem Glossary
V1/features.md§ Psyche live-session event taxonomy and latency targets- Hub: V1 Architecture