v0.5 (Live Assist — on-the-job voice companion) milestone complete. 4 phases: P0 (pre-execution, v0.1.10) → P1 (assist core + guardrail, v0.1.11) → P2 (integration + tech-debt + NFR, v0.1.12) → P3 (final review + ship, v0.1.13 = milestone release). 16/16 REQs covered (3 ASSIST + 4 NFR + 9 IDEATE). 4 v0.6 backlog. 469 tests passed, 0 failed. 1 P0 fixed (guardrail processor safety). 8 P1+ flagged for v0.6. 8 v0.4 P1+ tech-debt addressed. G-049 + G-067 grill MUSTs resolved. ESCALATION-01 (PIPEDA) OPEN for human legal review before assist surface go-live. ---ci--- project: praxis phase: 3 milestone: v0.5 status: complete requirements: covered: [REQ-ASSIST-01, REQ-ASSIST-02, REQ-ASSIST-03, REQ-NFR-ASSIST-01, REQ-NFR-ASSIST-02, REQ-NFR-ASSIST-03, REQ-NFR-ASSIST-04, REQ-IDEATE-01, REQ-IDEATE-02, REQ-IDEATE-03, REQ-IDEATE-04, REQ-IDEATE-05, REQ-IDEATE-06, REQ-IDEATE-07, REQ-IDEATE-08, REQ-IDEATE-09] partial: [] ---/ci---
76 KiB
Praxis — Research Findings (v0.5 Live Assist — On-the-Job Voice Companion)
Phase: v0.5 research (Live Assist) Branch:
phase/00-pre-executionStatus: research complete — pending orchestrator review Date: 2026-08-04 Method: Codebase inspection (server/pipeline.py,server/guardrails/,server/session_recorder.py,server/cohort/aggregator.py,server/services/base.py,server/__main__.py,db/migrations/,db/pg_migrations/), prior research (.ciagent/RESEARCH.mdv0.1/v0.2/v0.3,.ciagent/RESEARCH-v0.4-operator-tier.md), D-058..D-063 CLARIFY decisions. Web-verified: Picovoice Porcupine FAQ + general FAQ + Android quickstart (fetched 2026-08-04), Vosk toolkit (alphacephei.com), RealWear (realwear.com). Domain-knowledge claims (LLM guardrail patterns, on-the-job coaching AI products) carry explicit confidence scores.
This document grounds the v0.5 Live Assist architecture in ecosystem evidence. It covers all 6 research questions, validates the CLARIFY decisions D-058..D-063 against real-world evidence, and concludes with a consolidated risks table, an NFR refinement, and a persona-roster decision.
Summary of Findings (Executive 1-Pager)
-
Picovoice Porcupine is the right wake-word engine, but the free-tier assumption in D-058 needs refinement. (0.78) Porcupine is on-device, offline, low-power (~1 MB RAM, <4% of one core on RPi 3 — verified via Porcupine FAQ), accent-robust (universal, not voice-personalized), supports custom wake words trained via Picovoice Console, and ships an Android SDK (verified — quick-start page exists). However, the Picovoice general FAQ (fetched 2026-08-04) states: Porcupine is priced on monthly active users (MAU), there is a one-time Free Trial (not a recurring free tier), and "Picovoice is a B2B company focused on on-device AI tools for enterprises. At this time, there are no dedicated free or paid plans for personal or non-commercial use." This refines D-058: the "free-tier supports custom wake words" framing is too optimistic for a recurring pilot — Praxis needs to either (a) negotiate an educational/pilot tier with Picovoice sales, (b) budget for MAU-based pricing in the pilot, or (c) ship a built-in Picovoice wake word (no custom training, falls under the trial) for v0.5 and add custom training later. Flag for orchestrator: D-058 free-tier assumption is partially contradicted.
-
3-layer guardrail (D-060) is the correct pattern and matches industry practice. (0.85) Prompt-layer rules + output-filter patterns + audit logging is the standard defense-in-depth for LLM safety. The existing
CustomerServiceGuardrail(server/guardrails/customer_service.py, verified) already implements pattern-based output filtering (regex for legal/financial/medical advice + impersonation). v0.5 extends this with Live-Assist-specific patterns: detect "you should say X" / "tell the customer Y" / "the answer is Z" (direct-answer patterns) vs "what do you think the customer needs?" / "how could you acknowledge their frustration?" (coaching-question patterns). The output filter is a regex + keyword classifier on the LLM response before TTS; on hit, the response is either rewritten to a coaching redirect or blocked + re-prompted. Audit log = the existingturnstable (SQLite) extended with aguardrail_verdictfield; assist turns also flow to the v0.4 cohort aggregation assession_type=assistfor operator visibility. -
<600ms latency budget (C-8, D-061) holds for assist turns IF context-binding stays off the voice path. (0.80) The v0.1 budget breakdown (ARCHITECTURE.md): WebRTC ~50ms + Deepgram ~250ms + LLM ~200ms + Cartesia ~120ms + downlink ~50ms = ~670ms (marginally over). Adding context-binding tokens (path week, scenario tag, learner state) to the LLM system prompt adds prompt-processing latency, not network latency — ~50-200 extra input tokens on
gemma4:cloud(256K context, so no context-window risk). At ~50ms per 100 input tokens of prefill latency, 200 extra tokens ≈ +100ms to first-token. This pushes the all-cloud path to ~770ms — breaks C-8. The mitigation: (a) keep context-binding tokens minimal (≤100 tokens: path week, scenario id, one-line coaching focus — not the full rubric), and (b) use the Piper-on-pilot-server TTS path (R4 mitigation from v0.1, ~80ms TTS instead of ~120ms Cartesia) which the architecture already pre-stages. With Piper: ~50 + 250 + 200 + 80 + 50 + ~50 (prefill for ~100 context tokens) = ~680ms — still marginal. Recommendation: assist turns use a leaner system prompt than practice turns (assist = coaching questions only, no role-play character persona), targeting ≤150 input tokens total system prompt. This keeps prefill under 75ms and the total under 600ms with Piper. Confidence 0.70 — prefill latency for gemma4:cloud is not yet measured (R3 from v0.1); Phase 1 must measure. -
Shift-bounded session model (D-062) matches real on-the-job coaching patterns. (0.80) Real on-the-job coaching AI products bound sessions by work shifts or discrete interactions, not continuous always-on streams. Dialpad Ai Coach and Gong (industry knowledge, 0.65 confidence — vendor pages returned 404 on direct fetch; claims based on widely-documented product behavior) analyze call recordings post-hoc, not live-in-ear. RealWear (verified realwear.com) is hands-free AR glasses for frontline workers — visual + voice, industrial, hardware-first; not a phone-in-pocket voice companion. No direct competitor does "live-in-ear coaching during real customer calls on a $100 Android phone." This is Praxis's novel surface. The shift-bounded model ("I'm starting my shift" / "ending shift") gives a clean aggregation boundary + matches how retail/hospitality workers actually work (shifts are the unit of labor). Within a shift, each assist turn is a discrete coaching exchange (≤30s). Assist turns aggregate as
session_type=assistalongsidesession_type=practicein the v0.4 cohort pipeline. -
v0.1 voice pipeline reuse is minimal-delta. (0.85) The pipeline (
server/pipeline.py) is parameterized byscenario_idand builds aScenarioRuntimewith a system prompt + opening line. v0.5 adds an "assist mode" alongside the practice scenario loop: the samebuild_pipeline()is called with a newmode="assist"parameter (or a distinctbuild_assist_pipeline()) that swaps the system prompt (coaching persona, not role-play character), drops the opening line (assist is invoked mid-shift, no scripted opener), and injects context-binding (path week, scenario tag). The Deepgram/Cartesia/Piper/Ollama services are reused unchanged — no new voice-service deps. TheSessionRecorder(verified — 390 lines) is extended with anassistsession type; the_build_session_outcome()method (line 164) already builds the dict the cohort aggregator consumes — v0.5 adds asession_typefield. Minimal delta: ~1 new pipeline builder, ~1 new guardrail ruleset, ~1 new session-type field, ~1 new aggregation metric. -
Cohort aggregation integration (D-062) is a clean extension of the v0.4 pipeline. (0.85) The
aggregator.py(verified — 230 lines) upserts cells keyed by(path, metric, window_start). v0.5 adds assist-specific metrics:assist_turns_count,assist_active_learners_count,assist_avg_turns_per_shift,assist_guardrail_block_rate(how often the output filter fired — a safety signal for operators). These are newmetricstrings in the samecohort_aggregatestable — no schema change. The on-session-end hook (server/cohort/hook.py) is extended to acceptsession_type=assistoutcomes; assist shifts fire the hook on shift-end (not per-turn — per-turn is too granular and would double-count). k-anonymity ≥ 10 applies identically. Operators see assist usage patterns alongside practice patterns in the same dashboard views (D-053's 3 views extend naturally: practice volume becomes practice+assist volume, failure patterns gain an "assist guardrail blocks" breakdown). -
Picovoice Porcupine vs alternatives: Porcupine wins on Android integration + custom wake-word training; Vosk is the open-source fallback. (0.80) Vosk (verified alphacephei.com) is an offline ASR toolkit (20+ languages, runs on Android, 50MB models, pip-installable) — it's a full ASR, not a dedicated wake-word engine, but can do keyword spotting with a constrained vocabulary. Vosk is free/open-source (Apache 2.0) and offline. Trade-off: Porcupine is purpose-built for wake-word (lower CPU, faster detection, custom-trained models) but MAU-priced; Vosk is free but heavier (full ASR model loaded) and wake-word detection is a byproduct, not a primary feature. Snowboy is deprecated (acquired by Baidu, abandoned). On-device TensorFlow Lite wake-word is a build-it-yourself path (too much engineering for v0.5). Recommendation: Porcupine for v0.5 (pilot-tier MAU pricing or built-in wake word), Vosk as the documented fallback if Picovoice pricing blocks the pilot.
-
Persona roster for v0.5: 4 active (lead-developer, voice-engineer REACTIVATED, backend-engineer, security-engineer RETAINED, data-engineer RETAINED), 2 deactivated (devops-engineer, frontend-engineer). (0.85) v0.5 is voice-pipeline-heavy (wake-word + assist mode + latency tuning) + safety-critical guardrails + cohort aggregation extension. No deploy changes (v0.4 LXC carries forward) → devops-engineer deactivates. No new UI (wake-word is audio, assist is invoked by voice; the existing React app may need a small "assist mode" toggle but that's voice-engineer + backend territory, not a full frontend surface) → frontend-engineer deactivates unless the orchestrator decides an assist control surface is needed. See §7 for the full roster.
Domain 1: Wake-Word Invocation on $100 Android (D-058, REQ-NFR-ASSIST-02)
1.1 Picovoice Porcupine on Android — verified capabilities
Sources: Picovoice Porcupine FAQ (https://picovoice.ai/docs/faq/porcupine/, fetched 2026-08-04), Porcupine Android quick-start (https://picovoice.ai/docs/quick-start/porcupine-android/, fetched 2026-08-04), Picovoice general FAQ (https://picovoice.ai/docs/faq/general/, fetched 2026-08-04).
Finding (0.82): Porcupine Wake Word is an on-device, offline keyword-spotting engine. Verified capabilities relevant to Praxis v0.5:
- Android SDK exists (quick-start page confirmed at
/docs/quick-start/porcupine-android/). Also: React Native SDK (relevant if v0.5 upgrades the client from React web to React Native — currently v0.1 is React + WebRTC per D-015). - On-device + offline. No cloud round-trip for wake-word detection — critical for C-8 latency and for privacy (the mic isn't streaming to a cloud when listening for the wake word).
- Low resource. Per Porcupine FAQ: "The standard model uses about 1 MB of memory and less than 4% of a single core on a Raspberry Pi 3." On a $100 Android phone (typically a quad-core 1.4-2.0GHz Cortex-A53, 2-3GB RAM), this is negligible. Battery impact is minimal — Porcupine is a lightweight neural net, not a full ASR model. The FAQ also notes: "Porcupine Wake Word is a lightweight engine with minimal consumption and requirements."
- Custom wake words. Per FAQ: "You can train custom wake words with Porcupine on Picovoice Console, in seconds." This supports a Praxis-branded wake word (e.g., "Hey Praxis" or "Hey Coach"). Custom training is done on Picovoice Console (web UI), produces a
.ppnmodel file bundled with the app. - Accent-robust + universal. Per FAQ: "Porcupine Wake Word detection software is universal and trained to work with a variety of accents and people's voices." Canadian English is well within Porcupine's trained distribution (English is a supported language — verified).
- Background mode. Per FAQ: "Developers have been able to successfully run Porcupine Wake Word detection software on iOS and Android in background mode. However, this feature is controlled by the operating system, and we cannot guarantee that this will be possible in future releases of iOS or Android." Risk: Android background-mic access is OS-controlled and has tightened in recent Android versions (Android 14+ requires foreground service with mic type for background audio). Praxis v0.5 likely needs a foreground service (persistent notification) for wake-word listening while the phone is in pocket. This is a known Android pattern (used by "Hey Google", Shazam, etc.) — feasible but adds UX surface (notification) + battery.
- Multi-language. English, French, German, Italian, Japanese, Korean, Mandarin, Portuguese, Spanish. Canadian English + (future) Canadian French are covered.
Confidence 0.82 — vendor docs verified; the Android background-mic caveat is documented but the exact Android-version behavior needs a Phase-1 spike.
1.2 Picovoice pricing — the free-tier concern (D-058 refinement)
Finding (0.75): Per the Picovoice general FAQ (fetched 2026-08-04):
- Porcupine is priced on monthly active users (MAU). A "user" is "typically a unique device, app, or browser instance that initializes the engine within a 30-day period."
- There is a Free Trial ("No credit card is required. You can sign up at this link.") but it is a one-time offer, not a recurring free tier: "the Free Trial is a one-time offer, and it doesn't renew automatically once the trial ends."
- "Picovoice is a B2B company focused on on-device AI tools for enterprises. At this time, there are no dedicated free or paid plans for personal or non-commercial use."
This partially contradicts D-058's framing ("free-tier supports custom wake words"). The Free Trial allows custom wake-word training and evaluation, but a recurring pilot (v0.5 ships and runs for weeks/months) would exhaust the trial and require a paid MAU plan. Praxis is not a personal/non-commercial user — it's a B2B pilot — so Picovoice sales engagement is the expected path.
Resolution options for D-058 (flag for orchestrator):
(a) Engage Picovoice sales for a pilot/educational tier (RECOMMENDED). Praxis is a Canada pilot for an educational/upskilling product — a natural fit for a Picovoice pilot-tier or educational discount. The MAU pricing for Porcupine at small scale (tens of devices) is typically modest. This is the cleanest path but requires a vendor conversation before v0.5 ships.
(b) Use a built-in Picovoice wake word (not custom) for v0.5. Porcupine ships built-in wake words (e.g., "Picovoice", "Alexa", "Hey Google", "Terminus", "Blueberry", "Grapefruit", "Bumblebee"). These may fall under different terms than custom-trained models. The Praxis pilot could use "Bumblebee" or "Grapefruit" (unusual enough to avoid false triggers in a retail environment) without custom training. Reduces cost but loses the Praxis brand.
(c) Use Vosk as the wake-word engine (open-source fallback). Vosk (Apache 2.0) is free, offline, runs on Android. Wake-word detection = run Vosk with a constrained grammar containing only the wake phrase. Heavier than Porcupine (full ASR model loaded, ~50MB) but no MAU cost. Trade-off: free but more battery + CPU + engineering effort.
Recommendation: pursue (a) in parallel with (b) as the fallback. Ship v0.5 with a built-in wake word (option b) if Picovoice sales engagement isn't resolved by ship date; switch to a custom Praxis wake word (option a) when the pilot tier is negotiated. Document option (c) as the post-pilot cost-reduction path if MAU pricing is unsustainable.
Confidence 0.70 — the pricing concern is real (verified); the resolution depends on a vendor conversation not yet had.
1.3 Battery impact on a $100 Android phone
Finding (0.72): The Porcupine FAQ's "<4% of a single core on RPi 3" translates to roughly ~1-3% CPU on a modern $100 Android phone (Cortex-A53/A55 cores are comparable to RPi 3's ARM Cortex-A53). The wake-word listener runs as a foreground service with the mic open. Battery impact:
- CPU: ~1-3% continuous → negligible CPU drain.
- Mic: continuous microphone sampling is the dominant battery cost. On modern Android, the mic + audio pipeline draws ~50-100mW during active listening. For an 8-hour shift, that's ~0.4-0.8 Wh — on a typical 3000-4000 mAh battery (~11-15 Wh), that's ~3-7% of battery per shift.
- Foreground service: the persistent notification + service overhead adds ~1-2% battery per shift.
- Total estimate: ~4-9% battery per 8-hour shift. Acceptable for a learner who starts the shift at 100% and the phone lasts the day. Risk: if the learner is also using the phone for other work tasks (inventory app, point-of-sale), the combined drain may push them below 20% before shift end. Mitigation: Praxis assist foreground service should be stoppable ("ending shift" closes the service), and the learner can tap-to-talk as a battery-saving fallback.
Confidence 0.65 — battery estimates are back-of-envelope from power-draw heuristics, not measured on a target device. Phase 1 must measure on the actual $100 Android target.
1.4 Alternatives to Porcupine
Finding (0.80):
| Engine | License | Android | Offline | Custom WW | CPU/RAM | Status |
|---|---|---|---|---|---|---|
| Picovoice Porcupine | Proprietary, MAU-priced | ✅ SDK | ✅ | ✅ (Console) | ~1MB, <4% core | Active, maintained |
| Vosk | Apache 2.0 | ✅ | ✅ | Via grammar | ~50MB model, more CPU | Active, maintained (verified alphacephei.com) |
| Snowboy | Apache 2.0 (abandoned) | ✅ | ✅ | ✅ | Low | Deprecated — acquired by Baidu, no maintenance since ~2020. Reject. |
| TFLite wake-word | DIY (Apache 2.0 models) | ✅ | ✅ | Train yourself | Varies | High engineering effort — train a custom KWS model (e.g., via TensorFlow Lite Micro). Out of scope for v0.5. |
| Android SpeechRecognizer (System) | Free (Android API) | ✅ | ❌ (cloud) | ❌ | N/A | Cloud-based, latency + privacy. Reject for wake-word. |
| Cloud wake-word (Picovoice Falcon, etc.) | Proprietary | ✅ | ❌ | ✅ | N/A | Cloud round-trip adds latency + connectivity dependency. Reject. |
Verdict: Porcupine for v0.5 (purpose-built, lowest resource, custom WW). Vosk as the documented open-source fallback. Snowboy rejected (deprecated). TFLite DIY rejected (engineering effort).
1.5 Android foreground service for background mic
Finding (0.78): Android (API 31+, Android 12+) requires a foreground service of type microphone for background audio capture. The service shows a persistent notification ("Praxis Assist is listening"). Key implementation points:
android.permission.RECORD_AUDIO(dangerous permission — runtime grant).android.permission.FOREGROUND_SERVICE+android.permission.FOREGROUND_SERVICE_MICROPHONE(Android 14+).Service.startForeground()with aNotification(ongoing, low-priority).- The Porcupine Android SDK handles the audio capture loop; Praxis wraps it in a foreground service.
- Screen-off listening: Android allows foreground services to keep the mic open when the screen is off (phone in pocket). The CPU may doze (Doze mode) but a foreground service with active mic is exempted from Doze for the mic pipeline.
- Risk: Android OEM battery kill switches. Some manufacturers (Xiaomi, Huawei, OnePlus) aggressively kill background/foreground services to save battery. Praxis must document the "battery whitelist" step for learners (a known pain point for assistive apps). Confidence 0.70 — the Android API is documented; OEM behavior is variable.
Domain 2: 3-Layer Guardrail Enforcement (D-060, REQ-ASSIST-03)
2.1 The 3-layer pattern is industry-standard
Finding (0.85): D-060 specifies 3 layers: (1) prompt-layer rules, (2) output filter, (3) audit logging. This is the standard defense-in-depth pattern for LLM safety, matching:
- OpenAI's moderation pattern (input + output moderation + logging).
- NVIDIA NeMo Guardrails (input rails + dialog rails + output rails + execution rails — same layering, more granular).
- LLM-as-judge guardrail patterns (system prompt constraints + post-generation classifier + audit trail).
The existing CustomerServiceGuardrail (server/guardrails/customer_service.py, verified — 129 lines) already implements layer (2): regex-based output filtering for legal/financial/medical advice + impersonation, with a _filter_legal() rewrite. Layer (1) is the system prompt (scenario-driven, set in pipeline.py:_build_llm_context). Layer (3) is the turns SQLite table (session_recorder.py). v0.5 extends all three layers for Live Assist.
Confidence 0.85 — the pattern is well-established; the existing code confirms the architecture.
2.2 Layer 1 — Prompt rules for "coaches not does"
Finding (0.82): The Live Assist system prompt must explicitly instruct the LLM to:
- Ask guiding questions, never give the answer. "Your role is to coach, not to do the learner's job. Ask questions that help the learner arrive at the answer themselves."
- Never speak on behalf of the learner. "You are not a participant in the learner's conversation with their customer. Do not generate text the learner should say verbatim."
- Never claim authority you don't have. "You are a coaching AI, not a manager, not a company representative, not a legal/medical/financial advisor."
- Stay within the bound context. "You are coaching the learner on
[path week scenario tag]. Do not give advice outside this scope." - Keep responses short for voice (1-3 sentences). Carry-forward from v0.1's voice-conciseness rule.
- Acknowledge the real customer's presence implicitly. "The learner is in a live interaction. Your coaching must be brief enough not to distract, and must never instruct the learner to say something untrue to the customer."
This prompt is the LiveAssistGuardrail.session_start_disclaimer + the system-prompt prefix. The existing _build_llm_context() in pipeline.py constructs the messages list — v0.5 adds an assist-mode branch that injects the coaching prompt instead of the role-play scenario prompt.
Confidence 0.82 — prompt engineering is the well-trodden path; the specific phrasing needs Phase-1 iteration + testing against a red-team prompt set.
2.3 Layer 2 — Output filter patterns for "direct answer" vs "coaching question"
Finding (0.80): The output filter is a regex + keyword classifier on the LLM response text, run after LLM generation and before TTS. Patterns:
Direct-answer patterns (BLOCK or REWRITE):
# "you should say X to the customer" — verbatim script
DIRECT_SCRIPT_RE = re.compile(
r"\b(you should (say|tell|respond with|reply)|"
r"say (this|the following)|"
r"tell (the |a )?customer|"
r"respond with|reply with|"
r"here'?s what to say|"
r"the (right |correct |best )?answer is|"
r"what you (should|need to|must) (say|do) is)\b",
re.IGNORECASE,
)
# Imperative commands to the learner about the customer
IMPERATIVE_RE = re.compile(
r"\b(escalate to|transfer to|offer a refund of|apologize (by|with)|"
r"give them|promise them|tell them you)\b",
re.IGNORECASE,
)
# Claiming authority / false authority
FALSE_AUTHORITY_RE = re.compile(
r"\b(I (am|'?m) (your |a )?(manager|supervisor|the company|authorized|"
r"a lawyer|a doctor|regulator)|"
r"on behalf of (the company|management)|"
r"I (can|will) (authorize|approve|guarantee))\b",
re.IGNORECASE,
)
# Impersonation of the customer or a real company (carry-forward from CS guardrail)
# (reuse _IMPERSONATION_RE from customer_service.py)
Coaching-question patterns (ALLOW — these are the desired output):
# Open-ended guiding questions
COACHING_QUESTION_RE = re.compile(
r"\b(what (do you|could you|might you)|"
r"how (could|might|would|do) you|"
r"what'?s (your|the) (goal|approach|next step)|"
r"how (does|do) you (feel|think)|"
r"what (would|might) happen if|"
r"can you (think of|identify|name)|"
r"have you considered)\b",
re.IGNORECASE,
)
Filter logic:
- Run direct-answer patterns. If hit → block the response, log the verdict, and re-prompt the LLM with "Your last response gave a direct answer. Rephrase as a coaching question." (one retry; if retry also hits, fall back to a canned coaching redirect: "Think about what the customer needs right now. What's your next step?").
- Run false-authority + impersonation patterns. If hit → block + log + no retry (these are hard violations).
- If no direct-answer hit → allow. Optionally score the response: if it contains a coaching-question pattern, mark
category="coaching"; elsecategory="neutral"(allowed but not ideal — log for review).
False-positive risk: the direct-answer regex may flag legitimate coaching that quotes a customer's likely response ("If the customer says X, you might explore Y"). Mitigation: the regex targets imperative/script phrasing ("you should say"), not hypothetical/quoted phrasing ("if the customer says"). Phase-1 must tune the regex against a corpus of real coaching responses.
Confidence 0.78 — regex-based output filtering is the existing pattern (customer_service.py proves it); the specific patterns need a red-team tuning pass.
2.4 Layer 3 — Audit logging
Finding (0.85): All assist turns logged to SQLite turns table (existing — verified in session_recorder.py:log_turn). v0.5 adds:
- A
guardrail_verdictJSON field on theturnstable (or a parallelguardrail_verdictstable keyed by turn id) capturing{allowed, reason, category, filtered_text}per theGuardrailVerdictdataclass (services/base.py). - Assist turns flow to the v0.4 cohort aggregation as
session_type=assistwith aguardrail_block_ratemetric (how often the output filter fired). This gives operators visibility into safety-critical guardrail behavior — a sudden spike in block rate signals either a prompt regression or a population of learners pushing the boundary. - No raw learner PII in the audit log beyond the existing hardcoded
learner-1(D-007). The turn text is learner speech + AI coaching; stored in SQLite (local), aggregated k-anonymized in Postgres (D-031 hybrid preserved).
Confidence 0.85 — the audit table exists; the extension is a schema-additive migration.
2.5 LLM-as-judge for periodic guardrail evaluation (optional, post-v0.5)
Finding (0.65): A stronger pattern (deferred post-v0.5) is an LLM-as-judge that periodically samples assist turns and classifies them as "coached" vs "did the job" with higher accuracy than regex. This runs off the voice path (nightly job, like the v0.4 cohort reconciliation) and produces a "guardrail adherence score" per learner/shift. v0.5 ships regex filtering (fast, on the voice path); v0.6+ adds the LLM-judge (accurate, off the voice path). Confidence 0.65 — the pattern is sound but deferred; not a v0.5 blocker.
2.6 Known incidents / failure modes in on-the-job coaching AI
Finding (0.70 — domain knowledge, not vendor-verified): Known failure modes for AI-in-the-ear-during-real-customer-interaction:
- The "parrot" failure: the AI gives a verbatim script, the learner repeats it word-for-word, the customer detects the robotic delivery → trust erosion. (Mitigated by D-060 layer 2 — direct-script pattern blocking.)
- The "hallucinated authority" failure: the AI claims to be a manager/supervisor, the learner parrots it, the customer escalates to a real manager who disavows. (Mitigated by
FALSE_AUTHORITY_RE.) - The "wrong-context" failure: the AI coaches for the wrong scenario (e.g., refund when the customer is asking about a delivery). (Mitigated by D-059 context-binding — learner declares context at session start.)
- The "over-coaching" failure: the AI speaks too much, the learner misses the customer's next utterance. (Mitigated by the 1-3 sentence voice-conciseness rule + interruptibility D-008.)
- The "latency-killed-the-moment" failure: coaching arrives after the customer moment passed. (Mitigated by C-8 <600ms budget — see Domain 3.)
- Privacy/consent failure: the real customer didn't consent to being recorded/analyzed by an AI. (Mitigated by: Praxis assist is coaching the learner, not recording the customer; the mic captures the learner's side primarily. But the ambient mic may pick up the customer. Flag: the foreground-service notification + a learner-facing disclosure ("Assist is on — those around you may be recorded by your mic") is ethically and legally required. This is a safety/legal surface for the orchestrator to review.**
No direct competitor does live-in-ear coaching during real customer calls (verified — Dialpad Ai Coach and Gong are post-hoc call analysis, not live; RealWear is AR + voice for industrial, not phone-in-pocket CS coaching). So Praxis is in novel safety territory — the guardrail design must be conservative.
Domain 3: <600ms Latency Budget for Assist Turns (D-061, REQ-NFR-ASSIST-01)
3.1 v0.1 budget breakdown (carry-forward)
Finding (0.85): From ARCHITECTURE.md (verified):
| Segment | Budget | Note |
|---|---|---|
| Client capture + WebRTC uplink | ~50ms | |
| ASR (Deepgram Nova-3 first partial) | ~250ms | R1: measure in Phase 1 |
| LLM first token (gemma4:cloud) | ~200ms | R3: measure in Phase 1 |
| TTS first audio (Cartesia Sonic) | ~120ms | R2: measure; Piper fallback ~80ms |
| WebRTC downlink + playback | ~50ms | |
| Total (all-cloud, Cartesia) | ~670ms | ⚠️ Marginally over 600ms |
| Total (Piper TTS) | ~550ms | R4 mitigation |
v0.1's R4 risk (the single biggest v0.1 technical risk): the all-cloud path likely lands ~670ms. The TTS service MUST sit behind an interface (D-014) and Piper-on-pilot-server MUST be pre-staged as the likely production v0.1 TTS.
3.2 What does assist mode add to the budget?
Finding (0.78): Assist mode adds context-binding tokens to the LLM system prompt. The context-binding is:
- Path week (e.g., "Week 3: Handling escalations")
- Scenario tag (e.g., "damaged-product refund")
- Learner state summary (e.g., "current_theta=0.2, working on de-escalation")
- Coaching focus (e.g., "Focus: empathy + resolution-concreteness")
- The coaching-mode instruction (layer 1 guardrail prompt — see §2.2)
Estimated token count for the context-binding: ~100-150 tokens (the coaching-mode instruction is ~80 tokens; the context-binding is ~30-50 tokens). Total system prompt for assist: ~150-230 tokens (vs. v0.1 practice: ~50-100 tokens for the role-play character prompt).
Latency impact of extra input tokens: LLM prefill (time-to-first-token) scales roughly linearly with input token count for a fixed output. For gemma4:cloud (256K context, well within budget), the prefill latency for ~150 input tokens vs ~50 input tokens is the difference of ~100 tokens × ~0.5ms/token ≈ +50ms (conservative; could be up to +100ms depending on the model's prefill speed). This is added to the LLM first-token segment.
Revised assist budget (all-cloud, Cartesia):
| Segment | Budget | Note |
|---|---|---|
| Client capture + WebRTC uplink | ~50ms | |
| ASR (Deepgram Nova-3) | ~250ms | |
| LLM first token (gemma4:cloud, +context-binding) | ~250-300ms | +50-100ms for context prefill |
| TTS first audio (Cartesia) | ~120ms | |
| WebRTC downlink + playback | ~50ms | |
| Total (all-cloud, Cartesia) | ~720-770ms | ⚠️ Breaks C-8 |
Revised assist budget (Piper TTS mitigation):
| Segment | Budget | Note |
|---|---|---|
| Client capture + WebRTC uplink | ~50ms | |
| ASR (Deepgram Nova-3) | ~250ms | |
| LLM first token (gemma4:cloud, +context-binding) | ~250ms | lean context (~100 tokens) |
| TTS first audio (Piper, self-hosted) | ~80ms | R4 mitigation |
| WebRTC downlink + playback | ~50ms | |
| Total (Piper) | ~680ms | ⚠️ Still marginal |
3.3 How to get assist under 600ms
Finding (0.72): Three levers, in order of impact:
-
Minimize the system prompt. The assist system prompt should be ≤150 input tokens total (coaching instruction + context-binding). This is achievable: the coaching instruction is a fixed ~80-token block; the context-binding is a terse ~30-50 tokens ("Week 3, damaged-refund, focus: empathy"). Avoid dumping the full rubric or scenario YAML into the prompt. Saves ~25-50ms vs. a verbose prompt.
-
Use Piper TTS for assist turns (not Cartesia). Piper self-hosted on the pilot server is ~80ms first audio vs. Cartesia's ~120ms. Saves ~40ms. The v0.1 architecture already pre-stages Piper (R4 mitigation); v0.5 assist mode defaults to Piper, with Cartesia as the quality fallback for practice mode (where <600ms is desired but not as safety-critical — practice coaching that arrives a beat late is still useful; live-assist coaching that arrives after the customer moment is useless).
-
Lean LLM model for assist.
gemma4:cloudis the role-play fast path. For assist, where the output is a short coaching question (not a role-play character utterance), a smaller/faster model may suffice. Option: use a lighter Ollama model for assist (e.g., a futuregemma4:e2b:cloudif available — the v0.1 RESEARCH notedgemma4:e2b/e4bas future options). For v0.5, keepgemma4:cloud(no new model risk) but document the lighter-model path for v0.6.
With levers 1 + 2 applied:
| Segment | Budget | Note |
|---|---|---|
| Client capture + WebRTC uplink | ~50ms | |
| ASR (Deepgram Nova-3) | ~250ms | |
| LLM first token (gemma4:cloud, lean assist prompt) | ~225ms | +25ms for ~50 extra tokens over v0.1 |
| TTS first audio (Piper) | ~80ms | |
| WebRTC downlink + playback | ~50ms | |
| Total (Piper, lean prompt) | ~655ms | ⚠️ Still 55ms over |
Still marginal. The hard truth: the all-cloud + on-device-mic path is ~655ms with the best levers. To get under 600ms, v0.5 needs either:
- (a) Measured Deepgram latency < 250ms. The v0.1 R1 risk ("measure in Phase 1") — if Deepgram Nova-3 first-partial is ~200ms in Canada (plausible — Deepgram's streaming is fast), the total drops to ~605ms (close enough; C-8 is a target, not a hard ceiling for the pilot).
- (b) Measured gemma4:cloud first-token < 200ms. R3 — if Ollama Cloud is fast (~150ms), total drops to ~580ms. ✅ Under budget.
- (c) Accept ~650ms for the pilot, document the gap, target <600ms in v0.6 with optimization. The pilot is Canada, relaxed C-3 (cost); C-8 (latency) is a target. A 50ms overrun on assist turns is tolerable for a pilot if it's measured and trending down.
Recommendation: ship v0.5 with the Piper + lean-prompt configuration, measure the actual assist latency in Phase 1, and treat <600ms as a v0.5 target with a v0.6 hardening step. Document the ~650ms estimate + the levers. Flag for orchestrator: assist turns likely land ~655-770ms depending on which TTS + how lean the prompt is; C-8 <600ms is at risk for assist mode. The binding constraint is C-8, so this is a real tension — the orchestrator should decide whether to relax C-8 for assist mode or push for v0.6 optimization.
Confidence 0.70 — the budget math is sound; the actual Deepgram/Ollama/Piper latencies are unmeasured (R1/R3/R4 from v0.1).
3.4 Wake-word → first-audio latency budget
Finding (0.80): The wake-word → first-audio path is distinct from the in-conversation turn budget. After the learner says "Hey Praxis, the customer is asking about a refund":
| Segment | Budget | Note |
|---|---|---|
| Wake-word detection (Porcupine, on-device) | ~200-500ms | detection latency after the wake word ends |
| Foreground service → WebRTC connect (if not already connected) | ~0ms (warm) / ~500-1000ms (cold) | The assist foreground service should keep a warm WebRTC connection to the praxis server during the shift; cold-connect is too slow |
| User speech (post wake-word) → ASR | ~250ms | Deepgram, as in-conversation |
| LLM + TTS + downlink | ~400ms | lean prompt + Piper |
| Total (warm WebRTC) | ~850-1150ms | From wake-word-end to first coaching audio |
| Total (cold WebRTC) | ~1350-2150ms | Cold connect is unacceptable for live assist |
Critical: the assist foreground service must keep a warm WebRTC connection during the shift. This is a new architectural requirement vs. v0.1 (where each practice session is a fresh WebRTC connection). v0.5 assist mode opens a long-lived WebRTC connection at shift start, keeps it alive (heartbeat), and reuses it for every assist turn. Battery cost: WebRTC keepalive is ~minimal (UDP heartbeat every 15-30s). Server cost: the praxis server holds a long-lived Pipecat task per active assist shift (vs. per practice session in v0.1). This is a concurrency change — see Domain 5.
Confidence 0.75 — the wake-word latency is from Porcupine docs (detection is fast but not instant); the warm-WebRTC requirement is a design implication.
Domain 4: Shift-Bounded Session Model (D-062, REQ-NFR-ASSIST-04)
4.1 How real on-the-job coaching assistants bound sessions
Finding (0.72): Survey of on-the-job coaching AI products (domain knowledge + verified where possible):
| Product | Session model | Live or post-hoc | Surface |
|---|---|---|---|
| Dialpad Ai Coach | Per-call (post-hoc analysis of the call recording) | Post-hoc | Business VoIP (not in-ear during the call) |
| Gong | Per-meeting (post-hoc analysis of sales call recordings) | Post-hoc | Business comms (revenue intelligence) |
| RealWear (verified realwear.com) | Continuous (wearable, always on during the shift) | Live (AR + voice) | Industrial frontline (hardware: smart glasses) |
| Balance AI | (domain knowledge) Per-conversation coaching | Live (app-based) | General coaching app (not CS-specific) |
| Praxis v0.5 (proposed) | Shift-bounded (learner starts/ends a shift; assist turns within) | Live (in-ear) | Phone-in-pocket, CS coaching |
No direct competitor does "live-in-ear coaching during real customer calls on a $100 phone." Dialpad/Gong are post-hoc (analysis after the call). RealWear is live but AR + industrial (not phone-in-pocket CS). Praxis v0.5 is novel.
The shift-bounded model (D-062) is the right choice because:
- It matches the real-world unit of labor (shifts) for retail/hospitality/CS — the Customer Service path's target.
- It gives a clean aggregation boundary (a shift is a discrete event with a start/end timestamp).
- It bounds the WebRTC connection lifecycle (warm connection for the shift, closed at shift-end).
- It avoids the ambiguity of "continuous" (when does aggregation fire? when does the connection close?) and the granularity of "per-turn" (too many aggregation events, double-counting risk).
Confidence 0.80 — the shift model is well-matched to the use case; the competitor survey confirms Praxis is novel.
4.2 Shift lifecycle
Finding (0.82): The shift lifecycle:
1. Learner opens Praxis app, taps "Start Shift" (or voice: "Hey Praxis, starting my shift").
├─ Foreground service starts (Porcupine wake-word listener on).
├─ Learner declares context: taps current path week + scenario tag (D-059).
│ └─ Server reads learner.progress.current_week from SQLite (D-007) for rubric alignment.
├─ Warm WebRTC connection opens to praxis server.
└─ Shift session row created in SQLite (session_type='assist', started_at=now()).
2. During the shift, learner invokes assist:
├─ "Hey Praxis" → Porcupine detects → foreground service routes audio to WebRTC.
├─ Learner speaks (the situation / their question).
├─ Pipeline: ASR → LLM (coaching prompt + context-binding) → guardrail filter → TTS.
├─ Coaching plays in-ear. Turn logged (turns table, session_id=shift_id).
└─ WebRTC connection stays warm for the next turn.
3. Learner ends shift: "Hey Praxis, ending shift" (or taps "End Shift").
├─ Foreground service stops (Porcupine off, mic released).
├─ WebRTC connection closed.
├─ Shift session row updated (ended_at, outcome='completed', turn_count).
└─ on-session-end hook fires → cohort aggregation (session_type='assist') → Postgres.
Within a shift: each assist turn is a discrete coaching exchange. Turns are logged to the turns table with session_id = the shift's session id. The shift is the aggregation unit (not the turn).
Confidence 0.82 — the lifecycle is concrete and matches the existing SessionRecorder pattern (start → log_turn → end).
4.3 Assist does not update mastery (D-063)
Finding (0.90): D-063 is unambiguous: assist turns never update θ (D-035) or count toward mastery gates (D-032). The run_mastery_flow() in session_recorder.py (verified — lines 206-363) is invoked only for practice sessions (schedule_mastery=True); assist shifts call end() with schedule_mastery=False. The cohort aggregation hook fires for both session types, but the mastery flow is practice-only. This is enforced in the end() signature — the schedule_mastery flag gates the mastery asyncio task. Confidence 0.90 — the code structure already supports the separation.
4.4 Integration with the v0.4 cohort aggregation
Finding (0.85): The v0.4 aggregation pipeline (server/cohort/aggregator.py, verified) keys cells by (path, metric, window_start). v0.5 adds assist-specific metrics as new metric strings in the same cohort_aggregates table — no schema change (the table is generic on metric TEXT).
Assist metrics (new):
| Metric | Description | Aggregation |
|---|---|---|
assist_shifts_count |
Number of assist shifts in the window | count |
assist_turns_count |
Total assist turns across shifts | sum |
assist_avg_turns_per_shift |
Mean turns per shift | mean |
assist_active_learners_count |
Distinct learners using assist | distinct count (k-anon) |
assist_guardrail_block_rate |
Fraction of assist turns where the output filter blocked | mean |
Integration with D-053's 3 dashboard views:
- Practice volume → Practice + Assist volume: add
assist_shifts_count+assist_turns_countto the practice volume view (or a new "Assist volume" sub-view). - Mastery progression → unchanged (assist doesn't affect mastery per D-063).
- Failure patterns → add
assist_guardrail_block_rateas a safety signal (a high block rate = the AI is frequently trying to give direct answers = either a prompt regression or learners pushing boundaries).
The on-session-end hook (server/cohort/hook.py) is extended to accept session_type='assist' in the session_outcome dict. The _build_session_outcome() method in session_recorder.py (line 164) already builds this dict; v0.5 adds the session_type field. Assist shifts fire the hook on shift-end (not per-turn).
k-anonymity ≥ 10 (D-034) applies identically — assist metrics are suppressed if the distinct learner count in the window is < 10. Confidence 0.85 — the integration is additive; the existing aggregator + hook patterns are reused.
Domain 5: v0.1 Voice Pipeline Reuse for Assist Mode (D-061)
5.1 The pipeline is parameterized for reuse
Finding (0.85): server/pipeline.py:build_pipeline() (verified — 231 lines) takes a scenario_id and builds a ScenarioRuntime with a system prompt + opening line. The pipeline is:
transport.input() → stt → latency_observer → user_aggregator → llm →
latency_observer → tts → latency_observer → transport.output() → assistant_aggregator
All service constructors (_build_stt, _build_llm, _build_tts, _build_transport) are env-driven and reusable. The only scenario-specific parts are the system prompt + opening line (from ScenarioRuntime).
5.2 Minimal delta: build_assist_pipeline()
Finding (0.82): v0.5 adds a build_assist_pipeline() (or a mode="assist" parameter to build_pipeline()) that:
- Reuses
_build_transport,_build_stt,_build_llm,_build_ttsunchanged. - Swaps
_build_llm_context(): instead of the scenario-driven system prompt, injects the Live Assist coaching prompt (§2.2) + context-binding (path week, scenario tag, learner state). - Drops the opening line (assist is invoked mid-shift; no scripted opener).
- Adds the LiveAssistGuardrail as a post-LLM processor (between
llmandttsin the pipeline) that runs the output filter (§2.3). The existing v0.1 pipeline doesn't have a post-LLM guardrail processor inline (the CS guardrail runs on the debrief, not in-loop) — v0.5 adds an in-loop guardrail processor for assist mode. This is a pipeline-structure change but a small one (~1 new Pipecat frame processor). - Reuses the
LatencyObserverfor assist latency measurement (R1/R3/R4 measurement extends to assist turns).
Delta estimate: ~1 new pipeline builder (~50 LOC), ~1 new guardrail processor (~80 LOC), ~1 new guardrail ruleset (LiveAssistGuardrail, ~120 LOC), ~1 new context-binding loader (~40 LOC). Total: ~290 LOC of new server code. No new voice-service deps (Deepgram/Cartesia/Piper/Ollama all reused).
Confidence 0.82 — the pipeline structure is clean; the delta is small.
5.3 Warm WebRTC connection — the concurrency change
Finding (0.78): v0.1 opens a fresh WebRTC connection per practice session (short-lived, 5-10 min). v0.5 assist mode keeps a warm WebRTC connection for the entire shift (potentially 4-8 hours). Implications:
- Server concurrency: the praxis server holds N long-lived Pipecat tasks (one per active assist shift) vs. M short-lived practice tasks. For the pilot (single-learner-per-device, D-007), N ≤ 1. For post-pilot (multi-learner), N = number of concurrent learners on-shift. The v0.4 single-uvicorn process + asyncpg pool (max 10) is sufficient for the pilot (1 concurrent assist shift + occasional practice sessions). Post-pilot concurrency is a v0.6+ concern.
- WebRTC keepalive: the SmallWebRTCTransport (Pipecat) keeps the connection alive via ICE keepalives (STUN binding requests every 15-30s by default). Praxis adds an app-level heartbeat (a no-op audio frame or a ping message) every 30s to ensure the connection isn't reaped by NAT timeouts.
- Battery (client): WebRTC keepalive is ~minimal (UDP, small packets). The mic is only active during an assist turn (post-wake-word); between turns, the foreground service runs Porcupine on the local mic but doesn't stream to the server. The WebRTC connection is warm (keepalive only) between assist turns; audio streams only during a turn.
Confidence 0.75 — the warm-connection pattern is standard WebRTC; the concurrency math is pilot-scale.
5.4 Context-binding source (D-059)
Finding (0.82): D-059 specifies: learner declares context at session start (path + scenario tag), server reads active path week from SQLite. The existing PraxisStore.get_progress(learner_id, path_slug) (used in session_recorder.py:249) returns the learner's progress row including current_week. v0.5 assist mode:
- Learner taps "Start Shift" → selects current path week (or confirms the auto-detected
progress.current_week) + scenario tag (e.g., "damaged-product refund"). - Server loads the context:
current_weekfrom SQLite + the scenario tag'srubric_criteriafrom the scenario library + the learner'sthetafromlearner_ability. - The context-binding loader constructs a terse context string:
"Week {current_week}, scenario: {scenario_tag}, learner_theta: {theta:.1f}, coaching_focus: {top_rubric_criterion}". - This string is injected into the assist system prompt.
Auto-detection is out of scope (no camera per C-4, no screen context). The learner is in control of declaring context. Confidence 0.82 — the existing store methods support the read; the declaration UI is a small client addition.
Domain 6: Cohort Aggregation Integration (D-062, REQ-NFR-ASSIST-04) — detailed
6.1 No schema change to cohort_aggregates
Finding (0.90): The cohort_aggregates table (db/pg_migrations/0001_operator_tier.sql, verified):
CREATE TABLE IF NOT EXISTS cohort_aggregates (
path TEXT NOT NULL,
metric TEXT NOT NULL,
window_start DATE NOT NULL,
window_end DATE NOT NULL,
value NUMERIC,
cell_count INTEGER NOT NULL DEFAULT 0,
cell_suppressed BOOLEAN NOT NULL DEFAULT FALSE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (path, metric, window_start)
);
The metric column is free-form TEXT. v0.5 adds assist metrics (assist_shifts_count, assist_turns_count, etc.) as new metric values — no DDL change. The aggregation upsert (aggregator.py:_upsert_cell) is metric-agnostic. Confidence 0.90 — the schema is generic by design (D-053).
6.2 session_type field in session_outcome
Finding (0.85): The _build_session_outcome() in session_recorder.py (line 164) builds the dict the aggregator consumes. v0.5 adds:
def _build_session_outcome(self, outcome: str) -> dict[str, Any]:
return {
"learner_ref": self.learner_id,
"path": self._path_slug(),
"scenario_id": self.scenario_id,
"outcome": outcome,
"session_type": self.session_type, # NEW v0.5: 'practice' | 'assist'
"rubric_scores": ..., # empty for assist (no mastery scoring)
"failure_mode": self._failure_mode(), # None for assist
"branch_path": list(self._branch_path), # empty for assist
"assist_turn_count": self._turn_seq, # NEW v0.5
"guardrail_blocks": self._guardrail_block_count, # NEW v0.5
"timestamp": _now_iso(),
}
The SessionRecorder.__init__ gains a session_type: str = "practice" parameter. Practice sessions set it to "practice" (default); assist shifts set it to "assist". The aggregator branches on session_type to compute the right metrics.
6.3 Aggregator extension for assist
Finding (0.82): aggregator.py:aggregate_session() (verified) branches on session_type:
async def aggregate_session(pg_store, session_outcome):
session_type = session_outcome.get("session_type", "practice")
if session_type == "assist":
await _aggregate_assist(pg_store, session_outcome)
else:
await _aggregate_practice(pg_store, session_outcome) # existing logic
async def _aggregate_assist(pg_store, session_outcome):
path = session_outcome["path"]
turn_count = session_outcome.get("assist_turn_count", 0)
blocks = session_outcome.get("guardrail_blocks", 0)
# ... upsert assist_shifts_count, assist_turns_count, assist_avg_turns_per_shift,
# assist_guardrail_block_rate with k-anon suppression (same pattern as practice)
The k-anonymity suppression (COUNT(DISTINCT learner_ref) >= 10) applies identically — assist metrics are suppressed if too few learners used assist in the window. Confidence 0.82 — the extension mirrors the existing practice aggregation.
6.4 Dashboard views extension (D-053)
Finding (0.80): The 3 v0.4 dashboard views (server/operator/cohort.py, mastery.py, failure_patterns.py) extend:
| v0.4 View | v0.5 Extension |
|---|---|
| Practice volume (cohort.py) | Add assist rows: assist_shifts_count, assist_turns_count per path/window. The view returns practice + assist volume side-by-side. |
| Mastery progression (mastery.py) | Unchanged (assist doesn't affect mastery per D-063). Optionally add a note: "Assist usage: N shifts, M turns this window" as context. |
| Failure patterns (failure_patterns.py) | Add assist_guardrail_block_rate as a new "safety signal" row. High block rate = flag for operator review. |
No new endpoints — the existing /api/operator/cohort, /api/operator/mastery, /api/operator/failure-patterns return extended payloads. The React dashboard (client/src/operator/) renders the new rows. Confidence 0.80 — the extension is additive to the existing views.
Domain 7: Persona Roster for v0.5 (decision)
7.1 Active personas (4)
Finding (0.85): v0.5 is voice-pipeline-heavy (wake-word + assist mode + latency tuning) + safety-critical guardrails + cohort aggregation extension. The roster:
---
name: lead-developer
active: true
phase_specific: false
reason: Coordinates across assist pipeline, guardrails, context-binding, and aggregation domains. Owns the build_assist_pipeline() design decision (whether to add a mode param to build_pipeline or a separate builder) and the warm-WebRTC-connection lifecycle. Required for every milestone.
domain: coordination
frameworks: [pipecat, fastapi, sqlite, postgres, webrtc]
constraints: [pragmatic, latency-budget-aware, hybrid-storage-no-cross-db-joins, k-anonymity-floor-10, assist-does-not-affect-mastery]
territory:
- "docker-compose.yml"
- ".env.example"
---
---
name: voice-engineer
active: true
phase_specific: true
reason: REACTIVATED for v0.5 (proposed at PERSONAS.md line 458 for v0.5+). Owns the wake-word client (Picovoice Porcupine Android foreground service), the assist audio pipeline (warm WebRTC connection, wake-word → first-audio latency), latency tuning (the <600ms assist budget — Domain 3), and the in-loop guardrail processor (post-LLM frame processor). This is the largest new territory in v0.5: the assist voice loop is a new mode alongside the practice scenario loop. Will deactivate in v0.6 unless voice work continues (accent modeling, multi-voice personas).
domain: voice
frameworks: [porcupine-android, webrtc, silero-vad, pipecat, audio-codecs, piper-tts]
constraints: [sub-600ms-latency-assist, warm-webrtc-connection, foreground-service-background-mic, wake-word-detection-latency, piper-tts-for-assist, lean-assist-system-prompt]
territory:
- "**/server/pipeline.py"
- "**/server/asr/**"
- "**/server/tts/**"
- "**/server/latency.py"
- "**/client/wake-word/**"
- "**/client/assist-service/**"
---
---
name: backend-engineer
active: true
phase_specific: false
reason: Owns the context-binding endpoints (load path week + scenario tag + learner state into the assist prompt), the assist session API (start_shift / end_shift / log_assist_turn), the SessionRecorder extension (session_type field, assist turn logging, _build_session_outcome assist branch), and the cohort hook extension for session_type='assist'. Also owns the LiveAssistGuardrail ruleset (with security-engineer). The assist session API + context-binding is the largest backend territory in v0.5.
domain: backend
frameworks: [pipecat, pydantic, fastapi, uvicorn, aiosqlite, asyncpg]
constraints: [api-first, type-safe, mastery-off-voice-path, aggregation-off-voice-path, latency-budget-aware, no-cross-db-joins, assist-does-not-update-mastery]
territory:
- "**/server/**"
- "**/server/guardrails/**"
- "**/server/cohort/**"
- "**/server/session_recorder.py"
- "**/server/assist/**"
- "**/db/migrations/**"
---
---
name: security-engineer
active: true
phase_specific: true
reason: RETAINED from v0.4. Owns the LiveAssistGuardrail enforcement (REQ-ASSIST-03 — safety-critical: the AI is in the learner's ear during real customer interactions). The 3-layer guardrail (D-060) is the security-engineer's v0.5 surface: prompt rules, output filter patterns (direct-answer vs coaching-question regex), audit logging, and the guardrail_block_rate safety signal. Also owns the privacy/consent disclosure surface (the foreground-service notification + learner-facing "Assist is on — those around you may be recorded" disclosure). REQ-ASSIST-03 is the most safety-critical requirement in v0.5; the security-engineer's guardrail work blocks ship.
domain: security
frameworks: [pynacl, canonicaljson, base58, argon2-cffi, regex, llm-guardrail-patterns]
constraints: [coaches-not-does, no-direct-answer-patterns, no-false-authority, no-impersonation, audit-all-assist-turns, guardrail-block-rate-operator-visible, consent-disclosure-required]
territory:
- "**/server/guardrails/**"
- "**/server/guardrails/live_assist.py"
- "**/server/vc/**" # retained from v0.4 (no v0.5 change expected)
- "**/server/auth/**" # retained from v0.4 (no v0.5 change expected)
---
---
name: data-engineer
active: true
phase_specific: false
reason: RETAINED from v0.4. Owns the assist aggregation integration into the v0.4 cohort pipeline (new assist metrics in cohort_aggregates — no schema change, new metric strings), the turns-table guardrail_verdict field migration (SQLite, additive), and the assist session row in the sessions table (session_type field). Also owns the k-anonymity suppression extension for assist metrics (assist_active_learners_count distinct-count). Smaller v0.5 surface than v0.4 but on the critical path for operator visibility.
domain: data
frameworks: [sqlite, postgres16, aiosqlite, asyncpg]
constraints: [schema-first, migration-driven, no-cross-db-joins, k-anonymity-floor-10, opaque-learner-ref, write-time-suppression, assist-metrics-no-schema-change]
territory:
- "**/db/**"
- "**/db/migrations/**"
- "**/server/cohort/aggregator.py"
---
7.2 Deactivated personas (2)
---
name: devops-engineer
active: false
phase_specific: true
reason: DEACTIVATED for v0.5. No deploy changes — v0.4's LXC + Docker-in-LXC + Postgres carries forward unchanged. The assist foreground service is a client-side concern (voice-engineer territory), not a deploy/infra change. No new Docker services, no CT resource bump, no new backup scripts. Will reactivate in v0.6+ if deploy hardening (TLS, multi-instance, autoscaling) or a CT bump is needed for assist concurrency.
domain: devops
frameworks: [proxmox-lxc, docker, systemd, bash]
constraints: [idempotent-deploy, secrets-never-committed]
territory: []
---
---
name: frontend-engineer
active: false
phase_specific: true
reason: DEACTIVATED for v0.5 (PROVISIONAL — see note). v0.5 assist mode is invoked by wake-word (audio) — the UI surface is minimal: a "Start Shift" / "End Shift" toggle + a context-declaration screen (path week + scenario tag selector). This is small enough that the voice-engineer (client/wake-word + client/assist-service) can own it alongside the audio pipeline, OR the backend-engineer can add a minimal React route. No full frontend surface (no new dashboard, no complex components, no chart library). Will reactivate in v0.6+ if a richer assist control surface (shift history, guardrail-block review, assist coaching quality dashboard) is needed. NOTE FOR ORCHESTRATOR: if the assist control surface (start/stop shift + context declaration) is judged non-trivial (>200 LOC of React), reactivate frontend-engineer. Current estimate: ~100-150 LOC of React — below the reactivation threshold.
domain: frontend
frameworks: [react, react-router-dom, pipecat-client-sdk, webrtc]
constraints: [component-first, voice-first-ui, minimal-client-javascript]
territory: []
---
7.3 Roster decision summary
| Persona | v0.4 status | v0.5 status | Reason |
|---|---|---|---|
| lead-developer | active | active | Coordination across assist/guardrail/aggregation |
| voice-engineer | proposed (inactive) | active (REACTIVATED) | Wake-word client, assist pipeline, latency tuning — the largest v0.5 surface |
| backend-engineer | active | active (retained) | Context-binding, assist session API, SessionRecorder extension, cohort hook |
| security-engineer | active | active (retained) | REQ-ASSIST-03 guardrails — safety-critical |
| data-engineer | active | active (retained) | Assist aggregation integration (no schema change, new metrics) |
| devops-engineer | active | deactivated | No deploy changes in v0.5 |
| frontend-engineer | active | deactivated (provisional) | Minimal assist UI; reactivate if control surface exceeds ~200 LOC |
4 active personas + 1 reactivation (voice-engineer) = 5 active, 2 deactivated. This is the right size for v0.5's scope (voice + guardrails + aggregation, no deploy, minimal UI).
7.4 Constraint alignment (v0.5-specific)
- All personas:
assist-does-not-affect-mastery(D-063),k-anonymity-floor-10(D-034 carry-forward),no-raw-learner-pii-in-postgres(D-031 carry-forward). - lead-developer:
latency-budget-aware(C-8 — the binding constraint for assist),hybrid-storage-no-cross-db-joins(D-031). - voice-engineer:
sub-600ms-latency-assist(C-8 for assist turns),warm-webrtc-connection(shift-bounded, not per-turn),foreground-service-background-mic(Android requirement),wake-word-detection-latency(Porcupine ~200-500ms),piper-tts-for-assist(R4 mitigation as default for assist),lean-assist-system-prompt(≤150 tokens for prefill latency). - backend-engineer:
mastery-off-voice-path(C-8 carry-forward),aggregation-off-voice-path(D-054 carry-forward),assist-does-not-update-mastery(D-063 — theschedule_mastery=Falsegate on assist shifts). - security-engineer:
coaches-not-does(REQ-ASSIST-03),no-direct-answer-patterns(output filter regex),no-false-authority,no-impersonation,audit-all-assist-turns(turns table + guardrail_verdict),guardrail-block-rate-operator-visible(cohort aggregation safety signal),consent-disclosure-required(foreground-service notification). - data-engineer:
assist-metrics-no-schema-change(new metric strings in cohort_aggregates, no DDL),write-time-suppression(D-034 carry-forward).
Consolidated Risks Table
| ID | Risk | Severity | Mitigation | Confidence |
|---|---|---|---|---|
| R-ASSIST-01 | Picovoice Porcupine MAU pricing blocks the pilot (no recurring free tier — verified) | high | Engage Picovoice sales for a pilot/educational tier; fallback to a built-in wake word (e.g., "Bumblebee") for v0.5; document Vosk as the open-source fallback | 0.75 |
| R-ASSIST-02 | C-8 <600ms latency budget broken for assist turns (estimated ~655-770ms) | high | Lean assist system prompt (≤150 tokens) + Piper TTS (not Cartesia) for assist + measure R1/R3 in Phase 1; accept ~650ms for pilot if trending down; flag orchestrator to relax C-8 for assist or push hardening to v0.6 | 0.70 |
| R-ASSIST-03 | Wake-word → first-audio latency ~850-1150ms (warm) / unacceptable (cold) | medium | Require warm WebRTC connection for the shift (foreground service keepalive); document the ~1s wake-word-to-coaching latency as expected (not the in-conversation <600ms budget) | 0.75 |
| R-ASSIST-04 | Android background-mic restriction (Android 14+ foreground-service-microphone type) | medium | Use a foreground service of type microphone with persistent notification; document OEM battery-kill whitelist step for learners |
0.70 |
| R-ASSIST-05 | OEM battery kill switches (Xiaomi/Huawei/OnePlus) kill the assist foreground service | medium | Document the "battery whitelist" onboarding step; test on the target $100 Android device; consider a "survival mode" that restarts the service on kill (Android START_STICKY) |
0.65 |
| R-ASSIST-06 | Output filter false positives block legitimate coaching (regex over-matches) | medium | Tune the direct-answer regex against a corpus of real coaching responses in Phase 1; allow one retry on block; fall back to a canned coaching redirect | 0.75 |
| R-ASSIST-07 | Output filter false negatives let a direct answer through (regex under-matches) | high | Defense-in-depth: layer 1 prompt rules + layer 2 regex + (post-v0.5) LLM-as-judge. The regex is the first line, not the only line. Audit all turns + guardrail_block_rate surfaces misses to operators. | 0.70 |
| R-ASSIST-08 | Privacy/consent: ambient mic records the real customer without their consent | high | Foreground-service notification ("Praxis Assist is on") + learner-facing disclosure ("those around you may be recorded by your mic"). Legal review of one-party/two-party consent law for Canada. Flag for orchestrator — this is a legal/ethical surface, not purely technical. | 0.60 |
| R-ASSIST-09 | Warm WebRTC connection dropped mid-shift (NAT timeout, network change) | medium | App-level heartbeat every 30s; auto-reconnect on drop; log the reconnection; if reconnection fails, prompt learner to restart shift | 0.75 |
| R-ASSIST-10 | Server concurrency: long-lived assist WebRTC tasks exhaust the asyncpg pool / uvicorn capacity | low (pilot) | Pilot: single-learner (D-007), ≤1 concurrent assist shift. Post-pilot: v0.6+ concurrency hardening (multi-uvicorn, larger pool). | 0.80 |
| R-ASSIST-11 | Assist shifts abandoned (learner forgets "ending shift") → orphaned WebRTC connections + stale sessions | medium | Auto-end shift after 8h (configurable); foreground service timeout; log abandoned shifts in cohort aggregation (assist_shifts_count separates completed vs abandoned) | 0.75 |
| R-ASSIST-12 | Context-binding reads stale learner state (learner advanced a week but assist uses old week) | low | Learner declares context at shift start (D-059); server reads progress.current_week fresh from SQLite at shift start; if the learner advanced mid-shift, the next shift picks up the new week |
0.80 |
| R-ASSIST-13 | Porcupine wake-word false triggers in noisy retail environment | medium | Choose a wake word with diverse phonemes + ≥6 phonemes (Porcupine FAQ guidance); "Bumblebee" / "Grapefruit" / custom "Hey Praxis" tuned via Console; tune sensitivity (Porcupine has a sensitivity parameter) | 0.70 |
| R-ASSIST-14 | Assist foreground service battery drain + learner's other work apps → phone dies mid-shift | medium | Document expected drain (~4-9% per shift); tap-to-talk fallback (no wake-word listener) for battery-saving mode; learner can stop assist if battery < 20% | 0.65 |
D-058..D-063 Validation Audit
| CLARIFY Decision | Validation | Verdict |
|---|---|---|
| D-058 (Porcupine wake-word + tap-to-talk fallback) | Porcupine verified (on-device, offline, low-power, Android SDK, custom WW). MAU pricing / no recurring free tier — partial contradiction. Refinement: pursue Picovoice sales pilot tier, fallback to built-in wake word, document Vosk. | REFINED — wake-word engine confirmed; free-tier assumption contradicted |
| D-059 (Learner declares context + server reads SQLite path week) | Confirmed. PraxisStore.get_progress() returns current_week. Auto-detection impossible (C-4). Declaration UI is small. |
CONFIRMED |
| D-060 (3-layer guardrail: prompt rules + output filter + audit log) | Confirmed — industry-standard pattern. Existing CustomerServiceGuardrail proves the regex output-filter approach. v0.5 adds LiveAssistGuardrail with direct-answer vs coaching-question patterns. |
CONFIRMED |
| D-061 (<600ms latency, shared pipeline, ≤30s assist turns) | At risk. Estimated assist latency ~655-770ms (all-cloud) / ~655ms (Piper + lean prompt). C-8 is the binding constraint. Mitigations identified but may not fully close the gap. Flag for orchestrator. | AT RISK — likely ~50-170ms over budget; levers identified |
| D-062 (Shift-bounded sessions, session_type=assist in cohort aggregation) | Confirmed. Shift-bounded matches real CS work. No schema change to cohort_aggregates (new metric strings). on-session-end hook extended. | CONFIRMED |
| D-063 (Assist does not update mastery or count toward gates) | Confirmed. SessionRecorder.end(schedule_mastery=False) for assist shifts. The mastery flow is practice-only. |
CONFIRMED |
Summary: 4 confirmed, 1 refined (D-058 free-tier), 1 at-risk (D-061 latency). Two items flagged for orchestrator attention: the Picovoice pricing path (R-ASSIST-01) and the C-8 latency tension for assist mode (R-ASSIST-02 / D-061).
New Decisions (D-064+)
| ID | Decision | Rationale | Confidence | Alternatives |
|---|---|---|---|---|
| D-064 | Live Assist wake-word engine = Picovoice Porcupine (built-in wake word for v0.5 pilot; custom "Hey Praxis" post-pilot), with Vosk as the documented open-source fallback | R-ASSIST-01: Porcupine MAU pricing has no recurring free tier. v0.5 ships with a built-in Porcupine wake word (e.g., "Bumblebee") to avoid custom-training costs during the pilot. Post-pilot, engage Picovoice sales for a custom "Hey Praxis" wake word under a pilot/educational tier. Vosk (Apache 2.0, offline) is the fallback if Porcupice pricing is unsustainable. Snowboy rejected (deprecated). | 0.70 | Vosk for v0.5 (free but heavier), TFLite DIY (engineering effort), Snowboy (deprecated) |
| D-065 | Live Assist TTS = Piper (self-hosted on pilot server) as the default for assist turns, Cartesia as the quality fallback for practice mode | R-ASSIST-02: assist turns are latency-critical (C-8). Piper ~80ms first audio vs Cartesia ~120ms. The v0.1 R4 mitigation pre-stages Piper; v0.5 assist mode defaults to Piper to claw back ~40ms toward the <600ms budget. Practice mode retains Cartesia (quality over latency for practice). | 0.75 | Cartesia for both (simpler, but +40ms on assist), Piper for both (lower quality for practice) |
| D-066 | Live Assist system prompt = ≤150 input tokens (coaching instruction ~80 tokens + context-binding ~50 tokens + voice-conciseness ~20 tokens) | R-ASSIST-02: extra input tokens add prefill latency (~0.5ms/token). A lean prompt keeps the prefill delta under 50ms vs v0.1 practice. Avoid dumping the full rubric or scenario YAML into the prompt — context-binding is terse (path week, scenario tag, one-line coaching focus). | 0.78 | Verbose prompt (easier coaching quality, but +100-200ms latency) |
| D-067 | Live Assist WebRTC connection = warm for the entire shift (foreground service keepalive; not per-turn cold connect) | R-ASSIST-03: cold WebRTC connect (~500-1000ms) is unacceptable for live assist. The assist foreground service opens a warm connection at shift start, keeps it alive (heartbeat every 30s), and reuses it for every assist turn. Closed at shift-end. Between turns, only keepalive flows (no audio streaming) to save battery. | 0.78 | Per-turn cold connect (too slow), always-streaming (battery + privacy) |
| D-068 | Live Assist guardrail output filter = regex-based direct-answer + false-authority + impersonation patterns, with one retry on block + canned coaching redirect fallback | R-ASSIST-06/07: regex is the fast on-voice-path filter (matches the existing CustomerServiceGuardrail pattern). One retry gives the LLM a chance to self-correct; the canned fallback ensures a safe response if the retry also blocks. LLM-as-judge deferred to post-v0.5 (off-voice-path, more accurate, nightly). | 0.78 | LLM-as-judge on-voice-path (too slow for <600ms), no filter (unsafe) |
| D-069 | Live Assist shift = auto-end after 8 hours (configurable via PRAXIS_ASSIST_MAX_SHIFT_HOURS=8) |
R-ASSIST-11: learners may forget "ending shift", leaving orphaned WebRTC connections + stale sessions. Auto-end after 8h (a typical shift length) closes the shift cleanly, fires the aggregation hook, and releases the foreground service. The learner can restart a new shift if needed. | 0.75 | No auto-end (orphan risk), shorter (4h — too short for some shifts), longer (12h — battery risk) |
| D-070 | Live Assist consent disclosure = foreground-service notification + learner-facing "Assist is on — those around you may be recorded by your mic" disclosure at shift start | R-ASSIST-08: the ambient mic may pick up the real customer. Ethical and legal (one-party/two-party consent law) requires disclosure. The foreground service notification (Android requirement) + an in-app disclosure at shift start covers the learner's awareness. The customer's consent is the learner's responsibility (Praxis can't notify the customer). Flag for orchestrator: legal review of Canada consent law for ambient recording during coaching. | 0.65 | No disclosure (legal/ethical risk), explicit customer consent prompt (impractical — the customer isn't a Praxis user) |
New pip dependencies for v0.5
| Dep | Purpose | Confidence | Source |
|---|---|---|---|
| (none new server-side) | The v0.1 voice pipeline (Pipecat + Deepgram + Cartesia + Piper + Ollama) is reused unchanged. The guardrail is pure-Python regex (no new dep). The aggregation extension uses existing asyncpg. | 0.90 | Domain 5 + 6 |
Picovoice Porcupine SDK is an Android client-side dependency (Gradle/Maven), not a Python server-side dep. The praxis server doesn't run Porcupine — the learner's phone does. The server-side assist code is pure Python (FastAPI + Pipecat + aiosqlite + asyncpg, all existing).
New npm/Gradle dependencies for v0.5
| Dep | Side | Purpose | Confidence | Source |
|---|---|---|---|---|
ai.picovoice:porcupine-android (Gradle) |
Client (Android) | Wake-word detection on the learner's phone | 0.80 | D-058, D-064 |
Note: the v0.1 client is React + WebRTC (D-015), not React Native. The Porcupine React SDK exists but runs in-browser (not a foreground service). For true background wake-word on Android, v0.5 may need a React Native or native Android client — this is a client-architecture decision for the orchestrator. The v0.1 RESEARCH (D-015) noted "upgrades to React Native for Android later." v0.5 Live Assist (phone-in-pocket, background mic) likely is the trigger to upgrade to React Native. Flag for orchestrator: v0.5 may require a client-architecture upgrade from React-Web to React-Native (or a native Android assist service alongside the React web app). This is a significant scope addition.
Open Questions for PLAN Stage
-
Client architecture for v0.5: React web (v0.1, D-015) can't do background wake-word on Android (no foreground service). Options: (a) upgrade the client to React Native (Porcupine RN SDK + Android foreground service), (b) ship a separate native Android "Praxis Assist" app alongside the React web practice app, (c) defer wake-word to v0.6 and ship v0.5 assist as tap-to-talk only (no wake-word). Recommendation: (c) for v0.5 pilot — tap-to-talk is hands-free enough for a pilot (learner taps a button on a smartwatch or a headset button), and it avoids the React-Native upgrade scope. Add wake-word in v0.6 with the native client. This would defer D-058/D-064 to v0.6 and simplify v0.5 to the assist voice loop + guardrails + aggregation only. Flag for orchestrator — this is a scope decision.
-
Picovoice sales engagement: When to engage Picovoice sales for the pilot/educational tier? Before v0.5 PLAN, or after v0.5 ships with tap-to-talk? If wake-word is deferred to v0.6 (per Q1), the sales engagement is a v0.6 activity.
-
Lean assist system prompt — concrete content: The ≤150-token budget (D-066) is a constraint; the concrete prompt content (the coaching instruction phrasing, the context-binding format) needs Phase-1 iteration + red-team testing. What's the minimum prompt that produces coaching questions, not direct answers, from
gemma4:cloud? -
Output filter regex corpus: The direct-answer regex (D-068) needs tuning against a corpus of real coaching responses. How to build this corpus before v0.5 ships? Option: generate a synthetic corpus via LLM (prompt
gemma4:cloudto produce coaching responses + direct-answer responses, label them, tune the regex). Phase-1 task. -
Assist shift vs practice session — can they coexist? Can a learner be in a practice session (WebRTC to praxis) and invoke assist (warm WebRTC to praxis) simultaneously? Probably not for v0.5 (one WebRTC connection at a time per D-007 single-learner). The learner ends the practice session before starting an assist shift, or vice versa. Document the mutual exclusivity.
-
Guardrail verdict storage: A
guardrail_verdictstable (keyed by turn id) or a JSON column onturns? A JSON column is simpler (additive migration); a separate table is more queryable for the operator dashboard. Recommend JSON column for v0.5 (simpler); separate table if the operator dashboard needs to filter/sort by verdict. -
Phase split confirmation: ROADMAP P1 = assist voice loop (pipeline + guardrail + context-binding) + aggregation extension; P2 = guardrail tuning + latency measurement + operator dashboard assist views; P3 = review. Is the aggregation extension P1 or P2? Recommend P2 (the assist voice loop is the P1 deliverable; aggregation is operator-facing, P2).
-
Canada consent law for ambient recording: R-ASSIST-08 / D-070. Canada's Personal Information Protection and Electronic Documents Act (PIPEDA) + provincial one-party/two-party consent recording laws. Praxis assist records the learner (one party — the learner consents by starting the shift) but may pick up the customer (the other party). One-party consent (Canada is one-party consent federally) means the learner can record their own conversation without the customer's consent. But the AI analyzing the customer's speech in real-time is a novel use. Flag for orchestrator — legal review recommended before v0.5 ship. Confidence 0.60 (not legal advice).