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.
| Style | Commands | What changes |
|---|---|---|
| default | /normal, /default, /reset | Standard Leif |
| terse | /terse, /brief, /short | Fewest words, no filler |
| unfiltered | /unfiltered, /blunt, /raw | No diplomatic padding |
| contrarian | /contrarian, /push, /challenge | Stress-tests the idea |
| teaching | /teach, /explain, /learn | Explains the why |
| supportive | /supportive, /encourage | Grounded encouragement, leads with what’s working |
| gentle | /gentle | Distress mode: sarcasm off, acknowledge first, no fixes until asked |
| celebratory | /celebrate | Real win: match the energy for a beat before next steps |
| playful | /playful, /banter | Banter 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:
| Signal | Routed style |
|---|---|
| venting, overwhelmed, win | supportive |
| seeking validation (“am I crazy?“) | contrarian |
| learning | teaching |
| rushed | terse |
| avoidance | unfiltered |
| 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.
Affect trends
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 var | Disables |
|---|---|
STYLE_EXPANDED_SIGNALS=false | Auto-routing of distress/celebrating/playful (explicit commands still work) |
STYLE_REGISTER_MIRROR=false | Register mirroring |
LEIF_REPAIR_DETECTION=false | Conversational repair directives |
LEIF_PROACTIVE_CHECKINS=false | Conversation-start check-ins |
AFFECT_TRENDS_ENABLED=false | Affect tagging and trend injection |
VOICE_TTS_STYLE_MAP=false | Per-style TTS adjustments |
Related pages
- Memory & State — the persistent layers the affect log and check-ins build on
- Architecture — where the unified handler and voice pipeline sit