Two agents can each be excellent on their own and still fail together. The usual cause is not the model. It is memory. One agent learns that the customer prefers Telegram, another never sees it, and a third overwrites it with stale data.
If you are asking "how do I design shared memory for a multi-agent AI system?", this guide is for you. By the end you will know:
- What shared memory in a multi-agent system actually is, and what it is not
- Five architecture patterns, with a side-by-side comparison table
- A minimal working implementation with scoping and conflict detection
- The pitfalls that most often break shared memory in production
- A checklist for choosing a pattern
Table of Contents
- What is shared memory in a multi-agent system?
- Prerequisites
- The five core patterns
- Comparison table
- A minimal implementation
- Common pitfalls
- How to choose
- FAQ
- Conclusion
What is shared memory in a multi-agent system?
Shared memory is any persistent state that more than one agent can read, write, or both, outside of the messages passed directly between them.
It helps to separate three things people often blur together:
| Layer | What it holds | Lifetime |
|---|---|---|
| Context window | The current prompt and conversation | One model call |
| Message passing | Handoffs between agents | One task or workflow |
| Shared memory | Facts, decisions, and artifacts reused across agents | Minutes to months |
Note: Shared memory is not "a bigger context window." A larger window gives one agent more room. Shared memory gives many agents the same ground truth.
Prerequisites
Before you start, it helps to have:
- Basic familiarity with LLM agents and tool calling
- A rough idea of your agent topology (supervisor, pipeline, or peer-to-peer)
- Working knowledge of Python 3.10+ (the examples use it)
- Familiarity with a key-value store or relational database
The five core patterns
1. Shared scratchpad (blackboard)
All agents read from and write to one common workspace. This is the classic blackboard architecture, and it is the easiest pattern to start with.
Works well for: small teams of agents solving one task together.
Breaks when: the scratchpad grows and every agent pays to read all of it.
2. Key-value state store
Agents read and write named keys such as customer:42:preferred_channel. Redis, DynamoDB, or a Postgres table all fit.
Works well for: structured facts with clear ownership.
Breaks when: the knowledge is fuzzy and does not fit a key.
3. Vector (semantic) memory
Agents store text embeddings and retrieve by similarity. This is what most people picture when they say "agent memory."
Works well for: unstructured knowledge, past conversations, and documents.
Breaks when: you need exact answers. Similarity search returns related memories, not necessarily correct ones.
4. Event log (event sourcing)
Agents append immutable events (TaskCompleted, PreferenceUpdated), and current state is derived by replaying them.
Works well for: auditability, debugging, and replaying a failed run.
Breaks when: you need low-latency reads and have no materialized view.
5. Hierarchical (scoped) memory
Memory is split into scopes: private per agent, shared per team, and global per organization. Each agent sees only what its scope allows.
Works well for: larger systems, multi-tenant products, and permission boundaries.
Breaks when: the scope rules are unclear, so agents cannot find what they need.
Comparison table
| Pattern | Read latency | Write conflicts | Auditability | Scales to many agents | Best for |
|---|---|---|---|---|---|
| Shared scratchpad | Low (small) / High (large) | High | Low | Poor | Prototypes, single-task teams |
| Key-value store | Very low | Medium (manageable with versioning) | Medium | Good | Structured facts and flags |
| Vector memory | Medium | Low | Low | Good | Fuzzy recall, long-term knowledge |
| Event log | Medium (with views) | Low (append-only) | Very high | Good | Compliance, debugging, replay |
| Hierarchical scopes | Varies | Low (isolated scopes) | Medium–High | Very good | Multi-tenant and larger systems |
Tip: These patterns are not mutually exclusive. Most production systems combine two or three, for example a key-value store for facts, vector memory for recall, and an event log for audit.
Existing tools worth knowing
You do not have to build all of this yourself. Several frameworks and libraries already offer memory layers, including LangGraph (checkpointed state), CrewAI (shared crew memory), Letta (formerly MemGPT), and Mem0. Evaluate them against your constraints. Each makes different assumptions about scoping, storage backend, and how much control you keep.
A minimal implementation
The sketch below combines three ideas: scoped keys, optimistic concurrency (to catch conflicting writes), and a TTL (so stale facts expire).
import time from dataclasses import dataclass, field from typing import Any, Optional class VersionConflict(Exception): pass @dataclass class MemoryRecord: value: Any version: int = 1 author: str = "" expires_at: Optional[float] = None updated_at: float = field(default_factory=time.time) class SharedMemory: def __init__(self): self._store: dict[str, MemoryRecord] = {} @staticmethod def _key(scope: str, key: str) -> str: # scope examples: "org", "team:support", "agent:triage-1" return f"{scope}::{key}" def read(self, scope: str, key: str) -> Optional[MemoryRecord]: rec = self._store.get(self._key(scope, key)) if rec and rec.expires_at and rec.expires_at < time.time(): return None # expired: treat as missing return rec def write( self, scope: str, key: str, value: Any, author: str, expected_version: Optional[int] = None, ttl_seconds: Optional[float] = None, ) -> MemoryRecord: k = self._key(scope, key) current = self._store.get(k) # Optimistic concurrency: reject writes based on stale reads if current and expected_version is not None: if current.version != expected_version: raise VersionConflict( f"{k} is at v{current.version}, " f"writer expected v{expected_version}" ) rec = MemoryRecord( value=value, version=(current.version + 1) if current else 1, author=author, expires_at=(time.time() + ttl_seconds) if ttl_seconds else None, ) self._store[k] = rec return rec Here is how two agents would use it:
mem = SharedMemory() # Agent A records a preference mem.write("team:support", "customer:42:channel", "telegram", author="triage-agent") # Agent B reads it, then tries to update using the version it saw seen = mem.read("team:support", "customer:42:channel") try: mem.write( "team:support", "customer:42:channel", "whatsapp", author="billing-agent", expected_version=seen.version, ) except VersionConflict: # Re-read, reconcile, and retry instead of silently overwriting ... Note: This is an in-memory sketch to show the shape of the design. In production, put the same logic on a real store (for example, a Postgres row version column or Redis
WATCH/transactions) so the guarantees survive restarts and multiple processes.
Common pitfalls
Pitfall 1: Memory poisoning
One agent writes a wrong or hallucinated fact, and every other agent now treats it as truth. Because the fact is shared, the error spreads.
Mitigations:
- Store the author and source with every record
- Separate "observed" facts from "inferred" ones
- Require verification before promoting a fact from private to shared scope
Pitfall 2: Stale reads
An agent acts on memory that was true an hour ago and no longer is.
Mitigations: TTLs, version checks, and a last_verified_at field on anything that can change.
Pitfall 3: Write conflicts and lost updates
Two agents update the same record at nearly the same time and the last writer wins, silently.
Mitigations: optimistic concurrency (shown above), append-only logs, or a single writer per key.
Pitfall 4: Context bloat
Teams respond to "agents forget things" by dumping all memory into every prompt. Cost rises, latency rises, and accuracy often falls, because the model must find the relevant fact among the irrelevant ones.
Mitigations: retrieve selectively, summarize old memory, and keep a strict token budget for injected memory.
Pitfall 5: Leaking data across boundaries
In multi-tenant or multi-user systems, a missing scope check can expose one customer's data to another customer's agent.
Mitigations: enforce scope in the memory layer itself, not in the prompt. A prompt instruction is not an access control.
Pitfall 6: No way to debug
When an agent behaves strangely, you cannot tell which memory it read or who wrote it.
Mitigations: log every read and write with agent ID, timestamp, and version. An event log pays for itself here.
How to choose a pattern
Ask these questions in order:
- Is the data structured with clear keys? Start with a key-value store.
- Is the knowledge unstructured or fuzzy? Add vector memory, and keep exact facts out of it.
- Do you need audit trails or replay? Add an event log.
- Do you serve multiple tenants or teams? Use hierarchical scopes from day one. Retrofitting them is painful.
- Is it a prototype with two or three agents? A simple scratchpad is fine. Just plan the migration.
FAQ
What is shared memory in a multi-agent system?
It is persistent state that multiple agents can access outside direct message passing, such as facts, decisions, or artifacts reused across agents and sessions.
How do AI agents share memory?
Common approaches are a shared scratchpad, a key-value store, a vector database, an event log, or a scoped hierarchy. Many systems combine several.
Do multi-agent systems need a vector database?
Not always. If your knowledge is structured, a key-value store or relational table is simpler and more precise. Add vector memory when you need fuzzy, semantic recall.
How do you prevent agents from overwriting each other's memory?
Use optimistic concurrency (version checks), append-only event logs, or assign a single writer per key.
Conclusion
Shared memory is one of the most consequential design decisions in a multi-agent system, and one of the easiest to underestimate. The key takeaways:
- Memory is not a context window. Design it as a separate system with its own rules.
- No single pattern wins. Combine key-value, vector, and event-log storage based on the data you hold.
- Scope early. Private, team, and global scopes prevent leaks and reduce noise.
- Plan for failure. Poisoning, stale reads, and write conflicts are the normal case, not edge cases.
- Make it observable. If you cannot trace who wrote what, you cannot debug it.
These are the same concerns we think about at Botsailors when agents have to stay consistent across channels and conversations.
Your turn: Which memory pattern are you using in production, and which pitfall hit you first? Share it in the comments. I'd especially like to hear about failure modes I did not cover.
