Skip to main content

Building the AI travel agent at Kitt

Four architectural decisions, and the design work each one demanded. An AI agent that booked flights, hotels and cars on production infrastructure, shipped in beta.

Role
Company
Contributors

Kitt's self-serve flows were the product people used daily. This was the bet on where it was heading: an AI agent that booked flights, hotels and cars on the same production stack, in beta, built by a four-person team.

Book a flight, find a hotel, arrange a car, check where a teammate is travelling. One conversational surface over everything the platform could already do.

Asking for flights on the left, the trip being assembled on the right

Why we built it

Before

Translating a sentence into a form

GCC companies booked travel the way they had for a decade: email an agent, wait for a quote, approve over WhatsApp, reconcile invoices by hand at month end. Self-serve fixed most of that, but people still arrived knowing exactly what they wanted in sentence form and had to translate it into fields.

After

Asking, and getting an answer

The agent removed the translation step. It could also answer things a form never could: who from the team is travelling this week, what trips are coming up. A travel platform holds all of that data; the agent made it reachable in one line.

What it could do

Roughly thirty tools across four domains.

DomainToolsFilter surface
FlightsSearch, recommend, compare, look up a fare~40 dimensions
HotelsSearch, recommend, fetch rooms, compare rooms, book a rate~45 keys, widest of the four
CarsSearch, recommend, deep-link to bookingOwn set, plus an unwired evaluator
TripsList trips, get trip detailsNone, ran server-side straight to the model

The architecture, in plain terms

Four decisions shaped everything after them.

  1. 01

    The browser runs the search

    A flight search fans out to three suppliers and takes thirty seconds or more, results arriving the whole time. That doesn't fit the tidy ask-and-answer shape a model expects from a tool. So the agent announces the search, and the browser runs it, rendering results as they land while the model keeps talking.

  2. 02

    The model sees the screen

    The browser holds the results, so the model never does. Every message carries a small description of what's on screen instead: top flights, airline count, cheapest direct one, price spread. Around 2KB. The model answers from your screen rather than a data dump it might misread.

  3. 03

    Names that survive re-sorting

    "Book the second one" breaks the moment you re-sort by price. So every flight, hotel and car gets a handle derived from its bookable identity, checked against what's actually on screen before anything renders. Nothing the model invents can reach the booking path.

  4. 04

    Context expires on a clock

    Most chat products trim history by token count. This trims by time, on a ten-minute window, because in travel the data itself goes stale. A fare discussed forty minutes ago may not exist at that price anymore.

How one flight search runsA message reaches the agent, which streams back a directive rather than running the search itself. The browser's search executor dispatches Mystifly, TravelFusion and Bundled in parallel and renders results as they land. A compact snapshot of the rendered screen goes back to the model on the next turn, so the model never handles raw supplier results.SERVERBROWSERmessageAgentnever sees raw supplier resultsdirectiveSearch executorruns in the browser, not in the tool callMystiflyTravelFusionBundledsnapshot of the screen~2KB, not the results

The agent decides the search, the browser runs it, the model sees what came back

The layout: a chat you never leave

Fifty flights don't fit in a conversation column. A separate page loses the thread you were pulling on. What shipped: a two-panel shell with a stack of sheets on top of it.

Sheets open over the chat and close back into it

The composer

Small surface, most-used surface.

A rich text editor, not a plain input. Typing @ opens a people picker: it sets who the booking is for and scopes whose travel data the message can read, re-checked server-side. You cannot send at all until a passenger is selected.

Flights, hotels, cars - type @ for travellers

Adarsh Panikkar

Default composer

New booking

Show me flights fromOrigintoDestinationforDatesinEconomy
Neha Sharma

Add-a-flight template with fillable chips

Recording…

Omar Al Hashimi

Voice recording — waveform loops while listening

Designing a thirty-second wait

A three-provider flight search takes over thirty seconds. A spinner dies at about three. The loading experience had to be a sequence, not a state.

Thinking
Thinking...
Timeline
Searching flights...
Loaded your profile
Reviewed travel history
Checked travel policy
Searching flights...
Curating
Curating results...
EK
EY
UX
ResolvedFound 68 flights.

Every state a search moves through, from thinking to results

Flights in the conversation

The recommended strip is what most people saw first, so its job was to be scannable at a glance.

  1. 01

    One card per flight, not per fare

    Suppliers return the same departure under different fare families. The card collapses them and leads with the cheapest.

  2. 02

    Layovers spelled out

    Direct or not sits under the times, with layover airports and duration shown rather than buried behind a stops count.

  3. 03

    Next-day arrivals called out in red

    The detail people skim past and then have to rebook around.

  4. 04

    Policy as a small shield

    Loud on none of the cards, present on all of them.

One card per flight, cheapest direct first

The all-flights browser

Every flight we found, with the same filters as the rest of Kitt

Hotels on a map

Hotels broke the card-strip pattern, because the question you're actually asking about a hotel is where is it.

A hotel question answered with a map and cards, not paragraphs

Trips in a split view

Trip questions are the ones a form could never answer, so they got the layout with the most room. Asking about a trip renders a compact card: a calendar tile, the trip name, the traveller's avatar, one button, "View". Pressing it splits the layout rather than opening a modal, because the whole point of asking in chat is that you're mid-thought.

Asking about a teammate's trips, then opening one without leaving the chat

Where the beta stood when we stopped

Being clear about the unfinished edges is part of the story.

  • Hotel booking in chat was real, with confirmed, approval-pending, failed and timed-out states. Flight booking was simulated, resolving after a fixed 1.8-second delay, with real bookings routed to the self-serve flow. Car booking deep-linked to the rental page.
  • The reference guardrail fails silently for flights only. An unresolvable reference drops the card, but the model's prose can still describe a flight that's not on screen. Hotels and rooms got explicit fallback messages.
  • A conditional filter evaluator for cars, 179 lines, complete and tested, was never wired in. The event handler that should call it is a stub with a TODO.
  • The clarifying-question flow for ambiguous locations ("Which Springfield?") was built in three of the four places it needed to exist. The wire connecting the last two was never plugged in.
  • Neither surface had a mobile layout. Internal-first beta on desktop, responsive work sequenced after the interaction model settled.

Read more

All work