DEV Community

Tushar Agarwal
Tushar Agarwal

Posted on

My AI agents knew everything except what each other knew, so I gave them theory of mind

How I built theory-of-mind memory for multi-agent systems on FalkorDB for Graph Hacks 2026. Along the way, I found 147 inconsistent labels in a theory-of-mind benchmark.

The bug that isn't in any one agent

Picture a support crew of AI agents. Billing tells a customer, Maya, that her refund is approved. An hour later, in an internal channel Maya
never sees, Finance rejects it because the order is too old, and Escalation approves a replacement instead. Billing goes off shift. Maya
writes again, and the agent who answers, Shipping, saw none of it.

There's no hallucination here and no missing fact. Every fact exists somewhere. The failure is that nobody's memory records who knows which
facts
. Research on why multi-agent LLM systems fail (the MAST taxonomy) lists inter-agent misalignment among the top failure categories:
withheld information, ignored input, and agents working from different assumptions. Humans avoid this with theory of mind. We don't only track
what's true; we track what you know, and what you think I know.

Every agent memory I know of (vector stores, knowledge graphs, temporal graphs) stores facts. So I built one that stores minds.

One graph per mind

SecondOrder keeps a world graph in FalkorDB: events, and for every event, who witnessed it. Then every agent gets its own graph
(so:support:mind:Shipping), with perspectives nested inside:

(:Perspective {chain:'Shipping'})              what Shipping believes
(:Perspective {chain:'Shipping>Maya'})         what Shipping thinks Maya believes
(:Perspective {chain:'Shipping>Maya>Billing'}) what Shipping thinks Maya thinks Billing believes
(child)-[:INHERITS]->(parent)
(:Perspective)-[:BELIEVES {entity, active, t, via, src}]->(:Value)
Enter fullscreen mode Exit fullscreen mode

The rule that makes it work is perception routing. An event updates the perspective A>B>C only if A, B and C all witnessed it, because A
only thinks B and C know something if A saw them see it. Private messages reach only the listener's perspectives. Public announcements reach
everyone's perspectives of everyone. A report like "I told Maya her refund is approved" writes straight into second-order perspectives.

The Mind Theater: agents as orbs in their channels; each ring is the agent's own belief, each moon is what it thinks another agent believes

Copy-on-write minds

With 5 agents, depth 4 means 205 possible perspectives per scenario, so storing every belief in every perspective would be wasteful. Instead, a
perspective stores a belief edge only where it differs from its parent. Reading a nested belief is a single Cypher walk up the
INHERITS tree:

MATCH (p:Perspective) WHERE p.chain IN $prefixes
WITH p ORDER BY p.depth DESC LIMIT 1
MATCH path = (p)-[:INHERITS*0..6]->(q:Perspective)-[b:BELIEVES {entity: $entity, active: true}]->(v:Value)
RETURN v.name, b.via, b.src, q.chain ORDER BY length(path) LIMIT 1
Enter fullscreen mode Exit fullscreen mode

When an event updates a perspective, its children that didn't witness it are frozen at their old value first: copy-on-write, per belief
edge. Old beliefs are closed (active:false, until:t), never deleted, so "what did Shipping believe at step 7?" is just another query. That
is what powers the animated replay in the demo.

Here's a real mind graph, opened in FalkorDB's own Browser:

A SecondOrder mind graph in FalkorDB Browser

Theory of mind, not omniscience

This is where FalkorDB's multigraph design stops being a performance detail and becomes a security property. Each agent connects as its own
ACL user that may only run GRAPH.RO_QUERY on ~so:*:mind:<agent>. Shipping can ask what it believes, and what it thinks Maya believes,
but reading Escalation's mind returns NOPERM. The world graph is for supervisors only.

What the graph buys you

  • Handoffs that send only what's missing. handoff_delta(Escalation, Shipping) returns only what Escalation's model of Shipping lacks: 48 tokens instead of an 806-token channel transcript. That includes the fact that matters most: "Maya still believes the refund is approved."
  • Corrections that target the right person. Two Claude agents get the same facts. The one with a normal memory states the news. The one with SecondOrder sees what Maya was told and opens with "I need to correct something first…".
  • Announcements that create common ground. A launch date changed in a thread only 3 of 7 people saw. The planner forks every graph with GRAPH.COPY and simulates two fixes. Four private DMs fix everyone's beliefs but leave 28 wrong beliefs about each other. One announcement leaves 0.

The classic Sally–Anne false-belief test, stored in FalkorDB

The benchmark, and a surprise

Hi-ToM asks questions up to 4th order: "Where does Charlotte think Jack thinks Hannah
thinks William thinks the carrot is?"
Against the released labels, on all 1,200 questions:

overall 4th order
SecondOrder 87.8% 74.2%
Claude Opus 5.5 alone 80.4% 66.2%
Flat memory (knows the truth!) 53.1% 39.2%

I expected near-perfect scores, so I dug into the misses. Then I tried something simple: I took the benchmark authors' own belief-tracking
code (their Oracle class, unmodified) and replayed every story through it.

  • Their code agrees with SecondOrder on 1,200 / 1,200 questions.
  • Their code agrees with their own released labels on only 1,053.

So 147 released labels contradict the generator that supposedly produced them. The repo history has a commit called "fix permutation bug" that
changed how nested beliefs are written, which may explain the drift. I reported it to the authors with a reproducible script and the full list. Scored against the
reference code:

Hi-ToM vs. reference code
SecondOrder 100%
Claude Opus 5.5 alone 91.0%
Claude Haiku 5.5 alone 86.5%
Flat memory 50.0%

Two more checks:

  • No template parser needed. When Claude extracts events from the raw story text instead of my parser, the score is identical.
  • Free prose. On 100 stories that Claude rewrote as free-flowing prose, SecondOrder scores 100/100 and Opus alone scores 89/100.

Benchmarks

Does it help a real agent?

I built 20 support cases. In each one, a customer is told X, X changes in a channel they never see, and the case is handed to a new agent. The
customer then writes "Any update?", without restating X. A Claude judge checks whether the reply explicitly corrects the customer:

Agent memory Corrects the customer Input tokens (long channel history)
Flat memory 1/20 1,514
Full transcript handoff 20/20 8,218
SecondOrder context 20/20 1,723

A transcript gets there by re-reading everything, so its cost grows with history. SecondOrder's prompt stays constant, because the graph
already knows who believes what. That's the whole idea: a context layer, not a bigger context window.

This eval also caught a real bug. Customer names with accents ("Tomás") broke the agent's lookups into its own mind graph, because of how
parameters were escaped. It's fixed now, and a test covers it.

Does it scale?

FalkorDB's pitch is thousands of isolated graphs in one instance, so I tested exactly that: 1,700 tenants × 6 graphs = 10,200 graphs in one
FalkorDB.

  • Event routing: routing an event into every affected mind stays at about 2.9 ms p50, from 1k up to 10k graphs.
  • Reads: a nested read takes about 0.2 ms.
  • Indexes: a range index on Perspective(chain) cut event cost from 57 ms to 23 ms at 16 agents and depth 3.
  • Memory: the one thing to tune is per-graph memory, at about 490 KB baseline per graph with default settings.

Try it

from secondorder import Memory
mem = Memory("support")
mem.join(["Maya", "Billing"], "chat");  mem.say("Billing", "refund", "approved", room="chat")
mem.leave("Maya");  mem.set("refund", "rejected", room="chat", by="Finance")   # Maya never sees this
mem.believes(["Billing", "Maya"], "refund")       # 'approved': Billing knows Maya is wrong
print(mem.context_for("Billing", talking_to="Maya"))
Enter fullscreen mode Exit fullscreen mode

What ships:

  • the engine and an MCP server, so any agent can use it;
  • all 26 hand-written Cypher queries;
  • every benchmark result;
  • setup for macOS, Linux and Windows.

👉 Code: https://github.com/TusharTechs/secondorder

Built for Graph Hacks: Context for AI Agents (WeMakeDevs × FalkorDB), Track 2 (Agent Memory & Coordination) and Track 3 (Company Brain).

Top comments (0)