The problem: agents start from zero every time
You run a meta-harness search on a text classification benchmark. It takes 6 iterations to find the winning approach — ensemble voting with numbered chain-of-thought, JSON parsing wrapped in try/except. On iteration 3, your agent discovers that temperature=0 helps. On iteration 5, it hits the JSON parsing bug. On iteration 6, it finally puts it all together and hits 0.872 accuracy.
Next week you run the same search on a slightly different dataset. Your agent starts at 0 again. It re-discovers chain-of-thought on iteration 2. It hits the JSON bug on iteration 4. It finds ensemble voting on iteration 6. You just paid for 6 more iterations of work your agents already did.
This is the problem cross-agent memory solves.
The solution: a shared, searchable memory store
KAOS v0.6.0 adds a MemoryStore — a SQLite FTS5 table shared across all agents, all sessions, and (optionally) all projects. Any agent writes to it. Any agent searches it. Results are ranked by BM25 relevance, not insertion order.
Five memory types: result (what worked), skill (how to do something), error (what broke and why), observation (neutral findings), insight (derived conclusions).
from kaos.memory import MemoryStore
from kaos import Kaos
db = Kaos("project.db")
mem = MemoryStore(db.conn)
# Agent A finishes a search — writes what it learned
mem.write(
"search-agent-01",
"Ensemble voting with 3 Sonnet calls hit accuracy=0.872 on math_rag. "
"Key: temperature=0, numbered chain-of-thought, try/except around JSON parse.",
type="result",
key="math_rag:iter6:best"
)
# Agent B discovers a recurring error
mem.write(
"search-agent-01",
"JSON decode error in 40% of cases when model outputs extra text before the dict. "
"Fix: always wrap in try/except with regex fallback.",
type="error",
key="json-parse-fail"
)
# Next week — a brand new agent, different search session
hits = mem.search("ensemble accuracy math_rag", limit=5)
# => [result] math_rag:iter6:best — accuracy=0.872, ensemble, CoT...
# => [error] json-parse-fail — JSON decode 40%, try/except fix...
# Agent immediately applies what it found — skips 5 iterations of trial-and-error
How the search works: FTS5 + BM25
Memory entries are stored in a standard SQLite table and indexed in a parallel FTS5 virtual table with a porter unicode61 tokenizer. Three sync triggers (INSERT / UPDATE / DELETE) keep them in sync atomically.
When you call mem.search("ensemble accuracy"), KAOS runs:
SELECT m.*, fts.rank
FROM memory_fts fts
JOIN memory m ON m.memory_id = fts.rowid
WHERE memory_fts MATCH ?
AND (type_filter IS NULL OR m.type = type_filter)
ORDER BY fts.rank -- BM25: higher rank = more relevant
LIMIT ?
BM25 accounts for term frequency in the document and inverse document frequency across the whole corpus. "ensemble accuracy" will rank the entry about ensemble voting above a generic entry that only mentions "accuracy" once. No embeddings, no vectors, no external service. Pure SQLite, zero new dependencies.
Exact key lookup
If you know the key, you don't need to search:
entry = mem.get_by_key("json-parse-fail")
# => MemoryEntry(memory_id=6, type='error', key='json-parse-fail', ...)
What it looks like in practice
Here's the before/after on the math_rag benchmark across two consecutive search sessions:
| Metric | Session 1 (no memory) | Session 2 (with memory) |
|---|---|---|
| Starting accuracy | 0.641 (seed eval) | 0.641 (same seeds) |
| Iteration 1 score | 0.654 | 0.864 |
| Iterations to 0.86+ | 6 | 1 |
| JSON parse errors | Hit on iter 5 | Avoided (error memory) |
| Final best accuracy | 0.872 | 0.871 |
| API cost | $19.40 | $3.20 |
Session 2's proposer read the memory store before proposing. It found the ensemble approach, the temperature=0 finding, and the JSON error. Its iteration-1 harness applied all three and scored 0.864 — what session 1 needed 6 iterations to discover.
Proposer reads 4 prior-session memory entries, applies them, and scores 0.864 on iteration 1.
Meta-harness integration: automatic, non-intrusive
You don't have to manage memory manually. The meta-harness does it for you:
- After each iteration: improved harnesses are auto-written as
resultentries. Failed harnesses are written aserrorentries with the failure pattern as content. - Before proposing: the proposer queries the memory store for the current benchmark and injects the top 5 results into its prompt as a Cross-Session Memory block.
- Non-fatal: every memory read/write is wrapped in try/except. A memory failure never crashes a search.
# In search.py — auto-persists after each iteration
def _persist_to_memory(self, harness, result, iteration, attempt_status):
mem = MemoryStore(self.db.conn)
if attempt_status == "improved":
mem.write(agent_id, content, type="result",
key=f"{benchmark}:iter{iteration}:{harness_id[:8]}")
elif attempt_status == "failed":
mem.write(agent_id, content, type="error",
key=f"{benchmark}:iter{iteration}:fail")
The CLI
# Write a memory entry
kaos memory write agent-a "ensemble voting hit 0.87" \
--type result --key iter6-best
# Search across all agents and sessions (BM25)
kaos memory search "ensemble accuracy"
kaos memory search "json error" --type error
# List by type
kaos memory ls --type skill
kaos memory ls --agent search-agent-01 --limit 10
# JSON output for composability
kaos --json memory search "ensemble" | jq '.[].content'
MCP tools
All four memory operations are available as MCP tools for Claude Code, Cursor, and compatible clients:
agent_memory_write— write a typed memory entryagent_memory_search— BM25 full-text search with optional type filteragent_memory_list— list entries by agent, type, or recencyagent_memory_get— fetch a single entry by ID or key
Ask your AI assistant: "with kaos, search memory for past results on text classification" and it will call agent_memory_search directly.
The schema
CREATE TABLE memory (
memory_id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id TEXT NOT NULL REFERENCES agents(agent_id),
type TEXT NOT NULL DEFAULT 'observation'
CHECK (type IN ('observation','result','skill','insight','error')),
key TEXT, -- optional human-readable handle
content TEXT NOT NULL,
metadata TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
);
CREATE VIRTUAL TABLE memory_fts USING fts5(
content, key, type UNINDEXED, agent_id UNINDEXED,
memory_id UNINDEXED, created_at UNINDEXED,
tokenize = 'porter unicode61'
);
-- Three sync triggers keep memory_fts in sync automatically
The FTS table indexes content and key. Unindexed columns (type, agent_id, etc.) ride along in the FTS row so they can be read without a JOIN — but they don't inflate the inverted index.
Where to go next
- Full memory documentation — API reference, CLI, MCP, schema
- memory_search.py example — 3 agents, 6 entries, BM25 search in 50 lines
- Next: Shared Log → — how agents coordinate with intent, vote, and decide