Make the AI booking assistant respond to time suggestions with a focused set of nearby options instead of dumping every available slot, and ensure that mid-flow time changes correctly show workers instead of repeating time listings.
When a customer suggested a time, the bot returned every slot for the entire day. When a customer changed their time mid-flow, the bot showed times again instead of workers. Both problems had the same root cause: the tools returned too much data and the LLM made wrong choices at stage transitions.
AI agent tools should return exactly what the customer's message implied they needed, not everything the database has. And when the state machine transitions mid-loop, deterministic checks are more reliable than another LLM call.
The Build
The booking flow was working. A customer could pick a service, choose a date and time, select a worker, and confirm an appointment. The state machine tracked every field. The tools queried real data. The phase-based architecture kept each step focused.
But "working" meant working through the happy path. When I started testing edge cases — customers suggesting times with bare numbers, customers changing their mind about a time slot after workers were already shown — two related problems surfaced that exposed gaps in how the tools and the state machine worked together.
The first problem was about data volume. The second was about control flow. Both came down to the same question: when should the system trust the LLM to make the right choice, and when should it intervene?
The Problem
I was testing a real conversation flow and the customer said "5?" to suggest a time. The bot went and fetched every available slot for that day, all eighteen of them. Then it listed every single one.
In the raw output, the response was a markdown bullet list starting from 10:00 AM. In the widget, those bullets rendered as individual choice chips. Eighteen chips in a row, no grouping, no hierarchy, starting from morning even though the customer just asked about 5 PM. One block of the output rendered as a single run-on string, several times jammed together with no spaces between them. That was the customer's actual visible experience.
A receptionist who already told you "we don't have 4 PM, sorry" would never respond to "what about 5?" by reading the entire appointment book out loud. They would say "we have 4:30, 5:00, and 5:30 open." The bot broke that analogy completely. The customer gave a preference signal and the bot drowned it in irrelevant data.
Two things caused this. First, no proximity filter existed. The availability lookup always returned every slot for the date, whatever the customer had asked for. Second, bare numbers like "5" were not readable. The part that pulls a time out of a message needed an AM/PM marker or a 24-hour clock, so on its own "5" came back as nothing. Even after I added a proximity filter, bare numbers slipped through, because the filter had no time to filter against.
Then the second bug appeared. A customer was at the stage where they pick a staff member, with the 2 PM options already on screen. They said "3 pm?" to change their time. The booking flow correctly noticed the time had changed and cleared everything downstream of it, including the record that availability had been confirmed.
That record is exactly what the automatic staff lookup waits for. It had just been cleared, so the lookup did not fire. The model ran instead, checked that 3 PM was open, and the flow moved on to the stage where staff should appear. At that point the model was asked to act again, with two reasonable options in front of it: look up times, or look up staff. It picked times, the one it had used a moment earlier. Recency bias, plainly. The customer asked to move their appointment and got handed another list of times.
I found this in the logs while working on the slot problem. One entry showed a clean time match. The next showed the customer saying "3 pm?", the flow correctly registering the time change, and the bot answering with times instead of staff. The slot work surfaced the staff bug: I was already reading these logs, and the second problem walked past while I was looking at the first.
Assumption vs Reality
I assumed returning all available data gives the LLM the best chance to pick the right answer. If the model has all 18 slots, it can find the one nearest to the customer's request on its own. And if the stage rules are clear enough, the LLM will pick the right tool at each transition.
More data means more ways to be wrong. The LLM listed all 18 because the tool gave it 18. Constraining the tool output was more reliable than trusting the model to filter. And at stage transitions, the LLM's tool choice is influenced by recency — it picks the tool it just used, not the one the new stage requires. The fix was not a better prompt. It was less data and a deterministic check.
The Fix
For the slot dumping, the fix was in the availability lookup itself. When the customer has named a time, it now returns only the five slots nearest to it instead of the whole day. Five came from thinking about what a receptionist would offer: the exact time asked for, one or two options before it, one or two after. Three felt too narrow if the requested slot was taken. Seven started feeling like a list again. Five also fits cleanly in the chip UI on mobile without scrolling.
For bare numbers like "5", I added an AM/PM resolver that uses actual available slot proximity to decide. A nail salon open 10 AM to 7 PM has no slots anywhere near 5 AM. The distance from any available slot to 5 AM is always larger than the distance to 5 PM. So "5" resolves to 5 PM by construction, not by guessing. I chose silent resolution over asking "did you mean AM or PM?" because the question has an obvious answer in context, and adding a clarification step to every bare-number input would create friction that slows the flow for everyone.
For the staff bug, the fix was architectural. The moment the flow reaches the point where staff should be shown, and the customer named a specific time to get there, the system looks up the available staff itself and returns them. It no longer asks the model what to do next. One less turn, and no chance of picking the wrong one.
The condition about naming a specific time is the part that matters. Without it, browsing broke. A customer with 9:00 AM left over from an earlier exchange says "show me times for Saturday", the system checks a date with no time attached, and five afternoon slots come back. If the staff lookup fired anyway it would run against the stale 9 AM, find nobody, and announce that no one is available. The customer was browsing, not confirming. The condition separates the two: name a time and you are confirming, so act; leave it open and you are exploring, so let them choose.
Removing the extra LLM round-trip mattered for three reasons in this order. First, wrong-decision risk: every additional LLM call is another chance for the model to choose a wrong tool. The bug itself was caused by an extra call producing the wrong selection. Second, latency: each round-trip adds 400 to 800 milliseconds, and two extra calls add over a second to what should feel like an instant response in a conversational widget. Third, token cost: real but genuinely last at fractions of a cent per booking session.
Every piece of extra data in a tool result is noise. Eighteen slots is not more helpful than five. It diffuses the signal the customer just gave. The principle: the tool result should contain exactly what the customer's last message implied they needed, not what the database is willing to return. The same principle applies to tool availability at stage transitions. If two tools are plausible and one was just used, the LLM has recency momentum toward the wrong one. Prompting cannot reliably govern which tool the model picks on the second call in a loop when context just shifted. The deterministic check does not ask the LLM what to do next. It checks the state and acts. Three patterns came out of this debugging session. Return less data to the model — proximity-filtered slots instead of full-day dumps. Add deterministic checks at flow transitions — do not let the LLM re-decide what a state change already determined. Distinguish browsing from confirming, because "show me options" and "I want this time" have to be handled differently. These patterns are still being discovered reactively during debugging. But recognizing them twice means the third time should be caught before it ships. The design heuristic forming: any time the LLM has two plausible tools and one is the tool it just used, that is a flag to intercept deterministically.
When a customer says "5?" they expect the bot to respond the way a receptionist would — "we have 4:30, 5:00, and 5:30 open." Not to read the entire appointment book. The difference between a booking bot that feels helpful and one that feels broken is whether it respects the signal the customer just gave. The same applies when a customer changes their mind about a time. If they said "actually 3 PM" while looking at workers for 2 PM, they expect to see workers for 3 PM next. Not a new list of times. The bot should follow the same logic a receptionist would: update the time, check who is available, and show the options. Not start the whole conversation over. For a salon owner, these fixes mean fewer abandoned conversations. A customer who gets 18 time slots when they asked about one is more likely to give up and call. A customer who gets a focused answer is more likely to finish booking.
Builder Notes
Both bugs had the same root cause: the system gave the LLM too much data or too many choices at moments where a deterministic decision would have been more reliable.
Technical Note 1
The slot proximity filter is straightforward math: sort all slots by absolute distance from the requested time, take the first five. The AM/PM resolver works the same way — compare distances to both interpretations and pick the closer one.
Technical Note 2
The check for whether the customer named a time is made where that lookup actually happens, not read back off the booking record afterwards. It matters which one you ask. The booking record may still be carrying a time from an earlier exchange, so it answers a different question than the one in front of you.
Technical Note 3
Four existing tests had to be updated after the mid-loop deterministic check was added. They were asserting old behavior where the LLM made a tool call that the deterministic check now handles before the LLM runs. Understanding why each assertion changed was part of the verification.
Technical Note 4
The 600-test suite passes with these changes. The slot-limiting logic is deterministic and verifiable by construction. The AM/PM resolver produces correct results by the math of slot distances, not by heuristics.
Technical Note 5
The design heuristic to watch for going forward: any stage transition where the LLM faces two plausible tools and has momentum toward the wrong one is a candidate for a deterministic intercept.
Before / After
- The availability lookup returned every slot for a day regardless of what the customer asked
- Bare numbers like "5" were not parseable and bypassed filtering
- A time change while picking staff made the bot re-show times instead of staff
- The deterministic worker check only ran once at the top of the request
- No distinction between browsing availability and confirming a specific time
- Slots filtered to the 5 nearest when a time is specified
- Bare numbers resolved to AM or PM via available-slot proximity
- Mid-loop deterministic worker check fires on stage transitions with a time argument
- Browsing and confirming are told apart before the system decides what to show
- Each extra LLM round-trip at a stage transition is treated as a bug, not a feature
What I'd Do Differently
I would audit every tool result for data volume before it reaches the model. The question is not "does the tool return correct data" but "does the tool return the right amount of data for what the customer just asked." I would also look at every stage transition and ask: is there a point where two tools are plausible and the LLM might pick based on recency rather than intent? Those transitions are candidates for deterministic intercepts from day one.