Multi-Agent Shared Memory
Shared memory for multiple agents with explicit scope, consistency, conflict handling, and audit.
A research crew runs three agents in parallel: a web-search specialist, an internal-docs specialist, and a synthesis agent. Each maintains its own working-memory scratchpad and its own episodic store. The first two finish in 90 seconds; the synthesizer takes their outputs and produces a clean report in another 30. Total wall-clock: two minutes. Two days later the same three agents run an overnight job: the web specialist crawls 200 pages, the docs specialist reads 80 internal RFCs, the synthesizer is supposed to assemble a state-of-the-quarter brief. This time it takes eleven hours, the synthesizer’s report references “the customer churn data the docs agent surfaced”: except the docs agent never surfaced that, the synthesizer made it up, and somewhere in the middle of the run the web agent silently overwrote the docs agent’s “open questions” list with its own. The bug is not in any single agent. The bug is in what they share.
Pattern 1: The supervisor-mediated channel
Single-writer-multiple-reader. The supervisor owns the canonical state: a plan, an open-questions list, a synthesized brief: and updates it as worker responses come in. Workers read it as part of their input prompt but never write to it directly; their findings flow back through the handoff payload, and the supervisor decides what to commit.
This is the safest pattern. There are no concurrent writes, so there are no consistency questions. Every observation about distributed databases that follows simplifies away: the supervisor is the database, the workers are stateless clients, and the worst that can happen is a stale read in a worker (the supervisor updated the plan after the worker received its input) which the supervisor can detect on next handoff and re-dispatch.
The cost: the supervisor is a bottleneck and a context inflater. Every write goes through its loop; every write pays a model call to decide how to commit; the supervisor’s own conversation history grows as it sees every worker’s findings. The 15× token tax from the multi-agent orchestration article is partly an artifact of this: the supervisor pays for the union of all sub-tasks because it’s the only writer.
When this is right: small specialist counts (≤5), tasks where the supervisor’s per-step decision quality matters more than parallel write throughput, anywhere the audit story has to be clean. When this is wrong: highly parallel breadth-first tasks where a hundred subagents are emitting findings in parallel and routing every one through a single supervisor’s context window is a fork-bomb on the supervisor’s prompt rather than on its child processes.
Pattern 2: The shared block
The supervisor’s monopoly is broken. Two or more agents each have read and write access to the same typed slot. Concurrency control becomes mandatory.
This is the pattern Letta’s shared memory blocks productionize. A block is created once (client.blocks.create(...)) and attached to multiple agents via block_ids; each attached agent can read or write to it through three different operations with different safety properties. The published memory-operations table is worth quoting verbatim because the asymmetry it documents is exactly the design lesson:
memory_insert: append-only, multi-writer safe. Multiple agents can call it simultaneously without conflicts; the block grows monotonically. This is the operation you reach for when you don’t know whether you’ll have multi-agent contention.memory_replace: match-then-replace, mostly safe. Fails if the target string has changed since it was last read. This is optimistic concurrency control: the same primitive Postgres exposes as compare-and-swap and Git exposes as a non-fast-forward push reject.memory_rethink: last-writer-wins, unsafe under contention. The full block is rewritten with no coordination. Letta’s guidance: “Designate one agent (or sleeptime) as the ‘owner’ for heavy edits. Other agents append viamemory_insert.”
The pattern that falls out is a tier-based access policy. Most agents are allowed memory_insert only; one designated owner (often a sleep-time consolidation agent) gets memory_replace and memory_rethink and runs when no other agent is actively writing. The shape is a classic single-writer compaction over a multi-writer log: exactly the Kafka log-compaction model from the reflection article, specialized to a shared working-memory block.
CrewAI’s unified memory model makes a different design choice. Memory is shared by default across all agents in a crew, with hierarchical scopes (/project/alpha, /agent/researcher) carving private views out of the shared whole. Where Letta gives you tight control over which agents share which block, CrewAI gives you a global namespace with read-time scope filters. Both choices are defensible; the practical difference is that CrewAI’s default is “everyone sees everything” with opt-in privacy, and Letta’s default is “nothing is shared” with opt-in sharing: and the consistency problems show up at different stages of the design.
Pattern 3: The cross-thread store
The store-level shared memory. LangGraph’s Store API: backed by Postgres, Redis, MongoDB, or an in-memory implementation for development: exposes a namespaced key-value layer that any thread (any agent, any session) can put, get, or search against. The Store sits below the per-thread checkpointer; checkpointer state is private to one thread, Store state is shared across all of them.
The semantics are deliberately bare: put((namespace,), key, value) writes a row; get((namespace,), key) reads it; search((namespace,), query=...) does semantic retrieval if the namespace was configured with an embedding model. The concurrency model is whatever the backend implements: Postgres-backed stores get serializable transactions if you ask for them, Redis-backed stores get last-writer-wins by default, the in-memory dev store is racy. The store does not enforce consistency; it inherits the consistency of its backend.
The mental model that pays off is the Store is a shared blackboard with a key-value interface and no scheduler. Every agent can read and write; the namespace is the only scoping mechanism; nothing wakes an agent when a key it cares about changes. This is the cost of the API surface being small: coordination is up to the agents and the harness above them. Production deployments add the missing pieces: a notification mechanism on top of the Store (Redis pub/sub, Postgres LISTEN/NOTIFY), a versioning column for optimistic concurrency, an audit-log table that captures every write for compliance and debugging.
Pattern 4: The blackboard
The full distributed-shared-memory model. A typed store with multiple writers, multiple readers, and a scheduler that activates agents based on the store’s current state. The Hearsay-II revival in 2025 (the Lu & Sasaki paper and the follow-up information-discovery paper, reporting 13–57% relative gains over baselines on data-science discovery) is the modern reference, and the architecture is the right one to reach for when:
- The agent count is genuinely large (10+ specialists).
- Which agent runs next is data-dependent and not predictable in advance.
- Agents need to react to other agents’ writes, not just to the user or the supervisor.
The shape: a typed board with regions (hypotheses, evidence, open_questions, conclusions), a write log (every write is timestamped, attributed, and versioned), a scheduler that scans the board and dispatches the next agent whose preconditions match the current state, and explicit conflict-resolution rules on each region (hypotheses is append-only; conclusions is single-writer; open_questions is multi-writer with CRDT-style set-union semantics).
The blackboard is what you build when no single supervisor’s prompt is wide enough to route the whole task. The supervisor pattern’s bottleneck: the lead agent’s context window growing linearly with task complexity: is solved by making the board itself the coordination surface; agents look at the board to decide what to do, the supervisor (if there is one) just observes. The cost is that everything subtle about distributed shared memory now has to be designed, not avoided.
Consistency decisions
Any shared-memory design must specify visibility, conflict resolution, audit, and deletion semantics. These decisions turn “shared memory” from a vibes-based design into something an on-call engineer can debug at 3am.
When does a write become visible? Read-your-writes (an agent’s own writes are immediately visible to itself) is table stakes. Cross-agent visibility splits into strong consistency (every other agent sees the write on its next read) and eventual consistency (other agents may see a stale view for some bounded period). Letta’s docs are explicit: shared blocks are strong-consistency within a single Letta server because all blocks live in the same Postgres row and the database serializes the reads. LangGraph’s Store-with-Redis is eventual by default; Store-with-Postgres is strong if you ask for it. The right choice is workload-dependent: a fan-out research crew where agents work on independent regions of a problem can tolerate eventual visibility; a multi-agent code-edit crew where agents are touching the same file cannot.
How are concurrent conflicting writes resolved? Common conflict policies include last-writer-wins, optimistic concurrency, and CRDT-style merge. Last-writer-wins: the simplest, and the one most home-grown shared-memory systems start with: is unsafe whenever the lost write contained information that wasn’t in the winner. Optimistic concurrency (compare-and-swap, the model Letta’s
memory_replaceand Git’s non-fast-forward push use) catches conflicts at write time and forces the loser to retry against the updated view; the cost is the retry, the benefit is no silent data loss. CRDT-style merge (the CodeCRDT approach) defines a deterministic merge operator on the data type so that any two concurrent writes can be combined without conflict; the cost is the data type has to be designed for CRDT semantics (sets, counters, Y.js-style sequence types: not free-form prose), the benefit is the system never blocks on a conflict.What is the audit trail? Every shared-memory write needs who, when, what, and (sometimes) why. The “who” makes attribution possible; the “when” enables temporal queries; the “what” is the payload; the “why” is the optional reasoning the agent emitted with the write. Skipping this turns every shared-memory bug into a forensics nightmare: the report mentions a fact that wasn’t in any agent’s individual output, and there’s no way to find which write introduced it. The cheapest implementation is an append-only audit log on the side of the store; the most expensive is a full event-sourced reconstruction (the store is the event log, with materialized views for the current state). The middle ground: a store plus an audit table: is where most production systems land.
What is the deletion path? When a write is wrong, who can take it back, and what cascading invalidation does that require? The memory-conflict article covered the supersession-vs-deletion distinction for single-agent stores; in multi-agent, the answer is harder because other agents may already have read the wrong write and acted on it. The mature pattern is to surface the deletion as an event on the store rather than a silent state mutation: every reader can observe “the previous value at key K was wrong, here’s the corrected one” and choose whether to re-run its own logic against the new view. The Kafka-tombstone parallel is direct: deletion is just another write with special semantics, recorded in the same log as everything else.
Scope and namespace hierarchy
Whatever pattern you pick, the namespacing of the shared memory is what makes it tractable at scale. The naïve “one global store everyone reads and writes” is the worst case: every write contends with every other write, and the persona-leak production incident from the identity article generalizes from cross-persona to cross-tenant to cross-agent leakage.
Shared memory commonly uses these scopes:
- Per-task scope. Each multi-agent task gets its own namespace; nothing leaks between tasks. The default for blackboard architectures. The cost is no cross-task learning: useful patterns from one task don’t show up in the next unless explicitly promoted to a higher-scoped store.
- Per-tenant scope. All tasks for one organization share a namespace; different organizations are strictly isolated. The default for SaaS deployments. The cost is some agents that should learn across the tenant (the support agent learning from the engineering agent’s resolutions) need explicit cross-agent visibility within the tenant.
- Per-agent-role scope. All researchers across all tasks share a “researcher” namespace; all synthesizers share a “synthesizer” namespace. Useful for procedural memory: every researcher learns successful research strategies that future researchers can retrieve. The cost is that the role-level memory can carry biases from one task into another (a researcher who became confident about Acme Corp on Monday surfaces those beliefs as facts on a Wednesday task about a different company).
Most production systems run all three axes simultaneously, with explicit promotion rules between them. The LangMem-style namespace hierarchy (("tenant", tenant_id, "task", task_id) for hot scratchpads, ("tenant", tenant_id, "role", role) for role memories, ("system", "shared") for cross-tenant prior art that’s been reviewed) is the architectural pattern; the exact backing store is incidental.
Versioned shared scratchpad in Python
A minimal but production-shaped shared scratchpad. Two agents read and write to the same Postgres-backed block; writes are versioned, conflicts are caught at write time, and the audit log is the source of truth. Uses the Anthropic SDK and psycopg. Install: pip install anthropic psycopg[binary].
| |
The implementation has several relevant properties. insert is append-only and safe under any concurrency: the FOR UPDATE row-lock serializes the read-modify-write, but the operation itself never rejects a write. replace is CAS-style: it requires the caller to pass the expected version and rejects with ConflictError if anyone else has written in between. The caller’s recovery is to re-read, re-reason, and re-attempt; the conflict is visible rather than silent. every write writes the audit log in the same transaction: there is no shared-memory mutation that isn’t reflected in scratchpad_audit. When the synthesizer later produces a report that contains a fact nobody remembers writing, the audit log is the place to look.