Cross-Agent Memory Sharing
- The capability in one sentence
- The two namespaces
- Multi-tenant scoping with
user_identifier - Cross-language sharing: same contract, different SDKs
- A typical multi-agent architecture
- Worked example: ingestion + chat + analytics
- When agents collide: avoiding write conflicts
- Reasoning trace isolation
- Why this matters for AI engineering
- See Also
How multiple agents — written in different languages, using different frameworks, deployed as separate services — read and write the same memory graph.
Conversations are private. Knowledge is shared. That’s the design contract that makes Neo4j Agent Memory natural for multi-agent systems.
The capability in one sentence
If two agents point at the same backend (NAMS API key, or the same Neo4j cluster), an entity created by Agent A is queryable by Agent B as soon as the write returns — with no copy step, no message bus, no cache invalidation.
The two namespaces
Every memory operation lives in one of two namespaces:
| Namespace | Scoped by | What’s in it |
|---|---|---|
Conversation namespace (private) |
|
|
Entity namespace (shared) |
Global (within a backend / workspace) |
|
This split is the load-bearing design choice. It mirrors how human teams work: individual notebooks (conversations) plus a shared wiki (knowledge).
On NAMS, the shared boundary is the workspace (bound to your API key): every agent using the same workspace shares the entity graph, while different workspaces are fully isolated from one another. On bolt, the shared boundary is the Neo4j database. Either way, user_identifier= sub-divides users within that boundary. See Tenancy & scoping.
Multi-tenant scoping with user_identifier
For SaaS-style deployments where one backend serves many end-users, every memory call accepts a user_identifier= kwarg:
# Agent code, identical for every end-user
async def handle_request(user_id: str, message: str):
async with MemoryClient() as memory:
await memory.short_term.add_message(
session_id=f"chat-{user_id}",
role="user",
content=message,
user_identifier=user_id, # scopes the write
)
# Subsequent reads with the same user_identifier only see this user's data
prefs = await memory.long_term.search_preferences(
"communication style",
user_identifier=user_id,
)
user_identifier is stored as a property on every node and used as a WHERE filter on every read. See Multi-tenant memory for the full pattern.
Cross-language sharing: same contract, different SDKs
The Python SDK (neo4j-agent-memory) and the TypeScript SDK (@neo4j-labs/agent-memory) both implement the same NAMS REST contract. So an agent in either language can read what an agent in the other wrote.
Example: a Python ingestion agent and a TypeScript chat agent sharing entities:
# ingestion-agent (Python) — runs on a cron, scrapes podcasts
async with MemoryClient() as memory:
entity, _ = await memory.long_term.add_entity(
"Alice Johnson", "PERSON",
description="Former CTO of Acme, speaker on Lenny's podcast Ep. 42",
)
// chat-agent (TypeScript) — runs on Cloudflare Workers
const memory = new MemoryClient();
const matches = await memory.longTerm.searchEntities("Alice Johnson", { topK: 3 });
// matches[0].description includes the entity the Python agent created.
Both SDKs write nodes with the same labels, property names, and relationship types. The graph doesn’t know which language produced it.
A typical multi-agent architecture
| Layer | Responsibility |
|---|---|
Backend (NAMS or Neo4j) |
Single source of truth for the memory graph. NAMS is the natural choice when you don’t want to operate Neo4j; self-hosted Bolt is the choice when you need write-Cypher or geospatial features. |
Agents (any language, any framework) |
Each agent is a separate process or service. Each holds its own |
Orchestrator (optional) |
A coordinating agent that reads all sub-agents' reasoning traces, queries the shared entity graph, and synthesizes a response. Can be a workflow engine (LangGraph, Mastra) or a simple supervisor agent. |
When agents are independent services (containerized, on different runtimes), NAMS removes the "who runs Neo4j?" question — every service just needs an API key.
Worked example: ingestion + chat + analytics
Three agents that never talk to each other directly but cooperate via shared memory:
| Agent | Language | Framework | What it does |
|---|---|---|---|
Ingestion |
Python |
PydanticAI |
Cron job. Reads podcast transcripts, extracts |
Chat |
TypeScript |
Vercel AI SDK on Cloudflare Workers |
User-facing chat. On every turn, reads relevant entities and preferences from the shared graph, persists the conversation in its own session namespace. |
Analytics |
Python |
Plain script + |
Nightly. Reads all entities, computes co-mention graphs, surfaces trends. |
Each agent is its own deployment. None imports the others. They cooperate solely through reads and writes against the shared backend.
When agents collide: avoiding write conflicts
Because the entity namespace is shared, two agents can try to create the same entity at the same time. Neo4j Agent Memory handles this with:
-
Entity deduplication on ingest —
add_entity()checks for embedding-similar and fuzzy-name matches before creating a new node. Above theauto_merge_threshold(default 0.95), the new write merges into the existing node and the name is added as an alias. -
SAME_ASflagged pairs — betweenflag_threshold(0.85) and the auto-merge threshold, the new entity is created but linked via aSAME_ASrelationship for human review. -
Server-side dedup on NAMS — the hosted backend performs dedup transparently.
See Handle duplicates for the configuration knobs.
Reasoning trace isolation
While entities are shared, reasoning traces are per-session by design:
# Agent A starts a trace
trace_a = await memory.reasoning.start_trace(
session_id="agent-a-session-1",
task="Plan a trip",
)
# Agent B in the same process — its traces don't show up in A's session
trace_b = await memory.reasoning.start_trace(
session_id="agent-b-session-1",
task="Book a flight",
)
# A's get_session_traces returns only A's traces
await memory.reasoning.get_session_traces("agent-a-session-1")
But if an orchestrator wants cross-agent visibility for audit, it can query Cypher (Bolt) or the read-only Cypher endpoint (NAMS):
// All traces created in the last hour, across all sessions and agents
MATCH (t:ReasoningTrace)
WHERE t.created_at > datetime() - duration('PT1H')
RETURN t.session_id, t.task, t.outcome, t.success
ORDER BY t.created_at DESC
See Audit reasoning with TOUCHED edges for the full audit pattern.
Why this matters for AI engineering
The hardest problem in multi-agent systems is not coordination — it’s coherence. Two agents that "know" different things will give the user contradictory answers. A shared memory graph keeps every agent’s worldview in sync without bolting on a separate state management layer.
The architecture also degrades gracefully: a single-agent prototype written against MemoryClient becomes a multi-agent system by adding more agents that point at the same backend. No refactor.
See Also
-
Understanding the Three Memory Types — what conversations, entities, and traces are
-
Bolt vs NAMS — which backend to pick
-
Multi-tenant memory —
user_identifier=patterns -
Use NAMS — the hosted-service operational guide
-
NAMS Quickstart — end-to-end deployment
-
NAMS REST API — the wire-level contract both SDKs implement