I built a town that gossips: graph memory for multi-agent systems with FalkorDB
Built for Graph Hacks: Context for AI Agents (WeMakeDevs x FalkorDB).
Live demo: https://living-town.onrender.com · Code: https://github.com/thedevkansal/living-town
The question that started it
Most "agent memory" today is a private notebook: one agent, one store, a pile of embedded snippets. That works until you have more than one agent. The moment agents talk to each other, the interesting questions are no longer what an agent knows, but how it came to know it:
- Who told you that?
- Who told them?
- Did anyone actually see it happen, or is this the fifth retelling of a rumour?
- If two agents disagree, whose version wins?
Every one of those is a question about relationships, and relationships are exactly what a vector store throws away. So for this hackathon I built a small world where those questions are the whole game.
Living Town in 30 seconds
Thornbury has ten NPC agents: a blacksmith, a tavern keeper, a guard captain, a deputy, a priest and others. Each one's memory is a neighbourhood in a FalkorDB graph.
You walk into the Forge and steal Bram's silver hammer. Only Bram sees it. Then you let the day play out:
- At midday Bram tells Mira at the tavern.
- In the evening Mira tells Captain Oswin. Oswin never saw anything.
- Oswin posts an investigation on a shared task board. Finn, the deputy, claims it: the database only lets an agent claim a task if it
KNOWS_HOWto do it. - Finn traces the story back to its eyewitness and takes Bram's testimony.
- At dawn Oswin walks up to you: "You there! Mira told me, and it traces back to Bram, who saw it."
Ask Oswin "How do you know?" and the answer is not generated text. It is the path the query returned:
Ada -[stole]-> (theft) <-[WITNESSED]- Bram -[TOLD]-> Mira -[TOLD]-> Captain Oswin
Ask Sister Agnes the same question and she says "Silver hammer? Nothing in my memory connects to that." No path, no answer. I like that more than any confident hallucination.
Three kinds of memory, one graph
The track asked for episodic, semantic and procedural memory. In a graph they stop being three systems and become three edge types:
| Memory | Shape in the graph |
|---|---|
| Episodic: what I saw | (:NPC)-[:WITNESSED]->(:Event)-[:AT]->(:Location) |
| Semantic: what I believe, and how sure | (:NPC)-[:BELIEVES {confidence, stance, hop, via}]->(:Fact) |
| Procedural: what I know how to do | (:NPC)-[:KNOWS_HOW {proficiency, learned_from}]->(:Procedure) |
Two more pieces make it social memory:
-
Provenance. Every time a story moves, a
TOLDedge is written between the two agents. Each believer has exactly one primary incomingTOLDedge per fact, so the chain back to the witness is a tree you can walk. -
Social structure.
(:NPC)-[:KNOWS {trust, affinity}]->(:NPC). Bram dislikes the captain (affinity 0.1) and never talks to him, so the story has to route through Mira. That routing does not exist anywhere except in the graph.
The rules of gossip
The engine is small and deliberately boring:
- An eyewitness believes with confidence 1.0.
- A listener's confidence is
speaker confidence × listener's trust in the speaker × 0.9. Below 0.3 the story is doubted, not accepted. - Hearing the same story again from someone else corroborates it instead of creating a second chain.
- Every dawn, hearsay fades by 7%; eyewitness memory keeps its confidence but loses vividness.
- Lies are just facts that contradict other facts. If you tell Mira that Pip stole the hammer and she already believes otherwise with more confidence, she catches the lie, and that becomes a new story about you.
The Cypher that does the work
The agents never write Cypher. Every query is hand-written and parameterised (108 of them: 50 reads, 50 writes, 8 graph-algorithm calls), and agents, an LLM, or an MCP client only choose which query to run and with what arguments. Reads go through GRAPH.RO_QUERY.
The gossip handoff is one atomic statement:
MATCH (a:NPC {id:$from})-[:BELIEVES]->(f:Fact {id:$fid}), (b:NPC {id:$to})
MERGE (b)-[bel:BELIEVES]->(f)
ON CREATE SET bel.confidence = $conf,
bel.stance = CASE WHEN $conf >= $accept THEN 'accepted' ELSE 'doubted' END,
bel.hop = $hop, bel.via = $from
ON MATCH SET bel.corroborations = coalesce(bel.corroborations, 0) + 1
MERGE (a)-[t:TOLD {fact_id:$fid}]->(b)
"How do you know?" is a variable-length path over primary TOLD edges:
MATCH (me:NPC {id:$npc})-[b:BELIEVES]->(f:Fact {id:$fid})
OPTIONAL MATCH chain = (src)-[:TOLD*0..10 {fact_id:$fid, primary:true}]->(me)
WITH me, b, f, chain ORDER BY length(chain) DESC LIMIT 1
...
My first version filtered the edges after expanding the path (WHERE all(r IN relationships(chain) ...)). That enumerated every gossip path in town before throwing most of them away, and after about five simulated days the query timed out. Putting the filter on the relationship pattern itself makes FalkorDB prune while it expands: 4-9 ms, flat, no matter how much the town gossips.
And the task board settles races in the database, not in Python. Only one agent can claim a task, and only if it actually knows the procedure:
MATCH (t:Task {id:$tid}) WHERE t.status = 'open'
MATCH (n:NPC {id:$npc})-[:KNOWS_HOW]->(p:Procedure) WHERE p.id = t.required
SET t.status = 'claimed', t.claimed_by = $npc
CREATE (t)-[:CLAIMED_BY]->(n)
Graph algorithms as town insights
FalkorDB ships graph algorithms as procedures, and each one answers a question about the town:
-
algo.labelPropagationover close friendships finds the cliques, where gossip moves fastest. -
algo.betweennessoverKNOWSfinds the gossip broker (it is always Mira, the tavern keeper). -
algo.pageRankoverTOLDfinds who hears the most stories. -
algo.SSpathsover social distance forecasts who is likely to hear a story next, drawn as dashed rings on the map. -
algo.WCCandalgo.HarmonicCentralityfind rumour islands and who can spread news fastest.
One graph per player
Tenancy was the easiest part, and the one I was most happy with. FalkorDB holds many independent graphs in one instance, so every player gets their own: town:ada, town:bo, and so on. There is no WHERE tenant_id = ... anywhere. In the demo, two players side by side live in two different graphs; a crime in one never leaks into the other. Come back under the same name in a new session and the town greets you from what it remembers.
The bug that taught me the most: round trips
Locally a simulation tick (all ten agents perceiving, gossiping, claiming tasks) took about 100 ms. Deployed, with the app in Singapore and FalkorDB Cloud in Mumbai, the same tick took 2.8 seconds. Nothing was slow on its own; every query was simply a cross-region round trip, and a tick made about 25 of them one after another.
The fix was to stop thinking in queries and start thinking in round trips:
- Independent per-agent reads and writes run concurrently with
asyncio.gather. - Small queries were merged; writes were batched with
UNWIND. - The whole town is built in a single statement.
- A warm connection pool is opened at startup, because opening each new connection to the cloud instance cost about 350 ms.
A tick is now about six sequential round trips: 260 to 400 ms live (down from 2.8 s), and creating a new world dropped from ~15 s to ~1.4 s. A whole simulated day plays out in about ten seconds on the live site.
Agents, LLMs and MCP
By default the agents are rule-based, and that is on purpose: the memory, gossip and coordination are the product, and they behave the same with or without a model. With an Anthropic API key, NPC dialogue is generated by Claude using tool calls over the same fixed memory catalogue, and every person, item or place the model mentions is checked against that turn's tool results; if it names something the graph never returned, the reply is thrown away and a memory-grounded template is shown instead.
The same tools are exposed over MCP, so any MCP client (Claude Code, Claude Desktop, ...) can open a town and ask "In player ada's town, how does Captain Oswin know about the hammer?"
What I'd do next
- Let agents forget deliberately: compress old episodic memories into semantic summaries, keeping the provenance edges.
- Reputation that spreads: right now each NPC's opinion of you is derived from their own beliefs; I'd like opinions to be gossip too.
- Use the same engine outside games: support agents that share what they learned about a customer, with an auditable trail of who learned what from whom.
Try it
- Live demo: https://living-town.onrender.com (free tier, so the first load may take a minute)
- Code, data model and every Cypher query: https://github.com/thedevkansal/living-town
Built with Claude Code as a coding assistant, as disclosed in the repo.

Top comments (1)
Dude I wish you’d used retro pixed style character sprites instead of the round ones... it was a fun concept....Good luck mate