Context
Before calling end_call(deal_agreed) the agent must restate exact terms and get a second explicit yes (CFPB-aligned; a bare "okay" is insufficient). Today this cadence is enforced only by prompt text (data/prompts/resolution_voice_streaming/v1.yaml:189-231) — flagged in the repo's own known-limitations as brittle: any prompt edit, model swap, or adversarial borrower phrasing can produce a "deal" that was never explicitly confirmed (this exact failure was engineering-log incidents #11/#23). The chat path already has a code guard (_AFFIRMATION_PATTERNS + enforcement in apps/api/chat_session.py:73-82, 253-273); the streaming path has none.
Implementation plan
- Negotiation state tracker: a small per-call state object in
apps/voice/streaming.py recording the last validated propose_offer (canonical terms from _validate_offer) and recent user turns (subscribe to final TranscriptionFrames or the user context aggregator).
- Gate in
handle_end_call (streaming.py:537-548): when outcome == "deal_agreed":
- require a validated
propose_offer to exist,
- require the assistant's restatement turn to have occurred after that offer (assistant turn containing the canonical numbers),
- require the latest user turn to match an explicit-affirmation check — extract
_AFFIRMATION_PATTERNS from chat_session.py into a shared module (e.g. agents/affirmation.py) and reuse on both paths.
- Corrective tool result: if the cadence is unmet, return a structured tool error ("Do not end the call: restate the exact terms — $X total / $Y monthly for Z months — and ask for explicit confirmation") so the LLM self-corrects in-conversation instead of hanging up.
- Loop guard: allow max 2 rejections, then permit
no_deal end to avoid an unclosable call.
- Tests: unit tests on the gate (no offer → reject; offer + bare "okay" → reject; offer + restatement + "yes, I agree" → allow); scripted cooperative scenarios (
examples/01-02) still close via simulator; combative/evasive scenarios never yield an ungated deal_agreed.
Acceptance criteria
end_call(deal_agreed) is impossible in code without a validated offer + explicit second affirmation — verified by unit tests, independent of prompt wording.
- Cooperative scripted scenarios still reach
deal_agreed (no over-blocking).
- Shared affirmation module used by both chat and streaming paths (no duplicated pattern lists).
Dependencies
Context
Before calling
end_call(deal_agreed)the agent must restate exact terms and get a second explicit yes (CFPB-aligned; a bare "okay" is insufficient). Today this cadence is enforced only by prompt text (data/prompts/resolution_voice_streaming/v1.yaml:189-231) — flagged in the repo's own known-limitations as brittle: any prompt edit, model swap, or adversarial borrower phrasing can produce a "deal" that was never explicitly confirmed (this exact failure was engineering-log incidents #11/#23). The chat path already has a code guard (_AFFIRMATION_PATTERNS+ enforcement inapps/api/chat_session.py:73-82, 253-273); the streaming path has none.Implementation plan
apps/voice/streaming.pyrecording the last validatedpropose_offer(canonical terms from_validate_offer) and recent user turns (subscribe to finalTranscriptionFrames or the user context aggregator).handle_end_call(streaming.py:537-548): whenoutcome == "deal_agreed":propose_offerto exist,_AFFIRMATION_PATTERNSfromchat_session.pyinto a shared module (e.g.agents/affirmation.py) and reuse on both paths.no_dealend to avoid an unclosable call.examples/01-02) still close via simulator; combative/evasive scenarios never yield an ungateddeal_agreed.Acceptance criteria
end_call(deal_agreed)is impossible in code without a validated offer + explicit second affirmation — verified by unit tests, independent of prompt wording.deal_agreed(no over-blocking).Dependencies