---ci--- phase: 0 milestone: v0.1 status: complete requirements: covered: [REQ-VOICE-01, REQ-VOICE-02, REQ-VOICE-03, REQ-VOICE-04, REQ-SCEN-01, REQ-STATE-01, REQ-LLM-01, REQ-LLM-02, REQ-DEBRIEF-01, REQ-ORCH-01, REQ-ORCH-02, REQ-SCEN-FMT-01, REQ-NFR-LAT-01, REQ-NFR-SAFE-01, REQ-NFR-COST-01] partial: [] ---/ci---
30 KiB
Praxis — Phase 1 Plan (Minimal Viable Voice Loop)
Milestone: v0.1 (foundation) Phase: 1 — Minimal Viable Voice Loop Branch:
phase/01-minimal-voice-loop(created at EXECUTE) Status: plan Source artifacts: PROJECT.md (D-001..D-020), REQUIREMENTS.md, ARCHITECTURE.md, RESEARCH.md (R1-R10), PERSONAS.md, ROADMAP.md
1. Phase 1 Summary
Goal
A single learner can open the React web client, speak to an AI tutor playing a Customer Service role-play scenario ("angry customer requesting refund on damaged product", one branch point: escalate vs accept), hear the tutor respond with <600ms end-to-end latency target, receive a single end-of-session text+voice coaching debrief, and have the session logged to SQLite learner state.
Scope (in)
- Streaming voice loop: Deepgram Nova-3 ASR → Ollama Cloud LLM (
gemma4:cloud) → Cartesia/Piper TTS, orchestrated by Pipecat with Silero VAD - One branching Customer Service scenario (refund, one branch point,
failure_modefield present) - Interruptibility (abort-and-yield per D-008)
- Pluggable guardrail layer with Customer Service ruleset
- Single-learner SQLite session log + per-session cost logging
- End-of-session text+voice coaching debrief (
deepseek-v4-flash:cloud, no-think mode) - React + WebRTC client via Pipecat client SDK
- R1-R4 latency spike (the single biggest v0.1 technical risk — RESEARCH.md directive)
Scope (out — deferred per PROJECT.md)
- Mastery scoring, competency rubrics, credentials
- Multi-language (Canadian English only)
- Employer dashboard, Live Assist, WhatsApp/USSD
- Multi-learner / auth / multi-tenant
- Active failure-injection provocation (hook present, not provoked — D-009)
- Multiple personas / voice switching (one voice — D-006)
Risks addressed in this plan
| # | Risk (from RESEARCH.md) | How this plan addresses it |
|---|---|---|
| R1 | Deepgram first-partial latency from Canada unmeasured | SLICE-01 day-1 probe; SLICE-02 integrated measurement |
| R2 | Cartesia first-audio latency unmeasured | SLICE-01 probe; SLICE-02 integrated measurement |
| R3 | Ollama Cloud gemma4:cloud first-token latency unmeasured |
SLICE-01 probe; SLICE-02 integrated measurement |
| R4 | All-cloud three-hop path likely ~670ms (over 600ms) | SLICE-01 measures the integrated path; TTS behind interface from SLICE-02; Piper pre-staged as mitigation if R4 confirms. SLICE-01 is the wave-1 go/no-go gate. |
| R6 | Pipecat + Ollama direct-API integration depth unverified | SLICE-02 task verifies Pipecat Ollama service accepts custom host + bearer; thin adapter if not |
| R7 | Scenario branch detection (learner signal classification) | SLICE-03: LLM-as-judge (deepseek-v4-flash:cloud no-think) at session end, offline from voice loop |
Success criteria (Phase 1 exit)
- A learner can complete a full session: open client → hear disclaimer → speak to AI customer → AI responds <600ms (target; logged even if exceeded) → reach a branch outcome → receive text+voice debrief → session logged to SQLite.
- R1-R4 latency report exists with measured (not vendor-claimed) per-segment and end-to-end numbers; a documented TTS decision (Cartesia vs Piper) justified by data.
- All 15 P1 REQ-IDs verified as covered (see §5 coverage matrix).
- Per-session cost is logged (token counts + segment latencies + derived cost).
- Guardrail layer is pluggable (interface + one Customer Service ruleset implementation) and enforces the v0.1 ruleset (disclaimer, no legal/financial/medical advice, stay-in-role).
- Scenario is YAML → Pydantic → Pipecat Flows with
failure_modefield present.
2. Vertical Slices
Slices are ordered into 3 waves. Each slice delivers end-to-end value (a demoable behavior), not a horizontal layer. Wave N+1 depends on Wave N output.
SLICE-01 — Component & Integrated Latency Spike (R1-R4)
Wave: 1
REQ-IDs covered: REQ-VOICE-03, REQ-NFR-LAT-01, REQ-LLM-01 (probe), REQ-LLM-02 (probe)
Personas: lead-developer, backend-engineer
Dependencies: none (first slice)
Demoable outcome: A latency report (docs/latency-report.md or reports/latency-spike.md) with measured per-segment and end-to-end numbers, plus a recorded go/no-go decision on TTS (Cartesia cloud vs Piper self-hosted pre-stage). Running make latency-spike (or python scripts/latency_spike.py) reproduces the measurements.
Rationale: RESEARCH.md is explicit: "This is the single biggest v0.1 technical risk and must be spiked in Phase 1 week 1." The all-cloud three-hop path likely lands ~670ms. We measure before building the full loop so SLICE-02 can wire the correct TTS from the start.
Tasks:
| Task ID | Description | Verification |
|---|---|---|
| TASK-01-01 | Create repo skeleton: server/, client/, scenarios/, db/, guardrails/, llm/, asr/, tts/, scripts/, tests/ dirs; pyproject.toml (server) with pipecat, deepgram, cartesia, piper-tts, ollama, pydantic, aiosqlite deps; .env.example documenting DEEPGRAM_API_KEY, CARTESIA_API_KEY, OLLAMA_API_KEY, PIPECAT_* scopes. |
python -c "import pipecat" succeeds; dir structure matches PERSONAS.md territory. |
| TASK-01-02 | R1 probe: scripts/probe_deepgram.py — streaming WebSocket to Deepgram Nova-3, send a sample audio file (or synthesized PCM), measure first-partial-transcript latency from a Canada-region endpoint over 20 iterations; log min/median/p95. |
Running the script prints a latency table; results recorded in latency report. |
| TASK-01-03 | R2 probe: scripts/probe_cartesia.py — WebSocket to Cartesia Sonic, send a sample text chunk, measure first-audio-byte latency over 20 iterations; log min/median/p95. |
Running the script prints a latency table; results recorded. |
| TASK-01-04 | R3 probe: scripts/probe_ollama.py — direct API call to https://ollama.com/api/chat with OLLAMA_API_KEY bearer, model gemma4:cloud, stream=True, measure time-to-first-token over 20 iterations; also probe deepseek-v4-flash:cloud no-think mode TTFT. Log min/median/p95 + any throttle events (R5). |
Running the script prints TTFT tables for both models; results recorded. |
| TASK-01-05 | R4 probe: scripts/probe_e2e.py — integrated three-hop: feed a sample ASR transcript → Ollama gemma4:cloud streaming → Cartesia TTS streaming; measure end-to-end (transcript-in → first-audio-out). Run 10 iterations. Also measure the same path with Piper self-hosted (if Piper can be stood up locally in this task; otherwise note as pending and pre-stage in SLICE-02). |
Running the script prints the integrated e2e latency; recorded in report. |
| TASK-01-06 | Write docs/latency-report.md: per-segment measured latencies (R1-R4), integrated e2e, comparison vs the 600ms budget, and a TTS decision (Cartesia cloud vs Piper pre-stage) with rationale. If e2e >600ms with Cartesia, document Piper as the production v0.1 TTS and note pre-staging work for SLICE-02. |
Report file exists with measured numbers (not vendor claims) and a decision block. |
Must-have verification criteria:
scripts/probe_deepgram.py,probe_cartesia.py,probe_ollama.py,probe_e2e.pyall run and produce measured latency output.docs/latency-report.mdcontains real measured numbers for R1, R2, R3, R4 (not vendor claims).- Report contains an explicit TTS decision (Cartesia vs Piper) justified by the R4 integrated measurement.
- If R4 integrated path >600ms, Piper pre-staging is documented as a SLICE-02 task.
SLICE-02 — Thin Vertical Voice Loop (Walking Skeleton)
Wave: 1 REQ-IDs covered: REQ-VOICE-01, REQ-VOICE-02, REQ-VOICE-03, REQ-VOICE-04, REQ-ORCH-01, REQ-LLM-01, REQ-NFR-LAT-01 Personas: lead-developer, backend-engineer, frontend-engineer Dependencies: SLICE-01 (uses the TTS decision; latency budget confirmed feasible) Demoable outcome: A learner opens a minimal React page, clicks "Start", speaks one utterance, and hears the AI reply over WebRTC — end-to-end voice loop works, latency is displayed. Quality may be poor (hardcoded single-turn scenario, no branching, stub guardrail). This is the walking skeleton that makes latency measurable on the real integrated path.
Rationale: The first integrated slice must be minimal but complete (client → server → ASR → LLM → TTS → client) so we measure real latency, not probe latency. All swappable services (TTS D-014, LLM D-020, guardrail D-019) sit behind interfaces from this first slice so later swaps don't touch the pipeline.
Tasks:
| Task ID | Description | Verification |
|---|---|---|
| TASK-02-01 | Define service interfaces in server/services/: TTSProvider (async synthesize(text) -> audio_stream, voice_id), LLMProvider (async chat(messages, stream=True) -> token_stream, model), Guardrail (async check(text, context) -> verdict). ABCs/Protocols with type annotations. |
python -c "from server.services import TTSProvider, LLMProvider, Guardrail" succeeds; interfaces are abstract. |
| TASK-02-02 | Implement CartesiaTTS and PiperTTS adapters behind TTSProvider. Pre-stage Piper self-hosted on the pilot server per SLICE-01 decision (install piper-tts, download one voice model). TTS selection via env var `PRAXIS_TTS=cartesia |
piper`. |
| TASK-02-03 | Implement OllamaCloudLLM adapter behind LLMProvider — direct API to https://ollama.com/api/chat with bearer auth, stream=True, model param. Verify Pipecat's Ollama LLM service accepts custom host + bearer (R6); if not, wrap with this thin adapter so Pipecat consumes it as a generic LLM service. |
Adapter unit-tested with a mocked HTTP streaming response; a real call to gemma4:cloud returns a first token (confirms R6). |
| TASK-02-04 | Build Pipecat server pipeline in server/pipeline.py: Silero VAD → Deepgram Nova-3 STT (streaming) → OllamaCloudLLM (gemma4:cloud) → selected TTSProvider → WebRTC output. Wire interruptibility: learner VAD during TTS aborts TTS + yields floor (D-008, Pipecat built-in). Hardcoded single-turn system prompt (no YAML scenario yet). |
python -m server starts the Pipecat pipeline; a WebSocket/WebRTC connection is accepted; logs show VAD → STT → LLM → TTS frame flow. |
| TASK-02-05 | Build minimal React client in client/ (Vite + React + Pipecat client SDK): one page with "Start session" button, mic permission, WebRTC connect, audio playback, live transcript display (optional), and a latency readout. No branching UI, no debrief. |
npm run dev serves the client; clicking Start connects WebRTC; speaking produces an AI audio reply in the browser. |
| TASK-02-06 | Add an end-to-end latency probe to the pipeline: timestamp at final-transcript-ready, LLM-first-token, TTS-first-audio, client-playback-start; log to console and surface the ASR→TTS-first-audio number to the client for display. | The client displays a latency number after the first turn; logged numbers match probe_e2e.py within tolerance. |
| TASK-02-07 | Stub guardrail: NoOpGuardrail implementing Guardrail (always returns allow) so the pipeline has the pluggable hook in place. Real ruleset comes in SLICE-03. |
Pipeline calls guardrail.check() on each turn; swapping to a real impl requires no pipeline change. |
Must-have verification criteria:
- A learner can click Start, speak one utterance, and hear the AI reply in the browser.
- End-to-end latency (transcript-ready → first-audio) is measured and displayed.
- TTS is selected via env var; both Cartesia and Piper adapters exist behind the
TTSProviderinterface. - LLM is behind
LLMProvider;gemma4:cloudreturns tokens via direct API (R6 resolved). - Interruptibility works: speaking during AI TTS cuts the AI off (manual test).
- Guardrail slot exists and is swappable without touching the pipeline.
SLICE-03 — Branching Scenario + Guardrails + Interruptibility
Wave: 2 REQ-IDs covered: REQ-SCEN-01, REQ-SCEN-FMT-01, REQ-ORCH-02, REQ-VOICE-04, REQ-NFR-SAFE-01 Personas: lead-developer, backend-engineer, data-engineer Dependencies: SLICE-02 (voice loop + interfaces exist) Demoable outcome: The AI plays the "angry customer refund" scenario with a real branch point — the learner's approach either resolves (accept) or escalates — and the session-start disclaimer plays. Guardrails enforce the Customer Service ruleset. The scenario is defined in YAML, loaded via Pydantic, and drives Pipecat Flows.
Tasks:
| Task ID | Description | Verification |
|---|---|---|
| TASK-03-01 | Define Pydantic scenario schema in server/scenarios/schema.py: Scenario (id, path, market, language, title, difficulty, failure_mode, persona, setup, success_criteria, common_mistakes, branches[], debrief) matching the RESEARCH.md example. Branch has id, trigger.learner_signals, outcome, failure_mode (optional), debrief_focus. Validate at load time. |
Unit tests: a valid YAML parses; an invalid YAML raises a typed Pydantic error. |
| TASK-03-02 | Author scenarios/customer_service_refund_ca_v01.yaml per D-010 and the RESEARCH.md example: "Angry customer requesting refund on damaged product", one branch point (accept_resolution vs escalate), failure_mode: escalates_unresolved present, success criteria, common mistakes, debrief config (model deepseek-v4-flash:cloud, mode no_think). |
python -c "from server.scenarios.loader import load; load('customer_service_refund_ca_v01')" returns a valid Scenario object with both branches. |
| TASK-03-03 | Integrate Pipecat Flows: map the scenario branches to a Flows state machine. The system prompt is built from setup.system_prompt; opening line from setup.opening_line is the first TTS utterance. Branch transition logic is driven by learner-signal classification (TASK-03-06). |
Pipeline runs the scenario: AI speaks the opening line, then converses; reaching a branch transitions to the branch outcome. |
| TASK-03-04 | Implement CustomerServiceGuardrail behind the Guardrail interface (D-019): system-prompt constraints (no legal/financial/medical advice, no real-company impersonation, stay-in-role, concise-for-voice), debrief output filter (block recommendations that learner advise legal action), session-start disclaimer audio ("This is an AI practice session for training purposes. It is not a real conversation and no real company is involved."). Wire into pipeline replacing NoOpGuardrail. |
Unit tests: guardrail flags a "sue them" recommendation; allows a normal coaching line; disclaimer text is defined. Pipeline plays disclaimer as first audio. |
| TASK-03-05 | Verify interruptibility on branching turns: learner can cut the AI mid-utterance during any turn (including the opening line and post-branch turns); AI aborts TTS and yields (D-008). Manual + automated test. | Manual test: speaking during AI speech cuts it off; a test script confirms TTS abort event fires on VAD during TTS. |
| TASK-03-06 | Implement branch classifier (R7): at session end (or turn boundary), call deepseek-v4-flash:cloud in no-think mode as LLM-as-judge to classify learner signals into accept_resolution or escalate based on the turn transcripts + the scenario's learner_signals definitions. Offline from the voice loop (not on the latency-critical path). |
A scripted transcript classified as "empathy + concrete_resolution" → accept; "defensive + policy_first" → escalate. |
| TASK-03-07 | Replace the hardcoded system prompt from SLICE-02 with the scenario-driven prompt from the loaded YAML. The pipeline now starts a session by loading a named scenario. | Starting a session with scenario cs_refund_ca_v01 plays the correct opening line and uses the scenario's system prompt. |
Must-have verification criteria:
- Scenario is YAML → Pydantic → Pipecat Flows;
failure_modefield is present. - One branch point (accept vs escalate) is reachable and changes the session outcome.
- Session-start disclaimer audio plays as the first AI utterance.
CustomerServiceGuardrailis plugged into theGuardrailinterface (no pipeline change) and enforces the ruleset (unit-tested).- Interruptibility works on all turns (manual + automated).
- Branch classifier runs offline (not on the voice latency path) and correctly classifies two scripted transcripts.
SLICE-04 — Learner State + Cost Logging
Wave: 2
REQ-IDs covered: REQ-STATE-01, REQ-NFR-COST-01
Personas: lead-developer, backend-engineer, data-engineer
Dependencies: SLICE-02 (loop produces turns to log), SLICE-03 (scenario produces branch outcome to log)
Demoable outcome: After a session, praxis.db contains the session row with branch path and outcome, all turns with ASR/TTS text and per-turn latency, and a derived cost row. sqlite3 praxis.db "SELECT * FROM sessions" shows the last session.
Tasks:
| Task ID | Description | Verification |
|---|---|---|
| TASK-04-01 | Create SQLite schema in db/schema.sql + migrations (db/migrations/0001_init.sql): learner(id, display_name, created_at) with one hardcoded row (learner-1, "Alex"); sessions(id, learner_id, scenario_id, started_at, ended_at, branch_path_json, outcome, cost_estimated_cents); turns(id, session_id, seq, role, asr_text, tts_text, latency_ms, created_at); progress(learner_id, scenario_id, attempts, last_outcome, updated_at). Use aiosqlite for async access. |
Migration runs; sqlite3 praxis.db ".schema" shows all 4 tables; the hardcoded learner row exists. |
| TASK-04-02 | Implement db/store.py async access layer: start_session(learner_id, scenario_id), log_turn(session_id, seq, role, asr_text, tts_text, latency_ms), end_session(session_id, branch_path, outcome, cost_cents), update_progress(learner_id, scenario_id, outcome). Type-annotated, returns typed objects. |
Unit tests with a temp DB: start session → log 3 turns → end session → query returns the full session with turns. |
| TASK-04-03 | Wire the store into the Pipecat pipeline: on session start (create row), per turn (log turn with latency), on branch decision (update branch_path), on session end (set outcome + update progress). No auth — learner_id is the hardcoded learner-1. |
After a manual session, SELECT * FROM sessions and SELECT * FROM turns show the session and its turns. |
| TASK-04-04 | Implement cost logging (REQ-NFR-COST-01, D-012): per session, count LLM input/output tokens (gemma4 + deepseek-v4-flash), Deepgram audio minutes, Cartesia/Piper characters; derive an estimated cost in cents using a cost_rates.yaml config (no enforced ceiling). Store in sessions.cost_estimated_cents. |
After a session, SELECT cost_estimated_cents FROM sessions returns a non-null number; a cost_breakdown is logged (token counts, minutes, chars). |
Must-have verification criteria:
- SQLite
praxis.dbexists withlearner,sessions,turns,progresstables. - One hardcoded learner row exists (no auth).
- A completed session produces a
sessionsrow +turnsrows + aprogressupdate. cost_estimated_centsis non-null for a completed session and backed by a logged breakdown.
SLICE-05 — Coaching Debrief + Full Client UX
Wave: 3
REQ-IDs covered: REQ-DEBRIEF-01, REQ-LLM-02, REQ-NFR-SAFE-01 (debrief filter)
Personas: lead-developer, backend-engineer, frontend-engineer
Dependencies: SLICE-03 (branch outcome + scenario debrief config), SLICE-04 (session logged with turns)
Demoable outcome: At session end, the learner sees a text coaching debrief and hears a voice version, both generated from their actual turns + branch outcome + the scenario's debrief_focus. The React client shows a polished session flow: start → live turn indicators → interrupt feedback → end debrief view (text + audio playback + latency summary).
Tasks:
| Task ID | Description | Verification |
|---|---|---|
| TASK-05-01 | Implement debrief generation in server/debrief.py: on session end, load the session turns + branch outcome + scenario debrief.debrief_focus, call deepseek-v4-flash:cloud in no-think mode (per D-020 / scenario config) with the debrief prompt template. Produce a concise text summary (what you did well / what to improve / one next step). |
A scripted session (turns + outcome=escalate) produces a debrief text that references the learner's actual turns and the escalates_unresolved focus. |
| TASK-05-02 | Route the debrief text through CustomerServiceGuardrail output filter (block legal-action recommendations, keep focus on learner performance). |
Unit test: a debrief containing "tell the customer to sue" is filtered/blocked; a normal coaching debrief passes. |
| TASK-05-03 | Synthesize the debrief as voice via the TTSProvider (same voice as the role-play per D-006) and stream to the client over the existing WebRTC connection. |
At session end, the client receives and plays the debrief audio; the same TTSProvider interface is reused (no new TTS path). |
| TASK-05-04 | Build the full React client session UX: (a) start screen with scenario title + disclaimer acknowledgement, (b) live session view with turn indicators (learner/AI), interrupt feedback (visual on AI-yield), live latency readout, (c) end-of-session debrief view with debrief text + audio replay + latency/cost summary. Replace the SLICE-02 minimal page. | A full session flows through all three views; the debrief view shows text + an audio playback control + a latency summary. |
| TASK-05-05 | Wire debrief persistence: store the debrief text + the branch outcome in the session row (extend sessions with debrief_text column via migration 0002_debrief.sql). |
After a session, SELECT debrief_text FROM sessions WHERE id=? returns the generated debrief. |
| TASK-05-06 | End-to-end verification script (scripts/e2e_smoke.py or tests/test_e2e.py): start session → simulate 2-3 turns → trigger a branch → end session → assert debrief generated, session + turns + cost logged in SQLite, latency < budget (or logged if exceeded). |
Running the script passes; it asserts DB rows, debrief non-empty, cost non-null. |
Must-have verification criteria:
- At session end, a text coaching debrief is generated referencing the learner's actual turns and branch outcome.
- The debrief is spoken in the same voice as the role-play (D-006) via the
TTSProviderinterface. - Debrief text passes the guardrail output filter.
- React client shows a complete session flow: start → live → debrief views.
deepseek-v4-flash:cloudno-think mode is used for the debrief (REQ-LLM-02).- End-to-end smoke test passes (session → turns → branch → debrief → DB logged).
3. Wave Ordering
Wave 1 (foundation + risk spike — must pass before Wave 2)
├── SLICE-01 Latency spike (R1-R4) [lead-developer, backend-engineer]
└── SLICE-02 Thin vertical voice loop [lead-developer, backend-engineer, frontend-engineer]
↑ depends on SLICE-01 TTS decision
Wave 2 (scenario + state — builds on verified loop)
├── SLICE-03 Branching scenario + guardrails [lead-developer, backend-engineer, data-engineer]
└── SLICE-04 Learner state + cost logging [lead-developer, backend-engineer, data-engineer]
↑ SLICE-03 and SLICE-04 can run in parallel after Wave 1;
SLICE-04 wiring benefits from SLICE-03 branch outcome but schema is independent
Wave 3 (debrief + UX — completes the daily loop)
└── SLICE-05 Coaching debrief + full client [lead-developer, backend-engineer, frontend-engineer]
↑ depends on SLICE-03 (branch outcome + debrief config) and SLICE-04 (session turns logged)
Wave 1 gate: SLICE-01 produces the latency report + TTS decision. If R4 confirms e2e >600ms with Cartesia, Piper pre-staging becomes a SLICE-02 task before the loop is wired. Wave 2 does not start until the walking skeleton (SLICE-02) demonstrates a working end-to-end voice turn with measured latency.
Wave 2 parallelism: SLICE-03 (scenario + guardrails) and SLICE-04 (SQLite state) are largely independent — the schema is authored from REQUIREMENTS, not from scenario runtime. They can proceed in parallel; SLICE-04's pipeline wiring consumes SLICE-03's branch outcome, so the final wiring task in SLICE-04 depends on SLICE-03's branch classifier. In practice, start both, merge the wiring last.
Wave 3 gate: SLICE-05 requires both SLICE-03 (branch outcome + debrief config) and SLICE-04 (logged turns) to be verified.
4. Phase 1 Exit Criteria
All must be true for Phase 1 to ship:
- Full session works end-to-end: A learner opens the React client, hears the disclaimer, speaks to the AI customer (refund scenario), the AI responds, the conversation reaches a branch outcome (accept or escalate), the learner receives a text+voice coaching debrief, and the session is logged to
praxis.db. - Latency is measured, not assumed:
docs/latency-report.mdexists with real R1-R4 numbers. End-to-end latency is logged per session (even if >600ms — the target, with Piper mitigation if needed). - TTS is behind an interface and swappable:
PRAXIS_TTS=cartesia|piperselects the provider with no pipeline change (D-014). - LLM is behind an interface and swappable:
LLMProviderwraps Ollama Cloud direct API;gemma4:cloud(role-play) anddeepseek-v4-flash:cloudno-think (debrief) both callable (D-020, REQ-LLM-01, REQ-LLM-02). - Guardrail layer is pluggable:
Guardrailinterface +CustomerServiceGuardrailimplementation; disclaimer plays; ruleset unit-tested (D-019, REQ-NFR-SAFE-01). - Scenario is YAML → Pydantic → Pipecat Flows:
customer_service_refund_ca_v01.yamlloads, validates, drives the branching runtime, and carries thefailure_modefield (D-018, REQ-SCEN-FMT-01, REQ-SCEN-01). - Interruptibility works: Learner speech cuts AI TTS mid-utterance; AI yields (D-008, REQ-VOICE-04).
- Learner state persists: SQLite has session + turns + progress + cost; single hardcoded learner, no auth (D-007, REQ-STATE-01).
- Cost is logged per session:
cost_estimated_centsnon-null with a logged breakdown (REQ-NFR-COST-01, D-012 — no enforced ceiling). - End-to-end smoke test passes:
tests/test_e2e.py(orscripts/e2e_smoke.py) verifies the full loop including DB assertions.
5. REQ Coverage Matrix
Every P1 must/principle REQ-ID mapped to at least one slice.
| REQ-ID | Priority | Slice(s) | Covered by task(s) |
|---|---|---|---|
| REQ-VOICE-01 | must | SLICE-02 | TASK-02-04 (Deepgram Nova-3 streaming ASR in pipeline) |
| REQ-VOICE-02 | must | SLICE-02 | TASK-02-02, TASK-02-04 (TTS behind interface, one voice, Cartesia/Piper) |
| REQ-VOICE-03 | must | SLICE-01, SLICE-02 | TASK-01-05, TASK-02-06 (measured e2e latency) |
| REQ-VOICE-04 | must | SLICE-02, SLICE-03 | TASK-02-04, TASK-03-05 (interruptibility, abort-and-yield) |
| REQ-SCEN-01 | must | SLICE-03 | TASK-03-02, TASK-03-03 (refund scenario, one branch, failure_mode) |
| REQ-STATE-01 | must | SLICE-04 | TASK-04-01..04-03 (SQLite, single learner, session log) |
| REQ-LLM-01 | must | SLICE-01, SLICE-02 | TASK-01-04, TASK-02-03 (gemma4:cloud direct API callable) |
| REQ-LLM-02 | must | SLICE-01, SLICE-05 | TASK-01-04, TASK-05-01 (deepseek-v4-flash:cloud no-think for debrief) |
| REQ-DEBRIEF-01 | must | SLICE-05 | TASK-05-01..05-03 (end-of-session text+voice summary) |
| REQ-ORCH-01 | must | SLICE-02 | TASK-02-04 (Pipecat + Silero VAD + interruptibility) |
| REQ-ORCH-02 | must | SLICE-03 | TASK-03-04 (pluggable guardrail + Customer Service ruleset) |
| REQ-SCEN-FMT-01 | must | SLICE-03 | TASK-03-01, TASK-03-02 (YAML DSL → Pydantic → Pipecat Flows) |
| REQ-NFR-LAT-01 | must | SLICE-01, SLICE-02 | TASK-01-05, TASK-02-06 (<600ms measured + logged) |
| REQ-NFR-SAFE-01 | must (baseline) | SLICE-03, SLICE-05 | TASK-03-04, TASK-05-02 (guardrails + disclaimer + debrief filter) |
| REQ-NFR-COST-01 | must (logging) | SLICE-04 | TASK-04-04 (per-session cost logged, no enforced ceiling) |
Coverage: 15/15 P1 REQ-IDs mapped. No P1 REQ is uncovered.
Planning Decisions
| ID | Decision | Rationale | Confidence | Alternatives |
|---|---|---|---|---|
| D-P1-01 | 5 slices across 3 waves | Wave 1 = risk spike + walking skeleton (2 slices); Wave 2 = scenario + state (2 slices, parallelizable); Wave 3 = debrief + UX (1 slice). Balances risk-front-loading with vertical-slice discipline. | 0.85 | 4 slices (merge state into scenario), 6 slices (split client UX from debrief) |
| D-P1-02 | SLICE-01 is a standalone probe slice before SLICE-02 | RESEARCH.md mandates R1-R4 be spiked in week 1. Standalone probes are cheaper/faster than building the full loop first, and the TTS decision (R4) informs SLICE-02 wiring. | 0.90 | Fold probes into SLICE-02 (delays the go/no-go; risks building on the wrong TTS) |
| D-P1-03 | SLICE-02 is a thin walking skeleton (hardcoded single-turn, no branching) | Measures integrated latency on the real path before investing in scenario runtime. Quality is deliberately poor; completeness over polish. | 0.85 | Build the full branching loop directly (couples latency validation to scenario complexity) |
| D-P1-04 | SLICE-03 and SLICE-04 run in parallel in Wave 2 | The SQLite schema is authored from REQUIREMENTS, not from scenario runtime; only the final wiring task depends on the branch classifier. Parallelism shortens Wave 2. | 0.75 | Strict sequence (slower, no benefit) |
| D-P1-05 | Branch classifier (R7) uses LLM-as-judge offline at session end | Keeps the latency-critical voice loop free of a second LLM call. deepseek-v4-flash:cloud no-think is cheap and fast enough for a one-shot end-of-session classification. |
0.80 | Rule-based classifier (brittle), inline per-turn classifier (adds latency) |
| D-P1-06 | Debrief reuses the same TTSProvider (one voice, D-006) |
D-006 mandates one voice persona for both role-play and mentor. No second TTS config; the debrief is just another TTS utterance via the same interface. | 0.90 | Separate mentor voice (violates D-006, adds config risk) |
End of Phase 1 plan. Next step: orchestrator reviews, optionally grills (GRILL stage), then proceeds to EXECUTE on branch phase/01-minimal-voice-loop.