The reconciler was the point
For the past while I've been building a travel-planning assistant. You describe a trip the way you'd describe it to a friend, and the system goes off and finds real hotels, real flights, real things to do, then hands you back an actual itinerary.
The original plan was four specialist agents — flights, hotels, activities, transport — with an orchestrator on top keeping them in line. Very tidy on a whiteboard. What I ended up building looks nothing like that, and I think it's better for it.
The problem was never the agents. It was the state.
Give each agent its own loop and you've given each agent its own conversation thread, its own view of the itinerary, its own context window. Then the user says "actually, move the Lisbon leg to Wednesday," and now everyone needs to know. Flights re-search. The hotel stay slides forward. The activity schedule reshuffles.
Four parallel LLM conversations trying to agree on the truth is a distributed systems problem wearing a costume. I didn't want to solve it, and I especially didn't want to solve it by adding another coordination layer on top of the coordination layer I already had.
So I collapsed it. One orchestrator loop. One LLM conversation, one set of tool calls, one shared bundle.
The "specialists" turned out to be plain async functions — search_hotels, search_flights, search_activities — sitting in a tool registry, dispatched by the orchestrator through function-calling. The orchestrator decides what to look for, calls the tool, gets results, and either shows the user some options or commits a choice to the bundle. Less elegant than four autonomous agents. But there is exactly one source of truth for the itinerary at any given moment, and every tool call writes to the same workspace. I'll take that trade every time.
Then I found the gap
The orchestrator loop runs up to four turns: send messages to the LLM, get tool calls back, dispatch them, append the results, repeat. On the final turn the LLM stops calling tools and just talks.
And that's where things quietly go wrong.
The conversation has moved. New dates came up. A different destination. A budget the user mentioned in passing. But the bundle snapshot doesn't know any of it — maybe the LLM forgot to call the tool, maybe it decided the call wasn't necessary, maybe it just ran out of turns. Whatever the reason, the itinerary starts drifting away from what was actually agreed.
The fix is a reconciler: a separate LLM call that fires after every orchestrator turn. It gets two things — the current bundle snapshot and the last twelve messages of conversation — and it has exactly one job. Compare them.
Does the snapshot match what the user has actually said and agreed to? If not, propose a minimal patch. The patch is strict JSON, validated against the same Pydantic schema everything else uses, committed to the bundle inline so the frontend updates right away.
Why I like it
The reconciler works because it treats the LLM as what it actually is: unreliable at tool-calling, genuinely good at reading a conversation and noticing when something's off.
The orchestrator forgets to call search_hotels after the user picks a hotel from the list. The reconciler catches it. The user says "you know what, let's skip Kyoto," the orchestrator agrees warmly in prose and never touches the bundle. The reconciler catches that too.
It's a second opinion running quietly in the background, and the system is noticeably more correct with it than without.
The hard part was telling it to do less
Early versions of the reconciler tried to fix everything — flights, hotels, activities, transport, metadata, the lot. That was a mistake, and the reason is simple: the reconciler has no tools. No search APIs. No way to verify that a hotel exists or that a flight number is real. All it sees is conversation text.
So the rule became: the reconciler owns trip_meta — destinations, dates, travelers — and keeps its hands off concrete bookings, which belong to the dedicated picker tools.
If the user said "two nights at the Memmo," the orchestrator should have called add_hotel_to_bundle. If it didn't, that's a tool-calling failure. Not a reconciliation job. Papering over it with a reconciler patch just hides the bug somewhere less visible.
What's left is what the reconciler is genuinely good at: the things a conversation settles on its own, without anyone needing to call a tool. Dates. Destinations. Budget. Pace.
The thesis
Don't trust the LLM to manage state reliably. Give it tools, let it call them, and then run a second pass that catches what it missed.
The orchestrator does the creative work — reading the user, deciding what to search, presenting options. The reconciler does the bookkeeping — checking the snapshot against the conversation and patching the drift.
Neither one is enough on its own. Together they produce an itinerary that actually matches what the user asked for, which was the entire point.