Multi-Agent Shared Memory
Give your agent teams a shared memory space while keeping each agent's internal reasoning private. Each agent keeps its working notes under its own agent ID, verified outputs go to one shared agent, and API keys pinned to those agents' namespaces decide who reaches what: a publish/subscribe memory architecture where agents consume each other's knowledge without accessing raw internal state.
Connect Your Agents →- Running Dakera server (see Quickstart)
- A super_admin key to create API keys (
POST /admin/keys) - Per-agent API keys pinned to the agents' namespaces (a write key and, where needed, a read key per 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. Every agent ID has its own namespace, _dakera_agent_{agent_id}, and a key pinned to the researcher's namespaces gets a 403 on the writer's, even if you forget to add a tag. Tags are application-level conventions; namespaces are infrastructure-level guarantees.
Architecture
Dakera keeps each agent's memories in its own namespace, _dakera_agent_{agent_id}, and an API key can be pinned to a list of namespaces. That gives three layers:
- Private agents: each role stores internal reasoning, drafts and working notes under its own agent ID (
researcher,writer). Its write key is pinned to its own namespace and the shared one, so no other agent's key reaches its notes. - Shared agent: agents publish finalized, verified outputs under one shared agent ID,
team-shared. Every agent's write key includes_dakera_agent_team-shared; a quality-check agent gets a read key on it. - Coordinator agent (optional): the orchestrator writes task assignments, status updates and completion signals under agent ID
coordinator. Agents read it with a read key.
A key has one scope (read, write, admin or super_admin) across all of its namespaces, and write includes read. An agent that writes its own namespace and the shared one but only reads the coordinator therefore holds two keys: a write key and a read key.
Memory Flow Diagram
Agent and Key Setup
# One agent_id per role for private notes, one shared, one coordinator.
# v0.12.2: create them up front; otherwise each is created by its first stored memory.
for AGENT in researcher writer team-shared coordinator; do
curl -X POST http://localhost:3000/v1/agents \
-H "Authorization: Bearer $DAKERA_ROOT_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"agent_id\": \"$AGENT\"}"
done
# A key has one scope across all its namespaces, so the researcher gets two keys:
# write on its own agent and on team-shared...
curl -X POST http://localhost:3000/admin/keys \
-H "Authorization: Bearer $DAKERA_ROOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "researcher-write", "scope": "write", "expires_in_days": 90,
"namespaces": ["_dakera_agent_researcher", "_dakera_agent_team-shared"]}'
# ...and read on the coordinator's task list
curl -X POST http://localhost:3000/admin/keys \
-H "Authorization: Bearer $DAKERA_ROOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "researcher-read", "scope": "read", "expires_in_days": 90,
"namespaces": ["_dakera_agent_coordinator"]}'
# Same pair for the writer. The QC agent gets one read key on _dakera_agent_team-shared;
# the orchestrator a write key on _dakera_agent_coordinator and _dakera_agent_team-shared.
# Check what a key reaches (v0.12.2)
curl http://localhost:3000/v1/auth/whoami \
-H "Authorization: Bearer $RESEARCHER_WRITE_KEY"import os
from dakera import DakeraClient
URL = "http://localhost:3000"
# Keys are created once with the admin routes (see the REST tab).
# Each agent then works through clients built from its own keys.
researcher = DakeraClient(base_url=URL, api_key=os.environ["RESEARCHER_WRITE_KEY"])
researcher_tasks = DakeraClient(base_url=URL, api_key=os.environ["RESEARCHER_READ_KEY"])
writer = DakeraClient(base_url=URL, api_key=os.environ["WRITER_WRITE_KEY"])
writer_tasks = DakeraClient(base_url=URL, api_key=os.environ["WRITER_READ_KEY"])
qc = DakeraClient(base_url=URL, api_key=os.environ["QC_READ_KEY"])
orchestrator = DakeraClient(base_url=URL, api_key=os.environ["ORCHESTRATOR_WRITE_KEY"])import { DakeraClient } from '@dakera-ai/dakera';
const baseUrl = 'http://localhost:3000';
// Keys are created once with the admin routes (see the REST tab).
// Each agent then works through clients built from its own keys.
const researcher = new DakeraClient({ baseUrl, apiKey: process.env.RESEARCHER_WRITE_KEY });
const researcherTasks = new DakeraClient({ baseUrl, apiKey: process.env.RESEARCHER_READ_KEY });
const writer = new DakeraClient({ baseUrl, apiKey: process.env.WRITER_WRITE_KEY });
const qc = new DakeraClient({ baseUrl, apiKey: process.env.QC_READ_KEY });
const orchestrator = new DakeraClient({ baseUrl, apiKey: process.env.ORCHESTRATOR_WRITE_KEY });use dakera_client::DakeraClient;
// Keys are created once with the admin routes (see the REST tab).
// Each agent then works through clients built from its own keys.
let client_for = |var: &str| {
DakeraClient::builder("http://localhost:3000")
.api_key(std::env::var(var).unwrap_or_default())
.build()
};
let researcher = client_for("RESEARCHER_WRITE_KEY")?;
let researcher_tasks = client_for("RESEARCHER_READ_KEY")?;
let writer = client_for("WRITER_WRITE_KEY")?;
let qc = client_for("QC_READ_KEY")?;// Keys are created once with the admin routes (see the REST tab).
// Each agent then works through clients built from its own keys.
clientFor := func(envVar string) *dakera.Client {
return dakera.NewClientWithOptions(dakera.ClientOptions{
BaseURL: "http://localhost:3000",
APIKey: os.Getenv(envVar),
})
}
researcher := clientFor("RESEARCHER_WRITE_KEY")
researcherTasks := clientFor("RESEARCHER_READ_KEY")
writer := clientFor("WRITER_WRITE_KEY")
qc := clientFor("QC_READ_KEY")Building a CrewAI crew?
The CrewAI integration gives each agent of a crew its own Dakera memory, with session and namespace helpers.
Step-by-Step Implementation
-
Create the agentsOne-time setup: pick a private agent ID per role and a single shared agent ID, and optionally a coordinator agent for task assignment signals. Use a naming convention like
agent-{role}for private andteam-{project}for shared. From v0.12.2,POST /v1/agentscreates them up front (so keys can be pinned before the first memory); otherwise each one is created by its first stored memory. -
Issue keys pinned to agent namespacesEach agent gets a write key pinned to its private namespace and the shared one (
_dakera_agent_researcher,_dakera_agent_team-shared) and a read key pinned to_dakera_agent_coordinator; a key has one scope, so mixed access needs two keys. The server enforces the pins, so isolation does not depend on application-layer checks. -
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 (researcher-write key)
curl -X POST http://localhost:3000/v1/memory/store \
-H "Authorization: Bearer $RESEARCHER_WRITE_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 a verified finding to team-shared
curl -X POST http://localhost:3000/v1/memory/store \
-H "Authorization: Bearer $RESEARCHER_WRITE_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"],
"metadata": {"source": "researcher", "type": "finding", "verified": true}
}'
# Writer recalls from team-shared: only verified findings live there
curl -X POST http://localhost:3000/v1/memory/recall \
-H "Authorization: Bearer $WRITER_WRITE_KEY" \
-H "Content-Type: application/json" \
-d '{"agent_id": "team-shared", "query": "HNSW latency performance", "top_k": 5}'
# The writer's key does not reach the researcher's notes: 403
curl -X POST http://localhost:3000/v1/memory/recall \
-H "Authorization: Bearer $WRITER_WRITE_KEY" \
-H "Content-Type: application/json" \
-d '{"agent_id": "researcher", "query": "latency notes", "top_k": 5}'
# Writer publishes the completed article to team-shared
curl -X POST http://localhost:3000/v1/memory/store \
-H "Authorization: Bearer $WRITER_WRITE_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"],
"metadata": {"source": "writer"}
}'SHARED = "team-shared"
# --- ORCHESTRATOR: publish a task assignment ---
orchestrator.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. Private working notes (never visible to the 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"],
)
# 2. Publish verified findings to shared
researcher.store_memory(
agent_id=SHARED,
content="VERIFIED: HNSW achieves 5ms P50 at 10M vectors (ef=200, M=32). Scales sub-linearly: 100M vectors = ~12ms P50.",
importance=0.85,
memory_type="semantic",
tags=["finding", "verified", "hnsw", "latency"],
metadata={"source": "researcher", "papers_cited": 5, "confidence": "high"},
)
# Update task status
orchestrator.store_memory(
agent_id="coordinator",
content="Research task complete. 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, query="HNSW performance latency memory characteristics", top_k=6)
print(f"Writer retrieved {len(findings.memories)} verified findings:")
for f in findings.memories:
print(f" [{f.importance:.2f}] {f.content[:80]}")
# 4. Private draft notes
writer.store_memory(
agent_id="writer",
content="Opening with concrete numbers (5ms, 100M vectors) is stronger than abstract claims. Draft A.",
importance=0.25,
tags=["draft-note", "structure"],
)
# 5. Publish the completed article to shared
writer.store_memory(
agent_id=SHARED,
content="COMPLETED: 'HNSW at Scale: Making Billion-Vector Search Practical', 1,400 words, data-backed, reviewed.",
importance=0.9,
memory_type="semantic",
tags=["output", "article", "status:complete"],
metadata={"source": "writer", "word_count": 1400},
)
# --- QUALITY CHECK AGENT (read key on team-shared only) ---
outputs = qc.recall(agent_id=SHARED, query="completed articles and verified findings", top_k=10)const SHARED = 'team-shared';
// Researcher: private working notes
await researcher.storeMemory('researcher', {
content: 'Paper #2 claims 8ms but uses a different hardware config. May not be representative.',
importance: 0.3,
tags: ['internal-note', 'unverified'],
});
// Researcher: publish a verified finding to team-shared
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,
memory_type: 'semantic',
tags: ['finding', 'verified', 'hnsw', 'latency'],
metadata: { source: 'researcher' },
});
// Writer: recall from team-shared (only verified, high-importance findings)
const { memories: 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 the completed article to team-shared
await writer.storeMemory(SHARED, {
content: 'COMPLETED: HNSW at Scale article, 1,400 words, data-backed, QC ready.',
importance: 0.9,
memory_type: 'semantic',
tags: ['output', 'article', 'status:complete'],
metadata: { source: 'writer' },
});
// QC agent: read key on team-shared only
const { memories: outputs } = await qc.recall(SHARED, 'completed outputs and verified findings', { top_k: 10 });
console.log(`team-shared returned ${outputs.length} items`);use dakera_client::{RecallRequest, StoreMemoryRequest};
// Researcher: private notes
researcher.store_memory(
StoreMemoryRequest::new("researcher", "Paper #2 figures may be skewed by hardware; cross-checking now")
.with_importance(0.3)
.with_tags(vec!["internal-note".into()]),
).await?;
// Researcher: publish a verified finding
researcher.store_memory(
StoreMemoryRequest::new("team-shared", "VERIFIED: HNSW 10M vectors = 5ms P50 (ef=200). Sub-linear at scale.")
.with_importance(0.85)
.with_tags(vec!["finding".into(), "verified".into(), "hnsw".into()]),
).await?;
// Writer: recall only from team-shared
let findings = writer.recall(
RecallRequest::new("team-shared", "HNSW performance latency at scale").with_top_k(6),
).await?;
println!("Writer retrieved {} findings", findings.memories.len());
// Writer: publish the completed output
writer.store_memory(
StoreMemoryRequest::new("team-shared", "COMPLETED: HNSW at Scale article, 1,400 words, verified data.")
.with_importance(0.9)
.with_tags(vec!["output".into(), "article".into(), "status:complete".into()]),
).await?;ctx := context.Background()
const sharedNS = "team-shared"
low, high, done := float32(0.3), float32(0.85), float32(0.9)
// Researcher: private notes (the writer's key cannot read these)
researcher.StoreMemory(ctx, "researcher", dakera.StoreMemoryRequest{
Content: "Paper #2 hardware config skews results; cross-checking with 4 other sources",
Importance: &low,
Tags: []string{"internal-note", "unverified"},
})
// Researcher: publish a 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: &high,
Tags: []string{"finding", "verified", "hnsw", "latency"},
})
// Writer: recall from team-shared only
findings, _ := writer.Recall(ctx, sharedNS, dakera.RecallRequest{
Query: "HNSW performance latency at scale",
TopK: 6,
})
fmt.Printf("Writer retrieved %d findings\n", len(findings.Memories))
// Writer: publish the completed article
writer.StoreMemory(ctx, sharedNS, dakera.StoreMemoryRequest{
Content: "COMPLETED: HNSW at Scale article, 1,400 words, data-backed",
Importance: &done,
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(agent_id="researcher", content=..., importance=0.3) | storeMemory('researcher', {content, importance: 0.3}) | Write to the agent's private namespace |
| Publish to shared | store_memory(agent_id="team-shared", content=..., importance=0.85) | storeMemory('team-shared', {content, importance: 0.85}) | Publish verified output |
| Recall shared | recall(agent_id="team-shared", query=..., top_k=6) | recall('team-shared', query, {top_k: 6}) | Query all agents' outputs |
| Check a key | GET /v1/auth/whoami (v0.12.2) | The scope and namespaces a key reaches | |
| Search shared | search_memories(agent_id="team-shared", query=..., top_k=10) | POST /v1/memory/search | Search with tag and type filters |
| Forget private | forget(agent_id="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 stores both: each store creates its own memory with its own ID and embedding, and nothing is merged at write time (AutoPilot's deduplication may later merge true duplicates, the same text with the same entity tags). The sequence diagram below shows the order of the calls:
Performance Considerations
Pinned keys add no calls of their own: an agent passes its agent_id as usual and the server checks the key against _dakera_agent_{agent_id}. The performance consideration is the size of the shared agent: every agent publishes into one namespace, so its index grows fastest, and recalls that cannot start within 20 s are answered 503 with Retry-After. Keep it lean by setting ttl_seconds on task-status memories (short-lived operational data) and removing low-value historical outputs with batch_forget().
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: put the date in published memories and narrow the recall to the current run with its since / until window, or tag each run (for example run:2026-05-14) and recall with that tag.
5. API key compromise leaks shared namespace
A researcher API key is leaked. The attacker can now read all shared namespace memories. Fix: revoke it at once (DELETE /admin/keys/{key_id}) or rotate it (POST /admin/keys/{key_id}/rotate), and give keys an expiry. A write key always includes read, so to keep agents from reading each other's raw output, let each agent publish to its own outbox agent (for example team-outbox-researcher) and have the orchestrator promote reviewed memories into team-shared.
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
import os
from dakera import DakeraClient
URL = "http://localhost:3000"
orchestrator = DakeraClient(base_url=URL, api_key=os.environ["ORCHESTRATOR_WRITE_KEY"])
# Post task assignment
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 with its read key
researcher_tasks = DakeraClient(base_url=URL, api_key=os.environ["RESEARCHER_READ_KEY"])
tasks = researcher_tasks.recall(
agent_id="coordinator",
query="research tasks assigned to researcher",
top_k=5,
min_importance=0.7,
)
# Completion is signalled through the orchestrator (agents only read the coordinator)
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 a key with read scope pinned to _dakera_agent_team-shared. 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
#
# Namespace = _dakera_agent_{name}. A key has one scope, so an agent whose
# column mixes RW and R holds two keys: a write key and a read key.
# Keys are created and rotated via the admin API
Run this pattern against a real Dakera.
The Playground executes live API calls in your browser. No account, no install. Then run it on your own server in five minutes.