Skip to content

Build

Multi-agent shared memory

When multiple agents collaborate on a shared task, they need access to the same memory. SurrealDB Agent Memory's scope model makes this natural: agents that share a scope dimension (such as project) can all read from and write to the same experiential memory, while retaining individual isolation where needed.

There are two common patterns for multi-agent memory sharing:

PatternWhen to use
Shared scopeAll agents contribute to and read from a common project scope
Supervisor + workersA supervisor agent coordinates several worker agents, each with its own user/agent scope

The simplest approach - all agents are given the same scope when creating sessions. Memory written by one agent is immediately visible to others operating in the same scope.

from surrealdb.memory import AsyncMemory

client = AsyncMemory(
    context="acme-prod",
    endpoint="https://memory.example.com",
    api_key="sp-...",
)

SHARED_SCOPE = ["org/acme/project/market-research-q3"]

# Agent A: data collection agent
await client.remember(
    "Competitor X launched a new pricing tier at $299/mo targeting SMBs.",
    scopes=SHARED_SCOPE,
)

# Agent B: analysis agent - sees Agent A's memory
results = await client.recall(
    "recent competitor pricing changes",
    k=5,
    lens=SHARED_SCOPE,
)
# results.hits includes Agent A's finding

To prevent worker agents from writing to shared memory (read-only readers), issue separate API keys with restricted write capabilities. A key bound to a principal granted memory:read and memory:write on org/acme/project/market-research-q3 can read and write at that scope. For genuinely read-only agents, mint the key against a principal granted memory:read but not memory:write on that path.

A supervisor agent orchestrates several workers. Each worker writes to its own scope, which is the project scope AND its own agent scope, and the supervisor aggregates their findings into the shared project scope.

A scope list is a list of OR clauses, and each nested list is one AND clause. ["org/acme/project/research-pipeline", "agent/worker-1"] is an OR, so a record tagged with it is readable by anyone who can read the project scope. [["org/acme/project/research-pipeline", "agent/worker-1"]] is one AND clause, so only a reader whose grants cover both paths can read the record.

PROJECT = "org/acme/project/research-pipeline"
SUPERVISOR_SCOPE = [PROJECT]

async def run_worker(topic: str, worker_id: str, client: AsyncMemory) -> str:
    # One AND clause: the project scope and this worker's own scope
    worker_scope = [[PROJECT, f"agent/{worker_id}"]]
    session = await client.sessions.create(scopes=worker_scope)

    # Worker researches its topic
    context = await client.recall(topic, k=5, lens=worker_scope)
    findings = await llm.research(topic, context.hits)

    # Worker stores findings in its own scope
    await client.remember(
        findings,
        session_id=session.id,
        scopes=worker_scope,
        memory_category="knowledge",
    )

    # Return a summary for the supervisor
    reflection = await client.reflect(
        query=f"Summarise findings about {topic}",
        persist=False,
    )
    return reflection.reflection

async def supervisor(client: AsyncMemory):
    topics = ["pricing trends", "competitor features", "customer sentiment"]

    # Run workers concurrently
    results = await asyncio.gather(*[
        run_worker(topic, f"worker-{i}", client)
        for i, topic in enumerate(topics)
    ])

    # Supervisor aggregates findings into shared scope
    session = await client.sessions.create(scopes=SUPERVISOR_SCOPE)
    for topic, finding in zip(topics, results):
        await client.remember(
            f"[{topic}] {finding}",
            session_id=session.id,
            scopes=SUPERVISOR_SCOPE,
            memory_category="knowledge",
        )

    # Supervisor synthesises and stores a final report
    await client.reflect(
        query="Synthesise all research findings into a coherent executive summary.",
        persist=True,
    )

This example shares one client between the workers to stay short. A lens narrows what a read returns, but it does not restrict access. To stop one worker reading another worker's memory, give each worker its own API key, bound to a principal whose grants cover the project scope and its own agent/<id> scope only.

SurrealDB Agent Memory resolves scope visibility from OR-of-AND clauses on each record. A reader can read a record when its grants cover every path in at least one of the record's clauses. Grants reach down a scope tree, never up, so no read reaches a record tagged at an ancestor scope.

[["org/acme/project/alpha"]]                    visible to readers granted project alpha
[["org/acme/project/alpha", "agent/planner"]]   visible only to readers granted project alpha AND agent/planner
[["org/acme/project/alpha"], ["agent/planner"]] visible to readers granted project alpha OR agent/planner

A supervisor granted org/acme/project/alpha/* and agent/* sees:

  • Everything tagged at the project scope (shared findings)

  • Everything tagged with the project scope AND a worker's agent scope (individual worker findings)

A worker granted org/acme/project/alpha/* and agent/worker-1 sees the shared project findings and its own findings, but not records tagged with another worker's agent scope. Neither of them sees a record tagged only org/acme, because that is an ancestor of the project scope. Grant patterns accept /* only as a trailing subtree marker.

When many agents write to a shared scope, unrelated facts from different tasks can accumulate. Use metadata to tag memory items with their provenance:

await memory.remember(
    "Customer segment 'enterprise' values compliance features most.",
    memory_category="knowledge",
    labels=["source=customer-interview-2024-q3", "agent=interview-agent"],
)

Labels are "key=value" strings stamped on the rows the write produces. They narrow reads (labels on /query) but never widen access - the scope predicate is applied first.

Use the entity type system to keep memory structured. Rather than storing flat facts, extract structured entities so that conflicts are detected and superseded correctly:

# Good: structured entity extraction will happen automatically from this
await memory.remember(
    "The enterprise customer segment prioritises SOC2 compliance over cost.",
)

# The extraction pipeline creates (entity types come from a fixed set,
# so a customer segment is stored as `other`):
# entity: other/enterprise
# attribute: top_priority = "SOC2 compliance"
# relation: enterprise → values → compliance_features

When two agents write conflicting facts to the same scope at roughly the same time, SurrealDB Agent Memory's reconciliation pipeline detects the conflict and creates a supersession chain. The later write wins for attribute values. You can inspect the conflict:

state = await memory.state()

# /state returns only the current view, so a row that carries `supersedes`
# is one that replaced an earlier value.
for attr in state.knowledge["attributes"]:
    if attr["supersedes"]:
        print(f"{attr['entity']}.{attr['key']} is now {attr['value']} (replaced an earlier value)")

# Walk the full chain for one key:
history = await memory.entities.history("person", "alice", "role")

The supervisor pattern works best with a dedicated reflection pass that runs after all workers complete:

# Reflection reads the memory the supervisor's key can see,
# which includes every worker's findings
synthesis = await client.reflect(
    query="What are the top-level conclusions across all worker findings? What is still uncertain?",
    persist=True,
)

print(synthesis.reflection)

With persist=True, the attributes the reflection writes are returned in synthesis.persisted_attributes and become part of the shared memory, so later reads return them.

import { AgentMemory } from "@surrealdb/memory";

const client = new AgentMemory({
    endpoint: process.env.AGENT_MEMORY_ENDPOINT!,
    context: "acme-prod",
    apiKey: process.env.AGENT_MEMORY_API_KEY!,
});

const sharedScope = ["org/acme/project/market-research-q3"];

// One AND clause: readable only with grants on the project and on worker-1
await client.remember(
    "Competitor X raised prices by 20% in Q2.",
    { scopes: [["org/acme/project/market-research-q3", "agent/worker-1"]] },
);

const results = await client.recall("competitor pricing", { k: 10, lens: sharedScope });
console.log(results.hits);

Was this page helpful?