Travel & booking agents.
Everything a travel agent does before the booking is free and repeatable; the booking itself is a payment, a contract and a seat someone else can no longer have. Build the two halves as different systems — a cheap exploratory loop, and one narrow committed path with a fresh quote, an idempotency key and a spend cap — because the failure that ends the product is not a bad itinerary suggestion, it is two flights booked for one traveller on a Tuesday morning.
Four jobs, and only one of them is dangerous.
"Travel agent" bundles four products that share a domain and nothing else. Scope the build by naming which one you are shipping, because the engineering barely overlaps.
- Search and compare. Multi-leg, multi-vendor, multi-constraint exploration. Purely read-only, endlessly retryable, and the one an LLM is straightforwardly good at. Failure costs a wasted call.
- Book. One irreversible write against inventory that other people are simultaneously consuming, with money attached. Everything hard about this domain lives in this bullet.
- Service in flight. Seat changes, bag additions, name corrections, upgrade requests. Semi-reversible, policy-dense, and the volume driver.
- Disruption recovery. The cancelled 06:40, the missed connection, the hotel that has no record of the reservation. Time-critical, high-emotion, and where the product earns its keep.
Most teams build one and two, discover that four is where users actually feel value, and never quite fix the fact that two is wired into the same loop as one. That wiring is the bug this page is about. Shopping and checkout agents share the shape; travel is the harder case because inventory is scarce, prices move by the minute, and the rules attached to a purchase are longer than the purchase.
The price the user approved is a photograph.
Here is the failure mode that surprises teams from other domains. The agent searches at 14:02, presents an option at 14:03, the user reads it and confirms at 14:06, and the booking executes at 14:07 against inventory and pricing that have moved three times since the search. Nothing in the loop is wrong. The result is still a booking the user did not agree to.
Three distinct things go stale, and they need separate handling:
- Price. Usually small, usually up, and the one users forgive least because they feel misled. Never let the model restate a price from context — a number that has passed through a token stream is a claim, not a quote.
- Availability. Binary and unforgiving. The last seat at that fare is gone and the agent must not silently book the next one up.
- Conditions. The same route at the same price can carry different change fees, baggage allowance or refundability depending on the fare bucket that was actually available. This is the one that produces a complaint six weeks later.
The mechanism that fixes all three is the same and it is not a prompt instruction. Bind every approval to a quote object with an ID and an expiry, and re-price at commit. The booking tool accepts a quote ID, not a set of parameters the model assembled. If the quote has expired or the vendor returns a different price or fare class, the tool fails — it does not adapt — and the loop goes back to the user with the delta stated in the same units they approved. A tolerance band is legitimate ("re-confirm above $10 or any change in refundability") and it must be a number in your code, not a judgement in your prompt.
The general rule this is an instance of: an agent may never re-derive the terms of a commitment from its own context. Anything the user agreed to must be represented by a server-side object the agent can only reference, and the tool that commits must be the one that checks. A model that can restate a price can hallucinate one, and it will do so most convincingly on the transaction that matters.
One narrow committed path, and it does not retry.
The booking tool is the only place in this product where the ordinary agent-engineering defaults are actively wrong. Retry is a virtue everywhere else in a loop and a defect here; a helpful error message invites the model to try again with a variation, which is exactly what must not happen.
- Idempotency key, generated before the call and derived from the quote ID. A booking retried with the same key is the same booking. This is the single control that prevents the duplicate-reservation incident, and it belongs in your service, not in the vendor's hands — see idempotency and retries.
- The model does not retry writes. On a timeout or ambiguous response the correct action is reconcile, do not resend: query the vendor for a booking matching the key, and only act on what you find. Encode this in the tool, not in the prompt, because the prompt is advisory and the tool is not.
- Errors from the booking tool are terminal and terse. "Quote expired." "Fare no longer available." No suggestions, no alternatives, no parameters echoed back — a rich error message on a write tool is an invitation to improvise. Everywhere else, good error messages guide the model; here they are the attack surface.
- Spend cap enforced outside the loop. A per-booking and per-day limit checked by a service the agent calls, never a rule the agent is asked to respect. The reasoning is the same as for scoped credentials: a limit the agent enforces is a limit an injected instruction can argue with.
- Single writer. If you run parallel sub-agents for search — and you should, it is the one place fan-out pays — exactly one component may hold the booking capability, and it must not be one of the fan-out workers.
Then buy back reversibility wherever the market sells it. In the United States the Department of Transportation's 24-hour rule requires airlines to permit free cancellation within 24 hours of booking when the reservation was made at least seven days before departure; refundable fares, free-cancellation hotel rates and held-fare products exist for the same reason. A booking made inside a reversibility window is a fundamentally cheaper mistake, and paying a small premium to keep the agent inside that window is usually better economics than a tighter approval flow.
Three rulebooks, none of which belong in the prompt.
A booking is governed by more written policy than almost any other consumer transaction, and the rules come from three different owners who do not coordinate.
- Fare and rate rules — change fees, cancellation windows, baggage, seat selection, no-show behaviour. Vendor-owned, machine-readable, and they vary per fare bucket rather than per route.
- The traveller's own policy — corporate travel rules, preferred carriers, cabin class by seniority, per-diem caps, approval thresholds. Customer-owned, frequently a PDF, and the source of most escalations.
- Entry requirements — passports, visas, transit rules, name-matching between ticket and document. Government-owned, changeable, and the only category where being wrong strands someone at a border.
The temptation is to paste all three into the system prompt. Resist it in that order of severity. Fare rules should be looked up as structured data and shown to the user, not summarised by the model. Corporate policy should be evaluated by a policy service that returns allowed / needs approval / blocked plus a reason, so the answer is auditable and the same for every traveller. Entry requirements should never be asserted by the agent at all — surface the authoritative source, state the freshness of the check, and route the traveller to it.
The reason is not tidiness. Multi-turn policy adherence under pressure is measurably the weakest axis of current models, including strong ones, and a traveller who wants an exception is a user applying pressure across a dozen turns. A rule that lives in a prompt is a rule the model can be talked out of; a rule that lives in a service returns the same answer on turn twelve as on turn one. See policy enforcement.
Disruption is the product, and it inverts every assumption.
Booking is where the risk is; disruption recovery is where the value is. A traveller at 06:00 in a rebooking queue does not want an itinerary comparison, they want one good option executed now. Three things change:
Latency stops being a comfort metric. Seats on the recovery flight are being taken while your agent deliberates. A slower, better answer is a worse answer. This is the one part of the product where you should spend real engineering on parallel search and a warm path, and where a local pre-filter or a fast small model earns its place.
Autonomy should go up, not down. The instinct is to require more approval when stakes rise. In disruption the opposite is correct within a bounded envelope: pre-authorise the agent, at booking time, to rebook the same passenger onto an equivalent-or-better itinerary within a stated fare delta, and it can act while the traveller is asleep or airside without signal. An envelope agreed in calm conditions is worth more than a confirmation dialog nobody can answer. This is the standing-mandate pattern from progressive autonomy, and disruption is its best use case.
The agent must know what it already did. Recovery loops are the highest-risk place for duplicate action, because the traveller is also on the phone to the airline and the airline's own systems are rebooking automatically. Read current state from the vendor before every write, treat your own record as a cache, and reconcile loudly when they disagree.
Measure the two halves separately, or you will not see the failure.
An aggregate "booking success rate" hides exactly the incidents that matter, because the dangerous outcomes are counted as successes. Split the metrics along the same seam as the architecture.
- On the exploratory half: constraint satisfaction (did the itinerary meet every stated requirement, checked programmatically rather than by a judge), option quality against a held-out human-selected choice, and time to a shortlist.
- On the committed half, count events rather than rates. Duplicate bookings. Bookings executed against an expired quote. Bookings outside policy without an approval record. Price deltas above tolerance at commit. Each of these should be zero, and each should page rather than appear on a dashboard.
- Track the silent-substitution rate: bookings where the fare class, baggage allowance or refundability differed from what the user was shown. This is the number that predicts complaints and it is invisible in any outcome-only evaluation — the argument in outcome vs trajectory eval.
- Run committed actions in shadow first. Let the agent produce the exact booking payload it would submit, for weeks, against real traffic, and diff it against what a human agent did. This is cheap, it is the only honest pre-launch signal, and the diffs are more informative than any eval suite you would write.
- Reconcile daily against the vendor of record. Your database is not the booking; the airline's is. A nightly job that compares them catches the class of failure where your system believes something that is not true.
Build the search loop however you like — that half is forgiving and fan-out helps. Then treat the booking as a separate, boring, single-writer service with four properties and no cleverness: it accepts a quote ID rather than parameters, it re-prices at commit and fails on any delta outside a numeric tolerance, it carries an idempotency key and reconciles instead of retrying, and it checks a spend cap it does not own. Pre-authorise a rebooking envelope at booking time so disruption does not need a human who is unreachable. And measure duplicates, expired-quote commits and silent substitutions as incidents, not as percentages.
Related: undo & reversibility for designing around the actions you cannot take back, approval & confirmation UX for what a commit screen must show, browser agent failure modes for when the only integration is a website, and delegated access & consent records for proving afterwards what the traveller agreed to.