Skip to main content
Workflows Library MCP Directory Realtime AI News Sponsor Tier Subscribe

Build a Multi-Agent Team-Memory Workflow with LangGraph

Tencent Cloud's TencentDB Agent Memory crossed 20,000 GitHub stars in 90 days (August 13, 2026) and shipped Team Memory, extending long-term agent memory from a single user to a whole team - conversations, documents, code, and institutional knowledge shared across agents. This dispatch builds team-mem, a LangGraph workflow with a memory-writer that extracts durable facts into a vector + knowledge-graph + KV pool, a memory-reader that fuses vector similarity, entity linkage, and recency behind per-team namespaces and ACLs, and a conflict-resolution node that supersedes contradictions.

Deepak Bagada

Deepak Bagada

CEO, SaaSNext

Aug 19, 2026 Published
|
Aug 19, 2026 Updated
|
11 Minutes Reading Time
Core Takeaways for Founders & Builders
  • Team memory is a three-tier store: vector for fuzzy recall, knowledge graph for stable entity facts, KV for checkpoints and locks - none alone is enough.
  • Memory records are write-once and mutate by supersession, which is what makes conflict history and the audit trail reconstructable.
  • ACL checks fail closed and are enforced at retrieval time, so a denied read costs a retry but never a cross-tenant leak.
  • Dedup before persist: a re-stated fact at >=0.93 similarity becomes provenance on the existing record, not a new record.
  • Audit what each agent read - read provenance is how you debug hallucinations and satisfy compliance in one move.

By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.

Build a Multi-Agent Team-Memory Workflow with LangGraph

On August 13, 2026, Tencent Cloud's TencentDB Agent Memory crossed 20,000 GitHub stars in 90 days — and quietly shipped Team Memory, a feature that lets agents share conversations, documents, code, and institutional knowledge across a whole team, not just within one user's session. That single feature is a signal about where the agent platform wars are moving. Context is no longer something each agent drags along; it is becoming a core product layer that multiple agents read from and write to. The star count tells you the demand: memory for one user was useful, memory for a team is the thing enterprises actually pay for, because a team's knowledge is durable capital while any single session is disposable.

The engineering reality underneath is harder than the marketing. Team memory is a distributed read/write system with identity, isolation, and consistency problems on top of the usual retrieval problems. You need per-team namespaces so Tenant A's agents can never see Tenant B's contracts. You need a write path that extracts durable facts from noisy agent chatter without polluting the pool. You need conflict resolution when one agent's "decision" contradicts another's. And you need an audit of what each agent read, because in regulated teams — finance, legal, healthcare — "who saw what" is not optional. This dispatch builds team-mem, a LangGraph workflow that gives a team of agents a shared memory pool: a memory-writer node that checkpoints durable facts, a memory-reader node that pulls relevant context with a hybrid vector + knowledge-graph + recency signal, a namespace/ACL layer, and a conflict-resolution node.

Memory architecture: vector store plus knowledge graph plus KV

Before the graph, the storage design. A single vector store is not enough for team memory, and the reason is precision. A vector store answers "what is similar to this text" — it is weak at "what is the current state of project X" or "who owns the UPI sandbox". The three-tier model we use:

  • Vector store (embeddings + cosine): fuzzy recall of past decisions, code snippets, meeting notes.
  • Knowledge graph (entities + edges): stable facts — "Sprint 42 uses v2 API", "Siddharth owns payment-mock", "feature flag fast-pay is ON in staging". These are the facts you never want hallucinated.
  • KV store (short-lived, high-throughput): counters, locks, "last resolved state" for conflict checks, and the working-state checkpoints that the graph uses to resume.

The memory-reader fuses all three: vector hits get entity linkage, entity lookups get a recency boost, and both are filtered through ACL before anything reaches the model. This mirrors what Tencent's own design communicates — memory as a structured product, not a retrieval bolt-on.

The graph at a glance

flowchart TD
    A[Agent turn completes] --> B[memory_writer
checkpoint + extract + dedup]
    B --> C{conflict detected?}
    C -->|no| D[pool write
vector + graph + KV]
    C -->|yes| E[conflict_resolution]
    E --> D
    D --> F[memory_audit
record reads/writes per agent]
    G[Next agent about to run] --> H[memory_reader
hybrid retrieve + ACL scope]
    H --> I[prepended context]
    I --> J[agent execution]
    J --> A

The interesting loop is the bottom one: memory_reader → agent → memory_writer → memory_reader. Team memory is a feedback system — today's decisions become tomorrow's context, which is exactly why conflicts and pollution compound if you do not gate them.

Configuration: .env

# Storage
VECTOR_URL=redis://vector:6379   # RediSearch-backed vector index
GRAPH_URL=postgresql://kg:kg@localhost:5432/kg
KV_URL=redis://kv:6379
EMBEDDING_MODEL=text-embedding-3-small
EMBEDDING_DIM=1536

# Pool policy
POOL_NAMESPACE=acme/engineering
DEDUP_SIM_THRESHOLD=0.93
CONFLICT_WINDOW_MINUTES=60
MAX_CONTEXT_TOKENS=4000
RECENCY_BOOST_DAYS=7

# ACL
DEFAULT_TEAM_ROLE=member
AUDIT_SINK=postgresql://ledger:ledger@localhost:5432/memory_audit

# Agent runtime
LANGGRAPH_THREAD_ID=team-session-42
LOG_LEVEL=INFO

CONFLICT_WINDOW_MINUTES=60 is the window in which two memory-writes about the same entity are considered a live conflict worth resolving; older contradictions are resolved lazily on read with the write with the later timestamp winning. The trade-off is deliberate — resolving every historical contradiction synchronously would stall every agent turn.

Schemas: schemas.py

from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from typing import Any

from pydantic import BaseModel, Field

class MemoryKind(str, Enum):
    DECISION = "decision"
    FACT = "fact"
    ARTIFACT = "artifact"      # doc / code pointer
    PREFERENCE = "preference"

class ACLRole(str, Enum):
    READ = "read"
    WRITE = "write"
    ADMIN = "admin"

class Team(BaseModel):
    team_id: str
    namespace: str            # acme/engineering
    parent_namespace: str | None = None
    default_role: ACLRole = ACLRole.READ

class ACL(BaseModel):
    principal: str            # team_id or agent_id
    namespace: str
    role: ACLRole = ACLRole.READ
    allow_subnamespaces: bool = False

class MemoryRecord(BaseModel):
    record_id: str
    namespace: str
    kind: MemoryKind
    content: str
    entities: list[str] = Field(default_factory=list)
    embedding: list[float] | None = None
    source_agent: str
    thread_id: str
    created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    last_confirmed_at: datetime | None = None
    superseded_by: str | None = None
    conflict_group: str | None = None

class MemoryWriteOutcome(BaseModel):
    record_id: str
    deduped: bool = False
    conflict: bool = False
    conflict_group: str | None = None
    persisted: bool = False

class MemoryReadContext(BaseModel):
    namespace: str
    records: list[MemoryRecord] = Field(default_factory=list)
    provenance: list[dict[str, Any]] = Field(default_factory=list)  # per-record score source
    total_tokens: int = 0

class MemoryAuditEntry(BaseModel):
    entry_id: str
    agent_id: str
    thread_id: str
    action: str                # read | write | resolve
    record_ids: list[str] = Field(default_factory=list)
    namespace: str
    timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

The MemoryRecord is deliberately write-once. Mutations happen by superseding: when a fact changes, the writer creates a new record and sets superseded_by on the old one. That is what makes both the audit trail and the conflict history meaningful — you can reconstruct what each agent believed at any point in time, which is the same property regulators ask for.

External I/O with retry: tools.py

import asyncio
import hashlib
import random
from datetime import datetime, timezone

import httpx

from schemas import MemoryAuditEntry, MemoryRecord

class PoolUnavailable(RuntimeError):
    pass

async def with_retry(coro_factory, *, attempts=5, base=0.2, max_backoff=4.0):
    """Exponential backoff + jitter for pool writes/reads."""
    for attempt in range(1, attempts + 1):
        try:
            return await coro_factory()
        except (httpx.TimeoutException, httpx.HTTPStatusError) as exc:
            if attempt == attempts:
                raise PoolUnavailable(str(exc)) from exc
            delay = min(max_backoff, base * (2 ** (attempt - 1))) * (1 + random.uniform(0, 0.3))
            await asyncio.sleep(delay)

def embed(text: str) -> list[float]:
    """In-process embedding for sketch; swap for your provider SDK in prod."""
    return [float((hashlib.sha256(f"{text}:{i}".encode()).hexdigest())[:4], 16) % 1e3
            for i in range(1536)]  # placeholder deterministic vector

async def write_record(rec: MemoryRecord, client: httpx.AsyncClient) -> dict:
    async def call():
        r = await client.post(f"{client.base_url}/memory/records",
                              json=rec.model_dump(exclude={"embedding"}),
                              params={"embedding": ",".join(map(str, rec.embedding))})
        r.raise_for_status()
        return r.json()
    return await with_retry(call)

async def write_behind(rec: MemoryRecord, client: httpx.AsyncClient) -> dict:
    """Return fast, persist in background; outbox pattern for non-blocking turns."""
    return await with_retry(lambda: write_record(rec, client), attempts=3)

async def hybrid_search(query_emb: list[float], entities: list[str], namespace: str,
                        recency_boost_days: int, client: httpx.AsyncClient) -> list[dict]:
    """Vector + graph + recency fused by the pool's search endpoint."""
    async def call():
        r = await client.post(f"{client.base_url}/memory/search",
                              json={"query_emb": query_emb, "entities": entities,
                                    "namespace": namespace,
                                    "recency_boost_days": recency_boost_days})
        r.raise_for_status()
        return r.json()["hits"]
    return await with_retry(call)

async def check_acl(principal: str, namespace: str, role: str,
                    client: httpx.AsyncClient) -> bool:
    async def call():
        r = await client.post(f"{client.base_url}/acl/check",
                              json={"principal": principal, "namespace": namespace,
                                    "role": role})
        r.raise_for_status()
        return r.json()["allowed"]
    return await with_retry(call, attempts=3)

async def resolve_conflict(group_id: str, resolution_note: str, client: httpx.AsyncClient) -> str:
    """Resolve a conflict group; returns the surviving record_id."""
    async def call():
        r = await client.post(f"{client.base_url}/memory/conflicts/{group_id}/resolve",
                              json={"resolution_note": resolution_note})
        r.raise_for_status()
        return r.json()["survivor_record_id"]
    return await with_retry(call)

async def append_audit(entry: MemoryAuditEntry, client: httpx.AsyncClient) -> None:
    async def call():
        r = await client.post(f"{client.base_url}/memory/audit", json=entry.model_dump())
        r.raise_for_status()
    await with_retry(call, attempts=3)

Two details worth stealing. First, write_behind is the default write path so agent turns never block on the pool; the durable write happens in the background and the audit entry is written synchronously regardless. Second, every hybrid_search carries the namespace down to the storage layer — ACL is enforced at the point of retrieval, never post-hoc in the graph, because post-hoc filtering still wastes tokens on data the model must never see.

The graph: graph.py

from typing import TypedDict

from langgraph.graph import END, StateGraph

from schemas import MemoryReadContext, MemoryRecord, Team
from tools import (append_audit, check_acl, hybrid_search, resolve_conflict,
                   write_behind, write_record)

class MemoryState(TypedDict):
    namespace: str
    agent_id: str
    thread_id: str
    working_state: dict
    context: MemoryReadContext | None
    new_records: list[MemoryRecord]
    conflicts: list[str]
    audit: list[MemoryAuditEntry]

def acl_scope(state: MemoryState) -> bool:
    """Pre-node guard: can this agent write into this namespace at all?"""
    return True  # policy cached at session start; real impl calls check_acl()

async def memory_reader(state: MemoryState) -> MemoryReadContext:
    """Hybrid retrieve, filtered by ACL, token-budgeted."""
    namespace = state["namespace"]
    if not await check_acl(state["agent_id"], namespace, "read", client):
        return {"context": MemoryReadContext(namespace=namespace)}  # empty on deny

    query = summarize_working_state(state["working_state"])
    hits = await hybrid_search(embed(query), extract_entities(query), namespace,
                               recency_boost_days=7, client=client)
    records = [MemoryRecord(**h["record"]) for h in hits[:int(os.getenv("MAX_CONTEXT_TOKENS"))]]
    ctx = MemoryReadContext(namespace=namespace, records=records,
                            provenance=[{"record_id": h["record"]["record_id"],
                                         "vector": h["vector_score"],
                                         "graph": h["graph_score"],
                                         "recency": h["recency_score"]} for h in hits])
    await append_audit(MemoryAuditEntry(entry_id=uuid4().hex, agent_id=state["agent_id"],
                                        thread_id=state["thread_id"], action="read",
                                        record_ids=[r.record_id for r in records],
                                        namespace=namespace), client=client)
    return {"context": ctx}

async def memory_writer(state: MemoryState) -> MemoryWriteOutcome:
    """Checkpoint working state, extract durable records, dedup, flag conflicts."""
    extracted = extract_durable_records(state["working_state"], state["agent_id"],
                                        state["thread_id"], state["namespace"])
    outcomes, conflict_groups = [], []
    for rec in extracted:
        dup = await dedup_check(rec, threshold=DEDUP_SIM_THRESHOLD, client=client)
        if dup["is_dup"]:
            outcomes.append({"record_id": rec.record_id, "deduped": True,
                             "conflict": False, "persisted": False})
            continue
        conflict = await conflict_check(rec, window_minutes=CONFLICT_WINDOW_MINUTES,
                                        client=client)
        if conflict["is_conflict"]:
            rec.conflict_group = conflict["group_id"]
            conflict_groups.append(conflict["group_id"])
        await write_behind(rec, client=client)   # rec embeds first via embed()
        outcomes.append({"record_id": rec.record_id, "deduped": False,
                         "conflict": bool(rec.conflict_group),
                         "conflict_group": rec.conflict_group, "persisted": True})
        await append_audit(MemoryAuditEntry(entry_id=uuid4().hex,
                                            agent_id=state["agent_id"],
                                            thread_id=state["thread_id"], action="write",
                                            record_ids=[rec.record_id],
                                            namespace=state["namespace"]), client=client)
    return {"new_records": extracted, "conflicts": conflict_groups}

def route_on_conflict(state: MemoryState) -> str:
    return "conflict_resolution" if state["conflicts"] else "pool_sync"

async def conflict_resolution(state: MemoryState) -> None:
    """Newest-timestamp wins by default; agents re-affirm via note if human in loop."""
    for gid in state["conflicts"]:
        survivors = []
        for rec in state["new_records"]:
            if rec.conflict_group == gid:
                # resolution policy: latest created_at survives, others superseded
                survivor = max([rec], key=lambda r: r.created_at)
                survivors.append(survivor.record_id)
                await resolve_conflict(gid, f"superseded by {survivor.record_id}",
                                       client=client)
    return {"conflicts": []}

async def pool_sync(state: MemoryState) -> None:
    """KV checkpoint + graph entity reconciliation after conflict pass."""
    await write_record(working_state_checkpoint(state), client=client)
    return {}

def audit_reads(state: MemoryState):
    """Compact per-turn read audit into the team view."""
    return {"audit": state["context"].provenance if state["context"] else []}

builder = StateGraph(MemoryState)
builder.add_node("reader", memory_reader)
builder.add_node("writer", memory_writer)
builder.add_node("conflict", conflict_resolution)
builder.add_node("sync", pool_sync)
builder.add_node("audit", audit_reads)

builder.set_entry_point("reader")
builder.add_edge("reader", "writer")
builder.add_conditional_edges("writer", route_on_conflict,
                              {"conflict_resolution": "conflict", "pool_sync": "sync"})
builder.add_edge("conflict", "sync")
builder.add_edge("sync", "audit")
builder.add_edge("audit", END)

graph = builder.compile()

The conditional edge writer → conflict | sync is what keeps the pool honest. Most agent frameworks write memory and hope for the best; this graph treats a contradiction between a freshly extracted fact and a 30-minute-old one as a first-class event that must be resolved before the pool syncs. Note the security detail too: the reader node runs before every agent turn, so even a completely stateless agent gets the right team context — the graph, not the agent, owns memory.

Entrypoint: main.py

import asyncio
import os

from dotenv import load_dotenv
from graph import graph
from schemas import Team

load_dotenv()
client = httpx.AsyncClient(base_url=os.getenv("VECTOR_URL"))

TEAM = Team(team_id="team-payments", namespace="acme/payments", default_role="read")

async def run_agent_turn(agent_id: str, working_state: dict) -> None:
    initial = {"namespace": TEAM.namespace, "agent_id": agent_id,
               "thread_id": os.getenv("LANGGRAPH_THREAD_ID"),
               "working_state": working_state, "context": None,
               "new_records": [], "conflicts": [], "audit": []}
    async for event in graph.astream(initial):
        if "reader" in event:
            print(f"[{agent_id}] read {len(event['reader'].records)} records from {TEAM.namespace}")
        if "audit" in event:
            print(f"[{agent_id}] audit: {len(event['audit'])} provenance entries")

async def main():
    # Three agents, one shared team pool
    await run_agent_turn("engineering-agent",
                         {"task": "investigate upi-mock latency", "findings": [
                            "root cause: txn-id collision in mock", "owner: priya"]})
    await run_agent_turn("finance-agent",
                         {"task": "reconcile mock spend", "decision": "flag >INR 10k", "rule_id": "R-77"})
    await run_agent_turn("engineering-agent",
                         {"task": "fix upi-mock latency", "decision": "change mock to UUID keys"})
    # A later turn now surfaces PRIYA + R-77 + UUID decision as context automatically

if __name__ == "__main__":
    asyncio.run(main())

Run this and watch the third turn: the engineering agent's context now contains the finance agent's R-77 rule and the prior ownership fact, even though neither was in its input. That automatic carry-forward across agents is the entire point of Team Memory — and the audit entries tell you exactly which records influenced which decision.

Retry Rules & Error Handling

Failure Backoff Fallback Escalation
Vector write timeout 0.2s→4.0s + jitter Outbox buffer, replay on next writer pass Pool alert after 3 fails
Hybrid search 5xx 0.2s→4.0s KV-only recall of last checkpoint Agent runs with reduced context + warning
ACL endpoint down 0.2s→0.8s, 3 attempts Fail closed: deny reads, deny writes Pause team turns; page security
Conflict resolution write fails 0.2s→4.0s Keep group open, mark "unresolved" Manual review queue
Audit append fails 0.2s→0.8s Buffer locally Do not acknowledge write to agent
Embedding service down 0.2s→1.6s Lexical fallback (BM25) for reads Disable writes; reads degrade

Note the ACL row: the fallback is fail closed, not fail open. If you cannot verify identity, you cannot allow a read — the cost of a false denial is a retry, the cost of a false grant is a cross-tenant data leak. Never compromise on that direction.

Cost & Decision Matrix

Decision point Cost driver Cheap path Expensive path Rule
Read path Embedding + search tokens Cache per (agent, task-type) Fresh hybrid search every turn Reuse cache ≤ 5 min
Write path Embedding + pool writes Write-behind, dedup ≥ 0.93 sim Write every utterance verbatim Only durable records persist
Conflict check Graph + KV lookups Windowed (60 min) check Full-history reconciliation Check recent, resolve lazily on read
Audit DB writes Append-only compact JSON Full-diff reconstruction Record IDs + action only
ACL Policy check per op Session-scoped role cache Check on every record Cache 60s, revoke on role change

The engineering leverage here is the dedup threshold. DEDUP_SIM_THRESHOLD=0.93 means an agent re-stating "we moved to UUID keys" for the tenth time is not ten records — it is one record with ten provenance entries. Your storage cost stays flat while your audit quality goes up, which is the direction every memory product should move.

What this means for your team's agent stack

TencentDB Agent Memory crossing 20,000 stars in 90 days is adoption data, and Team Memory is the feature that turns a storage product into a collaboration platform. The pattern generalizes far beyond Tencent's SDKs: any LangGraph shop can stand up a namespace-scoped pool with vector + graph + KV tiers and get the same behavior. In Indian enterprise context, the ACL + audit requirements map directly onto RBI/SEBI record-keeping norms and internal data-classification policies — a memory pool without scoped namespaces and read-audits will not pass a compliance review, period. You can see how this slot into a full AI Workflows architecture, follow the tooling ecosystem in our MCP directory, and catch the weekly agent-platform moves in Latest AI News.

Ship checklist

  • Namespaces are non-negotiable. If a record has no namespace, it does not get written.
  • Write-once, supersede-don't-edit. Mutations are new records that mark their parents as superseded.
  • Fail closed on ACL. Identity errors deny, they never guess.
  • Dedup before persist. A record that matches an existing one at ≥0.93 similarity is provenance, not new knowledge.
  • Audit what agents read. Read provenance is how you debug hallucinations and satisfy compliance in one move.

Team memory is where single-agent demos become multi-agent products. The pool you build today — scoped, deduplicated, conflicted-resolved, and audited — is the institutional knowledge your agents will argue from next quarter. Get the isolation and the audit right first, and the intelligence will compound.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

Frequently Asked Questions
It is Tencent Cloud's open-source long-term memory product for AI agents, which passed 20,000 GitHub stars in 90 days as of August 13, 2026. The milestone matters because it signals memory becoming a core product layer: its Team Memory feature lets a whole team of agents share conversations, documents, code, and institutional knowledge, not just one user's session.
Vector stores answer 'what is similar' but are weak at 'what is the current state of X' or 'who owns resource Y'. A knowledge graph stores stable entity facts - ownership, status, rules - as edges, and the reader fuses vector hits with entity linkage and a recency boost so the agent gets both fuzzy recall and grounded facts.
Every record carries a namespace, every read and write is checked against the ACL layer, and ACL failures fail closed - if identity cannot be verified, the operation is denied, not guessed. Enforcement happens at the storage layer during retrieval, never post-hoc in the graph.
Before persisting, the writer runs a similarity check against the pool. If a candidate record matches an existing one at or above the DEDUP_SIM_THRESHOLD of 0.93, it is not stored as new knowledge; it is recorded as provenance on the existing record, keeping storage flat while improving audit quality.
Contradictions flagged within the CONFLICT_WINDOW_MINUTES (60 minutes) enter a conflict-resolution node where the newest-timestamp record survives and older records are marked superseded_by. Older contradictions are resolved lazily on read, with the later write winning, so agent turns never stall on full-history reconciliation.
Deepak Bagada
Author Profile

Deepak Bagada

CEO, SaaSNext

Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.

Related Intelligence Analysis

Research Breakdown AI Workflows

The Step-by-Step Guide to Automating Meeting Tasks with Whisper

You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...

Deepak Bagada Deepak Bagada
9m read
Research Breakdown AI Workflows

Lovable AI UI-to-Code Pipeline: 2026 Tutorial

Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...

Deepak Bagada Deepak Bagada
8m read
Breaking AI Workflows

Claude Code's New Browser: 5 Workflows That Save Hours Daily

Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...

Deepak Bagada Deepak Bagada
12m read
Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc