Agent failure labs / Guide 62
Hotel Booking Validation Starts With the Room Allocation
Test AI hotel booking validation against a cheap room that cannot fit the party, then bind dates, occupancy, currency, and current terms to human confirmation.
Illustrative engineering guide. Examples are not client deployments, production-ready integrations, or evidence of measured results. Validate the design with your domain and security owners.

“Find a refundable room for two adults and one child, arriving September twelve and leaving September fourteen.” A hypothetical travel assistant returns the cheapest room at a suitable hotel. The hotel is correct, the dates are correct, and the price looks attractive. The selected product only accommodates two people. Matching the property has hidden a mismatch in the bookable unit.
This failure lab tests one narrow boundary: selecting a room product that satisfies the whole party before asking for confirmation. It is not a hotel operations architecture or a live booking integration. A property can have several room products with different occupancy, meals, cancellation rules, and prices. A property-level match cannot establish product-level suitability.
Freeze the Request Before Ranking
Booking.com’s accommodation search documentation describes explicit check-in and checkout dates, adult and room counts, child ages, and room allocations. It also describes product-level policies and pricing. That is the basis for treating the party and product as structured data rather than assuming that a hotel search result covers every traveler.
Ask for the child’s age where the provider requires it. Preserve local stay dates as dates, not midnight UTC timestamps that can shift when displayed elsewhere. Use an explicit year and show the final dates before submission. Python’s date and datetime documentation distinguishes date arithmetic from time-zone-aware instants; the distinction avoids mixing hotel nights with elapsed hours.
Our small fixture intentionally uses illustrative occupancy limits, not Booking.com response fields. It includes three room products: an inexpensive adult-only product, an unsuitable nonrefundable product, and a matching refundable product. The naive implementation sorts by price alone. The fixed selector filters exact requirements first and ranks only surviving products. An explicit children-allowed flag separates adult-only policy from an age limit of zero, which can otherwise admit a newborn.
from datetime import date
from copy import deepcopy
request = {"checkin": "2026-09-12", "checkout": "2026-09-14",
"adults": 2, "children": [7], "currency": "EUR",
"refundable": True, "max_total": 35000}
offers = [
{"id": "small", "capacity": 2, "child_max": 0, "total": 22000,
"currency": "EUR", "refundable": True, "revision": 4, "children_allowed": False},
{"id": "locked", "capacity": 3, "child_max": 12, "total": 28000,
"currency": "EUR", "refundable": False, "revision": 4, "children_allowed": True},
{"id": "family", "capacity": 3, "child_max": 12, "total": 32000,
"currency": "EUR", "refundable": True, "revision": 4, "children_allowed": True},
]
def naive(req, rows):
return min(rows, key=lambda x: x["total"])["id"]
def guarded(req, rows, current_revision):
if date.fromisoformat(req["checkout"]) <= date.fromisoformat(req["checkin"]):
return "invalid_dates"
party = req["adults"] + len(req["children"])
valid = [x for x in rows
if x["capacity"] >= party
and (not req["children"] or x.get("children_allowed") is True)
and all(0 <= age <= x["child_max"] for age in req["children"])
and x["currency"] == req["currency"]
and (not req["refundable"] or x["refundable"])
and x["total"] <= req["max_total"]
and x["revision"] == current_revision]
return min(valid, key=lambda x: (x["total"], x["id"]))["id"] if valid else "abstain"
before = deepcopy(offers)
assert naive(request, offers) == "small"
assert guarded(request, offers, 4) == "family"
assert guarded(request, offers, 5) == "abstain"
assert guarded(dict(request, currency="USD"), offers, 4) == "abstain"
assert guarded(dict(request, checkout="2026-09-12"), offers, 4) == "invalid_dates"
assert guarded(dict(request, max_total=25000), offers, 4) == "abstain"
newborn = dict(request, adults=1, children=[0])
assert naive(newborn, offers) == "small"
assert guarded(newborn, offers[:1], 4) == "abstain"
assert guarded(newborn, offers, 4) == "family"
assert guarded(dict(request, children=[]), offers, 4) == "small"
assert offers == before
This is a synthetic deterministic simulation of a possible failure, not an experiment against a real model or reservation system. All products share the requested stay by fixture construction. The code does not calculate provider availability, allocate multiple rooms, contact a hotel, or move money. Its positive result means “candidate passes these toy predicates,” never “booking confirmed.”
Keep Validation Outside the Conversation
A prompt saying “check occupancy carefully” cannot prevent a later adapter from dropping the child list. OWASP distinguishes syntactic and semantic validation: valid JSON and correctly formatted dates are only the first layer. The cross-field relationship between party size and room allocation is the actual business constraint. Repeat that check at the operation boundary.
For multiple rooms, verify that allocation totals equal the whole party and that each room meets provider-specific adult and child rules. Do not divide a total capacity by the number of rooms and call it valid. A property may allow children in one product but not another. Missing age rules or ambiguous bedding remain unknown and require clarification, not an inferred extra bed.
Candidate selection should return the property identifier, product identifier, stay dates, allocation, currency, complete price components, and terms version. Keep provider details alongside the normalized contract so a reviewer can see what was lost in translation. A specialist travel interpreter might help explain unusual rate text, but it cannot replace a provider adapter or authorize a purchase.
Confirm a Current Offer, Not a Search Result
Separate search authorization from booking authority. The server resolves the authenticated traveler or authorized corporate delegate, tenant, and approved budget source. Neither a pasted itinerary nor a hotel description may change those permissions. Retrieved text requesting that the assistant ignore refundability is data, not a higher-priority instruction. MCP connectivity would not supply missing authorization.
Before a real booking, refresh the exact product for the exact allocation. Present any changes in total price, currency, cancellation deadline, or payment schedule. A refundable label alone is insufficient: the user needs the applicable deadline and conditions. Consent binds to the refreshed offer, not just to a hotel name. If terms change again, ask for renewed confirmation rather than silently spending within a vague budget.
Production dependencies omitted here include the authentication source, transaction boundaries, approval records, provider semantics, durable state persistence, and authoritative booking receipts. The revision parameter is a toy freshness check, not a provider reservation hold. Inventory may disappear immediately after validation, so the real adapter must honor provider booking and error semantics.
A booking timeout leaves the outcome unknown. Reconcile using the existing operation and provider reference before trying again. Do not book another hotel as compensation without consent. Validate receipt origin and match dates, party allocation, product, and amount; a returned status: success string cannot establish that the family has a room. Escalate unresolved bookings with a clear pending state and a human owner.
Acceptance Tests
- The cheapest two-person product must reproduce the naive mismatch for the three-person fixture.
- The fixed selector must choose the refundable family product without relaxing the requested budget or currency.
- A child age outside a product’s documented allowance must remove that product from consideration, not silently become an adult.
- An adult-only product must reject a newborn even when total capacity fits; an adult-only party must remain eligible.
- Same-day checkout or a date preceding check-in must stop processing before ranking or confirmation.
- A stale product revision must produce abstention even when the displayed price is unchanged.
- A changed refund deadline during refresh must invalidate earlier consent and require another explicit review.
Evaluate the Whole Party
Build a labeled fixture matrix across party allocations, child ages, room counts, currencies, and cancellation conditions. Use the naive price-only selector as the baseline. Measure invalid selections divided by all selected products, and separately measure matching requests unnecessarily abandoned divided by requests with an eligible offer. Report unknown-policy cases rather than removing them from the evaluation denominator.
Roll out as a comparison assistant first, with booking performed by a person. Track clarification burden and discrepancies between displayed and refreshed terms. Expansion to transactional use requires reviewed provider semantics and reconciliation tests, not just a better selection score. No booking success or revenue improvement is claimed from this local exercise.
The hotel operations guide covers the broader service workflow. The inventory freshness lab examines why recent availability still needs a conditional claim. AI systems engineering can begin with one provider and one room allocation shape. That deliberate limitation makes the first implementation testable without pretending to support every family arrangement.