Personality & Emotional Range

How Leif reads the room — response styles, automatic signal detection, register mirroring, repair, check-ins, and how the voice carries the same register.

Leif’s personality is fixed — direct, dry, a little sharp. What adapts is the delivery: a social-filter layer that reads the situation each turn and adjusts tone without changing identity. This page maps that layer end to end: the styles, the automatic detection that picks them, and the supporting systems (register mirroring, repair, check-ins, affect trends) that make the adaptation feel attentive rather than scripted.

Source of truth lives in superhitech/leif: src/utils/style_router.py, src/utils/response_styles.py, and the modules referenced per section below.

Response styles

Styles are prompt modifiers appended to the base personality. They can be set explicitly per message (/blunt review this) or per session (/style terse), or arrive automatically via signal detection.

StyleCommandsWhat changes
default/normal, /default, /resetStandard Leif
terse/terse, /brief, /shortFewest words, no filler
unfiltered/unfiltered, /blunt, /rawNo diplomatic padding
contrarian/contrarian, /push, /challengeStress-tests the idea
teaching/teach, /explain, /learnExplains the why
supportive/supportive, /encourageGrounded encouragement, leads with what’s working
gentle/gentleDistress mode: sarcasm off, acknowledge first, no fixes until asked
celebratory/celebrateReal win: match the energy for a beat before next steps
playful/playful, /banterBanter on: riff back, wit over density

The last three exist because the original set only modulated negative situations — a win got a firm handshake, never a high-five. Celebratory and playful stay in character: sarcastic delight and dry wit, not cheerleading.

Automatic signal detection

The style router (src/utils/style_router.py) classifies each user message — fast regex patterns first, an LLM classifier for ambiguous cases — and maps the detected signal to a style:

SignalRouted style
venting, overwhelmed, winsupportive
seeking validation (“am I crazy?“)contrarian
learningteaching
rushedterse
avoidanceunfiltered
distress (grief, shame, health, crisis)gentle
celebrating (big win, high energy)celebratory
playful (banter, lol/haha)playful

Three rules shape the routing:

  • Punctuation is arousal, not valence. Exclamation marks never create a signal on their own — “it works!!!” and “this is broken!!!” carry identical punctuation. They only amplify whichever direction the words point.
  • Styles decay at different rates. Auto-applied positive styles fade fast (celebration that lingers feels forced — 2–3 turns); gentle persists (8 turns / 30 minutes — no snapping back to sarcasm mid-grief); everything else decays after 5 turns / 10 minutes. Explicit choices never decay.

Register mirroring

A deterministic per-turn hint (src/utils/register_mirror.py) describes the user’s current register — short/long, playful/energetic/casual — so a one-line message gets a one-line reply and a detailed writeup earns depth. No LLM call; code-heavy and unremarkable messages produce no hint.

Conversational repair

When the conversation itself breaks down — an explicit “you’re not listening,” or a second consecutive correction — src/utils/conversational_repair.py injects a directive: restate the actual ask in one sentence, check it against the last two messages, and answer that, instead of repeating the previous approach louder. A single mild correction (“no, the other server”) is normal conversation and never triggers.

Proactive check-ins

At conversation start, the reflection service can surface at most one lingering item — the oldest stale pending decision or the most overdue commitment — phrased to ask once and drop it if deflected. Two hard limits keep it attentive rather than nagging: one check-in per 24 hours globally, and each item surfaces at most once, ever.

The memory synthesis cycle tags each window of recent conversations with an emotional tone (valence, energy, recurring states). A 14-day trend line is injected into the user context — “the tone has been mostly negative; recurring: stretched_thin” — so Leif can notice a two-week slump or a streak of wins instead of reading each message in isolation. A single data point is a mood, not a trend: two entries minimum. Entries age out after 28 days.

See Memory & State for where this sits among the other persistent layers.

Voice parity

The routed style leans the ElevenLabs TTS parameters per response, so the voice carries the same register as the text: celebratory and playful speak brisker and more expressive, gentle slower and steadier. Deltas apply on top of the env baseline and are clamped (src/utils/tts_style_map.py).

Kill switches

Every adaptive behavior is env-gated (default on) and can be disabled without a code revert:

Env varDisables
STYLE_EXPANDED_SIGNALS=falseAuto-routing of distress/celebrating/playful (explicit commands still work)
STYLE_REGISTER_MIRROR=falseRegister mirroring
LEIF_REPAIR_DETECTION=falseConversational repair directives
LEIF_PROACTIVE_CHECKINS=falseConversation-start check-ins
AFFECT_TRENDS_ENABLED=falseAffect tagging and trend injection
VOICE_TTS_STYLE_MAP=falsePer-style TTS adjustments
  • Memory & State — the persistent layers the affect log and check-ins build on
  • Architecture — where the unified handler and voice pipeline sit