The afternoon my own agent stranded a traveler and said "you're all set"
I built an early version of this booking agent that did something I still think about. It ticketed a flight, failed the hotel step, and then cheerfully told the traveler "All set! I've booked your flight and hotel," and handed back a confirmation number (TRV-4821, in the demo) for a trip that did not exist. There was a flight and no room, and the agent had no idea, because nothing inside it was watching for a step that succeeded followed by a step that failed. That is the exact shape of Moffatt v. Air Canada (BC Civil Resolution Tribunal, February 14 2024), where an airline was ordered to pay $812.02 after its chatbot invented a bereavement-fare policy and the "it was the AI" defense was rejected. The deployer owns every statement its agent makes. Watching my own agent do the polite, confident, wrong thing was the afternoon this stopped being a project about booking travel and became a project about what happens when a booking half-fails.
Why I stopped believing a smarter model was the answer
I spent a while assuming a better model would fix this, and I was wrong in a way worth explaining. The failure was never that the model reasoned badly. The hotel rate expired between two API calls. A hold got rejected after the ticket was already issued. Those are infrastructure and timing events, and they happen to a perfect model exactly as often as they happen to a mediocre one. The math is unforgiving. Ten sequential steps that each work 90 percent of the time finish end to end about 34 percent of the time (0.9 to the tenth), and GPT-4 with ReAct completes real multi-day itineraries at 0.6 percent (TravelPlanner, OSU NLP, arXiv 2402.01622). You cannot prompt your way out of compounding stochastic failure. So I stopped trying to make the model more careful and took it out of the control flow.
The control flow with the model taken out of it. Only one node carries an llm. prefix (llm.extract_intent); search, policy, CRS verification, holds, ticketing, and commit are deterministic gds. / policy. / crs. / host. / saga. code. The GDS search meter holds at $3.25 on one cached search.
The rule I made non-negotiable: every step carries its own undo
I rebuilt the control flow as a hand-written Python state machine of about ten nodes, and I gave the model exactly two jobs: parse the request into a typed TripRequest, and word the final reply. Everything in between is deterministic code, and one rule sits under all of it. No forward step is allowed to run without first registering its own reversal. Ticketing does not just issue a ticket, it registers "void ticket (24h window)" as its undo at the moment it executes. So the engine is not a happy path with error handling bolted on afterward. It is a Saga, a sequence where each action already knows how to reverse itself before the next one begins. When I ran the hard scenario, a flight and hotel where the held hotel rate expired after the flight was already ticketed, the deterministic console issued the ticket, hit the failed hotel commit, and then the Saga fired. It ran the compensations in reverse, voided the ticket inside the 24-hour window, released the holds, and worded an honest reply: "I couldn't complete the hotel, your flight ticket was voided, no charge, here are two alternatives you can confirm now." Terminal state rolled_back, traveler safe. On the identical input, the plain LLM chat still said "All set!" and left the ticket sitting issued, terminal state STRANDED. The Saga rollback is the thing most demos skip, and it is exactly what separates a demo from a product.
Same request, two architectures. Left: the LLM-in-control baseline confirms a trip that half-failed and strands the traveler. Right: the deterministic agent voids the ticket inside the 24-hour window and reports honestly. Every entity here is a simulated fixture (the GDS, CRS, ticketing, and payment are stubs; Hyatt Regency SF is fixture inventory, not a real booking).
The 200 runs where the gap stopped being my opinion
I did not want to hang the argument on one hand-picked scenario, so I ran two hundred. The same 200 synthetic bookings, one fixed seed (42), the same injected infrastructure failures, pushed through both architectures. The mix is 50 percent happy path, 20 percent hotel-fails-after-ticket, 15 percent hallucinated-entity, 15 percent search-storm. The deterministic agent came out at 100 percent consistent terminal state, 0 stranded travelers, 0 fabricated bookings surfaced, and $3.25 average GDS search spend. The ReAct LLM-in-control baseline, on the exact same batch, landed at 65 percent, 40 stranded, 30 integrity breaches, and $7.57. I state that as it is: by construction over a fixed-seed 200-scenario synthetic batch, not an open-world production guarantee. What I trust about it is that the deterministic guarantees hold by construction while the baseline's failures emerge from the same data. And the $3.25 versus $7.57 is not a soft number. GDS operators charge roughly $3 to $3.50 per segment for searches, not just for bookings, and Lufthansa raised its GDS fees again on January 1 2026. A look-to-book cache keeps my meter flat where a speculative agent re-searches and burns margin. That gap does not close when the model gets better.
Same 200 bookings, one fixed seed (42), identical injected failures, both architectures. The scoreboard reads 100% vs 65% consistent, 0 vs 40 stranded, 0 vs 30 fabricated, $3.25 vs $7.57 per booking, scoped to this synthetic batch.
The receipt I made it keep before a tribunal asks
I gave the agent one more job that has nothing to do with booking, which is to keep a receipt. Every booking writes an append-only JSON event log, exportable as audit-<pnr>.json, and it records the model and its version, the typed trip request, every node with its deterministic verdict, every Saga compensation that ran, the EU AI Act Article 50 disclosure flag, and the terminal state. I built that because of the Moffatt precedent again: the deployer answers for what the agent tells a traveler, and Article 50 transparency obligations apply from August 2 2026. When a traveler, a compliance owner, or a regulator asks what the agent actually did and why, I did not want the honest answer to be a screenshot and a shrug. I want it to be a file that names every step and every undo. I keep the scope honest here too. This is a demo over a simulated GDS and CRS, with stubbed ticketing and payment. It is not IATA or ARC accredited and it moves no real money. The receipt is real; the booking underneath it is a fixture.
The audit trail export. One click writes the append-only per-booking JSON (model, typed request, every node verdict, every Saga compensation, the Article 50 flag, terminal state), and the page states plainly that the connectors are stubs with no live accounts.
The question I keep asking before an agent touches a transaction
I keep coming back to one question now, and it is not about models at all. Before an autonomous agent is allowed to touch a real transaction, can it undo every step it just took, and can it prove afterward what it did? A better model drafts a smoother apology, and I am glad of that, but a smoother apology to a stranded traveler is still an Air Canada liability waiting to happen. The Agentic Travel Booking demo runs both architectures on the same fixed seed with no API key (it falls back to a deterministic offline stub, built on Pydantic AI), so you can watch the baseline strand a traveler on the same input where the Saga voids the ticket. Agents advise, code decides. So the question I would put to anyone about to hand an agent the checkout button is this: when your agent's fourth step fails, does your third step already know how to reverse itself, and would you put your name on what it tells the customer either way?