When multiple agents are alive at once and need to coordinate, how they communicate is more constrained than it looks. Three patterns exist: tool-result return (children just return to parents — no mailbox at all), module-level dicts (Strix’s elegant in-process solution), and external queues (Redis/SQS/Kafka). Most teams reach for the third when the first or second is correct. Build the simplest queue that works.
Mailbox & message-passing
Multi-agent systems make people reach for distributed-systems infrastructure on day one. They are usually wrong.
The default: tool-result is the only message
Most multi-agent systems in the corpus don’t have peer-to-peer messaging. Children don’t message siblings. They return to the parent. The parent decides whether to invoke another child with the result.
This works because the parent’s loop is already doing dispatch — a child is just an unusually long-running tool. Nothing new to wire up. No queues, no race conditions, no order-of-delivery questions.
Most teams should use this pattern. Until they’re forced into something fancier.
When you actually need a mailbox
If two agents run concurrently and need to share state mid-flight:
- A long-running monitor agent reporting to a coordinator.
- Worker agents claiming work from a shared queue.
- Producer/consumer between specialists.
Strix uses this for inter-agent reporting during long pentests: a sub-agent that finds a credential while testing one endpoint posts it to other agents that might benefit.
Strix’s module-level approach
class BaseAgent:
_agent_messages: dict[str, list[Msg]] = {}
def send_to(self, target_id: str, msg: Msg):
BaseAgent._agent_messages.setdefault(target_id, []).append(msg)
def receive(self) -> list[Msg]:
return BaseAgent._agent_messages.pop(self.id, [])
Plain dict. No locks needed because Python’s GIL serializes individual dict operations — setdefault().append() is atomic, pop(key, []) is atomic. Single-process only. Cleaned up when the process exits.
When the inbox is the whole API
Strix’s mailbox is an optimization layered onto parent-child return. Omnigent inverts the relationship: the server-backed inbox is the primitive, and everything else is built on it. “Everything is a child session” — delegation, review, even a web_fetch all reduce to post to a child, await the inbox (sys_read_inbox / inbox_awaken). Because the inbox is persisted server-side rather than a module-level dict, it survives a restart and spans machines — the thing Strix’s in-process version deliberately gives up. See everything-is-a-child-session.
Don’t reach for Redis early
External queues have real costs:
- Latency (every message is a round-trip to a separate service).
- Operational dependency (now you have a Redis to keep running).
- Debug complexity (state is in two places).
Reach for them when:
- Agents run on different machines.
- You need durable messages that survive a process crash.
- You want pub/sub across many subscribers.
Pick a mailbox
- Children just return to parents No mailbox. Use tool-result return. default
- Concurrent agents share state in one process Module-level dicts à la Strix.
- Agents on different machines External queue (Redis / SQS).
- Need durable messages across crashes External queue with persistence.
Recommended default: Default to tool-result. Move up the ladder only when forced.
Pitfalls
- Mailbox + retry + dedupe is a hard distributed-systems problem. Skip it if you can.
- Cycles in the agent graph that pass messages → infinite chatter → bankruptcy. Cap message count or hops.
- No back-pressure. A slow consumer falls behind, queue grows unbounded. Cap queue size; drop or block.
Projects that implement this
- Omnigent — Open-source meta-harness: one orchestration layer over Claude Code, Codex, Cursor, Kimi, Pi and YAML-defined custom agents. Native bridges per agent, ALLOW/DENY/ASK policy gates, 8-provider cloud sandboxes, vendor-diverse code review.
Related insights
For one-process multi-agent coordination, plain Python dicts are the right answer. No Redis, no broker, no race conditions you need locks for.
When delegation, cross-agent review, and even a web fetch all reduce to 'post to a child conversation and await the inbox,' every sub-task becomes shareable, resumable, and forkable for free — with no per-feature plumbing.