The One Where Everything Happens At Once

Last time on Obi the Explorer, the agent got a critic. One LLM wrote the recommendation, a second one judged it, and nothing went out until it passed. I closed by wondering how much of a good host is really just knowing what's going on in the room, and whether an agent can ever know that from the outside.
This week, we find out. This is the last iteration, so I'll land it properly.
If you've worked front-of-house, you'll know there's never just one door. The phone rings while an email comes in, someone's waiting at the desk, and there's a message sitting in the Instagram inbox from last night. Assuming your place is trendy like that.
Meanwhile, the kitchen is telling you it's slammed, and you're quoting wait times based on whatever you last heard (Yeah, it gets chaotic, but it's rewarding at the end of the day). So you will notice that in the same restaurant there are four different conversations, and the customer gets a different experience depending on which one they pick.
The new requirement
Two things, for the final time. First, the chatbot should work across every channel the restaurant uses. The website, SMS, Facebook Messenger and Instagram DMs. Second, it should know what's happening in the restaurant right now. That includes the kitchen capacity, current wait times, special events, anything that changes the answer basically.
Up to now, every requirement has added a feature, but this iteration changes where the agent lives and operates.
What it replaces
For multi-channel communication:
- Staff monitor the phone, email, social media and booking sites
- The host tries to keep answers consistent across all of them
- Different staff often look after different channels
- Someone manually keeps information in sync
- Customers get a different experience depending on how they got in touch
For real-time conditions:
- The host keeps checking in with the kitchen on capacity and timing
- Quoted wait times get adjusted on the fly when it's busy
- The manager updates staff when something unexpected happens
- Special events change what's available
- Everyone improvises when service gets disrupted
Same exercise as always. Checking in with the kitchen is retrieval, while updating staff is a tool call. But look at the first list again. Nothing on it is about what to say. It's all about where the conversation came from, and making sure the answer comes out the same regardless.
That's the main focus of this iteration.
The thing that actually changed
For five weeks, the agent has done one thing after another. Read the message, look up the customer, route it, answer it. At every step, the bot waited politely for the one before it. In technical terms, each task was handled sequentially. This could take a while depending on how long the previous step needed to complete its task. Some of that waiting was necessary because you genuinely can't tell the kitchen to cook before you've checked the stock. But some of it could have been handled differently.
Take what the agent needs before it can route a message. It needs to know who the customer is and now, it needs to know what's happening in the restaurant. Those two lookups have nothing to do with each other. The customer's taste profile doesn't change the kitchen's capacity, and the kitchen's capacity doesn't change who the customer is. So why wait for one before starting the other?
Introducing Parallel Delegation, everyone. For the uninitiated, it's a pattern where you split work into independent pieces, run them in parallel, then bring the results back together before moving on. It's the host who, the moment a customer walks in, asks the maître d', "Do we know them?" and radios the kitchen, "How busy are we?" at the same time. Of course there's a little time lag in practice, but this can be triggered at the same time with code.
Obviously, this is where I insert what movie or show my study reminded me of. Today, that's Everything Everywhere All at Once. It follows a laundromat owner being audited by the IRS, who feels like a disappointment to her father, is at odds with her daughter, and whose husband wants a divorce. The fun bit is that there are more versions of reality than anyone can keep track of, and she jumps between them in almost every scene.
No spoilers (you know the drill by now), but the whole film is about one person trying to handle everything at the same time without everything around her falling apart. That's this iteration. Four channels, live conditions, and lookups happening side by side. The trick isn't doing everything at once; rather, it's knowing which things can happen at once, and which can't.
Back to being serious...
What changed from iteration 5
| Component | Change |
|---|---|
| Channel Adapter | New: turns every channel's message into one standard internal format |
| Retrieval — Real-time Context | New: fetches kitchen capacity, wait times and active events |
| Parallel fetch | New: runs the customer lookup and the real-time lookup at the same time |
| Merge into short-term memory | New: waits for both lookups, then combines them |
| Semantic Router | Upgraded: now sees live conditions, and handles real-time status questions |
| Capacity check | New: stops the booking flow when the kitchen is full |
| Channel-aware Response Formatter | New: shapes every reply for the channel it's going back to |
| Everything else | Carried over untouched |
Six new components, and every path from the last five weeks still runs inside them, unchanged.
The new front of the system
Channel Adapter
↓
[Run at the same time]
↙ ↘
Customer Real-time
lookup conditions
↘ ↙
[Wait for both, then merge]
↓
Semantic Router
↓
[Everything from iterations 1–5]
↓
Response Formatter → back out the right door
That merge step is the important one. It can't fire until both lookups are finished. When steps depend on each other like that, and nothing ever loops back, you've got what's called a DAG, a Directed Acyclic Graph. Fancy name, but simply it means do what you can in parallel, wait where you have to, never go round in circles.
%%{init: {"flowchart": {"curve": "linear", "htmlLabels": true, "nodeSpacing": 36, "rankSpacing": 44, "padding": 12, "useMaxWidth": true}}}%%
flowchart TD
WEB([Web Chat]) --> ADAPTER
SMS([SMS]) --> ADAPTER
FB([Facebook Messenger]) --> ADAPTER
IG([Instagram DM]) --> ADAPTER
subgraph ADAPTER["Channel Adapter - Grounding"]
CA1["Normalise all channel inputs<br>to canonical internal message format<br>Tag: channel_source · message_id · timestamp"]
end
ADAPTER --> PARALLEL_START{ }
PARALLEL_START -->|Fan out| CUST_RET
PARALLEL_START -->|Fan out| RT_RET
subgraph CUST_RET["Retrieval - Customer Profile DB"]
CR1["Look up customer<br>Fetch: preferences · taste profile<br>occasion history · pre-order history"]
end
subgraph RT_RET["Retrieval - Real-time Context DB"]
RT1["Fetch live restaurant state:<br>kitchen capacity · current wait times<br>active events · service disruptions"]
end
CUST_RET --> MERGE
RT_RET --> MERGE
subgraph MERGE["Short-Term Memory - Merge"]
MG1["Combine: customer profile + real-time context<br>into unified agent working context"]
end
MERGE --> ROUTER
subgraph ROUTER["Semantic Router - LLM"]
SR1["Classify intent with full context:<br>Reservation · Menu · Seasonal · Pre-order<br>Recommendation · Real-time Status Query<br>Route differently if kitchen at capacity"]
end
ROUTER -->|Menu or Seasonal Inquiry| RET_MENU
ROUTER -->|Pre-order Request| RET_INV
ROUTER -->|Reservation Request| FIELD_CHECK
ROUTER -->|Recommendation Request| OCC_CHECK
ROUTER -->|Real-time Status Query| REASON_RT
subgraph REASON_RT["Reasoning - LLM"]
R_RT["Generate real-time status response<br>e.g. current wait · kitchen capacity · tonight's events"]
end
REASON_RT --> FORMAT
subgraph RET_MENU["Retrieval - Menu KB + Seasonal Calendar"]
RM1["Fetch dish details + seasonal availability"]
end
RET_MENU --> REASON_MENU
subgraph REASON_MENU["Reasoning - LLM"]
R_M1["Generate personalised menu answer"]
end
REASON_MENU --> LEARN
REASON_MENU --> FORMAT
FIELD_CHECK{All required<br>reservation fields<br>present?}
FIELD_CHECK -->|Missing fields| REASON_FU
subgraph REASON_FU["Reasoning - LLM"]
R_FU["Generate personalised follow-up question"]
end
REASON_FU --> FORMAT
FIELD_CHECK -->|All fields collected| RT_CAP_CHECK{Kitchen<br>capacity<br>available?}
RT_CAP_CHECK -->|At capacity| REASON_CAP
subgraph REASON_CAP["Reasoning - LLM"]
R_CAP["Inform customer of real-time capacity issue<br>Suggest alternative times based on live data"]
end
REASON_CAP --> FORMAT
RT_CAP_CHECK -->|Capacity available| RET_SEASON
subgraph RET_SEASON["Retrieval - Seasonal Calendar"]
RS1["Check seasonal dishes for booking date"]
end
RET_SEASON --> RET_AVAIL
subgraph RET_AVAIL["Retrieval - Reservation DB"]
RA1["Check slot availability: date + time + party size"]
end
RET_AVAIL --> AVAIL_CHECK{Slot<br>available?}
AVAIL_CHECK -->|Yes| DEC_BOOK
subgraph DEC_BOOK["Decision Module"]
D1["Confirm booking + reference number"]
end
AVAIL_CHECK -->|No| RET_ALT
subgraph RET_ALT["Retrieval - Reservation DB"]
RA2["Query next available slots"]
end
RET_ALT --> REASON_ALT
subgraph REASON_ALT["Reasoning - LLM"]
R_A1["Generate alternative slot suggestions"]
end
REASON_ALT --> FORMAT
DEC_BOOK --> LTM_RES
subgraph LTM_RES["Long-Term Memory - Reservation DB"]
LM1["Write booking record"]
end
LTM_RES --> LEARN
LTM_RES --> FORMAT
subgraph OCC_CHECK["Special Occasion Detector"]
OC1["Sub-classify: Birthday · Anniversary<br>Business Dinner · Standard"]
end
OCC_CHECK -->|Special occasion| REFLECT_ACTOR
OCC_CHECK -->|Standard| RET_SEASON
subgraph REFLECT_ACTOR["Actor - LLM"]
ACT1["Generate draft experience plan"]
end
REFLECT_ACTOR --> REFLECT_CRITIC
subgraph REFLECT_CRITIC["Critic - LLM"]
CRIT1["Evaluate: taste profile · dietary needs<br>occasion type · real-time availability"]
end
REFLECT_CRITIC --> QUAL{Quality<br>threshold<br>met?}
QUAL -->|No| REFLECT_ACTOR
QUAL -->|Yes| SERVICE_TOOL
subgraph SERVICE_TOOL["Tool - Service Team Coordination"]
ST1["Notify front-of-house:<br>table decor · complimentary items · manager flag"]
end
SERVICE_TOOL --> RET_SEASON
subgraph RET_INV["Retrieval - Inventory DB"]
INV1["Check ingredient stock for pre-order dishes"]
end
RET_INV --> STOCK{Sufficient<br>stock?}
STOCK -->|Yes| KITCHEN_TOOL
subgraph KITCHEN_TOOL["Tool - Kitchen Notification"]
KT1["Write pre-order to kitchen system"]
end
STOCK -->|No| RET_ALTS
subgraph RET_ALTS["Retrieval - Menu KB"]
RAS1["Fetch substitute dishes"]
end
RET_ALTS --> REASON_SUBS
subgraph REASON_SUBS["Reasoning - LLM"]
R_S1["Generate substitute suggestions"]
end
REASON_SUBS --> FORMAT
KITCHEN_TOOL --> DEC_PRE
subgraph DEC_PRE["Decision Module"]
DP1["Confirm pre-order + attach to reservation"]
end
DEC_PRE --> LEARN
DEC_PRE --> FORMAT
subgraph LEARN["Learning Module"]
L1["Update Customer Profile DB:<br>preferences · history · taste profile · channel preference"]
end
subgraph FORMAT["Channel-aware Response Formatter"]
FMT1["Format response for originating channel:<br>SMS: plain text under 160 chars<br>Web: rich HTML with booking card<br>Facebook/Instagram: conversational + emoji"]
end
FORMAT --> WEB_OUT([Web response])
FORMAT --> SMS_OUT([SMS response])
FORMAT --> FB_OUT([Facebook response])
FORMAT --> IG_OUT([Instagram response])
%% Role colours come from docs/stylesheets/mermaid.css (light + dark).
class WEB,SMS,FB,IG source
class ADAPTER,CUST_RET,RT_RET,MERGE,ROUTER,REASON_RT,RET_MENU,REASON_MENU,REASON_FU,REASON_CAP,RET_SEASON,RET_AVAIL,DEC_BOOK,RET_ALT,REASON_ALT,OCC_CHECK,REFLECT_ACTOR,REFLECT_CRITIC,SERVICE_TOOL,RET_INV,KITCHEN_TOOL,RET_ALTS,REASON_SUBS,DEC_PRE,LEARN,FORMAT process
class PARALLEL_START,FIELD_CHECK,RT_CAP_CHECK,AVAIL_CHECK,QUAL,STOCK control
class LTM_RES,WEB_OUT,SMS_OUT,FB_OUT,IG_OUT outcome
Three decisions worth arguing about
Why does the Channel Adapter sit before everything, even memory?
Because nothing else in the system can work until the message looks the same.
An Instagram DM full of emojis and a text that says "book 4ppl sat 8 pm" mean the same thing, but they look nothing alike. Every module downstream, from grounding to the router to the Critic, expects one clean, standard format. So the adapter's only job is to take whatever came through whichever door and turn it into that format.
Think of it as the front door of the restaurant. However you arrived, by car, by bus, or on foot, everyone gets greeted the same way once they're inside. The adapter also tags the message's source, so the reply knows which door to leave by.
Why does the parallel lookup happen before the router?
You may recognise this argument. Back in iteration 3, I said anything that changes how a decision should be made has to be loaded before that decision is made. I also said I'd be writing some version of that sentence in every remaining post. Here it is, for the last time.
The router now needs two kinds of context. Who the customer is, and how busy the restaurant is. If the kitchen is at capacity, a booking request for 8 pm tonight shouldn't be considered or enter into the full booking flow at all. The agent should go straight to providing alternatives. The router can only make that call if both lookups have finished.
This ties back to what I mentioned in iteration 4: the stock check and the kitchen notification had to happen in order, because one depended on the other. These two lookups don't depend on each other at all. The order of your steps should come from their dependencies, not your habits. Sequential by default is a choice; it is not compulsory.
The capacity check, by the way, sits inside the booking path rather than inside the router. The router's job is working out what you want. Whether the kitchen can handle it is a separate decision, and keeping it in its own box means the restaurant can tune that threshold without touching the routing.
Why does every path end at one formatter?
The same booking confirmation needs to look completely different depending on where it's going. Plain text under 160 characters for SMS. A proper booking card on the website. Something chattier, maybe with an emoji, on Instagram. It's about delivering the same human and natural experience without having a response that feels robotic every time.
I could have made every path format its own replies. This includes the menu answer, follow-up questions, alternatives, substitutes, and capacity messages—each one tailored to four different channels. If we add WhatsApp next year, it would require editing a dozen different boxes.
Instead, every path sends its answer to a single place, and only that place knows anything about channels and how messages should be formatted. For the uninitiated, software engineers call this the Facade pattern: one simple front that hides the messiness behind it. This way, adding WhatsApp means changing one box or node.
At least, that's how I have chosen to design this.
So, can an agent know what's going on in the room?
I owe you an answer from last week. The honest answer is partly.
It knows whatever the restaurant writes down. How busy the kitchen is, how long the wait is, whether there's a private party tonight. Put that information somewhere the agent can check, and it'll stop recommending a quiet anniversary dinner on the busiest night of the year.
What it can't know is anything nobody writes down. The table that was meant to leave twenty minutes ago and just ordered another bottle. The new starter who's struggling. The moment a busy service is about to turn chaotic. A good host notices all of that just by being on the floor. The agent only knows what the restaurant records, so it can only ever be as aware as the restaurant's data allows.
Of course, you could install cameras and sensors that try to read how people are feeling. But I think the whole place would feel empty and impersonal. I love new technology, but I like seeing different people too.
In the end, context is king, and the best scenario is not an agent replacing a host. Instead, it is an agent that handles all the tedious parts, like communication and context gathering, while everyone else is allowed to focus on the humans. The parts machines can't do.
The capability, all of it
Six iterations in: - Iteration 1: The agent executes a task - Iteration 2: The agent routes between tasks - Iteration 3: The agent remembers across tasks - Iteration 4: The agent acts outside itself - Iteration 5: The agent checks its own work - Iteration 6: The agent works everywhere, aware of the room
Four channels, six intents, six data sources, two teams it can instruct, live conditions, a critic, and a memory that improves over time.
What six iterations taught me
Back in the first post, I counted the boxes and found the LLM only did two of the seven jobs. Now that the design is finished, I counted again. In the final design, there are over thirty boxes, and only about a third of them involve an LLM. The rest are lookups, decisions, writes and formatting. Turns out AI agents still aren't mostly AI. It's about passing data and context throughout a system so that when AI needs to make a decision or perform an action, it can do its best.
The bigger surprise was this. The agent stayed on rails the entire time. Every path was predefined. It never once planned its own steps or decided to do something nobody had designed, and it still ended up handling everything a real front-of-house team handles.
There's a lot of noise right now about fully autonomous agents. Some problems need them, but I've just watched a system go from taking one booking to running every channel a restaurant has, and it never needed to make up its own plan. It needed good questions, a clear look at how the humans already did the job, and a lot of patience about the order of things.
So, I'll leave the series on the question I keep coming back to. How many of the agents being built right now actually need autonomy, and how many need a better diagram?
I hope to find an answer by designing and building more systems. Of course, I'll write about these as well, and that's mostly what I'll post on Thursdays.
If you're building agents and trying to figure it out, I'd genuinely like to hear about it.
-
Let's have a virtual coffee together!
Want to see if we're a match? Let's have a chat and find out. Schedule a free 30-minute strategy session to discuss your AI challenges and explore how we can work together.