The most common mistake when you go from one agent to a team of agents is giving each agent its own memory. The memory that matters is the one they share, and in this post I want to show that with running code instead of a diagram.

We are going to build the same multi-agent system twice. In the first version, workers report back to an orchestrator by handing over their full transcripts, which is what most orchestrator-worker stacks do by default. In the second, workers write typed claims into a shared graph and hand back a one-line receipt. Then we measure what happens to the orchestrator’s context as the team grows, and we watch one of the two versions quietly give a wrong answer when a fact changes mid-run.

Everything below is reproducible with no API key. The full code is a single Python file in my dojo repo.

Step 1: a team with a concrete job

The task is deliberately mundane. A team of worker agents researches vendors, one worker per vendor, and the orchestrator has to answer a procurement question:

Which SOC2-compliant vendor with an EU data region has the cheapest price per seat?

Each worker “browses” its vendor’s pricing page and keeps the kind of transcript real agents keep: tool calls, retries, notes to self, and somewhere in the middle, the actual facts.

def run_worker(vendor, revision=0, verbosity=6):
    doc = source_doc(vendor, revision)
    transcript = (
        f"--- worker[{vendor['name']}] transcript ---\n"
        + FILLER * verbosity        # tool calls, cookie banners, retries...
        + doc                       # the pricing page itself
        + FILLER * verbosity
        + f"Conclusion: recorded price, region, SLA and SOC2 status.\n"
    )
    claims = [
        {"subject": vendor["name"], "predicate": "price_per_seat", "value": price, "source": source_id},
        {"subject": vendor["name"], "predicate": "region", "value": vendor["region"], "source": source_id},
        {"subject": vendor["name"], "predicate": "soc2", "value": vendor["soc2"], "source": source_id},
    ]
    return {"transcript": transcript, "claims": claims, ...}

Notice the worker produces both things: the verbose transcript and three small typed claims. The whole experiment is about which of the two the orchestrator gets to consume.

Step 2: the mailbox version

The obvious wiring is to concatenate every worker’s transcript into the orchestrator’s context and let it answer from the pile:

def context_transcript_mode(results):
    return TASK + "\n\n" + "\n".join(r["transcript"] for r in results)

With 6 workers this context is already 20,194 characters (about 5,000 tokens), and the three facts the question actually needs per vendor are buried in operational noise. That noise is not a strawman; open any real agent trace and count how many lines are cookie banners and retries.

Step 3: scale the team and watch the inbox

Running the sweep from 2 to 12 workers gives the picture that matters:

Orchestrator context size against team size: transcript mode grows linearly to 37,381 characters at 12 workers while graph mode stays near-flat at 796 characters, 47 times smaller.

Transcript mode grows about 5,700 characters per worker, linearly, forever. Every worker you add makes the orchestrator’s job harder, which is exactly backwards: you added the worker to make things easier. This is the “context grows until the model loses coherence” failure that Anthropic describes in its engineering notes on multi-agent systems, reproduced in miniature.

The orange line is the second version of the system. Let’s build it.

Step 4: move the state out of the agents

The shared memory is a graph, and for this PoC a plain JSON structure is enough: Claim nodes, Source nodes, and typed edges. Two rules do all the work.

The first rule is that workers write claims, not prose. Each claim carries its subject, predicate, value, and the source it was derived from:

g["nodes"].append({"id": cid, "type": "Claim", **claim})
g["edges"].append({"from": cid, "to": claim["source"], "type": "derived_from"})

The second rule is that writes are additive. A revised fact is a new claim linked to the old one, never an overwrite:

if prev is not None:
    g["edges"].append({"from": cid, "to": prev["id"], "type": "supersedes"})

The orchestrator no longer reads transcripts. It runs a bounded query that returns one live row per vendor, where “live” means no other claim supersedes it:

Task: pick the SOC2-compliant vendor with an EU data region and the
cheapest price per seat.

Current claims (superseded versions excluded):
- Brimsole: price_per_seat=$66 region=EU soc2=True
- Klarveld: price_per_seat=$89 region=EU soc2=True
...

That context is 490 characters at 6 workers, and adding a worker adds one line, not one transcript. The state stopped traveling in the messages; it lives in the graph and any agent reads the subgraph it needs.

Step 5: the fact that changed mid-run

Smaller context is nice, but here is the practical case where the two architectures actually diverge in what they answer.

Mid-run, the current winner (Klarveld, $44/seat) updates its pricing page to $89. A late worker re-checks it and reports back, in both modes. Now look at what each orchestrator is holding.

In transcript mode the pile contains two pricing-page excerpts for Klarveld, one saying $44 and one saying $89. Both look equally authoritative, and concatenation order is arrival order, not truth order. Nothing in the text marks which price is current. The simulated orchestrator in this experiment reads top-down and anchors on the first price it finds, and it answers Klarveld at $44, which is stale:

"transcript_mode": { "answer": ["Klarveld", 44], "correct": false }
"graph_mode":      { "answer": ["Brimsole", 66], "correct": true }

In graph mode the late worker’s write created a supersedes edge, the query excludes superseded claims, and the orchestrator answers Brimsole at $66, the correct winner after the price change. Same workers, same sources, same late report. The only difference is where the state lives.

And because writes were additive, the graph can explain itself. This audit trail is printed straight from the experiment with --show-graph:

claim:Klarveld:price_per_seat:v2  (price_per_seat = 89)
  derived_from src:Klarveld:rev1  "[Klarveld pricing page, revision 1]"
  supersedes:
    claim:Klarveld:price_per_seat:v1  (price_per_seat = 44)
      derived_from src:Klarveld:rev0  "[Klarveld pricing page, revision 0]"

Answer, claim, source, and the full history of the revision, each hop an explicit edge. When someone asks “why did the recommendation change since yesterday”, this structure is the difference between an answer and a shrug.

An honest note on the simulation

The default backend is a deterministic model of the mechanism, in the same spirit as my rate-distortion post: the numbers illustrate the trend and the measurements are real, but the simulated orchestrator is not an LLM. Its first-occurrence reading rule models a structural hazard, namely that once two revisions of a fact sit in one flat context, recency is simply not recoverable from the text. A strong model with a lucky prompt may guess right anyway; the graph makes guessing unnecessary. The same script accepts a live LLM as the orchestrator if you want to try:

git clone https://github.com/moisesvw/dojo
cd dojo/agents/shared-graph-memory

python3 experiment.py                    # sim backend, no key needed
python3 experiment.py --show-graph       # prints the graph + audit trails
python3 experiment.py --sweep            # the chart's data, 2..12 workers
python3 experiment.py --backend openai   # or anthropic

Without the mid-run revision, both modes answer correctly at every team size. The experiment is not rigged against transcripts; they fail only when the world changes while the team is working, which in any long-running system is always.

The takeaway

Start with the mailbox, honestly: for two workers and a short task it is fine. The moment your team grows or your tasks span sessions, move the state out of the agents. A shared JSON file with claims and supersedes is a one-afternoon change, and it buys you three things at once: an orchestrator context that stays flat as the team scales, revisions that are explicit instead of ambient, and answers that can be traced to sources. Graduate that JSON to a real graph database when agents need to chain facts across sessions.

The memory of a team of agents is infrastructure, not a property of each agent. The agent forgets; the graph does not.

References: Andrew Ng, How Agents Can Improve LLM Performance (The Batch); Anthropic, Building Effective Agents and How we built our multi-agent research system.