← Choice Health scheduler-demo-problem-statement.md raw .md

Problem Statement — the scheduler demo doesn't reliably book a slot

Date: 2026-07-02 For: working session with Fable 5 Goal of the demo we want: a caller (or operator) can, over the phone / in the voice UI, do the three things that define "the scheduler works":

  1. See what's blocked/off on a given day.
  2. See the available free slots on a day / soonest.
  3. Book an hour into one of those slots — and have it actually land on the calendar, not escalate.

Today the demo does not reliably do #3, and there is no clean path for #1/#2. This document states the problem precisely so we can fix it together.


1. What actually happens right now (evidence)

Driving flows/tutorial_book_appointment (or serene_mobile_massage / skyline_apartments — all use the book_appointment_v1 module) in the voice UI, the call escalates to a human instead of booking. The live log (logs/live.log, 2026-07-02) shows why — the schedule_event tool errors:

hook_result name='schedule_event' state='ERROR'
  error="time_range must be morning|afternoon|evening|HH:MM or a recognizable
         phrase like 'noon'/'2 PM', got '12:00 to 15:00'"
hook_result name='schedule_event' state='ERROR'
  error="time_range must be ... got '14:00:00-05:00'"

The FSM then routes book_session → handoff_to_human on booking_failed.

Root cause: the LLM naturally emits time_range values the slot-mapper doesn't accept — a range ("12:00 to 15:00") or a full ISO datetime ("14:00:00-05:00"). The mapper (slot_mapper.py::_coerce_time_range) accepts a narrow set (morning|afternoon|evening|HH:MM|"noon"/"2 PM") and raises on anything else, which the flow treats as a terminal booking failure → escalate. It is intermittent: one call in the same session succeeded (state='DONE') when the LLM happened to format the arg correctly. So the backend works; the LLM↔tool argument contract is brittle, and its failure mode is a hard escalate rather than a graceful re-ask.

2. Two deeper structural problems the demo exposes

(a) There are two different scheduler implementations, and they diverge.

System Availability/offer Book tool Offer selection Used by
scheduling_v1 prefetch via schedule_event(intent=check) schedule_event scheduling_offer_slots — a deterministic locked_choice (press/say 1,2,3) Defined in austin_plumbing but bypassed by its intake happy-path
book_appointment_v1 propose_booking schedule_event offer_slotsLLM free-text collect The live slot-booking flows (tutorial / serene / skyline)

The path we hardened this session (a deterministic simulator + two real bug fixes, B-CAL-1/2 — see calendar-scheduler-sim-report.md) is scheduling_v1, whose offers are a locked_choice and therefore robust. But the flows a user actually reaches in the UI use book_appointment_v1, whose offer/booking args are LLM free-text — exactly where the time_range failure happens. We hardened one engine and demo the other.

(b) The obvious entry point (austin_plumbing) is intake-only. Its happy path collects "possible days" and "blocked days" and ends at a callback summary terminal — it never offers bookable time slots. A first-time tester (correctly) reads this as "the scheduler only shows days, not slots." The booking states exist in the flow (via the module) but are unreachable from its prompt.

3. What the caller-facing capabilities should be (the ask)

The scheduler should expose, reliably and demonstrably:

The cal-provider backend already has the primitives: get_available_slots(...), check_availability(...), propose_booking(...), confirm_booking(...) (riff/providers/cal_provider_adapter/adapter.py). The gap is surfacing them robustly through a flow and not hard-failing on arg format.

4. The problems to solve (proposed framing for Fable 5)

5. What already exists to build on

6. Acceptance criteria for "fixed"

  1. In the voice UI, a caller can ask a day's free slots and blocked times and get a correct, spoken answer.
  2. A caller can book an hour into an offered slot and it lands on the calendar with an event id — no escalation on well-intentioned phrasing.
  3. A malformed/ambiguous time never hard-escalates; it re-asks or maps gracefully.
  4. The live booking path has deterministic permutation coverage (like scheduling_v1 has today), so regressions are caught before a demo.

7. One-paragraph summary

The scheduler's backend can book, but the live conversational flow (book_appointment_v1) is brittle at the LLM↔tool boundary: the model emits time_range formats the slot-mapper rejects (ranges, ISO datetimes), and the failure mode is a hard escalation to a human rather than a graceful re-ask. We also have two divergent scheduler engines — we hardened the robust, locked_choice one (scheduling_v1) but demo the LLM-free-text one (book_appointment_v1). The fix is to make booking select a backend-vetted offer (the #20 "offers are quotes" direction) so the LLM never hand-formats a time, converge on one engine, and give the live path the deterministic coverage the other one now has.