NewRelease 0.12.3 is out: Engine v0.12.3, Dashboard 0.5.2, Helm 0.12.3, the four SDKs, CLI 0.8.2 and MCP 0.12.3Release 0.12.3 is outSee what's new →
Engineering

Building Multi-Agent Memory Systems: Architecture Patterns

How to design memory architectures where multiple AI agents share context, maintain isolation, and collaborate through a unified memory layer.

Dakera AI TeamPublished Updated 11 min read
On this page
  1. The Multi-Agent Memory Problem
  2. Pattern 1: Namespace Isolation with Shared Layers
  3. Pattern 2: Session-Based Memory with Agent Scoping
  4. Pattern 3: Knowledge Graph as Shared Context
  5. Pattern 4: Event-Sourced Memory
  6. Isolation Guarantees
  7. Production Deployment Patterns
  8. Failure Modes and Recovery Strategies
  9. Scaling Multi-Agent Memory
  10. Real-World Example: Multi-Agent Code Review
  11. Anti-Patterns to Avoid
  12. Choosing the Right Pattern

The Multi-Agent Memory Problem

Modern AI applications rarely consist of a single agent. A customer support system might have a triage agent, a technical agent, and a billing agent. A coding assistant might have a planning agent, an implementation agent, and a review agent. Each agent needs its own working memory, but they also need to share relevant context.

Architecture diagram. A support, a billing and a research agent each write to a private namespace (agent-a, agent-b, agent-c) and to one shared hub namespace for cross-agent context; the billing agent searches both its own namespace and the shared one.Architecture diagram. A support, a billing and a research agent each write to a private namespace (agent-a, agent-b, agent-c) and to one shared hub namespace for cross-agent context; the billing agent searches both its own namespace and the shared one.
Private namespaces per agent, plus a shared hub for context every agent can use.Full size ↗Full size ↗

The naive approach — giving every agent access to every memory — creates noise. A billing agent doesn't need to see debugging sessions. A planning agent doesn't need to know about code formatting preferences. The challenge is designing a memory architecture that enables collaboration without creating an undifferentiated soup of context.

Pattern 1: Namespace Isolation with Shared Layers

The most common pattern uses separate namespaces for each agent's private memory, plus a shared namespace for cross-agent context:

from dakera import DakeraClient

client = DakeraClient(base_url="http://localhost:3000", api_key="your-api-key")

# Each agent has a private namespace
TRIAGE_NS = "agent-triage"
TECH_NS = "agent-technical"
BILLING_NS = "agent-billing"

# Plus a shared namespace for cross-agent context
SHARED_NS = "shared-customer-context"

# Triage agent stores its observations
client.store_memory(
    agent_id=TRIAGE_NS,
    content="Customer expressed frustration about repeated billing errors"
)

# Triage also writes to shared for other agents
client.store_memory(
    agent_id=SHARED_NS,
    content="Customer cust_42 has ongoing billing issue, handle with care"
)

When the billing agent picks up the conversation, it searches both its own namespace and the shared one:

# Billing agent retrieves context from both namespaces
private_context = client.recall(
    agent_id=BILLING_NS,
    query="customer cust_42 billing history",
    top_k=5
)

shared_context = client.recall(
    agent_id=SHARED_NS,
    query="customer cust_42",
    top_k=3
)
for m in private_context.memories + shared_context.memories:
    print(m.content)

When to Use This Pattern

  • Agents have distinct roles with different memory needs
  • You want explicit control over what gets shared
  • Agents may run on different schedules (async handoffs)

Pattern 2: Session-Based Memory with Agent Scoping

When multiple agents collaborate on a single task in sequence (like a pipeline), sessions provide a natural boundary. Each session represents one unit of work, and agents contribute memories within that session:

# Create a session for this customer interaction
session = client.start_session(
    agent_id="support-pipeline",
    metadata={"customer_id": "cust_42"}
)

# Triage agent adds to the session
client.store_memory(
    agent_id="support-pipeline",
    content="Initial diagnosis: billing discrepancy on invoice INV-5678",
    session_id=session["id"],
    metadata={"source_agent": "triage"}
)

# Technical agent picks up the session and adds its findings
client.store_memory(
    agent_id="support-pipeline",
    content="Root cause: proration calculation error during plan upgrade on March 3",
    session_id=session["id"],
    metadata={"source_agent": "technical"}
)

# Billing agent resolves using full session context
session_memories = client.session_memories(session["id"])
for m in session_memories:
    print(m["content"])

# Close the unit of work
client.end_session(session["id"], summary="Proration error on INV-5678 refunded")

When to Use This Pattern

  • Agents process work in a pipeline/sequence
  • Each unit of work has a clear beginning and end
  • You need full audit trail of which agent contributed what

Pattern 3: Knowledge Graph as Shared Context

For complex multi-agent systems where relationships between entities matter more than individual memories, the knowledge graph becomes the shared layer. Each agent contributes entities and relationships, building a collective understanding:

# Turn on entity extraction for the shared agent's namespace (once)
client.create_agent("company-intelligence")  # v0.12.2; otherwise the first store creates it
client.configure_namespace_ner(
    "_dakera_agent_company-intelligence",
    extract_entities=True,
    entity_types=["person", "organization"]
)

# Research agent stores discoveries as natural language memories
# Each one is tagged entity:<type>:<value> and linked into the graph
memory = client.store_memory(
    agent_id="company-intelligence",
    content="Alice Chen works at Acme Corp",
    importance=0.8
)

# Sales agent adds deal context
client.store_memory(
    agent_id="company-intelligence",
    content="Acme Corp is considering the Enterprise Plan Q2 deal",
    importance=0.8
)

# Any agent can traverse the graph
connections = client.memory_graph(memory["id"], depth=2)
for edge in connections.edges:
    print(edge.source_id, edge.edge_type.value, edge.target_id)
# Memories are the nodes; these two share the Acme Corp entity (shares_entity)

Dakera's knowledge graph links memories with five edge types that cover most agent collaboration scenarios: related_to (similar content), shares_entity (the same entity tag), precedes (time order), supersedes (a newer version of a fact) and linked_by. For domain-specific relationships, create a linked_by edge yourself with memory_link and give it a label.

When to Use This Pattern

  • Agents build understanding of interconnected entities
  • The relationships between entities are as important as the entities themselves
  • You need multi-hop reasoning across agent discoveries

Pattern 4: Event-Sourced Memory

In systems where agents need to react to each other's discoveries in real-time, an event-sourced pattern works well. Each agent publishes memories as events, and other agents subscribe to relevant namespaces:

from datetime import datetime, timedelta, timezone

# Monitoring agent detects an anomaly
client.store_memory(
    agent_id="system-events",
    content="CPU usage on prod-server-3 exceeded 95% for 5 minutes",
    tags=["severity:high"]
)

# Diagnosis agent polls for new high-severity events
window_start = datetime.now(timezone.utc) - timedelta(minutes=15)
recent_events = client.recall(
    agent_id="system-events",
    query="high severity anomaly",
    top_k=5,
    tags=["severity:high"],
    since=window_start.strftime("%Y-%m-%dT%H:%M:%SZ")  # Only the last 15 minutes
)

# Or subscribe instead of polling (SSE; v0.12 needs an admin key)
for event in client.stream_memory_events():
    if event.event_type == "stored" and event.agent_id == "system-events":
        handle(event.memory_id)

Isolation Guarantees

Regardless of which pattern you choose, multi-agent memory requires strong isolation guarantees. A bug in one agent shouldn't corrupt another's context. Dakera provides isolation at multiple levels:

LevelScopeIsolation
API keyNamespaces a key may reachKey pinned to namespaces (403 elsewhere)
NamespaceAgent or domainSeparate HNSW index + BM25 index
SessionSingle task/conversationFiltered within namespace
TagsCustom scopingQuery-time filtering

Production Deployment Patterns

Theory and production diverge quickly. Here are the patterns that work in practice for teams running multi-agent systems at scale.

The Hub-and-Spoke Topology

The most common production topology has a single Dakera instance serving all agents, with each agent type owning a dedicated namespace. Agents write to their private namespace and optionally to a shared "hub" namespace for cross-agent communication. This is simple to operate, easy to monitor, and scales to dozens of agents without coordination overhead:

from dakera import DakeraClient
import os

DAKERA_API_URL = os.environ["DAKERA_API_URL"]
DAKERA_API_KEY = os.environ["DAKERA_API_KEY"]

def get_client() -> DakeraClient:
    return DakeraClient(base_url=DAKERA_API_URL, api_key=DAKERA_API_KEY)

class ResearchAgent:
    PRIVATE_NS = "research-private"
    HUB_NS = "hub-intelligence"

    def __init__(self):
        self.client = get_client()

    def store_finding(self, content: str, customer_id: str, importance: float = 0.5):
        # Always write to private namespace
        self.client.store_memory(
            agent_id=self.PRIVATE_NS,
            content=content,
            importance=importance,
            tags=[f"customer:{customer_id}"]
        )
        # Promote high-importance findings to the shared hub
        if importance >= 0.7:
            self.client.store_memory(
                agent_id=self.HUB_NS,
                content=content,
                importance=importance,
                tags=[f"customer:{customer_id}", "source:research"]
            )

Importance-Weighted Promotion

Noisy shared namespaces are one of the most common causes of retrieval degradation in multi-agent systems. Use Dakera's importance scoring as a natural filter — only memories above a threshold are promoted to shared namespaces:

def promote_if_important(client: DakeraClient, content: str,
                          private_ns: str, shared_ns: str,
                          metadata: dict, importance: float,
                          threshold: float = 0.7):
    """Store privately always; promote to shared only if important enough."""
    result = client.store_memory(
        agent_id=private_ns,
        content=content,
        importance=importance,
        metadata=metadata
    )
    # Dakera keeps the importance you pass (0.5 when you pass none);
    # your agent decides how important a finding is
    if result["importance"] >= threshold:
        client.store_memory(
            agent_id=shared_ns,
            content=content,
            importance=result["importance"],
            metadata=metadata
        )
    return result

TypeScript SDK: Cross-Agent Coordination

For teams building with the TypeScript SDK in Node.js or Next.js agent frameworks:

import { DakeraClient } from '@dakera-ai/dakera';

const client = new DakeraClient({
  baseUrl: process.env.DAKERA_API_URL!,
  apiKey: process.env.DAKERA_API_KEY!,
});

// Triage agent writes a handoff note to shared namespace
// (sessionId comes from client.startSession('shared-handoffs'))
async function triageHandoff(sessionId: string, customerId: string,
                              summary: string): Promise<void> {
  await client.storeMemory('shared-handoffs', {
    content: summary,
    session_id: sessionId,
    importance: 0.85,
    tags: [`customer:${customerId}`],
    metadata: { customer_id: customerId, source_agent: 'triage' },
  });
}

// Downstream agent reads the handoff before starting work
async function loadHandoffContext(customerId: string) {
  const results = await client.recall('shared-handoffs', `customer context for ${customerId}`, {
    top_k: 5,
    tags: [`customer:${customerId}`],
  });
  return results.memories;
}

Failure Modes and Recovery Strategies

Multi-agent memory systems fail in specific, predictable ways. Understanding these failure modes in advance lets you build systems that degrade gracefully rather than catastrophically.

Failure Mode 1: Context Poisoning

A misbehaving agent writes incorrect or misleading information to a shared namespace. Downstream agents retrieve this poisoned context and produce wrong outputs, potentially cascading across the pipeline.

Prevention: Tag every memory with its source agent (for example source:research). Rate-limit writes to shared namespaces. Use importance scoring so low-confidence agent outputs don't pollute the shared layer.

Recovery: Dakera's batch forget API allows targeted deletion by tag and time filter. If an agent produced bad output between timestamps T1 and T2, purge only those memories:

from dakera import BatchForgetRequest, BatchMemoryFilter

# Remove poisoned memories from a specific agent during an incident window
deleted = client.batch_forget(BatchForgetRequest(
    agent_id="hub-intelligence",
    filter=BatchMemoryFilter(
        tags=["source:research"],
        created_after=T1,   # Unix seconds
        created_before=T2,
    ),
))
print(f"Removed {deleted.deleted_count} poisoned memories")

Failure Mode 2: Namespace Sprawl

Teams add namespaces for every new agent, feature, or experiment without a cleanup strategy. After several months, the instance has hundreds of namespaces, many abandoned. Retrieval quality degrades because engineers lose track of which namespace contains what.

Prevention: Establish a naming convention early. Use a hierarchical format: {environment}/{domain}/{agent-role} — for example prod/support/triage or dev/research/web-scraper. Audit namespaces quarterly using the metrics endpoint to identify ones with zero searches in the past 30 days.

Failure Mode 3: Memory Accumulation Without Decay

A multi-agent system running without decay configuration will accumulate memories indefinitely. Search latency increases as the HNSW index grows. More critically, old irrelevant context competes with fresh context during ranking, degrading retrieval quality.

Prevention: Configure decay strategies per namespace from the start. Ephemeral working memory should use exponential decay (working memories decay three times faster than the server-wide half-life). Long-term shared knowledge should use power-law decay or no decay at all (flat). See the memory decay documentation for strategy selection guidance.

Failure Mode 4: Session Boundary Leakage

Reusing the same session ID across multiple distinct tasks blends memories from separate contexts. Always start a fresh session (start_session returns a new ID) per distinct task unit — never reuse session IDs across customers or work units:

# Start a fresh session for each distinct task
session = client.start_session(
    agent_id="support-pipeline",
    metadata={"customer_id": "cust_9921"}
)

result = client.store_memory(
    agent_id="support-pipeline",
    content="Customer complaint about invoice INV-9921",
    session_id=session["id"]  # Fresh ID per customer interaction
)

Failure Mode 5: Memory Server Unavailability

Agents that call memory operations when Dakera is briefly unavailable will receive connection errors. Without proper handling, this can halt agent pipelines entirely. Implement a circuit breaker so agents degrade to operating without memory (using only context window) rather than failing completely:

from dakera.exceptions import ConnectionError, ServiceUnavailableError, TimeoutError

def safe_recall(client: DakeraClient, agent_id: str, query: str,
                top_k: int = 5) -> list:
    """Recall with graceful fallback when Dakera is unavailable."""
    try:
        return client.recall(
            agent_id=agent_id, query=query, top_k=top_k
        ).memories
    except (ConnectionError, ServiceUnavailableError, TimeoutError) as e:
        # Log but don't raise — agent continues without memory context
        print(f"WARNING: Memory unavailable ({e}). Continuing without context.")
        return []

Scaling Multi-Agent Memory

Concurrent Access

Dakera handles concurrent reads and writes from multiple agents without locking at the namespace level. Each namespace maintains its own write-ahead log, so agents writing to different namespaces never contend. Agents writing to the same namespace experience serialized writes but parallel reads — which matches the typical access pattern where many agents read shared context but fewer write to it.

Memory Pruning

In multi-agent systems, memory accumulates fast. If 5 agents each store 100 memories per hour, you have 12,000 new memories per day. Dakera's decay strategies automatically reduce the relevance of old memories, and you can configure per-namespace retention:

# A memory policy belongs to the agent's namespace, _dakera_agent_{agent_id}
SCRATCH_NS = "_dakera_agent_agent-triage-scratch"
LONG_TERM_NS = "_dakera_agent_shared-customer-context"

# Configure aggressive decay for ephemeral agent working memory
scratch = client.get_memory_policy(SCRATCH_NS)
scratch.working_decay = "exponential"
scratch.working_ttl_seconds = 86400  # delete working memories after 24 hours
client.set_memory_policy(SCRATCH_NS, scratch)

# Cap how many memories the namespace may hold (admin key)
client.admin_set_quota(SCRATCH_NS, {"max_vectors": 10000})

# Configure slow decay for long-term shared knowledge
shared = client.get_memory_policy(LONG_TERM_NS)
shared.semantic_decay = "power_law"
shared.episodic_ttl_seconds = 2592000  # 30 days
client.set_memory_policy(LONG_TERM_NS, shared)

Real-World Example: Multi-Agent Code Review

Here's a complete example of three agents collaborating on code review using Dakera's memory:

from dakera import DakeraClient

client = DakeraClient(base_url="http://localhost:3000", api_key="your-api-key")

# The orchestrator starts one session per review:
# session_id = client.start_session(agent_id="review-shared")["id"]

class SecurityAgent:
    NS = "review-security"

    def review(self, pr_diff: str, session_id: str):
        # Check memory for known vulnerability patterns
        past_vulns = client.recall(
            agent_id=self.NS,
            query=f"vulnerability patterns in {pr_diff[:200]}",
            top_k=5
        )

        # Store findings for other agents
        client.store_memory(
            agent_id="review-shared",
            content=f"Security review: no SQL injection risks, "
                    f"but found hardcoded timeout of 30s in retry logic",
            session_id=session_id
        )

class PerformanceAgent:
    NS = "review-performance"

    def review(self, pr_diff: str, session_id: str):
        # Reference shared findings from security agent
        security_notes = client.recall(
            agent_id="review-shared",
            query="security findings",
            top_k=3
        )

        # Add performance perspective
        client.store_memory(
            agent_id="review-shared",
            content=f"Performance review: the hardcoded 30s timeout "
                    f"(flagged by security) will cause connection pool exhaustion "
                    f"under load. Recommend configurable timeout with 5s default.",
            session_id=session_id
        )

class SummaryAgent:
    def summarize(self, session_id: str):
        # Pull all findings from the review session
        all_findings = client.session_memories(session_id)
        return [m["content"] for m in all_findings]

Anti-Patterns to Avoid

  • Global namespace free-for-all — Don't let every agent read/write a single namespace. It becomes noisy and impossible to reason about.
  • Over-sharing — Not everything needs to be in shared memory. Agent scratch work should stay private.
  • Missing provenance — Always tag memories with source_agent metadata. When something goes wrong, you need to trace which agent contributed bad context.
  • Ignoring decay — Without decay or pruning, shared namespaces grow unbounded and retrieval quality degrades as old irrelevant memories compete with recent ones.

Choosing the Right Pattern

Most production multi-agent systems combine patterns. A typical architecture uses:

  1. Namespace isolation for each agent's private working memory
  2. Sessions for pipeline-style collaboration within a single task
  3. Knowledge graph for long-lived entity relationships that span tasks
  4. Shared namespace with metadata filtering for cross-agent coordination

Start with the simplest pattern that meets your needs (usually Pattern 1), and add complexity only when you observe specific limitations. The memory architecture should reflect the communication topology of your agents — if two agents never need to share context, don't create infrastructure for it.

Next Steps

Once you have a multi-agent memory architecture designed, the operational details matter as much as the patterns. If you are self-hosting Dakera to serve your agent fleet, the complete self-hosting guide covers security hardening, TLS setup, Prometheus monitoring, and high-availability configuration. If your agents connect via MCP (Claude Desktop, Claude Code, Cursor, or Windsurf), the MCP setup guide walks through connecting multiple MCP clients to a single shared Dakera instance — enabling exactly the cross-tool memory sharing described in the patterns above.

For teams still evaluating whether a purpose-built memory engine like Dakera is the right choice compared to building on top of a vector database, the vector database vs. agent memory comparison covers the architectural trade-offs with concrete code examples showing the difference in implementation complexity.

Benchmark Context

Memory system performance matters significantly in multi-agent deployments where many agents compete for the same underlying store. Dakera's 88.2% Recall@20 on LoCoMo score on the 2026 AI agent memory benchmark measures retrieval quality over 10 conversations; it is not a concurrency test. If your architecture has 10 or more agents writing to shared namespaces simultaneously, review the benchmark methodology to understand how retrieval quality is measured, and load-test recall under your own write concurrency.

Related: Multi-Agent Shared Memory pattern · Namespace Isolation pattern · CrewAI integration · AutoGen integration · Knowledge Graphs for AI Agents

Ready to build shared memory for your agent fleet?

Namespace isolation, cross-agent retrieval, and concurrent write handling — all in a single ~101 MB binary. Self-hosted and free to run.

Stay sharp on agent memory
Benchmark releases, engineering deep-dives, and product updates. Once a week max, no fluff.
✓ Subscribed. Thanks!
Prefer a reader? RSS · Atom