NewDakera v0.11.107 — temporal precision re-ranking, session-cohesion scoring, and reliability fixesSee what's new →
Intermediate Architecture

Multi-Agent Shared Memory

⏱ ~25 min to implement 📦 Requires: Dakera v0.11+

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 →
Prerequisites
  • 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:

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.

Why Namespaces, Not Tags

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:

Memory Flow Diagram

Orchestrator assigns tasks | monitors status ns: coordinator tasks | status | signals Researcher Agent finds + verifies facts key: dk-researcher-key Writer Agent synthesizes + creates key: dk-writer-key ns: researcher private reasoning draft notes, hypotheses private ns: writer private drafts revision notes private ns: team-shared verified findings published outputs task completions all agents: read + write shared publish publish recall recall publish (write to shared) recall (read from shared) private write

Namespace Setup

# Create shared + private namespaces (admin key required)
curl -X POST http://localhost:3000/v1/namespaces \
  -H "Authorization: Bearer dk-admin-key" \
  -H "Content-Type: application/json" \
  -d '{"name": "team-shared"}'

curl -X POST http://localhost:3000/v1/namespaces \
  -H "Authorization: Bearer dk-admin-key" \
  -d '{"name": "researcher"}'

curl -X POST http://localhost:3000/v1/namespaces \
  -H "Authorization: Bearer dk-admin-key" \
  -d '{"name": "writer"}'

# List all namespaces (to verify)
curl http://localhost:3000/v1/namespaces \
  -H "Authorization: Bearer dk-admin-key"
from dakera import DakeraClient

admin = DakeraClient(base_url="http://localhost:3000", 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:3000', 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:3000", "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:3000", "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.

See Multi-Agent Example →

Step-by-Step Implementation

Implementation

# Researcher stores private working notes
curl -X POST http://localhost:3000/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:3000/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:3000/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:3000/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:3000/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:3000", api_key="dk-researcher-key")
writer = DakeraClient(base_url="http://localhost:3000", api_key="dk-writer-key")
coordinator = DakeraClient(base_url="http://localhost:3000", 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:3000", 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:3000', apiKey: 'dk-researcher-key' });
const writer = new DakeraClient({ baseUrl: 'http://localhost:3000', 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:3000", "dk-researcher-key");
let writer = Client::new("http://localhost:3000", "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:3000", "dk-researcher-key")
writer := dakera.NewClient("http://localhost:3000", "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

Before — single shared namespace, no isolation
[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.

After — namespaced, private+shared separation
-- 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

OperationPythonTypeScriptPurpose
Store privatestore_memory("researcher", content, ...)storeMemory("researcher", {content, ...})Write to agent's private namespace
Publish to sharedstore_memory("team-shared", content, importance=0.85)storeMemory("team-shared", {content, importance: 0.85})Publish verified output
Recall sharedrecall("team-shared", query, top_k=6)recall("team-shared", query, {top_k: 6})Query all agents' outputs
List namespaceslist_namespaces()listNamespaces()Admin: audit namespace state
Search sharedsearch_memories("team-shared", query, top_k=10)searchMemories("team-shared", query, {top_k: 10})Exact-match search in shared
Forget privateforget("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:

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.

Provenance Tracking Pattern

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:

Researcher Writer Dakera Memory team-shared namespace store_memory("team-shared", "HNSW 5ms P50") t=0ms store_memory("team-shared", "Article complete") t=2ms write mem_001 stored (researcher finding) mem_002 stored (writer output) ack: mem_001 ack: mem_002 ── QC Agent joins, calls recall() ── QC Agent recall("team-shared", query, top_k=5) Returns both mem_001 + mem_002 (read-after-write: consistent) Both writes visible to all agents immediately within the namespace — zero lag

Performance Considerations

<5ms
Namespace routing overhead per request
1200+
Concurrent writes/sec to shared namespace (single node)
Zero
Cross-namespace leakage — enforced at storage layer
Operationp50p99Notes
store_memory to shared namespace16ms38msIncludes vector embedding + HNSW index write
recall from shared (top_k=5)11ms26msHybrid BM25 + vector, namespace-scoped
recall from shared (top_k=20)15ms35msCoordinator-style wide recall
10 agents writing concurrently18ms54msSerialized per-key, parallel across agents
list_namespaces (admin)4ms11msMetadata 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.

Do Not Store Raw Reasoning in Shared 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:3000", 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:3000", 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
Try it live

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.