Module 03
AutoGen Actor Model
On this page
In plain words
In the hostel mess you do not stand in the kitchen waiting for your roti. You give a slip and walk away. The actor model does the same for agents: each agent has private state and its own inbox, and the only way to interact is to drop a message and move on. Because sending and handling are separate moments, one agent crashing never drags the others down.
How it flows
- 1Agent sends message
- 2Message enters inbox
- 3Runtime delivers it
- 4Handler runs
- 5Crash stays local
- 6Reply sent back
A tiny example
runtime = Runtime()
runtime.register(reviewer)
runtime.register(checklist)
# send returns at once, nobody waits here
runtime.send("reviewer", "checklist", "check", diff)
runtime.run()
print(len(runtime.dead_letters)) # messages that crashedNotice that send() finishes immediately and the crashed messages end up parked in dead_letters instead of killing the sender.
What you will learn
- What the actor model is, in one line: private state, an inbox, and messages.
- Why sending a message instead of calling a function keeps one broken agent from taking down the rest.
- The three layers of AutoGen and what each one is for.
- How to build a tiny actor runtime yourself in plain Python.
The problem, simply
Think about your hostel mess. You want extra rotis. You do not walk into the kitchen, open the tawa, and cook it yourself. You tell the person at the counter, he writes it on a slip, the slip goes to the kitchen, and food comes back.
Now imagine the other way. You go inside and stand next to the cook, doing nothing, until your roti is ready. If the gas goes off, you are stuck too. And the twenty people behind you are stuck behind you.
That second picture is how most agent code is written. Agent A calls a function on Agent B and waits. B is slow, A waits. B throws an error, that error travels up and kills A also.
See, the fix is the slip system. Every agent gets its own counter, its own slip box. You drop a slip and walk away. Somebody else picks it up and does the work. That is the actor model, and AutoGen was rebuilt around this idea.
- 1You order
- 2Slip written
- 3Slip goes in
- 4Cook works
- 5Food comes back
The idea
An actor is three things
Basically an actor has only three parts:
- Private state. Nobody outside can touch it. No shared dictionary.
- An inbox. A queue of messages waiting for it.
- A handler. One function: given a message, do something. It can reply, message another actor, start a new actor, change its own state, or stop itself.
Two actors can never read each other's variables. They can only send messages. That single rule is what buys you everything else.
- 1Actor A sends
- 2Message in inbox
- 3Runtime delivers
- 4Actor B handles
- 5B sends reply
IMPNote: The word "message" here does not mean a chat message from a human. It is any small typed packet: who sent it, who it is for, what topic, what body.
Why dropping the slip changes everything
In the old style, agent_a.chat(agent_b) blocks: A is frozen till B finishes. In the actor style, send(agent_b, msg) just puts the message in B's inbox and returns immediately. The runtime, which is the small engine that moves messages around, delivers it whenever it gets to it.
Three things fall out of this for free:
- Fault isolation. If B's handler crashes, the runtime catches it right there inside B. A never knew, A never waited. Log it, retry it, or park it — B's problem stays B's problem.
- Natural concurrency. Many messages can be in flight together. Nobody is standing in the kitchen waiting.
- Ready for distribution. Inbox plus some way to carry messages is the same idea whether B is in the same Python process or on a server in Mumbai. You only swap the transport.
Remember: send and handle are two separate moments in time. Everything good about actors comes from that gap.
The three layers of AutoGen
AutoGen splits its API into three parts, and interviewers do ask this:
- Core — the low-level actor framework. Runtime, agent, message, topic. Async message passing, event driven.
- AgentChat — the friendly high-level API for task-shaped work. Assistant agents, a user proxy agent, and ready-made team shapes.
- Extensions — the plug-ins. Model providers, tools, memory.
Team shapes you should know
- RoundRobinGroupChat — agents speak in a fixed rotation, like a viva where everyone answers in roll-number order.
- SelectorGroupChat — a selector agent looks at the conversation and picks who should speak next.
- Magentic-One — Microsoft's reference multi-agent team for web browsing, running code and handling files. It is built on top of AgentChat, so it is basically a worked example you can read.
A worked example
Suppose Priya builds a code-review bot for her final-year project. Two agents: a Reviewer that reads the diff and comments, and a Checklist agent that checks boring rules — tests present, no hardcoded password, no leftover print statements.
Old way: Reviewer calls Checklist directly. One day a diff has a weird filename, Checklist raises an exception, and the whole review job dies. Priya sees nothing.
Actor way: Reviewer drops a message in Checklist's inbox and moves to the next file. Checklist crashes on that one message, the runtime logs it and parks it, and Checklist carries on. Priya still gets the review for the other nine files, plus one parked message she can look at herself.
Watching what happened
AutoGen has OpenTelemetry built in — that is the common industry standard for emitting traces. Every message emits a span, and tool calls carry gen_ai.* attributes so any tracing dashboard can read them. You get a timeline of who messaged whom, without writing print statements everywhere.
One honest note about status
AutoGen v0.7 is stable and fine for research and prototyping, but Microsoft has moved active development to the Microsoft Agent Framework, the production successor. Do not let that scare you. The actor model is the durable part, and the patterns carry over almost unchanged.
Build it
"""A tiny actor runtime. Standard library only. Run: python3 file.py"""
from collections import deque
class Message:
def __init__(self, sender, recipient, topic, body):
self.sender = sender
self.recipient = recipient
self.topic = topic
self.body = body
class Runtime:
"""Holds every actor's inbox and moves messages around."""
def __init__(self):
self.actors = {} # name -> actor object
self.queue = deque() # messages waiting for delivery
self.dead_letters = [] # messages whose handler crashed
def register(self, actor):
actor.runtime = self
self.actors[actor.name] = actor
def send(self, sender, recipient, topic, body):
# Note: this returns at once. Nobody waits for the handler.
self.queue.append(Message(sender, recipient, topic, body))
def run(self, max_steps=20):
steps = 0
while self.queue and steps < max_steps:
msg = self.queue.popleft()
steps += 1
print(f"[deliver] {msg.sender} -> {msg.recipient} ({msg.topic})")
try:
self.actors[msg.recipient].receive(msg)
except Exception as err:
# Fault isolation: the crash stops here, not in the sender.
print(f"[isolated] {msg.recipient} failed: {err}")
self.dead_letters.append(msg)
class Reviewer:
name = "reviewer"
def receive(self, msg):
if msg.topic == "verdict":
print(f" reviewer heard: {msg.body}")
return
self.runtime.send(self.name, "checklist", "check", msg.body)
class Checklist:
name = "checklist"
def receive(self, msg):
if "\x00" in msg.body: # the one file that breaks us
raise ValueError("cannot parse this file")
ok = "print(" not in msg.body
verdict = "clean" if ok else "found a stray print"
self.runtime.send(self.name, "reviewer", "verdict", verdict)
rt = Runtime()
for actor in (Reviewer(), Checklist()):
rt.register(actor)
for diff in ["def add(a, b): return a + b", "bad\x00file", "print('debug')"]:
rt.send("git", "reviewer", "review", diff)
rt.run()
print(f"parked messages: {len(rt.dead_letters)}")Look at the trace. The bad file blows up inside checklist, but the very next line still shows a delivery — the reviewer never paused. At the end, exactly one message sits parked in the dead-letter list, waiting for a human. That is fault isolation in about sixty lines.
Where you will see this
- Claude Code and Cursor running background jobs — indexing, linting, answering you — without one stuck job freezing the editor.
- Customer-support bots where a routing agent hands your ticket to a billing agent, and one failing lookup does not drop your chat.
- Swiggy or Zomato style order flows: restaurant, rider and payment services exchange events, never touching each other's memory.
- Any backend you have seen with RabbitMQ or Kafka in it — same idea, older name.
Common mistakes
- Sneaking in a shared global dict. The moment two actors read and write the same object, fault isolation is gone and you are back to debugging race conditions at 2 a.m.
- Treating
sendlike a function call. You will writesend(...)and then immediately use the reply on the next line. There is no reply yet. Handle the response inside a handler. - Swallowing every exception silently. Isolating a failure means catching it and recording it. If you just
except: pass, messages vanish and nobody knows. - No limit on the loop. Two actors that message each other forever will happily run forever. Always cap steps or turns.
If they ask in an interview
Q: What is the actor model and why use it for agents?
A: Each agent is an actor with private state, an inbox and a handler, and messages are the only way actors interact. Because sending is separate from handling, one agent crashing does not crash the others, concurrency comes free, and you can move an actor to another machine by changing only the transport.
Q: Why does decoupling delivery from handling give fault isolation?
A: send only drops the message in the recipient's inbox and returns, so the sender is never inside the receiver's call stack. When the receiver's handler raises, the runtime catches it locally and can log, retry or dead-letter it. The sender was already off doing other work.
Q: What is the difference between RoundRobinGroupChat and SelectorGroupChat?
A: Round robin gives every agent a turn in a fixed rotation, which is predictable and easy to debug. Selector uses a selector agent to decide who should speak next based on the conversation, which is smarter but harder to reason about and can loop.
Try these
- Add a real dead-letter report: after
run(), print each parked message with its sender, topic and the error text. Count how often it fires when you feed in messier inputs. - Add a third actor, a Summariser, that collects every verdict and prints one final line. Notice you did not touch the other two actors at all.
- Turn the fixed rotation into a selector: write a small actor that reads the last verdict and decides whether the reviewer or the checklist gets the next message.
- Split the runtime across two processes. Keep the queue in one, and let the other send messages as JSON lines over a socket. What breaks first?
Words, simply
| Word | Meaning in simple words |
|---|---|
| Actor | An agent with its own private state, its own inbox, and one handler function |
| Message | The small packet actors send each other; the only way they interact |
| Inbox | The queue of messages waiting for one actor |
| Runtime | The little engine that picks messages off the queue and hands them to the right actor |
| Topic | A named channel, so many actors can listen for the same kind of message |
| Fault isolation | One actor breaking does not break the others |
| Dead-letter queue | A parking spot for messages whose handler crashed, so a human can look later |
| Concurrency | Many things in flight at the same time, nobody standing and waiting |
Quick recap
- An actor is private state plus an inbox plus a handler, and messages are the only way in or out.
- Sending returns immediately; the runtime delivers later, and that gap is what gives you fault isolation, concurrency and easy distribution.
- AutoGen wraps this in three layers — Core for the actor machinery, AgentChat for ready-made teams, Extensions for integrations.