Cross-Agent Memory Sharing

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)

session_id (and optionally user_identifier=)

Conversation, Message, ReasoningTrace, ReasoningStep, ToolCall. Each agent sees only the sessions it wrote to. One agent’s chat history is invisible to another by default.

Entity namespace (shared)

Global (within a backend / workspace)

Entity (POLE+O), Preference, Fact, Relationship between entities. Any agent on the same backend reads what any other agent wrote.

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 MemoryClient pointed at the same backend. Each owns its session IDs but reads the shared entity graph.

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 Person/Organization entities, writes them to long-term memory.

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 + client.query.cypher(…​)

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 ingestadd_entity() checks for embedding-similar and fuzzy-name matches before creating a new node. Above the auto_merge_threshold (default 0.95), the new write merges into the existing node and the name is added as an alias.

  • SAME_AS flagged pairs — between flag_threshold (0.85) and the auto-merge threshold, the new entity is created but linked via a SAME_AS relationship 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