Multi-Agent Shared Memory
Give your agent teams a shared memory space while keeping each agent's internal reasoning private. This pattern uses Dakera namespaces and scoped API keys to implement a publish/subscribe memory architecture — agents publish verified findings to a shared namespace and consume each other's knowledge without accessing raw internal state.
Connect Your Agents →- Running Dakera server (see Quickstart)
- Admin API key for namespace creation
- Per-agent scoped API keys (one per agent role)
Problem
In multi-agent systems — CrewAI crews, AutoGen teams, LangGraph graphs, or custom orchestrators — agents need to share knowledge without leaking internal state. Three failure modes are common:
- State pollution — when all agents share a single memory store, a researcher's speculative notes contaminate the writer's recall with unverified claims
- Duplication without synthesis — agents redundantly re-derive the same facts because they cannot query each other's outputs
- Invisible hand-offs — agent B starts from scratch because there is no canonical location for agent A's completed outputs
The shared memory pattern resolves this with a two-namespace architecture: each agent has a private namespace for internal reasoning, and a shared namespace is the single source of truth for completed, verified outputs.
You might consider using a single namespace with metadata tags to separate private vs. shared memories. Don't. Namespaces enforce isolation at the storage and API-key level — a researcher API key physically cannot query the writer's private namespace even if you forget to add a tag. Tags are application-level conventions; namespaces are infrastructure-level guarantees.
Architecture
Use Dakera's namespace system to create a layered memory architecture with three layers:
- Private namespaces — each agent writes internal reasoning, drafts, and working notes here. Keys are scoped to read/write their own private namespace only.
- Shared namespace — agents publish finalized, verified outputs here. All agent keys have read/write access to the shared namespace.
- Coordinator namespace (optional) — the orchestrator writes task assignments, status updates, and completion signals. Agent keys have read-only access to the coordinator namespace.
Memory Flow Diagram
Namespace Setup
# Create shared + private namespaces (admin key required)
curl -X POST http://localhost:3300/v1/namespaces \
-H "Authorization: Bearer dk-admin-key" \
-H "Content-Type: application/json" \
-d '{"name": "team-shared"}'
curl -X POST http://localhost:3300/v1/namespaces \
-H "Authorization: Bearer dk-admin-key" \
-d '{"name": "researcher"}'
curl -X POST http://localhost:3300/v1/namespaces \
-H "Authorization: Bearer dk-admin-key" \
-d '{"name": "writer"}'
# List all namespaces (to verify)
curl http://localhost:3300/v1/namespaces \
-H "Authorization: Bearer dk-admin-key"
from dakera import DakeraClient
admin = DakeraClient(base_url="http://localhost:3300", api_key="dk-admin-key")
# Create all namespaces upfront
for ns in ["team-shared", "researcher", "writer", "coordinator"]:
admin.create_namespace(ns)
# Verify
namespaces = admin.list_namespaces()
print([n["name"] for n in namespaces])
import { DakeraClient } from '@dakera-ai/dakera';
const admin = new DakeraClient({ baseUrl: 'http://localhost:3300', apiKey: 'dk-admin-key' });
// Create all namespaces
for (const ns of ['team-shared', 'researcher', 'writer', 'coordinator']) {
await admin.createNamespace(ns);
}
const namespaces = await admin.listNamespaces();
console.log(namespaces.map(n => n.name));
use dakera_rs::{Client, CreateNamespaceRequest};
let admin = Client::new("http://localhost:3300", "dk-admin-key");
for ns in &["team-shared", "researcher", "writer", "coordinator"] {
admin.create_namespace(ns, CreateNamespaceRequest::default()).await?;
}
let namespaces = admin.list_namespaces().await?;
println!("{:?}", namespaces.iter().map(|n| &n.name).collect::<Vec<_>>());
admin := dakera.NewClient("http://localhost:3300", "dk-admin-key")
ctx := context.Background()
for _, ns := range []string{"team-shared", "researcher", "writer", "coordinator"} {
admin.CreateNamespace(ctx, ns, nil)
}
namespaces, _ := admin.ListNamespaces(ctx)
fmt.Println(namespaces)
Want a working multi-agent memory example?
The Dakera quickstart includes a 3-agent CrewAI example with shared memory pre-configured.
Step-by-Step Implementation
-
Create namespaces with admin keyOne-time setup: create a private namespace per agent role and a single shared namespace. Optionally create a coordinator namespace for task assignment signals. Use a naming convention like
agent-{role}for private andteam-{project}for shared. -
Issue scoped API keys per agentEach agent gets an API key scoped to: (1) read/write to their private namespace, (2) read/write to the shared namespace, (3) read-only to the coordinator namespace. This is enforced at the infrastructure level — you cannot rely on application-layer checks alone.
-
Store internal reasoning in private namespaceDuring execution, each agent writes intermediate steps, hypotheses, and working notes to its own private namespace with low importance (0.2–0.4). This data never appears in another agent's recall, keeping the shared knowledge pool clean.
-
Publish verified outputs to shared namespaceWhen an agent completes a sub-task or verifies a finding, it publishes to the shared namespace with high importance (0.7–0.9) and structured metadata:
{"source": "researcher", "type": "finding", "verified": true}. The metadata enables provenance tracking and filtering. -
Downstream agents recall from shared namespaceThe writer, reviewer, or any downstream agent calls
recall()against the shared namespace. Because only verified, high-importance outputs land there, recall is precise and noise-free — no intermediate reasoning pollutes the results. -
Implement a coordinator status channelThe orchestrator stores task assignments and completion signals in the coordinator namespace. Agents poll this namespace periodically to know what to work on next and when to publish results. Use tags like
["task","status:in-progress"]for structured task querying.
Implementation
# Researcher stores private working notes
curl -X POST http://localhost:3300/v1/memory/store \
-H "Authorization: Bearer dk-researcher-key" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "researcher",
"content": "Paper #2 claims 8ms latency at 10M vectors. Need to cross-check with Paper #3 data.",
"importance": 0.3,
"tags": ["internal-note", "needs-verification"]
}'
# Researcher publishes verified finding to shared namespace
curl -X POST http://localhost:3300/v1/memory/store \
-H "Authorization: Bearer dk-researcher-key" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "team-shared",
"content": "VERIFIED: HNSW indexes at 10M vectors achieve ~5ms P50 latency with ef=200. Three sources confirmed.",
"importance": 0.85,
"tags": ["finding", "verified", "hnsw", "performance"]
}'
# Writer recalls from shared namespace — only gets verified findings
curl "http://localhost:3300/v1/memory/recall?agent_id=team-shared&query=HNSW+latency+performance&top_k=5" \
-H "Authorization: Bearer dk-writer-key"
# Writer stores private draft notes
curl -X POST http://localhost:3300/v1/memory/store \
-H "Authorization: Bearer dk-writer-key" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "writer",
"content": "Lead sentence draft: HNSW makes billion-scale vector search practical. Too bold? Check with researcher.",
"importance": 0.25,
"tags": ["draft", "needs-review"]
}'
# Writer publishes completed article to shared
curl -X POST http://localhost:3300/v1/memory/store \
-H "Authorization: Bearer dk-writer-key" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "team-shared",
"content": "COMPLETED: Blog post on HNSW at scale — 1,200 words, peer-reviewed, ready for editor.",
"importance": 0.9,
"tags": ["output", "article", "status:complete"]
}'
from dakera import DakeraClient
# Each agent uses its own scoped client
researcher = DakeraClient(base_url="http://localhost:3300", api_key="dk-researcher-key")
writer = DakeraClient(base_url="http://localhost:3300", api_key="dk-writer-key")
coordinator = DakeraClient(base_url="http://localhost:3300", api_key="dk-coordinator-key")
SHARED_NS = "team-shared"
# --- ORCHESTRATOR: Publish task assignment ---
coordinator.store_memory(
agent_id="coordinator",
content="Task: Research HNSW scaling characteristics at 1M, 10M, 100M vectors. Deadline: 2 hours.",
importance=0.8,
tags=["task", "assigned:researcher", "status:in-progress"]
)
# --- RESEARCHER AGENT ---
# 1. Store private working notes (never visible to writer)
researcher.store_memory(
agent_id="researcher",
content="Paper #2 claims 8ms at 10M vectors but uses different hardware. Potentially misleading.",
importance=0.3,
tags=["internal-note", "unverified"]
)
researcher.store_memory(
agent_id="researcher",
content="Cross-referencing 5 papers: all agree on sub-linear scaling. Latency variance due to ef parameter.",
importance=0.4,
tags=["internal-note", "analysis-in-progress"]
)
# 2. Publish verified findings to shared
researcher.store_memory(
agent_id=SHARED_NS,
content="VERIFIED: HNSW achieves 5ms P50 at 10M vectors (ef=200, M=32). Scales sub-linearly: 100M vectors = ~12ms P50.",
importance=0.85,
tags=["finding", "verified", "hnsw", "latency"],
metadata={"source": "researcher", "papers_cited": 5, "confidence": "high"}
)
researcher.store_memory(
agent_id=SHARED_NS,
content="VERIFIED: Memory footprint ~1.2GB per 1M 768-dim vectors with HNSW. 4x higher than flat index but 20x faster at recall.",
importance=0.8,
tags=["finding", "verified", "hnsw", "memory"],
metadata={"source": "researcher"}
)
# Update task status
coordinator.store_memory(
agent_id="coordinator",
content="Research task complete. 2 verified findings published to team-shared.",
importance=0.7,
tags=["status-update", "task:research", "status:complete"]
)
# --- WRITER AGENT ---
# 3. Recall verified findings from shared
findings = writer.recall(
agent_id=SHARED_NS,
query="HNSW performance latency memory characteristics",
top_k=6
)
print(f"Writer retrieved {len(findings)} verified findings:")
for f in findings:
print(f" [{f['importance']:.2f}] {f['content'][:80]}")
# 4. Store private draft notes
writer.store_memory(
agent_id="writer",
content="Opening with concrete numbers (5ms, 100M vectors) — stronger than abstract claims. Draft A.",
importance=0.25,
tags=["draft-note", "structure"]
)
# 5. Publish completed article to shared
writer.store_memory(
agent_id=SHARED_NS,
content="COMPLETED: 'HNSW at Scale: Making Billion-Vector Search Practical' — 1,400 words, data-backed, reviewed.",
importance=0.9,
tags=["output", "article", "status:complete"],
metadata={"source": "writer", "word_count": 1400, "artifact_url": "/articles/hnsw-at-scale"}
)
# --- QUALITY CHECK AGENT (reads shared, never sees private) ---
qc = DakeraClient(base_url="http://localhost:3300", api_key="dk-qc-key")
outputs = qc.recall(
agent_id=SHARED_NS,
query="completed articles and verified findings",
top_k=10
)
import { DakeraClient } from '@dakera-ai/dakera';
const researcher = new DakeraClient({ baseUrl: 'http://localhost:3300', apiKey: 'dk-researcher-key' });
const writer = new DakeraClient({ baseUrl: 'http://localhost:3300', apiKey: 'dk-writer-key' });
const SHARED = 'team-shared';
// Researcher: private working notes
await researcher.storeMemory('researcher', {
content: 'Paper #2 claims 8ms but uses different hardware config. May not be representative.',
importance: 0.3,
tags: ['internal-note', 'unverified']
});
// Researcher: publish verified finding to shared namespace
await researcher.storeMemory(SHARED, {
content: 'VERIFIED: HNSW at 10M vectors achieves 5ms P50 (ef=200, M=32). Sub-linear scaling confirmed across 5 papers.',
importance: 0.85,
tags: ['finding', 'verified', 'hnsw', 'latency'],
memoryType: 'semantic'
});
await researcher.storeMemory(SHARED, {
content: 'VERIFIED: HNSW memory: ~1.2GB per 1M 768-dim vectors. 4x flat index cost, 20x faster recall.',
importance: 0.8,
tags: ['finding', 'verified', 'hnsw', 'memory'],
memoryType: 'semantic'
});
// Writer: recall from shared (only verified, high-importance findings)
const findings = await writer.recall(SHARED, 'HNSW performance latency memory', { top_k: 6 });
console.log(`Retrieved ${findings.length} verified findings`);
// Writer: private draft notes
await writer.storeMemory('writer', {
content: 'Lead with the 5ms number — concrete, compelling. Draft A structure.',
importance: 0.25,
tags: ['draft-note']
});
// Writer: publish completed artifact to shared
await writer.storeMemory(SHARED, {
content: 'COMPLETED: HNSW at Scale article — 1,400 words, data-backed, QC ready.',
importance: 0.9,
tags: ['output', 'article', 'status:complete'],
memoryType: 'semantic'
});
// Batch recall from shared to check all outputs
const allOutputs = await researcher.recall(SHARED, 'completed outputs and verified findings', { top_k: 10 });
console.log(`Team shared namespace has ${allOutputs.length} items`);
use dakera_rs::{Client, StoreMemoryRequest, RecallRequest};
let researcher = Client::new("http://localhost:3300", "dk-researcher-key");
let writer = Client::new("http://localhost:3300", "dk-writer-key");
// Researcher: private notes
researcher.store_memory("researcher", StoreMemoryRequest {
content: "Paper #2 figures may be skewed by hardware — cross-checking now".into(),
importance: Some(0.3),
tags: vec!["internal-note".into()],
..Default::default()
}).await?;
// Researcher: publish verified finding
researcher.store_memory("team-shared", StoreMemoryRequest {
content: "VERIFIED: HNSW 10M vectors = 5ms P50 (ef=200). Sub-linear at scale.".into(),
importance: Some(0.85),
memory_type: Some("semantic".into()),
tags: vec!["finding".into(), "verified".into(), "hnsw".into()],
..Default::default()
}).await?;
// Writer: recall only from shared namespace
let findings = writer.recall("team-shared", RecallRequest {
query: "HNSW performance latency at scale".into(),
top_k: Some(6),
..Default::default()
}).await?;
println!("Writer retrieved {} findings", findings.len());
// Writer: publish completed output
writer.store_memory("team-shared", StoreMemoryRequest {
content: "COMPLETED: HNSW at Scale article, 1400 words, verified data.".into(),
importance: Some(0.9),
tags: vec!["output".into(), "article".into(), "status:complete".into()],
..Default::default()
}).await?;
researcher := dakera.NewClient("http://localhost:3300", "dk-researcher-key")
writer := dakera.NewClient("http://localhost:3300", "dk-writer-key")
ctx := context.Background()
const sharedNS = "team-shared"
// Researcher: private notes (writer can't see these)
researcher.StoreMemory(ctx, "researcher", dakera.StoreMemoryRequest{
Content: "Paper #2 hardware config skews results — cross-checking with 4 other sources",
Importance: 0.3,
Tags: []string{"internal-note", "unverified"},
})
// Researcher: publish verified finding
researcher.StoreMemory(ctx, sharedNS, dakera.StoreMemoryRequest{
Content: "VERIFIED: HNSW 10M vectors achieves 5ms P50 latency (ef=200, M=32). Confirmed across 5 papers.",
Importance: 0.85,
MemoryType: "semantic",
Tags: []string{"finding", "verified", "hnsw", "latency"},
})
// Writer: recall from shared only
findings, _ := writer.Recall(ctx, sharedNS, dakera.RecallRequest{
Query: "HNSW performance latency at scale",
TopK: 6,
})
fmt.Printf("Writer retrieved %d findings
", len(findings))
// Writer: publish completed article
writer.StoreMemory(ctx, sharedNS, dakera.StoreMemoryRequest{
Content: "COMPLETED: HNSW at Scale article, 1400 words, data-backed",
Importance: 0.9,
Tags: []string{"output", "article", "status:complete"},
})
Before / After Memory State
[0.4] Paper #2 may have wrong hardware data
[0.4] Need to verify 8ms claim before citing
[0.4] Draft opener A: "HNSW scales..."
[0.4] Draft opener B: "Vector search today..."
[0.4] HNSW achieves ~5ms at 10M vectors
[0.4] Article word count: currently 800
[0.4] HNSW memory: 1.2GB per 1M vectors
[0.4] Draft B sounds more authoritative
Writer recalls researcher's unverified drafts. QC agent sees revision notes. Signal buried in noise. Provenance unknown.
-- ns: researcher (private) --
[0.3] Paper #2 hardware may skew results
[0.4] Cross-checking 5 sources now
-- ns: writer (private) --
[0.25] Draft opener A — too aggressive?
[0.25] Word count: 1,400 - QC ready
-- ns: team-shared --
[0.85][verified] HNSW 10M = 5ms P50 (ef=200)
[0.80][verified] HNSW memory: 1.2GB/1M vecs
[0.90][complete] Article ready for editor
Writer sees only verified findings. QC sees only completed outputs. Private reasoning is invisible to other agents. Provenance tracked via metadata.
SDK Reference
| Operation | Python | TypeScript | Purpose |
|---|---|---|---|
| Store private | store_memory("researcher", content, ...) | storeMemory("researcher", {content, ...}) | Write to agent's private namespace |
| Publish to shared | store_memory("team-shared", content, importance=0.85) | storeMemory("team-shared", {content, importance: 0.85}) | Publish verified output |
| Recall shared | recall("team-shared", query, top_k=6) | recall("team-shared", query, {top_k: 6}) | Query all agents' outputs |
| List namespaces | list_namespaces() | listNamespaces() | Admin: audit namespace state |
| Search shared | search_memories("team-shared", query, top_k=10) | searchMemories("team-shared", query, {top_k: 10}) | Exact-match search in shared |
| Forget private | forget("researcher", memory_id) | forget("researcher", memoryId) | Clean up private working notes |
Real-World Example: Automated Research Pipeline
A fintech company runs a 4-agent pipeline to produce daily market analysis reports. The system runs every morning at 6 AM:
- Data Agent: fetches pricing data, stores raw notes in private namespace, publishes
clean_data_snapshotto shared with importance 0.9 - Analysis Agent: recalls the data snapshot from shared, performs analysis, stores intermediate calculations in private namespace, publishes
analysis_summarywith importance 0.85 - Risk Agent: reads analysis summary from shared, runs risk models, publishes
risk_flagswith importance 0.95 (high importance — always surfaces first) - Report Agent: reads data snapshot + analysis + risk flags from shared, writes final report, publishes
daily_reportwith importance 0.9
After 30 days, the shared namespace contains 120 daily reports, 120 analysis summaries, and 120 risk flag sets — a rich historical corpus that enables trend analysis across reports. No agent ever sees another's scratch work.
Always include a source field in metadata when publishing to shared: metadata={"source": "researcher", "version": "1.0", "confidence": "high"}. This lets downstream agents filter by source (e.g., "only recall verified findings from the researcher, not drafts from the writer") and enables audit trails when outputs are questioned.
Write Concurrency: How Multiple Agents Stay Consistent
When two agents publish to the shared namespace within milliseconds of each other, Dakera processes both writes independently — each becomes a separate memory with its own ID and embedding. There is no write lock or merge. The sequence diagram below shows the exact timing and consistency guarantee you can rely on:
Performance Considerations
| Operation | p50 | p99 | Notes |
|---|---|---|---|
| store_memory to shared namespace | 16ms | 38ms | Includes vector embedding + HNSW index write |
| recall from shared (top_k=5) | 11ms | 26ms | Hybrid BM25 + vector, namespace-scoped |
| recall from shared (top_k=20) | 15ms | 35ms | Coordinator-style wide recall |
| 10 agents writing concurrently | 18ms | 54ms | Serialized per-key, parallel across agents |
| list_namespaces (admin) | 4ms | 11ms | Metadata scan only, no memory fetch |
Namespace routing adds less than 5ms per request — negligible. The key performance consideration is the shared namespace index size: as multiple agents publish over time, recall latency grows with the index. Mitigate this by setting TTLs on task-status memories (short-lived operational data) and using importance-based filtering to age out low-value historical outputs.
Edge Cases
1. Race condition on shared namespace writes
Two agents publish conflicting findings at the same time (e.g., both researcher and a second analyst publish different latency numbers). Recall may return both. Fix: include a confidence metadata field and have a dedicated arbitrator agent run search_memories() periodically to detect conflicts and call forget() on the lower-confidence duplicate.
2. Agent writes to wrong namespace
A writer accidentally stores a draft in "team-shared" instead of "writer". Other agents now recall draft text as a verified output. Fix: enforce a "status" tag contract — only memories with tags=["status:verified"] are treated as canonical by downstream agents. Add a CI check or schema validation on publish.
3. Shared namespace grows unbounded
After 6 months of daily pipeline runs, the shared namespace has 10,000+ memories. Recall slows as index grows. Fix: implement a weekly compaction agent that summarizes old findings (importance < 0.5, older than 30 days) into a single compressed memory and calls batch_forget() on the originals.
4. Agents recall their own stale published outputs
An analysis agent recalls from shared namespace and gets its own previous day's analysis — which it then erroneously uses as ground truth for today's analysis. Fix: include a date metadata field in all published memories and filter recalls by recency: recall(..., min_importance=0.8) combined with date-based metadata filtering.
5. API key compromise leaks shared namespace
A researcher API key is leaked. The attacker can now read all shared namespace memories. Fix: rotate the key immediately via admin API. Consider splitting the shared namespace into a write-only agent namespace and a separate aggregated read namespace — agents can publish but not read each other's raw output until the orchestrator promotes memories to the read namespace.
The most common mistake: treating the shared namespace as a general scratchpad. Every unverified note, every speculative claim, every partial draft that lands in the shared namespace becomes noise in every other agent's recall. Enforce a strict "only completed, verified outputs" policy for the shared namespace. Keep everything else in private namespaces with short TTLs.
Advanced Configuration — Coordinator Patterns & Access Policies
Task Queue via Coordinator Namespace
from dakera import DakeraClient
orchestrator = DakeraClient(base_url="http://localhost:3300", api_key="dk-orchestrator-key")
# Post task assignment
task = orchestrator.store_memory(
agent_id="coordinator",
content="Research task T-001: Analyze HNSW scaling at 1M, 10M, 100M vectors.",
importance=0.8,
tags=["task", "assigned:researcher", "status:pending", "task_id:T-001"]
)
# Agent polls for its tasks
agent = DakeraClient(base_url="http://localhost:3300", api_key="dk-researcher-key")
tasks = agent.recall(
agent_id="coordinator",
query="research tasks assigned to researcher",
top_k=5,
min_importance=0.7
)
# Agent signals completion
orchestrator.store_memory(
agent_id="coordinator",
content="Task T-001 complete. 2 findings published to team-shared.",
importance=0.7,
tags=["status-update", "task_id:T-001", "status:complete"]
)
Read-Only Consumer Pattern
Issue your quality-check or monitoring agents read-only access to the shared namespace. They can recall and search but cannot write. This prevents downstream agents from accidentally polluting the shared knowledge pool.
Namespace Access Matrix
# Recommended access matrix for a 3-agent team:
# Namespace | researcher | writer | qc-agent | orchestrator
# ----------------------------------------------------------------
# researcher | RW | - | - | R
# writer | - | RW | - | R
# team-shared | RW | RW | R | RW
# coordinator | R | R | R | RW
#
# Enforce via scoped API keys issued per agent
# Keys are created and rotated via the admin API
Build Your Multi-Agent Memory Layer
Dakera's namespace API makes shared agent memory a 5-minute setup. No custom infrastructure — just namespaces and scoped keys.
Connect Your Agents →