> Full SurrealDB documentation index: https://surrealdb.com/docs/llms.txt

# Multi-agent shared memory

Supervisors, reflection, and shared scopes.

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.

## Patterns

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

| Pattern | When to use |
|---|---|
| Shared scope | All agents contribute to and read from a common project scope |
| Supervisor + workers | A supervisor agent coordinates several worker agents, each with its own user/agent scope |

## Pattern 1: shared project 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.

```python
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
```

### Read/write access control

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.

## Pattern 2: supervisor and workers

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.

```python
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.

## Scope hierarchy

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.

```text
[["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.

## Preventing memory pollution

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

```python
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:

```python
# 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
```

## Handling write conflicts

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:

```python
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")
```

## Supervisor reflection

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

```python
# 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.

## JavaScript example

```javascript
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);
```
