Proposed — pending review · 2026-08-13 · Fede via Claude Code · sources: five deep audits of the repo, git history, live ElevenLabs configs, 301 recorded calls, and industry research (21 cited sources)
One Clara: a cross-channel architecture
How we stop building every behavior twice, and make Clara provably the same person on the phone, over text, and in email.
The goal
Build each behavior once, in one shared core. Every channel adds only a thin delivery layer — voice speaks briefly, SMS keeps it to a few sentences, email writes a fuller reply. Feature parity is part of the goal: a capability that exists on one channel exists on all of them unless we deliberately decided otherwise. Consistency is enforced by construction and proven by a harness built from our archive of real conversations.
Strategy in one paragraph: first build the measurement (a replay-first, cross-channel harness from past interactions — valuable even if nothing else ships), then stop the bleeding (shared modules for the worst-duplicated voice rules), then unify domain-by-domain using the pattern that already works in this codebase (renewals runs one shared rule body rendered per channel today). No big-bang rewrite; every phase is independently valuable and gated by the harness.
Why now
A single bad prospect call on Aug 13 surfaced four defects; tracing them exposed the architecture. Every one of them was a consistency failure: a rule or fix that existed in one place and not another.
And this doc has a sibling being built in parallel tonight: the escalation journey rethink, whose own audit independently named "channel as an architectural boundary" a first failure of framing — its channel-fork audit found ten landmines where knowledge or behavior exists on one lane and not another — most never decided, just forgotten — and callers experience the gaps as Clara lying: utilities answered on a call Aug 5 but "not on file" on Jul 17; parking answered correctly by email, transferred on voice. That doc's escalation redesign (the decision ladder with a deterministic rail "identical on every channel," and the teaching loop that turns each escalation into a knowledge entry) assumes the shared core this doc proposes — the two plans are halves of the same move: one behavior store, one knowledge store, every lane a renderer.
11 + 1
hand-maintained voice prompts + one text/email rulebook, zero shared prose between any two — ever
~90k
tokens of duplicated voice-prompt English (≈360k chars, measured)
~7 of 8
prompt commits since the May fork touched only one side — each is a drift event
51%
of recent Camellia agent-to-agent transfers open with a redundant second acknowledgment
Where we are (measured, not vibes)
Text + email — already the right shape
Clara personality (shared)
One specialist body, chosen by identity (leasing / maintenance / turnover / renewal)
Per-property fact sheet (shared with voice)
Delivery layer: SMS ≤3 sentences · email fuller
Outbound safety gate (fair-housing) before every send
Voice — 11 independent forks
Triage · Leasing · Maintenance ×2 · Lease & Billing · Renewal ×2 · Turnover · Unknown Caller · Emergency · Vendor
Each: own persona, own transfer etiquette (3 divergent generations), own closing rules
Per-property fact sheet (the one thing shared)
Settings split: 5 surfaces synced from git, the rest dashboard-only
No safety gate on anything Clara says aloud
The fork, in numbers
- Sharing was real once. One shared leasing rulebook served text and voice for 73 days (Mar 10 → May 22). The sibling-agents launch seeded hand-written voice prompts instead of deriving them; the shared path was deleted three days later. The code comment claiming voice shares the text rules has been false for ~15 months.
- Since then: ~77 commits touched the two leasing rulebooks; only ~1 in 8 touched both. Concrete costs: the vacant-unit-count secrecy rule was written three times over 30 days before all channels had it; the buzz-code safety default has been text-only for a month; the no-repeating rule was mirrored to voice and deliberately reverted the next day ("no eval coverage") — a drift test then pinned its absence.
- The copies contradict each other on the same call. One agent's prompt recommends bridging a transfer with "hang on a sec"; the agent it transfers to bans that exact phrase. Hang-up etiquette diverged: several agents hang up on "I'm good", Leasing was taught not to. The fence test that should catch this silently checks 1 of 5 agents (stale agent names — bug card filed).
- Both July prompt diets fully regressed. Triage is ~18.6k tokens today vs ~5.4k post-diet (2.3× the size that triggered the diet); Leasing ~14.4k vs 11.4k. Nothing fences prompt size, so latency work quietly evaporates (bug card filed).
- Tool descriptions are a second, unreviewed rulebook. 7 of the leasing voice agent's 20 tools are never mentioned in its prompt; four actively contradict it (the email-ask bug was exactly this class). Prompts also command tools that don't exist on their channel — including one agent instructed to hang up with a tool it doesn't have.
- Evals are asymmetric and mostly non-blocking. No eval workflow is a required check. The 315-scenario voice harness — our only genuinely multi-turn suite — runs with a flag that makes failure impossible. Half of text eval cases are single-shot; only 7 cases in the estate replay a real incident across turns. Rules with zero eval pinning include gate-code withholding (where voice currently instructs the opposite of text — decision card filed).
Full evidence lives in the five audit reports (architecture, git archaeology, prompt quality, eval quality, transfer seams) — session 2026-08-13; operational bugs already filed as Trello cards.
What the industry does (research, 21 sources)
- One base rulebook + variables, never parallel prompts — OpenAI's agents guide; Anthropic's agent-patterns guidance ("simple, composable patterns"); ElevenLabs' own architecture blog warns against over-splitting and notes every mid-call agent hop costs latency and risks conflicting decisions.
- Prompt "partials" are a solved problem — Google Dotprompt partials, LangChain pipeline prompts, OpenAI's shared handoff-prompt prefix. The voice layer must be able to subtract, not just add: ElevenLabs says prompts over ~2k tokens cost latency and money on every turn.
- The platform will never dedupe for us — ElevenLabs docs: on agent transfer the child inherits nothing of the prompt. Dedup must happen at build time, before sync. Turn-taking is platform config, not prompt prose — any turn-taking text in our prompts is dead weight.
- Cross-channel consistency testing has a name and a target — identical queries to every channel, semantic-equivalence scoring, >95% consistency threshold in CI (Hamming's multi-modal testing method). Vendors (Sierra, Decagon) sell "one agent, every channel" as the product promise — it's a defensible external claim once we can prove it.
- Prompt-as-code with eval-gated merges — immutable versions, labels, CI that fails on golden-set regression (Langfuse/Braintrust patterns). We already have the sync half; the gate half is currently soft.
Target architecture
One shared behavior core (source of truth)
Clara personality + judgment rules (fair housing, safety, honesty, escalation, no-repeat, contact policy)
Domain bodies: leasing · maintenance · renewals · turnover · billing (the renewals pattern, generalized)
Capability registry: every feature × channel, gaps are explicit decisions
Per-property knowledge plane (already shared — extended, e.g. guarantor/application process)
Thin channel renderers (build-time)
Voice: brief + spoken style layer, subtracts text-only sections, per-specialist delta; output = the synced ElevenLabs prompts
SMS: ≤3 sentences, one action
Email: fuller prose, no brochure
All settings config-as-code with drift fences (turn timing, voices, transfer flags — no dashboard-only dials)
Design principles (each traces to a scar or a measurement):
- Identity routes before the conversation starts. We know the caller before Clara speaks; a known tenant lands on the maintenance specialist directly instead of an audible mid-call transfer. The front door shrinks to unknown callers and intent changes. (Text already works this way.)
- Transfers are invisible when they do happen. One shared hand-off module — silent seam between agents, one warm word to a human — imported everywhere, with the receiving agent's first utterance graded by evals (never tested today; fix card filed).
- Composition is server-side, order-preserving, cache-friendly. ElevenLabs template variables can't express conditionals (shipped a prod bug); prompt position is load-bearing (a rail that works at the end demonstrably failed mid-prompt); shared core goes first for prompt-cache stability.
- Not all redundancy is redundancy. A worked example that pins a specific past failure is kept even when an abstraction seems to cover it — cutting three "redundant" examples re-opened a live incident within days in July.
- Budgets are fenced, not remembered. Every generated voice prompt carries a token budget; growing past it without an explicit bump fails CI. (Both diets regressed invisibly because no fence existed.)
Phase 0 — the harness (the gold mine)
Built primarily from real past interactions: the full ElevenLabs call archive, every SMS/email thread in the database, and the curated corpora (434 graded triage calls, 41 labeled turnover calls, the renewals bug corpus, incident calls including Aug 13's). ElevenLabs' test framework natively generates regression tests from past conversations and supports starting a simulation mid-conversation — a real call becomes a replayable test without hand-writing it.
- One scenario format, three drivers — voice via the ElevenLabs simulation harness (how we test voice today), SMS and email via the text agent loop on the same fixtures.
- Two grades per scenario — substance must match across channels (semantic-equivalence diff of claimed facts; target >95%), form graded against each channel's delivery contract.
- Capability parity matrix — every feature × channel: full / partial / missing / intentionally-absent. Every gap becomes a decision on a list, never a surprise on a live call.
- Budgets recorded per run — voice latency percentiles and prompt token sizes, so "unified" can never quietly mean "slower."
- Baseline before any change — the full grid runs on today's system first; that scoreboard is the before/after proof for everything that follows. Subscription runners only; pinned to the test property; report-only until proven, then promoted to blocking.
Phases 1–3 (each independently shippable)
| Phase | What ships | Effort (estimate) | Proof |
| 0 · Harness | Replay-first cross-channel grid + baseline scoreboard + parity matrix; flip the voice suite's soft gate | ~1–2 weeks of fleet work | Baseline report published as a PropFlow doc |
| 1 · Shared voice modules | Extract the worst-duplicated blocks (hand-off etiquette ×7, language ×7, AI disclosure ×8, closing rules ×5) into shared modules composed at build time into the same synced strings. Kills the on-call contradictions. No new engine, no sync changes. | ~1 week | Harness diff vs baseline; transfer-seam rate drops from 51% |
| 2 · Domain unification | One domain at a time onto the renewals pattern (shared body + channel renderers). Suggested order: renewals (3 sources of truth today → 1), then leasing, then maintenance. Tool schemas reviewed with prompts (they're one rulebook). | ~2–4 weeks per domain | Consistency score per domain; latency + token budgets hold |
| 3 · Full compiler + hard gates | All prompts (11 voice + text + email) are build artifacts of the core; eval workflows become required checks; prompt-size ratchets; nightly consistency report over live traffic | ~2–3 weeks | >95% cross-channel consistency in CI, sustained |
Efforts are estimates, not measurements. The Aug 13 fix fleet (name-only tours, email persistence, 7-second silence, repetition fix) ships independently and first — its scenarios seed the harness.
Decisions
Pick one per question — answers save on this page and are shared with everyone who opens it.
D1 — How far do we take the architecture?
D2 — Do we consolidate the 11 voice specialists?
D3 — When do evals start blocking merges?
Risks & constraints (the scars this plan respects)
- Template variables can't branch — all conditional logic lives in our injection code, never ElevenLabs template syntax (already caused one prod bug).
- Position is semantics — the composer preserves block order; a rail that must render last, renders last (proven by eval regression).
- Latency is a product feature — voice renderers subtract; budgets are ratcheted in CI; the July diets' fate is the cautionary tale.
- Examples that pin incidents survive dedup — July's diet-regression incident is the precedent.
- Sync stays one-writer — the existing five sync scripts and drift tests keep their contracts; renderers feed them, never bypass them. Live branch/A-B copies remain outside sync and are handled per the existing A/B doctrine.
- Eval-gate globs must move with the code — the "prompt change requires an eval change" check matches file paths; a refactor that relocates prompts without updating it silently disarms the gate.
Related work in flight
- Escalation journey rethink — sibling doc, in progress tonight: the channel-fork landmines (L1–L10), the "one knowledge store consulted by every lane" principle, and the escalation→knowledge teaching loop. The guarantor/application-process knowledge entry decided on Aug 13 is that loop's pattern applied by hand: a question Clara free-styled becomes a verified per-property fact all channels answer from.
- Aug 13 fix fleet (independent, ships first): name-only virtual tours, volunteered-email persistence, 7-second silence timeout under config-as-code for all agents, first-mention-only specials.
- Voice humanness recon (Sean's "more human" ask): three-lens investigation running; companion doc to follow — its recommendations layer cleanly on this architecture (the shared voice-style module is where "more human" gets written once).
- Trello cards filed from the audits: iron-law fence stale names · prompt diets regressed (add size ratchet) · gate-code decision (D5 here) · soft-gate flip (D3 here) · transfer-seam fix · renewal outbound opt-out + unknown-caller hang-up + unit-number variable · vendor-outbound prompt ships nowhere.