Personas

Werbos's voice is composed, not hardcoded. Three rungs: the dialed default, the always-full character, and the named opt-ins (tom, html, ...).

A persona is a small markdown file that shapes how werbos sounds in a session. Personas are layered, not flat — what the model sees as its system prompt is composed at session start from up to three pieces:

character    →  the always-full 13%  (werbos's own taste, hard rules)
+ voice      →  the dialed 87%       (the persona that takes the mirror slider)
+ user.md    →  who YOU are          (operator profile from `werbos init`)
= the system prompt's persona section

The mirror slider (Sprint F.2) controls the blend ratio at the character / voice line. The user-profile rung was added in Sprint A.5 day 3.

Where personas live

Two locations:

werbos/personas/<slug>.md     # ships in the package — load by slug
~/werbos/personas/<slug>.md   # operator-owned — load by file path

Packaged personas (the slugs that ship with werbos):

SlugRoleLoaded via
defaultVoice baseline. The dialed 87%.implicit (no flag)
werbos-characterThe always-full 13%.implicit + --character-file to override
tomMemoryless persona (anterograde-amnesia primitive). Replaces both default and character entirely.werbos chat --tom
htmlSingle-file-HTML expert (Sprint Sk-day-8).werbos chat --persona-name html

How to swap the voice

Four CLI surfaces, in precedence order (later wins over earlier when both are set):

werbos chat                              # 1. built-in default (lowest precedence)
werbos chat --persona "be a pirate"      # 2. literal string override
werbos chat --persona-name html          # 3. load packaged persona by slug
werbos chat --persona-file ~/x.md        # 4. load arbitrary file (highest precedence)

Why --persona-name and --persona-file are both surfaces. Once werbos is pip install-ed, the package data lives inside a wheel; there’s no obvious filesystem path for --persona-file to point at. --persona-name html resolves through werbos.personas.load_persona("html") which works whether werbos is running from source or from a wheel. --persona-file stays for operator-authored personas in ~/werbos/personas/ or anywhere else on disk.

Unknown name fails loudly:

$ werbos chat --persona-name nope
--persona-name 'nope': no packaged persona by that name. Available
personas live in werbos/personas/*.md.

The mirror slider — character vs voice

The blend ratio between the always-full character (13%) and the dialed voice (87%) is the mirror slider:

werbos chat --mirror 87       # baseline best-friend (default)
werbos chat --mirror 0        # pure character — no voice mirroring
werbos chat --mirror 100      # full voice — character still appears, voice at full strength

Inside the REPL:

werbos> /mirror               # show current mirror %
werbos> /mirror 50            # change persistently for this session
werbos> /mirror 30 disagree with me on this
                              # one-turn override; resets after the AI replies

The character layer is never scaled by the slider — at --mirror 0, the voice persona is omitted entirely but the character file still anchors the session. The slider scales the voice’s strength, not the character’s existence.

The mirror also persists per-chat in the .pfm archive (SessionMeta.mirror_ratio) and in localStorage in the SPA, so resumed chats remember their setting.

The user profile — who YOU are (Sprint A.5 day 3)

werbos init writes a six-section template at ~/werbos/user.md:

~/werbos/user.md
├── # Background          (career, projects, what you're building)
├── # Working style       (how you like to work)
├── # Communication       (terse / verbose, hedging tolerance, ...)
├── # Recurring contexts  (codebases, domains you live in)
├── # Anti-scope          (things werbos should NOT do for you)
└── # Evolving notes      (things in flux this week/month)

Fill it in. ContextBuilder hydrates it on every session AFTER the persona and project_context, BEFORE history. A KIND_USER_PROFILE_LOADED event records {path, sha256, byte_size} into the chat archive — metadata only, never the body. Same shape as Tom’s KIND_TOM_ATTESTATION: auditors get an envelope they can verify without the contents leaking.

16 KB cap; over-cap returns a truncated profile + a warning flag.

The Tom rung — memoryless by design

--tom replaces both the character and the voice entirely with a single memoryless primitive. Every turn, the model receives:

  1. werbos/personas/tom.md (the persona)
  2. The notebook (~/werbos/tom_notebook.json, 4 KB cap) if non-empty
  3. The current user message

That’s it. No prior turns. No memory bank. No tools except notebook_write. LEASH_EPHEMERAL enforces “shell forbidden by design” regardless of --shell-yolo.

werbos chat --tom

Each Tom turn emits a KIND_TOM_ATTESTATION event proving history_length: 0. Auditors grep archived .pfm files for the event kind and verify the value. The signature of build_tom_history(*, system, notebook, current_turn) cannot accept prior turns — type-level enforcement of the no-leak claim, not just a runtime check.

The Tom rung is the floor of the memory ladder (K.0 → K.1 → K.2 → K.3 = no memory → repo context → user memory bank → proactive surfacing). The 10-second-Tom consumer wedge (“Hi, I’m Tom”) doubles as the marketing surface for a B2B zero-retention agent (HIPAA / multi-tenant / injection containment).

The HTML rung — voice-layer specialization

--persona-name html swaps the dialed voice with a character that treats every HTML/CSS/JS task as a chance to ship a complete, runnable, single-file artifact:

werbos chat --persona-name html --vision-model llama3.2-vision:11b

Style mined from werbos/ui/public/tom.html (the canonical single-file reference in this repo). Pins:

  • Single-file HTML — markup, styles, script all inlined
  • No build pipeline, no package.json, no React, no transpiler
  • CSS custom properties at :root; system fonts; semantic markup
  • textContent for user data (never innerHTML)
  • Head-comment + script-comment discipline

Anti-patterns named explicitly: no Vite, no Tailwind CDN, no localStorage by default, no speculative abstractions. The character + user.md still apply — --persona-name swaps the voice slot, not the whole stack.

File format

Persona files are markdown with an HTML-comment header:

<!--
version: 0.1.0
created: 2026-05-09
updated: 2026-05-09
tuned-for: any base model. Voice-layer persona; pair with the
           default werbos-character.md for the always-on 13%.
role: One-line description of when this persona applies.
notes: Pinning rationale, source of style data, gotchas.
-->

You are Werbos in HTML-expert mode.

[full persona body]

The HTML-comment block is metadata for human readers + future tooling; werbos doesn’t parse it. The body is what gets injected as the persona section of the system prompt.

Writing your own

  1. Decide which slot you’re filling: voice or character.
  2. Drop the file at ~/werbos/personas/<slug>.md (or werbos/personas/<slug>.md if you want it to ship in the package).
  3. Use a similar comment-header to the packaged personas — versioned, tuned-for, role, notes.
  4. Use second person (“You are…”, “You don’t…”) and at least one specific don’t per persona. The tom.md and html.md files are good references.
  5. Test:
    werbos chat --persona-file ~/werbos/personas/myslug.md
    

The runtime never validates the body; the model is the only judge. Iterate until the responses feel right.

See also

  • Chat surfaces — /mirror, /private, /notebook, /skill, every other slash.
  • Skills — knowledge-injection alongside the persona; skills append to the system prompt as a SystemMessage AFTER the persona.
  • Vision — /see slash + the --vision-model flag pair, useful with --persona-name html.