How AI Agents WorkCourse map

Chapter 13 · Systems

Agent communication

Agents pass information through ordinary channels such as return values, queues, and databases. A model can act only on the part that reaches its context.

Two layers: coordination and context

Two coding agents work in the same repository. Before one edits auth/session.py, it must make sure the other is not editing it. Later, a reviewer agent must tell the coder that a test still fails.

These are two kinds of communication. The first is coordination: who works on what, who owns a file, whether a task is done. Your code handles it with locks, queues, and status fields, and none of it needs to reach a model. The second is information a model must act on, such as the failing test. That has to enter the coder's context, because a model sees nothing else (chapter 4).

Agent A's code Channel: queue, file, or table Agent B's code The part B's model needs

Everything in a context costs tokens and attention. It can also carry instructions, because a model treats all of its input as guidance. So send a reference instead of the content when you can: a file path, a task id, or a commit hash. For example, a research agent saves 40 KB of notes to a file and returns one line with the path. The parent's context grows by one line instead of 40 KB.

Parent and child: return values

A coding agent needs to know where expired tokens are accepted. It hands the question to a subagent, which searches the code in its own loop and returns five lines.

result = delegate(
    "Find where expired tokens are accepted. "
    "Return at most 5 lines: file:line and one sentence each.",
    tools=["search_files", "read_file"],
)
# only `result` enters the parent's context

This is the simplest channel. The parent calls the child like a function, and the child's answer comes back as one tool result. It fits a clear question with a short answer, which is where most subagent designs start (chapter 7).

The parent waits, or checks back later if the child runs in the background. Anything the child leaves out is lost, and a long answer floods the parent's context. So ask for a fixed format and a length limit, and put the details in a file.

Shared state

A research subagent writes notes.md into a set of files that it shares with its parent. The parent reads the file later, and only if it needs the details.

Here several agents read and write the same state inside one program. It might be a dictionary, a todo list, or a set of files. Nothing enters a context until an agent reads it, which keeps contexts small.

The risk is hidden coupling: any agent can change something another depends on, and nobody is told. Agents that write at the same moment can also overwrite each other. So you need a merge rule, often called a reducer:

def merge_findings(old, new):        # a reducer for writes that arrive together
    return old + [f for f in new if f not in old]

state = {"findings": [], "files": {}}   # one object, shared by parent and children

Shared state fits agents in one program and one run, with few writers.

Messages

A reviewer agent sends the coder a message: the test still fails, and the details are in a file.

{"from": "reviewer", "to": "coder", "re": "task-412",
 "kind": "change_request",
 "note": "test_login_expired_token still fails",
 "ref": "/work/review/412.md"}

With messages, one agent writes to another agent's inbox. The receiver's code decides when each message enters its model's context, usually as a new message on the next turn. Messages fit a few long-lived agents that go back and forth, such as a review loop.

Every message enters a context, so chatty agents get expensive, and two agents can keep replying to each other forever. Treat each message as untrusted input, too. If the sender read a hostile web page, its message can pass the attack along. So give messages fixed fields, and keep the free text short.

Event bus

The build system announces that a build failed. A triage agent starts a run, and a notifier posts in the team chat. The build system does not know that either of them exists.

bus.publish("build.failed", {
    "repo": "web-api", "commit": "9f2c1e7",
    "log": "ci/runs/5512/log.txt",          # a reference, not the log itself
    "event_id": "evt-77a1",                  # so repeats can be spotted
})

That is an event bus, also called publish and subscribe. A component publishes an event on a topic, and every subscriber to that topic reacts. You can add subscribers without changing the publisher, so a bus suits the triggers that start event-driven agents (chapter 1).

The costs are in tracing and delivery. When one event sets off a chain of reactions, it gets hard to answer "what started this run?". Many buses can deliver an event twice, so subscribers check event ids. And an agent that reacts to its own events can loop. To catch that, give each event an id that links it to its cause, and count the hops.

Let events carry facts, such as "build 5512 failed", not instructions, such as "please fix the build".

Task queue

A planner agent adds task 7 to a queue. A research worker claims it, writes the answer onto the task, and marks it done. Then the planner plans the next step.

{"id": 7, "kind": "research", "status": "ready",
 "ask": "Which auth tests touch token expiry?",
 "parent": 3, "result": null}

In a task queue, the message is the task record itself: the request, its status, its owner, and the result. Anyone can look at the board, including a person, who can also change a task. A task also survives when its worker crashes, because a lease hands it to another worker (chapter 12).

A queue fits durable work, many workers, and people who need to watch or steer. The costs are slower handoffs, more moving parts, and tasks that can run twice. A task that keeps failing usually moves to a dead-letter queue, a separate queue where a person can inspect it.

Blackboard

Three specialist agents debug the failing login test together. They share one board with sections for symptoms, possible causes, evidence, and ideas ruled out.

board = {
    "symptoms":   ["expired tokens still accepted",
                   "test_login_expired_token: 200 != 401"],
    "hypotheses": [
        {"id": "h1", "by": "code", "text": "10-minute grace period in auth/session.py"},
        {"id": "h2", "by": "logs", "text": "token cache serves stale entries"},
    ],
    "evidence":   [{"for": "h1", "by": "test", "ref": "runs/882/test.log"}],
    "ruled_out":  ["clock skew on the test runner"],
}
next_agent = controller.pick(board)     # a set of rules, or a model call

This is the blackboard architecture, an old idea from AI research. Specialists watch the board and add to it when they can help. A controller picks who acts next, based on the board. Here, the controller might see that h2 has no evidence yet and wake the test agent.

Unlike plain shared state, the board is organized like the problem, and nobody fixes the order of work in advance. That suits problems such as diagnosis, where the next step depends on what the specialists find.

Database

Two coding agents must never edit the same file at once. Each one claims a file by inserting a row into a table. The database refuses a second row for the same file:

-- one row per file being edited; a second claim fails
CREATE TABLE claims (path TEXT PRIMARY KEY, agent TEXT, run_id TEXT);
INSERT INTO claims VALUES ('auth/session.py', 'coder-2', 'run-77');

The second agent's insert fails, and its code tells the model "file taken". But the claim protects the file only if every way of writing checks it. The edit tool must refuse to write without a claim. A general shell can write any file and skip that check. So agents that share files either lose the shell or work in separate copies of the repository.

Claims also need an end. An agent deletes its claim when it finishes. A claim whose agent died expires after a timeout, like a lease (chapter 12).

A database is shared state that lives outside any program. It survives crashes, many programs can use it, and constraints keep its data consistent. Wrap it in narrow tools such as claim_file and get_task, rather than letting models write raw SQL against shared tables.

Tradeoffs

Each channel trades the same six properties. Coupling is how much each side depends on the other. Consistency is whether every agent sees the same data. Observability is how easily you can see what happened. The other three are how failures are handled, how much enters a context, and whether things arrive in order.

Channels compared, part 1: coupling, consistency, and observability
ChannelCouplingConsistencyObservability
Return valuesTight: the parent waitsSimple: one answer per callGood, if child logs are kept
Shared stateHidden: any writer affects every readerNeeds merge rules for writes that arrive togetherPoor, unless writes are logged
MessagesDirect: the sender names the receiverEach agent's view can driftGood, if messages are logged
Event busLoose: publishers do not know subscribersSubscribers catch up eventuallyHard: causes spread across subscribers
Task queueLoose, through task recordsThe task's status is the truthGood: the board shows every task
BlackboardLoose, but tied to the board's layoutA controller settles conflictsGood: the board is the record
DatabaseLoose, through a schemaTransactions and constraintsGood: the history can be queried
Channels compared, part 2: failures, context, and order
ChannelFailure handlingContext costOrdering
Return valuesThe parent sees the error and decidesThe whole answer enters the parentCall order
Shared stateA bad write spreads silentlyNone until readUndefined when writers overlap
MessagesLost or repeated messages need rulesEvery message enters a contextPer sender, if the channel keeps it
Event busEvents can arrive twice; check idsOnly what a subscriber passes onWithin one topic, at best
Task queueLeases, retries, and a dead-letter queueThe task's request and referencesRoughly queue order
BlackboardA wrong entry misleads every readerGrows with the board, unless splitChosen by the controller
DatabaseTransactions undo failed writes; constraints refuse bad onesOnly the rows queriedPer transaction

A sensible order to try them:

Protocols: MCP and A2A

Say the team's ticket system runs an MCP server. Patch's code asks it which tools it offers, shows them to the model, and forwards the model's requests.

MCP, the Model Context Protocol, connects an agent's code to tools, data, and prompts offered by another program (chapter 2). It is not an agent loop, because your code still runs the loop. It is not a security boundary either. The server's tools run with the server's access, and the spec itself says MCP "cannot enforce these security principles at the protocol level".

A2A, the Agent2Agent protocol (version 1.0), covers the other pairing. In its own words, "MCP standardizes agent-to-tool communication; A2A standardizes agent-to-agent communication." Each agent publishes an Agent Card, a JSON document that lists its skills, its address, and how to sign in. Work travels as a Task with an id and a lifecycle, much like a record in the task queue above. The agents do not need to share their memory or tools.

Neither protocol decides what enters a context or who may do what. Those decisions stay with you.

Running example: Patch talks through files

Patch's subagents already return short reports (chapter 7). Now each subagent's final message has two parts: a summary of at most five lines, then its full notes. The runtime saves the notes in a shared workspace and passes the parent only the summary and the file path. Briefs work the same way, so the reviewer gets paths to the diff and the test output, and the subagents stay read-only.

# Patch v12: reports and briefs carry full contents
- report = delegate(explorer, task)
- review = delegate(reviewer, brief + diff_text + test_output)
# Patch v13: the final message is a summary (5 lines at most), then the full notes
+ report = delegate(explorer, task, format="summary, then full notes")
+ path = workspace.save(f"explore/{task.issue}.md", report.notes)   # the runtime saves the notes
+ parent.add(tool_result(report.summary + "\nFull notes: " + path))  # the parent sees only this
+ review = delegate(reviewer, brief + "Diff: /work/412.diff  Tests: /work/412-tests.txt")

What goes wrong

The parent's context floods

Why it happens
A child returns its whole transcript or a huge result, which buries the parent's earlier instructions.
How to spot it
Track the size of each tool result, and watch for jumps right after a delegation.
What to do
Set a length limit and a format for answers, and return file paths instead of contents.

Two agents overwrite each other

Why it happens
They share files or state, write at the same moment, and nobody owns anything.
How to spot it
Lost changes, merge conflicts, or reducer errors.
What to do
Give each file or field one writer, make every write tool check a claim first, or give each agent its own copy of the repository.

An attack travels between agents

Why it happens
A research agent reads a hostile page and passes its demand along in a message. The next agent treats it as a request.
How to spot it
Agent messages contain instructions that no user gave.
What to do
Treat other agents' output as data, give messages fixed fields, and enforce permissions in every agent's code (chapter 10).

You decide, AI builds

Decide yourself

  • Which channel each pair of components uses, and why.
  • What enters each model's context, in what format, and how big it may get.
  • Who owns each file, record, and field, and how conflicts are settled.
  • Which messages count as requests, and which are only data.

Delegate to AI

  • Queue, bus, and database plumbing.
  • Message formats and validation.
  • Ids that link events to their causes, and logs across agents.
  • Connections to MCP servers and other protocols.

Check yourself

How do agents communicate? (Q26)

Answer

Through ordinary channels: return values, shared state, messages, an event bus, a task queue, a blackboard, or a database. Coordination stays in code. Only what a model must act on enters its context, ideally as a short answer or a reference.

A subagent produced 30 KB of notes. What should the parent receive?

  1. All 30 KB, so nothing is lost
  2. A short summary with evidence, plus the path to the full notes
  3. Nothing. The parent can search the workspace later.
Answer

A short summary plus a path, so the parent's context stays small and the details are one read away.

Two agents must never edit the same file at once. Where should that rule live?

  1. In both agents' system prompts
  2. In a claim that the database records and every write tool checks
  3. In a message each agent sends before editing
Answer

In an enforced claim. The database refuses a second claim, and the write tools refuse to edit without one. A prompt or a message can be ignored.

Takeaways

Going deeper