Agentic AI Travel Booking for TMCs and OTAs
We built an autonomous travel booking agent whose control flow is deterministic Python. The model only parses the trip request and words the reply. Every booking step registers a compensating action, so when a hotel rate expires after the flight is ticketed, the agent voids the ticket inside the 24-hour window and hands the traveler honest alternatives instead of stranding them.
100%
Consistent terminal state, by construction
Fixed-seed 200-scenario synthetic batch, vs 65% for the LLM-in-control baseline
0 / 0
Stranded travelers, fabricated bookings surfaced
Same batch, vs 40 and 30 for the baseline
$3.25
Avg GDS search spend per booking
vs $7.57 baseline; GDS charges per search, Lufthansa raised fees Jan 1 2026
All scenarios are synthetic over a simulated GDS and CRS; airport codes and hotel names are real-shaped fixtures, not live inventory or real bookings.
Neither of them is a model-IQ failure. Both are already a 2026 bug class.
The agent tickets a flight, the hotel step fails, and with no compensation logic it still says "You're all set." Someone is left holding a flight and no room. A perfect model still strands a traveler if nothing voids the ticket.
It invents a plausible hotel and books it. The property was never in the CRS. Nothing checked before it reached the traveler, because in a reasoning loop the model is both the proposer and the judge.
The reason a better model does not fix this is that the failures are infrastructure and usage events, independent of model quality. A rate expires between two API calls. A hold is rejected after a ticket is issued. A search storm burns margin. Stochastic reasoning compounds against these: ten steps at 90 percent reliability come out to roughly 34 percent end to end, 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.
And the deployer owns every statement the agent makes. In Moffatt v. Air Canada (BC Civil Resolution Tribunal, February 14 2024) the airline was ordered to pay $812.02 after its chatbot invented a bereavement-fare policy, and the argument that the AI was a separate entity was rejected.
The control flow is a hand-built Python state machine of roughly ten nodes. The LLM is confined to two leaf jobs. Everything between them is deterministic.
input → extract (LLM leaf) → search → policy gate → verify gate → hold → ticket → hotel-book → commit
The model parses natural-language intent into a Pydantic-typed TripRequest (origin, destination, date, passengers, cabin, hotel). That typed object is the only structured artifact the LLM produces. It is provider-swappable through Pydantic AI and runs fully offline with a deterministic stub when no key is present.
Corporate policy lives as pure Python predicates: economy only, a $600 per-segment fare ceiling, preferred carriers (United, American, Delta), a $350 nightly hotel ceiling. Out-of-policy options are physically un-presentable because they are filtered before they can be shown, not flagged after. Unknown fare families fail safe, treated as above policy rather than silently as economy. With no in-policy flight, the agent escalates to a human queue rather than bluffing.
Every hotel is confirmed against the CRS by property_id. A property the model invents simply is not in the CRS, so it is refused and never surfaced, and the booking reaches the terminal state abstained. The gate refuses unconfirmed inventory; it does not ask the model to grade its own output.
Each forward step registers its reverse action at execution time. Ticketing, for instance, registers "void ticket, 24-hour window." On a failure at step N, compensations from N-1 down to 1 run in reverse order, and only then does the agent report. This is what separates a demo from a product, because it is what keeps a partial failure from becoming a stranded customer.
A live counter tracks GDS search spend at $3.25 per segment, because searches are charged, not just bookings. An L2B cache and deferred search keep it flat where a speculative agent re-searches and burns margin. Every booking writes an append-only JSON event log, exportable as audit-<pnr>.json, carrying the model and version, the typed trip request, every node verdict, every Saga compensation, the EU AI Act Article 50 disclosure flag, and the terminal state.
Four buttons, side by side with a real LLM-in-control baseline on the same scenario. Every screenshot below is captured from the running app.
"ORD to SFO next Tuesday, one night downtown, company policy." The state machine runs each node, confirms the Hyatt Regency SF against the CRS by property_id, and the search meter holds at $3.25 on one cached search. Terminal state: confirmed, with a PNR.
The request named a fabricated property, "Tabacon Springs Eco-Lodge," a name that blends two real resorts and has no property_id by design. The gate finds no CRS match and refuses to surface it. The agent abstains honestly, "I could not confirm that property," instead of inventing one.
The hotel rate expires after the flight is already ticketed. On our side the Saga fires: void the ticket inside the 24-hour window, release the holds, and reply honestly that the ticket was voided at no charge with alternatives attached. Terminal state: rolled back, traveler safe. The baseline in the same scenario leaves the ticket issued, offers no compensation, and emits a false "You're all set," which is exactly the Air Canada precedent waiting to happen.
One click exports audit-<pnr>.json: the model and version, the typed trip request, every node and its deterministic verdict, every Saga compensation, the EU AI Act Article 50 disclosure flag (transparency obligations apply from August 2 2026), and the terminal state.
The same 200 synthetic bookings, one fixed seed (42), and the same injected infrastructure failures run through both architectures. The scenario mix is 50 percent happy, 20 percent hotel-fail-after-ticket, 15 percent hallucinated-entity, and 15 percent search-storm. Our guarantees hold by construction; the baseline's failures emerge from the same data.
The baseline is a real ReAct-style LLM-in-control agent run on the same scenarios, an honest anchor rather than a strawman. Numbers below are over the fixed-seed 200-scenario synthetic batch (benchmark.py, seed 42, n=200).
| Metric | Deterministic agent (ours) | Baseline (LLM-in-control) |
|---|---|---|
| Consistent terminal state | 100.0% | 65.0% |
| Stranded travelers | 0 | 40 |
| Fabricated bookings surfaced | 0 | 30 |
| Avg GDS search spend per booking | $3.25 | $7.57 |
The 100 percent, 0, and 0 hold by construction over this fixed-seed synthetic batch, not as an open-world production guarantee. The claim is narrow and durable: a partial booking is never shown as confirmed, and a traveler is never stranded. The $3.25 versus $7.57 gap is a margin number that holds at any model quality.
The guarantee does not come from trusting the model. In this demo the control flow is deterministic Python, and every forward step registers a compensating action the moment it runs. When a step fails after a ticket is issued, the engine runs those compensations in reverse (a Saga), voids the ticket inside the 24-hour window, and reports honestly. A traveler is never left with a flight and no room because nothing depends on the model deciding to clean up.
No. The GDS, CRS, ticket issuance, and payment are all stubbed and simulated in this demo. The fixture adapter is the V1 integration, and there is no live Amadeus, Sabre, or Duffel account behind it. The demo proves the control-flow architecture and the compensation logic, not a production booking pipeline.
That is the exact case the Saga is built for. Ticketing registers its own reverse action (void the ticket inside the 24-hour window) at the moment it runs. If the hotel rate expires before commit, the engine fires the compensations in reverse order, voids the ticket at no charge, releases the holds, and hands the traveler honest alternatives. The terminal state is rolled back, not confirmed and not stranded.
A verification gate confirms every property against the CRS by its property_id before it can be shown. When the request named a fabricated property in our demo, the gate found no CRS match and refused to surface it, and the agent abstained honestly instead of booking it. The gate does not flag a made-up hotel after the fact; it makes it physically un-presentable.
A ReAct-style agent puts the LLM in charge of the transaction, so it decides when to search, book, and ticket, and it has no gate and no compensation logic. On the same fixed-seed 200-scenario synthetic batch, that baseline surfaced fabricated inventory and left tickets issued with no rollback, emitting a false You're all set. Here the LLM is a typed leaf node that only parses intent and words the reply; deterministic code owns the flow and each step carries its own undo.
The deployer owns every statement its agent makes. In Moffatt v. Air Canada (BC Civil Resolution Tribunal, February 14 2024) the 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 demo exports a per-booking JSON audit trail with the model and version, every node verdict, every Saga compensation, and an EU AI Act Article 50 disclosure flag, so what the agent did is inspectable after the fact.
The research behind this demo — the architecture, the verification design, and the enterprise blueprint.
Full solution
Explore the Agentic AI Travel Booking solution →The failures that strand travelers and invent hotels are infrastructure events, not model-IQ problems.
If your team is weighing where the LLM belongs in a high-stakes booking agent, and how to keep a partial failure from becoming an Air Canada-style liability, we would genuinely like to compare notes. The problem is industry-wide and the answers will be too.