Three analyst agents ingest conflicting source documents concurrently. Watch Statewave detect the contradiction, supersede the stale fact, and serve the correct answer automatically, with no merge logic written by you.
Part of Statewave — the open-source memory runtime for AI agents.
📦 Core runtime · 🐍 Python SDK · 🟦 TypeScript SDK · 🔌 Connectors · 📘 Docs · 💡 Examples · 🖥️ Admin · 🌐 statewave.ai
📋 Issues & feature requests: statewave/issues (centralized tracker)
- Multi-Agent Memory with Statewave
- Contents
- What is Statewave
- The problem this demo solves
- What happens when you run it
- How it works
- Architecture
- Prerequisites
- Setup and run locally
- Usage
- Source documents
- Environment variables
- Statewave API endpoints used
- Audit inspector
- Adapting to your domain
- Using Statewave with multi-agent frameworks
- Developer reference
- License
Statewave is an open-source memory runtime for AI agents. You give it raw events (episodes); it compiles them into typed, conflict-resolved memories; your agents query it to get ranked, token-bounded context ready to drop into a prompt. No GPU. No vector database. No application-level merge logic.
The loop: Ingest → Compile → Use
Full documentation at statewave.ai.
In a typical multi-agent pipeline, two agents can read sources of different freshness and commit contradicting facts to the same shared store. The usual options are: blow up the context window by sending everything to the LLM and hoping it figures it out, or write custom merge logic that is brittle and hard to audit.
Statewave is the third option. When two memories carry the same structured claim about the same entity and disagree on the value, the compiler automatically supersedes the older one; memories with no structured claim fall back to a word-overlap threshold. The loser is not deleted: it stays in the timeline with status: superseded and a valid_to timestamp marking where it stopped being current. Your agents query context and only ever see the winner.
You click Run pipeline. Three agents: Bloomberg, TechCrunch, and Earnings, start concurrently. Each one reads its source document, extracts structured findings, and commits an episode to the shared Statewave subject market-intel. As each agent compiles, its memories appear live in the browser panel.
Then TechCrunch's compilation finishes. The Bloomberg Stripe entry goes red with a strikethrough. The status bar reads "1 conflict resolved". You did not write any code to make that happen.
The moment that matters: Bloomberg committed Stripe's old rate of 3.5% + 35¢. TechCrunch committed the post-reversal rate of 2.9% + 30¢. Both memories carry the same structured claim,
pricing.processing_ratefororganization:stripe(card, standard, USD, per transaction), with different values, so the compiler flipped the Bloomberg memory tostatus: supersededand set itsvalid_toto where the TechCrunch memory starts. The wording of the two sentences never entered into it. When you ask "What is Stripe's current processing fee?", the synthesis agent queries context and gets back 2.9%, it never sees the stale figure.
Every agent follows the same three-step loop:
-
Ingest. The agent reads its source document, uses the LLM to extract structured findings, and calls
POST /v1/episodesto append a raw, content-hashed episode to the shared subject. Episodes are append-only; nothing is overwritten. -
Compile. The agent calls
POST /v1/memories/compile. Statewave's heuristic compiler extracts typed memories from the episode log and runs conflict detection. Two memories that carry the same single-valued claim key for the same entity are resolved on the claim: a newer, different value supersedes the older one regardless of wording. Memories with no structured claim fall back to word overlap (Jaccard ≥ 0.6 for facts). Either way the older memory is flipped tostatus: supersededwithvalid_toset to where the newer one starts, and it keeps thesource_episode_idsit was compiled from. -
Use. The synthesis agent calls
POST /v1/contextwith the subject ID and the user's question. Statewave returns a ranked, token-boundedassembled_contextcontaining only active (non-superseded) memories. The agent passes this bundle directly to the LLM and streams the answer back to the browser.
| Concept | What it means |
|---|---|
| Episode | Append-only raw event: subject ID + source + type + payload. The immutable source of truth. |
| Memory | Extracted, typed, compiled summary. Traces back to source episodes with confidence scores and provenance. |
| Compile | Idempotent episodes → memories conversion. Heuristic (local) or LLM compiler. No GPU required. |
| Conflict resolution | Same claim key and entity with a different value, or, for unkeyed memories, enough word overlap: the older is automatically superseded by the newer. Deterministic. |
| Context API | POST /v1/context: ranked, token-bounded context bundle ready for prompts. Same query, same bytes. |
| Subject | Any entity you track: user, agent, account, repo. Here: one subject (market-intel) per pipeline run. |
Three concurrent agents share a single Statewave subject. The FastAPI server orchestrates the agents and pushes live updates to the browser via SSE. Statewave runs as a separate local service.
Demo interface during a full multi-agent run:
- Node.js 20+: required for Option A (fastest; boots Statewave via
npx) and for the audit inspector - Python 3.11+: required to run this demo app in all options
- LLM API key from your Groq account (default) or any provider supported by LiteLLM (set
LLM_MODELaccordingly) - Docker and Docker Compose: only needed for Option B
Statewave ships a one-line launcher: no Docker, no account, runs offline. It boots the API, admin console, and Postgres, and wires itself into your MCP clients.
1. Clone this repo
git clone https://github.com/smaramwbc/statewave-multi-agent-memory
cd statewave-multi-agent-memory2. Start Statewave
npx @statewavedev/statewaveThis starts the Statewave backend at http://localhost:8100 (leave this running in its own terminal). macOS/Linux/Windows all work; you can also use the install script instead of npx:
curl -fsSL https://www.statewave.ai/install | sh # macOS/Linux
irm https://www.statewave.ai/install.ps1 | iex # Windows PowerShell3. Install Python dependencies and configure environment
pip install -r requirements.txt
cp .env.example .envOpen .env and set LLM_API_KEY to your LLM provider API key.
4. Start this demo
python server.pyOpen http://localhost:8000 and click Run pipeline.
Use this if you want Statewave running alongside the demo in containers (e.g. for a production-like Postgres setup), rather than the lightweight npx launcher.
1. Clone this repo
git clone https://github.com/smaramwbc/statewave-multi-agent-memory
cd statewave-multi-agent-memory2. Configure environment
cp .env.example .envOpen .env and set LLM_API_KEY to your LLM provider API key.
3. Start everything
docker compose upOpen http://localhost:8000 and click Run pipeline.
Use this if you prefer to run the demo server outside Docker while still running Statewave via Docker.
1. Clone this repo
git clone https://github.com/smaramwbc/statewave-multi-agent-memory
cd statewave-multi-agent-memory2. Install Python dependencies
pip install -r requirements.txt3. Configure environment
cp .env.example .envOpen .env and set LLM_API_KEY. Set STATEWAVE_URL if your Statewave instance is not at the default http://localhost:8100.
4. Start the Statewave backend
docker compose up -d api db5. Start this demo
python server.pyOpen http://localhost:8000 and click Run pipeline.
- Click Run pipeline. Three agent panels appear and begin logging in real time.
- Watch the Memory panel as each agent commits its findings. When TechCrunch's memory lands, the Bloomberg Stripe entry is immediately struck through in red.
- The status bar updates to "1 conflict resolved" once compilation finishes.
- Type a question in the chat input, e.g. "What is Stripe's current processing fee?" and the synthesis agent answers using active memories only.
- Click Reset to clear the subject and run again.
| File | Stripe fact | Role |
|---|---|---|
sources/bloomberg.json |
3.5% + 35¢ (stale, pre-reversal) | Committed first; the fact to be superseded |
sources/techcrunch.json |
2.9% + 30¢ (correct, post-reversal) | Contradicts Bloomberg; triggers supersession |
sources/earnings.json |
2.9% + 30¢ + Square miss | Corroborates TechCrunch; contributes Square data |
The Bloomberg document intentionally contains a pre-reversal figure. The conflict is synthetic but structurally identical to what happens in real pipelines when agents pull from sources of different freshness.
| Variable | Required | Default | Description |
|---|---|---|---|
LLM_API_KEY |
Yes | (none) | API key for your LLM provider |
LLM_MODEL |
No | groq/llama-3.3-70b-versatile |
LiteLLM model string. Change to use a different provider, e.g. openai/gpt-4o |
STATEWAVE_URL |
No | http://localhost:8100 |
Statewave server base URL |
STATEWAVE_API_KEY |
No | (none) | API key if your Statewave instance has auth enabled |
APP_SECRET |
No | (none) | When set, all demo API endpoints require X-API-Key: <value>. Leave unset for local dev. |
SUBJECT_ID |
No | market-intel |
Shared Statewave memory namespace. Change when adapting to a different domain. |
SYNTHESIS_SYSTEM_PROMPT |
No | (built-in analyst prompt) | System instruction given to the LLM when answering questions. Override to match your domain. |
DEMO_SEED_BLOOMBERG_STRIPE |
No | true |
Pre-seeds the stale Bloomberg Stripe fact. Set to false when using your own source files. |
| Endpoint | Purpose |
|---|---|
POST /v1/episodes |
Ingest a raw episode from an agent |
POST /v1/memories/compile |
Trigger conflict detection and memory extraction |
POST /v1/context |
Retrieve ranked, token-bounded context for a query |
GET /v1/timeline |
Fetch full episode + memory timeline for the inspector |
DELETE /v1/subjects/{id} |
Reset subject between pipeline runs |
The inspector/ directory contains a TypeScript tool that reads GET /v1/timeline and prints the audit trail for any subject: every episode grouped by the agent that wrote it, the memories agents currently see, and the memories that were superseded along the way.
cd inspector
npm install
npx tsx src/index.ts --subject-id market-intelA header banner is followed by three kinds of section:
One block per agent: the raw, append-only episodes that agent committed, oldest first. Each entry shows the ingest timestamp, the episode type, a short episode ID, an excerpt of the payload, and any memory IDs the agent cited in that episode.
Active memories: the compiled, typed facts agents currently see. Each line shows the memory kind, its confidence, and the first 80 characters of its content.
Superseded memories: the facts that were replaced. Each line shows the kind, the content preview, and the valid_to timestamp at which the memory stopped being current. This section is printed only when the subject has superseded memories.
What the inspector does not show: it does not print which memory replaced which, and it does not compute or print similarity scores. Statewave records a supersession as status: superseded plus a valid_to boundary on the losing memory, so the winner-to-loser pairing is not in the timeline payload for the inspector to print. Read it off the two sections instead: the superseded entry and the active entry that took its place describe the same fact, and the valid_to timestamp is where the handover happened.
Example output after a demo run, abbreviated:
╔════════════════════════════════════════════════════════════════════════╗
║ Statewave Audit Trail — Cross-Run Intelligence ║
║ Subject: market-intel ║
║ 8 episode(s) · 20 active · 4 superseded ║
╚════════════════════════════════════════════════════════════════════════╝
── BLOOMBERG (3 episode(s)) ──────────────────────────────────────────
2026-09-24T22:34:18.180Z │ agent.analyst.findings
ep: eac33f6e…
{"text":"bloomberg (2026-05-16) on Stripe — pricing: 3.5% plus 35 cents per transaction for card payments, raised from 2
cited memories: (none — writing phase)
[two more bloomberg episodes]
── TECHCRUNCH (2 episode(s)) ─────────────────────────────────────────
2026-09-24T22:34:19.742Z │ agent.analyst.findings
ep: 0a4891d5…
{"text":"techcrunch (2026-05-23) on Stripe — pricing: Reverted to 2.9% plus 30 cents per transaction for card payments a
cited memories: (none — writing phase)
[one more techcrunch episode, then the earnings block]
── ACTIVE MEMORIES (20 — what agents see now) ────────────────────────
[profile_fact conf:0.90] Stripe key differentiators (bloomberg): Developer APIs, Global coverage, Instant
[profile_fact conf:0.90] Stripe market positioning (bloomberg): Premium pricing signals Stripe is moving
[profile_fact conf:0.90] Square pricing (bloomberg, 2026-05-16): 49 dollars per month flat rate for Squar
[11 more active memories]
[profile_fact conf:0.90] Stripe pricing (earnings, 2026-05-20): Developer ecosystem growth accelerating d
[5 more active memories]
── SUPERSEDED MEMORIES (4 — overwritten by newer intel) ──────────────
[SUPERSEDED profile_fact ] Stripe pricing (bloomberg, 2026-05-16): 3.5% plus 35 cents per transaction for c
└─ replaced at: 2026-09-24T22:34:19.742Z
[SUPERSEDED profile_fact ] Square key differentiators (bloomberg): Hardware ecosystem, POS integration, SMB
└─ replaced at: 2026-09-24T22:34:19.825Z
[two more superseded memories]
↳ These memories existed but were automatically superseded when newer
contradicting intelligence was compiled. Agents never see them.
The key insight: Bloomberg's independent Square facts are separate atomic memories that carry neither Stripe's claim identity nor enough word overlap with the Stripe memories, so the Stripe pricing supersession left them untouched. Only the stale Stripe pricing fact was replaced by it.
The demo is wired for competitive intelligence on payment processors. To run it for a different domain, you only need to change .env and drop in new JSON source files. No Python changes required.
1. Set your subject ID and prompts in .env
# The shared memory namespace for this pipeline run
SUBJECT_ID=healthcare-news
# The instruction given to the LLM when answering questions in the chat panel
SYNTHESIS_SYSTEM_PROMPT=You are a healthcare policy analyst. Answer using ONLY the provided memory context. Do not invent facts. Be concise: 3-5 sentences.
# Disable the payment-processor seed (only relevant for the default demo sources)
DEMO_SEED_BLOOMBERG_STRIPE=false2. Add your source files to sources/
Each JSON file becomes one agent. The agent ID is the filename stem (e.g. reuters.json → agent reuters). Source files follow this schema:
{
"source": "reuters",
"published": "2024-11-01",
"headline": "Optional headline for context",
"competitors": [
{
"name": "Entity Name",
"pricing_model": "Relevant factual claim about pricing or cost",
"market_positioning": "How this entity positions itself",
"key_differentiators": ["differentiator one", "differentiator two"],
"confidence_notes": "Source and date of this data"
}
]
}The field names (pricing_model, market_positioning, key_differentiators) are generic; they map to "primary fact", "positioning fact", and "list of facts" in Statewave. Name them whatever fits your domain. Only name is required.
3. Run
python server.pyAll JSON files in sources/ are discovered automatically. The pipeline runs one agent per file. Conflict detection works the same way regardless of domain.
statewave-multi-agent-memory/
├── agents/
│ ├── analyst.py # Agent logic: ingest, compile, diff
│ ├── base.py # AsyncStatewaveClient wrapper
│ └── candidates.py # Deterministic structured memory candidate builder
├── sources/
│ ├── bloomberg.json # Stale Stripe pricing (3.5%) to be superseded
│ ├── techcrunch.json # Corrected Stripe pricing (2.9%) supersedes Bloomberg
│ └── earnings.json # Corroborates TechCrunch, adds Square data
├── tests/
│ ├── test_candidates.py # Unit tests for build_competitor_candidates
│ └── test_memory_diff.py # Unit tests for the memory diff logic
├── inspector/
│ └── src/index.ts # Audit trail: episodes, memories, supersessions
├── static/
│ └── index.html # Browser UI (SSE-driven, zero build step)
├── server.py # FastAPI app: /run /ask /events /memories
├── statewave_tools.py # Drop-in helpers: remember(), compile_subject(), recall()
├── Dockerfile # Builds the demo server as a container image
├── docker-compose.yml # Starts db + Statewave API + demo in one command
├── requirements.txt
└── .env.example
The browser receives a single /events stream. Event types:
| Event | Payload | Purpose |
|---|---|---|
agent_log |
{ agent, msg } |
Appends a log line to the named agent panel |
memory_update |
{ agent, diff } |
Applies a DOM diff to the Memory panel |
agents_done |
{ supersessions } |
Enables chat input; updates status bar count |
synthesis_token |
{ token } |
Streams one token into the active chat bubble |
synthesis_done |
(none) | Finalizes the chat bubble |
Most developers building multi-agent systems reach for a framework like CrewAI, LangGraph, or the Claude SDK rather than writing raw orchestration. Statewave slots in as the shared memory layer for any of them. The three API calls (/v1/episodes, /v1/memories/compile, /v1/context) are framework-agnostic; you wrap them in a tool, a node, or a hook.
Copy statewave_tools.py from this repo into your project. It wraps the official Statewave SDK in three simple functions (pip install statewave):
from statewave_tools import configure, remember, compile_subject, recall
configure("http://localhost:8100") # point at your Statewave instance
# 1. Ingest: write what your agent found
remember("my-subject", "agent-a", "Stripe charges 2.9% + 30¢ per transaction.")
# 2. Compile: detect conflicts and supersede stale facts
compile_subject("my-subject")
# 3. Use: get ranked, conflict-resolved context for the next prompt
context = recall("my-subject", "What does Stripe charge?")
# → drop context directly into your LLM system prompt or user messageThese three functions are all you need regardless of which framework you use.
from crewai.tools import tool
from statewave_tools import configure, remember, compile_subject, recall
configure("http://localhost:8100")
@tool("Remember a finding")
def remember_finding(subject_id: str, source: str, text: str) -> str:
"""Commit a finding to shared memory and compile it."""
remember(subject_id, source, text)
compile_subject(subject_id)
return "committed"
@tool("Recall context")
def recall_context(subject_id: str, question: str) -> str:
"""Retrieve conflict-resolved memory context for a question."""
return recall(subject_id, question)Assign both tools to whichever agents need shared memory. Statewave handles conflict resolution when two agents write contradicting facts; you write no merge logic in the crew.
from langgraph.graph import StateGraph, MessagesState
from statewave_tools import configure, remember, compile_subject, recall
configure("http://localhost:8100")
SUBJECT = "research-subject"
def ingest_node(state: MessagesState):
finding = state["messages"][-1].content
remember(SUBJECT, "researcher", finding)
compile_subject(SUBJECT)
return state
def recall_node(state: MessagesState):
question = state["messages"][-1].content
context = recall(SUBJECT, question)
return {"messages": [{"role": "system", "content": context}] + state["messages"]}
graph = StateGraph(MessagesState)
graph.add_node("ingest", ingest_node)
graph.add_node("recall", recall_node)import anthropic
from statewave_tools import configure, remember, compile_subject, recall
configure("http://localhost:8100")
client = anthropic.Anthropic()
tools = [
{
"name": "remember",
"description": "Commit a finding to shared memory.",
"input_schema": {
"type": "object",
"properties": {
"subject_id": {"type": "string"},
"text": {"type": "string"}
},
"required": ["subject_id", "text"]
}
},
{
"name": "recall",
"description": "Retrieve conflict-resolved context for a question.",
"input_schema": {
"type": "object",
"properties": {
"subject_id": {"type": "string"},
"question": {"type": "string"}
},
"required": ["subject_id", "question"]
}
}
]
def handle_tool(name, inputs):
if name == "remember":
remember(inputs["subject_id"], "claude-agent", inputs["text"])
compile_subject(inputs["subject_id"])
return "committed"
if name == "recall":
return recall(inputs["subject_id"], inputs["question"])Multiple Claude agents sharing the same subject_id automatically get conflict-resolved memory. This is the same mechanism the demo uses, exposed as native tool calls.
Whichever framework you use, Statewave removes the same set of problems:
- Conflict detection: no custom similarity checks or "latest wins" rules
- Supersession with provenance: full audit trail of which episode beat which, and why
- Token management:
max_tokenson/v1/contextreturns a ranked bundle that fits your prompt budget - Concurrent write safety: episodes are append-only; compile is idempotent
Apache-2.0. See LICENSE.


