Agent Brain
Enforced, persistent decision memory for AI code agent teams — that survives context compaction. Agents log decisions and outcomes, learn from past rejections, and resume their pending roadmap after a /compact instead of re-researching it. Hooks make it mandatory (not "stored and hoped for"). And code reads route through SAN compression (~80% fewer tokens, tokenizer-measured) so the context window fills slower in the first place.
Portable across agent runtimes — your knowledge isn't trapped in one tool. Agent Brain is an MCP server, so Claude Code and Codex can both drive the same brain with MCP tools, standing protocol, and lifecycle hooks — switch agents for price or capability without resetting your project's accumulated decisions and SAN. Any MCP host works (Cursor, Cline, Continue, …). See docs/adapters.md; run ./setup.sh for the easiest install.
Contents
- What This Does · Features
- Quick Start — install in 2 minutes
- How To Use It — the agent loop, a worked example, what you get
- Architecture — what lives where, performance & internals
- MCP Tools (21) — the agent-facing API
- Agent Team — bundled role templates
- Model Routing — provider-aware advice, scoped reasoning escalation, plan handoff
- Brain Protocol — the enforced decision loop
- SAN Protocol — code compression: is it worth it, measuring savings (
token_savings) - SAN Setup — turning SAN on, model choice, other platforms
- Adaptive Warnings · Web Dashboard — 3D office, decisions browser (live feed), SAN search trace
- Verification · Requirements · Configuration · Customization
What This Does
AI coding agents start fresh every session: no memory of past decisions, no learning from rejections, no cross-agent knowledge sharing — and they burn tokens re-reading the same source files task after task. Agent Brain fixes both:
- Memory — decisions, outcomes, and review feedback persist across sessions and agents:
Agent → pre_check() → "WARNING: similar approach was rejected last week"
Agent → log_decision() → records what you decided and why
Agent → does work → PR created
Reviewer → log_outcome() → "rejected: violates DIP (dependency inversion)"
Next time, any agent → pre_check() → sees that rejection → avoids the mistake
- Cheap code reading — the optional SAN protocol compresses source files to ~17-27% of their original tokens (81% saved, tokenizer-measured), and
token_savingsshows you exactly how much it saved, per session, in numbers and %.
Features
Quick Start
Recommended:
git clone https://github.com/sandeep84397/agent-brain.git
cd agent-brain
chmod +x setup.sh
./setup.sh
./setup.sh installs Agent Brain for every supported runtime it detects on your
machine: Claude Code, Codex, or both. If you want to force one runtime:
./setup.sh --claude
./setup.sh --codex
./setup.sh --all
The setup wizard will:
- Create a Python venv and install dependencies
- Prompt for your repo paths (or use the template config)
- Register the MCP server globally with Claude Code and/or Codex
- Install the brain hooks (decision-gate, amnesia re-inject, Read/Bash→SAN routing)
- Add the SAN tool-ladder to
~/.claude/CLAUDE.mdfor Claude Code or~/.codex/hooks.jsonguidance for Codex — no manual edit needed (idempotent; safe to re-run) - Offer to customize agent names interactively
- Run verification checks
No
setup.sh? The server works standalone. Justpip install mcp networkxand register manually:claude mcp add --transport stdio --scope user agent-brain -- python3 /path/to/server.pyThe server gracefully handles a missing
config.json— it starts with an empty brain.
Where things land: setup.sh installs a copy of the server and hooks to ~/.agent-brain/ with its own venv — that installed copy is what Claude Code and Codex run. The repo checkout keeps the source. CLI examples in this README use python3 brain/server.py <cmd> from the repo root after installing dependencies; against the installed copy, the equivalent is ~/.agent-brain/.venv/bin/python ~/.agent-brain/server.py <cmd>. If you edit the repo copy, re-run setup.sh and restart your agent runtime.
Existing users: upgrade to Claude + Codex
If you already installed Agent Brain before Codex support, update the checkout and re-run the same command:
git pull
./setup.sh
The installer is idempotent. It preserves ~/.agent-brain/config.json,
decisions.json, decisions.journal, records, metrics, and existing Claude
agent files unless you explicitly choose to overwrite templates. It refreshes
the installed server and hooks under ~/.agent-brain/, keeps Claude Code wired,
and adds Codex config when Codex is detected.
For each repo you want both runtimes to use:
./setup.sh --link-project=/absolute/path/to/your/project
Then restart Claude Code and Codex. In Codex, run /mcp and /hooks; trust the
Agent Brain hooks when prompted. Verify with:
~/.agent-brain/.venv/bin/python ~/.agent-brain/server.py diagnose --project=/absolute/path/to/your/project
Let an AI agent upgrade it
If an AI coding agent already has terminal access to your Agent Brain checkout, you can ask it to do the upgrade for you:
Update Agent Brain for Claude and Codex compatibility in this repo, link this
project, then run diagnose and validation.
The agent should run:
git pull
./setup.sh
./setup.sh --link-project="$(pwd)"
~/.agent-brain/.venv/bin/python ~/.agent-brain/server.py diagnose --project="$(pwd)"
~/.agent-brain/.venv/bin/python ~/.agent-brain/server.py validate
The only manual step Codex may still require is hook trust: after setup,
restart Codex, run /hooks, and trust the Agent Brain hooks. That trust step is
intentional Codex safety behavior for non-managed command hooks.
Codex setup
When Codex is installed, ./setup.sh writes:
~/.codex/config.toml—[mcp_servers.agent-brain]using the installed brain venv~/.codex/hooks.json— decision gate, roadmap injection, brain-before-research nudge, and SAN Read/Bash routing
After install, restart Codex. In Codex, run /mcp to confirm agent-brain is
enabled, then run /hooks and trust the Agent Brain hooks when prompted. Codex
requires explicit trust for non-managed command hooks.
To link a project for all supported runtimes:
./setup.sh --link-project=/absolute/path/to/your/project
This writes Claude project MCP files and appends an idempotent Agent Brain
protocol block to <project>/AGENTS.md for Codex. Restart your agent runtime in
that project so the instructions and MCP config reload.
Linking a project (so subagents can use brain)
./setup.sh registers brain at the user level. For project-local guidance and
subagent/project wiring, run:
./setup.sh --link-project=/absolute/path/to/your/project
This is idempotent and writes/merges:
<project>/.mcp.json— adds theagent-brainserver entry alongside any existing entries<project>/.claude/settings.local.json— setsenableAllProjectMcpServers: trueand addsagent-braintoenabledMcpjsonServers<project>/AGENTS.md— adds the Agent Brain protocol for Codex<project>/.gitignore— appends.mcp.json,.san/.san_hashes.json,.san/_cache/
After running it, restart Claude Code and/or Codex in the project, then verify:
~/.agent-brain/.venv/bin/python ~/.agent-brain/server.py diagnose --project=/absolute/path/to/your/project
Subagents not seeing brain tools? See the 4-layer model under Verification.
How To Use It
Once set up, you don't call brain tools yourself — your agents do, automatically, as part of their normal work. Your job is just to give agents tasks and (optionally) review the memory that builds up.
The loop every agent runs
For any non-trivial task, an agent follows this cycle (enforced by the hook — see Enforcement Hook):
1. pre_check(agent, area, action) ← "has anyone tried this before? did it fail?"
2. log_decision(agent, repo, area, ← records the plan; unlocks code edits
action, reasoning,
handoff_summary?, validation?, git metadata?)
3. … writes the code …
4. log_outcome(decision_id, outcome, ← records accepted / rejected / failed + why
outcome_by, reason)
You just say "add rate limiting to the signup endpoint". The agent does the rest.
Worked example — across two sessions
Monday — a decision gets rejected:
You: "Add rate limiting to /login"
Agent: pre_check(agent="karan", area="auth", action="rate limit login")
→ "No past failures in 'auth'. Proceed."
Agent: log_decision(... action="in-memory counter per IP", reasoning="simplest")
→ dec_20260609_..._a1b2c3
Agent: …writes code, opens PR…
PE: log_outcome(dec_..._a1b2c3, outcome="rejected", outcome_by="marcus",
reason="in-memory won't survive multi-instance deploy; use Redis")
Friday — a different agent, a related task, a different machine/session:
You: "Add rate limiting to the signup endpoint"
Agent: pre_check(agent="dev", area="auth", action="rate limit signup")
→ "SIMILAR REJECTIONS (1, 78% match):
[2026-06-09] karan tried: in-memory counter per IP
REJECTED by marcus: in-memory won't survive multi-instance deploy; use Redis"
Agent: …goes straight to a Redis-backed limiter, skips the mistake…
No human re-explained the Redis constraint. The brain carried it forward.
How it behaves
What you get out of it (outcomes)
Inspecting the memory yourself
You rarely need to, but from the repo root (where you cloned agent-brain):
python3 brain/server.py stats # overall health: how many decisions/agents/repos
python3 brain/server.py office # who's working on what right now
python3 brain/server.py savings # tokens SAN saved (last session / today / all-time)
From any agent/MCP client you can also ask in plain language — "show me the team dashboard", "what decisions touched the payment service?", "what's karan's scorecard?", "how many tokens did SAN save this session?" — and the agent picks the right tool (team_dashboard, decisions_for, agent_scorecard, token_savings).
Architecture
┌─────────────────────────────────────────────────┐
│ Your Machine (global) │
│ │
│ ~/.agent-brain/ │
│ ├── server.py ← MCP server (17 tools) │
│ ├── config.json ← your repos + team │
│ ├── decisions.json ← memory snapshot │
│ └── decisions.journal← append-only deltas │
│ │
│ ~/.claude/agents/ │
│ ├── project-manager.md │
│ ├── product-owner.md │
│ ├── principal-engineer.md │
│ ├── backend-engineer.md │
│ ├── frontend-engineer.md │
│ └── qa-engineer.md │
│ │
│ project-repo/.san/ ← SAN-compressed code │
│ ├── _index.json │
│ └── src/**/*.san │
│ │
│ dashboard/ ← 3D office + decisions │
│ ├── server.py (python, zero deps) │ + SAN search (web)
│ └── static/ (Three.js + SSE) │
└─────────────────────────────────────────────────┘
Performance & internals
The brain is built to stay fast as the decision history grows into thousands of entries:
Files in
~/.agent-brain/:decisions.json(snapshot) +decisions.journal(deltas) are the decision memory — both are needed; don't delete one without the other.office-state.jsonis live dashboard state (self-pruning),san_savings.jsonlis the token-savings log. All are per-machine and git-ignored.
MCP Tools (22)
Core (every agent uses these)
Query
Records & pruning (keep the brain lean)
Code Bridge
Patterns
Scorecards
Office Dashboard
SAN (Structured Associative Notation)
Admin (CLI only — not exposed via MCP)
Run from the repo root. These live on the CLI (not MCP) to keep the agent-facing tool surface lean:
python3 brain/server.py validate # full brain self-tests (117 checks)
python3 brain/server.py validate-san # SAN subsystem self-tests
python3 brain/server.py san-index <repo> # rebuild _index.json from .san/
python3 brain/server.py stats # overall brain health
python3 brain/server.py office [repo] # current office state (debug)
python3 brain/server.py savings # SAN token savings (last session / today / all-time)
python3 brain/server.py roadmap [repo] # open-work digest (same as get_roadmap)
python3 brain/server.py records [repo] # export the dated records dir
python3 brain/server.py prune [repo] [--before-days=N] [--apply] # forget old decisions (dry-run default)
python3 brain/server.py resolve-stale [repo] [--apply] # mark abandoned pending as superseded
Managing what the brain remembers
The brain only grows — it learns, it never forgets on its own. That's good for the valuable memory (what failed and why) but old, resolved decisions eventually add noise to pre_check and inflate get_roadmap. Two safe ways to keep it lean:
1. Browse the records. Every decision is exportable as dated, human-readable markdown:
python3 brain/server.py records # writes ~/.agent-brain/records/YYYY-MM-DD.md + INDEX.md
open ~/.agent-brain/records/INDEX.md # browse by date
Each entry shows the date, repo, area, agent, action, and outcome. The records are regenerated from the graph — deleting a day file just re-renders on the next export, so the graph stays the source of truth. To actually forget something, prune it.
2. Prune (or let the AI do it). prune_decisions is dry-run by default — it shows what would go without touching anything:
python3 brain/server.py prune --before-days=90 # preview: old, resolved decisions
python3 brain/server.py prune --before-days=90 --apply # archive + remove for real
What it does on --apply:
- Archives each pruned decision to
~/.agent-brain/decisions.archive.jsonl(recoverable — nothing is hard-deleted). - Removes it from the live graph, so
pre_check/get_roadmapstop surfacing it. - Keeps the learning: rejections/failures and
roadmap/blocker-tagged decisions are never pruned. - Re-exports the records dir to reflect the change.
You can also just ask an agent: "prune decisions older than 3 months, dry run first" — it calls prune_decisions(before_days=90, dry_run=True), shows you the list, and only applies after you confirm.
Is unbounded growth a problem? Disk is trivial (~28 MB/year) and reads stay sub-millisecond (mtime-cached). The real cost is relevance noise — a 2-year-old rejected approach surfacing as a "similar failure". Prune resolved/old decisions periodically; keep the rejections, which are the cheapest lesson you'll ever get.
Agent Team
The repo includes 6 agent templates. Each has the Brain Protocol baked in:
Model pins live in each template's model: frontmatter — change them to fit your budget. See Model Routing for the full strategy.
Scale by duplicating templates (e.g., backend-engineer-2.md).
Placeholders
Each template has {{ROLE_NAME}} / {{ROLE_NAME_LOWER}} placeholders:
setup.sh offers to replace these interactively. Or do it manually:
sed -i 's/{{BE_NAME}}/Arjun/g; s/{{BE_NAME_LOWER}}/arjun/g' ~/.claude/agents/backend-engineer.md
Already have custom agents?
If you already have agent .md files, don't overwrite them. Instead, add the Brain Protocol block to each:
# Brain Protocol
Before starting any task:
1. Call `pre_check(agent="<name>", area="<area>", action_description="<plan>")`
2. If warnings exist, adjust approach
3. Call `log_decision(agent="<name>", repo="<repo>", area="<area>", action="<plan>", reasoning="<why>", files_touched=["<paths>"])`
After feedback:
4. Call `log_outcome(decision_id="<id>", outcome="<result>", outcome_by="<who>", reason="<why>")`
NON-NEGOTIABLE.
Critical: do NOT set the frontmatter tools: field. Claude Code subagents inherit ALL tools from the parent session — including every mcp__agent-brain__* tool — only when tools: is omitted. Setting it (even with ToolSearch included) turns it into a literal allowlist that silently strips MCP tools, because mcp__* is not a valid wildcard. Reference: Claude Code subagents — Available tools.
---
name: my-agent
description: ...
model: claude-sonnet-4-6
# No `tools:` — inherits everything from the parent session, including MCP.
# To restrict tools, use `disallowedTools:` instead.
---
What if I really must restrict tools? Add
ToolSearchto yourtools:allowlist and bootstrap brain tools at the top of every task withToolSearch(query="agent-brain", max_results=25). This is a fallback for the rare case where you genuinely need a tool denylist; for normal use, omittools:entirely.
For reviewers (PE, QA), also add:
5. Call `log_feedback(agent="<name>", decision_id="<their-id>", feedback="<detail>", severity="blocker|warning|info")`
setup.sh shows this snippet if it detects existing agents (choose [m] for manual).
Model Routing (quality per cost)
Routing is advice, never model execution. Choose a route from task risk and the host's configured capabilities; the server does not spawn, switch, or fall back between providers.
OpenAI defaults keep bounded exploration on Luna, planning and implementation on Terra/medium, consequential review on Sol/high, and the hardest architecture work on Astra/high. Configure a provider explicitly:
"model_routing": {
"providers": {
"openai": {
"explore": {"model": "gpt-5.6-luna", "reasoning_effort": "low"},
"plan": {"model": "gpt-5.6-terra", "reasoning_effort": "medium"},
"implement": {"model": "gpt-5.6-terra", "reasoning_effort": "medium"},
"review": {"model": "gpt-5.6-sol", "reasoning_effort": "high"},
"architecture": {"model": "gpt-6-astra", "reasoning_effort": "high"}
},
"claude": {"review": {"model": "claude-sonnet-4-6"}}
}
}
pre_check(provider="openai"|"claude") reports compact routing advice. Claude receives only configured entries; legacy flat maps are labeled LEGACY/UNVERIFIED. A recommendation does not prove a model can run on the current host.
Escalation is narrow: two recent, unresolved reasoning failures by the same agent in the same repo and area, since its latest accepted decision. Environment and information failures require diagnosis; old unclassified failures do not mandate a stronger model. There is no percentage allocation, fixed escalation ladder, or cross-provider fallback.
Plan files as handoff artifacts
Pay for deep thinking once, reuse it across many cheap executions. The planner writes the plan to a file and logs it:
log_decision(agent="marcus", repo="my-app", area="payments",
action="Designed payment module architecture",
reasoning="...", plan_file="docs/plans/payments-plan.md")
Every later pre_check in that area surfaces it:
PLAN AVAILABLE: docs/plans/payments-plan.md (by marcus, 2026-06-12).
Read it before re-deriving the approach — execute against it, don't re-plan.
The pointer stays active while the decision is pending or accepted; a rejected plan stops being advertised.
Brain Protocol
Every agent reuses relevant context before starting a new approach:
1. After a fresh session or resume, call get_roadmap(repo). Before a new approach,
call pre_check(agent, area, action_description, repo).
→ See scoped failures and plan pointers. Adjust approach if warnings.
2. log_decision(agent, repo, area, action, reasoning)
→ Record your plan before implementing.
3. [do the work]
4. log_outcome(decision_id, outcome, outcome_by, reason)
→ Record what happened after review.
This is enforced in every agent's .md file as NON-NEGOTIABLE.
log_outcome can also accept optional caller-reported execution metadata:
source, model, effort, input_tokens, output_tokens, reasoning_tokens,
retries, and elapsed_seconds. Missing values remain unknown; this records a
caller report and does not claim server-side model telemetry.
The hooks (6)
setup.sh installs six hooks idempotently. They are workflow guardrails, not shell-edit security controls.
All fail-open (any error → allow, never break a session) and respect BRAIN_SKIP_ENFORCE=1.
Enforcement Hook
The enforcement hook is a workflow guardrail. It checks canonical repository +
host-session activity before protected aliases (apply_patch, Workflow,
Agent, Task, spawn_agent); parent and subagent work shares that scope.
It does not secure shell edits or replace code review.
Install (setup.sh does this automatically):
// ~/.claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "apply_patch|Workflow|Agent|Task|spawn_agent",
"hooks": [
{
"type": "command",
"command": "python3 /path/to/agent-brain/brain/hooks/enforce_brain_protocol.py",
"timeout": 5000
}
]
}
]
}
}
Fail-open: If scoped activity cannot be read or a hook errors, it allows the action (exit 0).
Bypass: Set
BRAIN_SKIP_ENFORCE=1before launching the host to skip guardrails:{ "env": { "BRAIN_SKIP_ENFORCE": "1" } }Parent and subagent activity shares the same host-session scope.
Surviving compaction (the amnesia fix)
A long session eventually hits /compact, and the summarizer can drop the pending work and roadmap you discussed earlier — the agent then re-researches things the brain already knows. Two hooks close that gap by pushing the brain's open-work digest back into context at the exact moments memory is born or lost:
This is the same digest get_roadmap returns, so push and pull never diverge. Startup is bounded to the current repo (top 5); compact/resume context is bounded to 8. Terminal decisions are excluded from open work, and stale pending entries are surfaced for verification rather than treated as proven unfinished.
Tagging durable work: put roadmap or blocker in a decision's area (e.g. area="kmp-foundation/roadmap") and it floats to the top of the digest above transient pending edits. Untagged pending decisions still appear, newest first — so the fix works on an existing brain with no migration.
Optional hard research gate: the reminder is soft by default. To block fan-out work until a brain read happened in the canonical repository + host session, set "research_gate": "hard" in ~/.agent-brain/config.json. pre_check, query_decisions, or get_roadmap satisfies that scoped activity record. Parent and subagent activity shares the host session; BRAIN_SKIP_ENFORCE=1 bypasses it.
Install (setup.sh wires every brain hook idempotently, backing up settings.json.bak):
// ~/.claude/settings.json
{
"hooks": {
"SessionStart": [
{ "matcher": "startup|resume|compact",
"hooks": [{ "type": "command",
"command": "~/.agent-brain/.venv/bin/python /path/to/agent-brain/brain/hooks/inject_brain_context.py",
"timeout": 15000 }] }
],
"PreToolUse": [
{ "matcher": "Workflow",
"hooks": [{ "type": "command",
"command": "~/.agent-brain/.venv/bin/python /path/to/agent-brain/brain/hooks/remind_brain_before_research.py",
"timeout": 10000 }] }
]
}
}
Making SAN the default read path
SAN is far cheaper for reading code (see Is SAN worth it?) — but agents kept defaulting to raw reads because nothing routed them to SAN, and a tool ladder isn't obvious. These changes close the gap (none touch the SAN format or quality):
The tool ladder (the standing rule agents are taught + nudged toward):
How it's enforced:
Soft vs. hard — set read_enforcement in config.json:
"soft"(default): all of the above nudge, never block. Raw reads always proceed."hard": a 2nd+ raw read/dump of the same fresh-.sanfile is blocked (exit 2) with an actionable message — forcing real SAN use (real, not-ignorable savings). First reads are never blocked; escape any block withBRAIN_SKIP_READ_BLOCK=1when you genuinely need raw bytes.
When raw reads stay correct (every mechanism stays silent for these): files you're about to edit, non-code files, files with no/stale .san, anything outside a configured repo, literal grep on a file, and discovery grep across files. SAN is for understanding; raw read is for editing; grep is for exact literals.
SAN Protocol
Structured Associative Notation compresses code to ~17-27% of its original tokens (81% saved blended, tokenizer-measured) while preserving all facts. See san/README.md for the full spec.
# Before: 80 lines, ~1,200 tokens
class AuthServiceImpl(...) : AuthService { ... }
# After: ~220 tokens
AuthServiceImpl @svc {
impl: AuthService iface
deps: UserRepository + TokenProvider + RateLimiter
fn:login(email, pwd) → AuthResult [validate → verify → issue_jwt]
fn:register(RegisterRequest) → AuthResult [validate → create → issue_jwt]
layer: application/service
patterns: DIP-clean
}
The escalation ladder
Agents never load the whole compressed repo — they climb four levels, paying more only when needed:
The signatures tier is also the most staleness-resistant: public surfaces churn far slower than function bodies.
Is SAN worth it? (measured numbers)
Measured with real tokenizers (tiktoken o200k_base and cl100k_base — both agree within 0.1%) across 3 production repos: 954 source/SAN file pairs, Kotlin/Java/TS/JS, ~1.12M source tokens. Compression varies by code style — boilerplate-heavy Android code compresses to ~17%, dense backend logic to ~27%; 18.9% blended (81% saved):
Do SAN's unicode operators (→ ⇒ ×) waste tokens? Not on modern tokenizers — measured: → = 1 token on both, and a typical SAN line costs exactly the same in unicode and ASCII form (19 vs 19 tokens). One caveat: standalone ⇒ is 1 token on o200k but 3 on the older cl100k — if you target older models, prefer the ASCII equivalents (->, =>, xN), which the spec allows everywhere.
Savings recur on every read by every agent; generation cost is one-time per file (plus regeneration when the file changes):
Use SAN when:
- Agents repeatedly explore the same codebase (every task re-reads files)
- The repo is too big to fit in context raw — SAN makes whole-repo reasoning possible
- Multiple agents work the same repo (generation cost amortizes across the team)
Skip SAN when:
- The repo is small enough to fit in context anyway (< ~50 files)
- Files churn rapidly — stale SANs need regeneration, eroding the one-time-cost advantage
- One-off scripts / repos agents rarely revisit (won't reach break-even)
Numbers above are tokenizer-measured (tiktoken). The live
token_savingstracker uses tiktoken too when it's installed in the brain venv (pip install tiktoken) — exact counts, same methodology as the table. Without tiktoken it falls back to a ~4 chars/token estimate (measured ~1.4 points optimistic: 17.5% vs 18.9% ratio). Measure your own repos:pip install tiktoken python3 -c " import tiktoken; from pathlib import Path enc = tiktoken.get_encoding('o200k_base') raw = sum(len(enc.encode(f.read_text(errors='replace'))) for f in Path('.').rglob('*.kt')) san = sum(len(enc.encode(f.read_text(errors='replace'))) for f in Path('.san').rglob('*.san')) print(f'raw={raw:,} san={san:,} ratio={san/raw:.1%}')"
Measuring your savings (token_savings)
You don't have to estimate — the brain measures it live. Every get_san call records what the raw source read would have cost vs the SAN tokens actually served. Ask any agent:
"how many tokens did SAN save this session?" → agent calls token_savings()
=== SAN TOKEN SAVINGS ===
This session:
SAN reads: 14
Raw source cost avoided: 16,380 tokens
SAN tokens served: 3,080 tokens
SAVED: 13,300 tokens (81%)
Today (2026-06-11): ...
All time: ...
Or from the shell (reports the last recorded session instead of a live one):
python3 brain/server.py savings
How it counts — a partial proxy, not a savings guarantee:
- Only
get_sanreads count (a read that replaced opening the raw file) query_sansearches and decision-memory benefits are not included- Reads where SAN wouldn't have saved anything are skipped
- Events persist in
~/.agent-brain/san_savings.jsonl; raw reads the host actually made are not observable.
Use it to decide whether SAN is paying off: if "All time" savings stay near zero after a week, your agents aren't reading via SAN — check coverage with recompile_san(dry_run=True).
SAN Setup
SAN (Structured Associative Notation) compresses source code to ~17-27% of its original tokens for LLM context. This is optional — the decision memory works without it.
Supported languages: the brain-compiler is language-agnostic, and the server's housekeeping (freshness, indexing, orphan cleanup) recognizes these extensions: .kt .java .py .ts .tsx .js .jsx .swift .go .rs .rb .c .cpp .h .cs .php .scala .m .mm. Files in other languages can still be compiled by hand, but won't be tracked by the freshness sweep until their extension is added to SOURCE_EXTS in brain/server.py.
-
Create
.san/in your repo:mkdir -p your-repo/.san -
Install the managed compiler adapter for your host (once):
./setup.sh --claude # or --codex, or --allThis installs a managed
brain-compileragent/skill rendered from the provider-neutral contractsan/compiler-contract.md, pinned to the configured model (seedocs/adapters.md). -
Generate SAN files by invoking that adapter on your host:
# e.g. "Convert src/services/AuthService.kt to SAN"The adapter reads the source, then calls the server's
publish_santool, which validates the content and atomically writesyour-repo/.san/src/services/AuthService.kt.san(the source path with.sanappended — mirrors the source tree). The server itself never invokes a model. -
Build the index:
python3 brain/server.py san-index my-backend # admin CLI; recompile_san also rebuilds it -
Query SAN:
query_san("my-backend", "Auth") # search by keyword get_san("my-backend", "src/services/AuthService.kt") # get specific file plan_san_refresh("my-backend") # read-only: what needs (re)generation recompile_san("my-backend", dry_run=True) # human-readable freshness report
SAN Commands
How SAN Generation Works
SAN content is generated by the host (an LLM — Claude Code or Codex), never by the agent-brain server. The server plans, validates, and publishes; it never invokes a model or a provider CLI. The division:
- Host (via its managed
brain-compileradapter): reads source, emits SAN, callspublish_san. - Server:
plan_san_refresh(what needs work),publish_san(validate + atomically write, digest-bound), staleness/orphan housekeeping.
Workflow:
plan_san_refresh("repo")reports missing/stale files with their source SHA-256.- The active host's
brain-compileradapter generates SAN for each and callspublish_san(...)with the source digest. publish_sanvalidates the candidate structurally, verifies the source hasn't changed (digest compare-and-swap), then atomically replaces the SAN and updates the hash + index. On any failure it rolls back all prior state.query_san/get_sanread the published SAN;plan_san_refreshre-reports what's still stale.
See docs/adapters.md for the full provider-aware flow, managed paths, config, and the no-fallback policy.
Which model runs the compiler
The compiler model is configured, not chosen at call time, and there is no automatic fallback — if the configured model is unavailable the adapter stops rather than silently downgrading. Defaults (override in config.json under san_compiler):
SAN conversion is mechanical (read source → emit facts), so a cheap tier is the right default; your engineering agents consuming SAN can run on bigger models since SAN cuts their input cost to ~20% of raw anyway. To change the compiler model, edit config.json and re-run ./setup.sh for that provider (managed adapters re-render; an unmarked hand-edited adapter is preserved and reported, never overwritten).
Other hosts
The MCP server is host-agnostic — any MCP client can call plan_san_refresh / publish_san / query_san / get_san / recompile_san. The SAN grammar itself is defined once in the provider-neutral contract san/compiler-contract.md; each host gets a managed adapter rendered from it. To wire a host that setup.sh doesn't yet manage, point it at that contract and have it call publish_san (mirror the source tree, append .san). The server's digest-based staleness tracking works identically regardless of which host wrote the file.
Content Hashing
SAN staleness detection uses sha256 content hashing to avoid false positives:
- Source file hashes are stored in
.san/.san_hashes.json - When checking freshness, if the source content hash matches the stored hash, the file is skipped (even if mtime changed)
- This catches false positives from
git checkout,git stash pop,touch, or editor save-without-change - Hashes are updated when
recompile_sanruns
Orphan Cleanup
When a source file is deleted, its SAN file becomes an orphan. Orphans are detected and cleaned up automatically:
- Every source tracked in
.san_hashes.jsonis checked for existence - If the source is gone, the corresponding
.sanfile and hash entry are removed .sanfiles with no matching source (even if not in hash tracker) are also cleaned up- Stats report
orphans_removedso you can see what was cleaned up
Important: SAN refresh is NOT automatic. Staleness checks run when you call
plan_san_refresh,query_san,get_san, orrecompile_san(dry_run=True)— they report stale SANs but do NOT regenerate them. To force a full metadata refresh (e.g., after a large merge or branch switch), callrecompile_san("repo"). To regenerate stale SAN content, invoke your host'sbrain-compileradapter on the reported files (it callspublish_san).
.san/is local generated output. SAN files are regenerated from source by the host adapter and published transactionally, so./setup.sh --link-projectadds.san/to your project's.gitignore(an existing anchored/.san/is accepted). Keep them out of version control; regenerate them per checkout.
Adaptive Warnings
Agents with high rejection rates get progressively stricter warnings:
Agents with fewer than 3 logged decisions always get NORMAL — no judgment on a tiny sample.
Old rejections age out gracefully. Warnings older than ~3 months are tagged
[6mo old — verify the reason still applies] — the codebase may have moved on
since the rejection, so the agent is told to re-check the reasoning instead of
treating it as current. The reason is always shown so the agent judges applicability;
nothing is silently filtered or silently blocked.
Web Dashboard (Live Visualization)
One local server, three views — a 3D office, a decisions browser with a real-time feed, and a SAN search trace.
python dashboard/server.py
# Opens http://localhost:3333 in your browser
# / 3D office (live agent activity)
# /decisions browse + live-stream decisions as they're logged
# /san trace how a query finds data through SAN
Decisions view (/decisions)
A searchable, filterable view of every decision the brain holds — and it updates in real time as agents log decisions, with no refresh:
- Live feed — a Server-Sent Events stream watches
decisions.json/decisions.journal; the moment alog_decision/log_outcomelands, the new card appears (with a brief highlight pulse). A "live" indicator shows the connection. - Filter by repo, area, agent, and outcome; search across action, reasoning, and outcome reason.
- Color-coded outcomes — green accepted, red rejected/failed, amber pending, purple revised, grey superseded — with at-a-glance counts (total / accepted / rejected / pending).
- Roadmap markers (
★) on decisions taggedroadmap/blocker, so durable work stands out.
It reads the decision graph the same way the MCP server persists it (snapshot + journal replay), as a standalone process — no extra setup, just open the page while agents work.
SAN search trace (/san)
See how the AI finds code through SAN — type a symbol or keyword and watch the same traversal get_san/query_san runs:
- resolve — the repo's
.sandirectory - index — qualified-name lookup over
_index.json(N symbols → name matches) - content — scan of
.sanbodies for matches the name didn't catch - block — each landing point shown with its kind, file, and
src:line range (where to jump in the raw source)
Pick a repo, type, and the pipeline + result tree update live. It's the read-only view of the index → file → block → source-line path that makes SAN ~5-11x cheaper than raw reads.
3D office
A 3D virtual office that shows your agents working in real-time. Agents sit at desks, gather at the meeting table, show speech bubbles during discussions, and display status pips.
Features:
- Orbitable 3D office (drag to orbit, scroll to zoom)
- Status pips: 🟢 working, 🟡 planning, 🟠 reviewing, 🔵 discussing, 🔴 blocked, ⚫ offline
- Speech bubbles with actual message content
- Activity feed sidebar — messages auto-expire after 12h so stale chatter clears itself (run
python brain/server.py clear-activityto wipe it now) - Team panel with live agent list
How it works:
- Brain tools (
pre_check,log_decision, etc.) auto-update agent status — zero changes to your agents needed - For richer state (idle, messages, discussing), agents can call
heartbeat()explicitly - Dashboard reads
~/.agent-brain/office-state.jsonvia SSE (polls every 500ms)
Auto-heartbeat (free, no agent changes):
Explicit heartbeat (richer state):
heartbeat(agent="arjun", status="discussing", talking_to="marcus", message="DIP violation in AuthService?")
→ Both agents walk to meeting table, speech bubbles appear, message shows in chat log.
Tip: Add
heartbeat(agent="<name>", status="idle")to agent templates for when they finish a task. Otherwise agents stay at their last status until the 2-minute timeout.
Verification
After setup, run the full validation from the repo root:
python3 brain/server.py validate
# Expected: "Agent Brain Validation: 117 passed, 0 failed ✓ ALL TESTS PASSED"
This tests every subsystem in isolation using a temp directory:
You can also run just the SAN subsystem: python3 brain/server.py validate-san
Or verify basic connectivity with python3 brain/server.py stats:
Brain Stats:
Nodes: 0 | Edges: 0
Decisions: 0 | Feedback: 0 | Code refs: 0
Areas: none
Repos: none
Agents: none
Troubleshooting:
Diagnose CLI
~/.agent-brain/.venv/bin/python ~/.agent-brain/server.py diagnose [--project=/path/to/project]
Runs a standalone health check from the shell — no Claude session required.
Always verifies:
- MCP tools are registered in the server
config.jsonis valid JSON (or absent — empty brain is OK)~/.agent-brain/is writable (decision marker round-trip)decisions.jsonis readable if presentagent-brainis registered as an MCP server in Claude config and/or~/.codex/config.toml- Codex hooks exist when configured (
~/.codex/hooks.json) - Every
~/.claude/agents/*.mdis subagent-MCP-safe: omits thetools:frontmatter field (preferred — inherits MCP) or listsToolSearchintools:(fallback bootstrap) - Per-repo team resolution: which agents the brain considers in-team for each configured repo
With --project=<path>, also verifies:
- Claude project MCP files when present (
.mcp.json,.claude/settings.local.json) - Codex project guidance when present (
AGENTS.md) <project>/.gitignorecovers brain artifacts (informational)
Exit code is 0 when all checks pass, 1 otherwise — safe to call from a CI pre-flight or a dotfiles bootstrap.
How brain MCP reaches Claude Code subagents (4-layer model)
Brain tools work in BOTH the main Claude Code session AND spawned subagents only when all four layers are correctly configured:
Plus the agent frontmatter rule (see Already have custom agents? below): omit the tools: field so MCP tools are inherited. Setting tools: [Read, Write, ...] makes it an allowlist that silently strips every mcp__* tool, and mcp__agent-brain__* is not a valid wildcard.
After any config change, restart Claude Code (/exit then claude) — MCP and agent definitions are loaded at session start. Then run server.py diagnose --project=<path> to confirm all four layers are wired up.
Requirements
- Any MCP-compatible AI code agent (Claude Code, Cursor, Windsurf, Cline, etc.)
- Python 3.10+
- Optional:
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMSfor multi-agent orchestration - Optional: code-review-graph for code bridge features
Configuration
Edit ~/.agent-brain/config.json:
{
"repos": {
"my-backend": "/absolute/path/to/backend",
"my-frontend": "/absolute/path/to/frontend"
},
"team": [
{"name": "marcus", "role": "principal-engineer"},
{"name": "arjun", "role": "backend-engineer"}
]
}
Per-repo team scoping
A flat team list applies to every repo — fine when one team owns everything. When you run multiple repos with different staffing, scope members per repo so heartbeats from arjun on my-backend don't pollute the my-frontend office state.
Two ways to scope:
-
Per-entry
reposfilter (simplest — extends the flat list):{ "team": [ {"name": "marcus", "role": "principal-engineer"}, {"name": "arjun", "role": "backend-engineer", "repos": ["my-backend"]}, {"name": "priya", "role": "frontend-engineer", "repos": ["my-frontend"]} ] }marcushas norepos→ global, applies to every repo.arjunonly resolves onmy-backend.priyaonly resolves onmy-frontend.
-
teams_per_repooverride (full replacement for one repo):{ "team": [ /* default global team */ ], "teams_per_repo": { "experimental-repo": [ {"name": "marcus", "role": "principal-engineer"}, {"name": "neha", "role": "product-owner"} ] } }When
teams_per_repo[repo]is present, the flatteamlist is ignored for that repo.
Backwards compatible: configs without teams_per_repo and no repos field on entries behave exactly like before.
How it's used: brain tools that take a repo arg (heartbeat, log_decision, etc.) feed it through _get_team_for_repo() for role resolution and dashboard filtering. The office CLI command (python3 brain/server.py office my-backend) shows only that repo's agents.
Model routing (optional)
"model_routing": {
"providers": {
"openai": {
"plan": {"model": "gpt-5.6-terra", "reasoning_effort": "medium"},
"review": {"model": "gpt-5.6-sol", "reasoning_effort": "high"}
},
"claude": {
"review": {"model": "claude-sonnet-4-6"}
}
}
}
Pass provider="openai" or provider="claude" to pre_check for advisory routing. See Model Routing.
Customization
Adding more agents
Copy any template, rename, change the {{PLACEHOLDER}} values.
Adding domain terms
Edit _DOMAIN_TERMS in server.py to boost similarity matching for your domain.
Custom warning thresholds
Edit _adaptive_warning_level() in server.py.
License
MIT