原文リンク:https://x.com/Av1dlive/status/2048800691943309698
AIエージェント構築者にほとんど誰も教えてくれない真実がある。
自分でモデルを作る必要はない。
本当に構築すべきなのは、
すべてのモデルの下に存在するmemory layerだ。
TL;DR:全文を読みたくないなら、このリンクをagentに投げて、直接質問すればいい:github.com/codejunkie99/brain
核心となる論点:memoryは移植可能であるべき
Claude Codeでも、Cursorでも、同じstorageを使うべきだ。
sessionがリセットされても、modelがオフラインになっても、toolが切り替わっても、生き残れるindex。
すべてのharnessが同じbrainに接続できるprotocol。
Harrison Chaseは4月21日、3つの文でこの点を的確に指摘した。
Mem0のTaranjeetは4月24日、さらに鋭く表現した。
私は両氏の意見に同意する。ただ、それをもう一歩先に進めただけだ。
memoryは単にオープンであるだけでなく、移植可能であるだけでなく、型定義を持ち、インデックス可能で、監査可能で、同期可能で、高いセキュリティを持つべきだ。agentは、検査不可能なテキストチャンクとベクトルデータベースだけに依存して動くべきではない。
私は1ヶ月以上かけて、まさにこれらの要件を満たすものを構築した:
- Gitベースのevent log。各noteが1つのcommitに対応する。
- BM25ランキングとプレフィックスマッチング機能を備えた、SQLite FTS5で構築されたindex。
- MCP標準入出力サーバー。互換性のあるすべてのagentに5つのツール機能を提供する。
- 人間が介入するシナリオ向けのCLIとフルスクリーンTUI。
この記事では、どうやって作ったのか、なぜ各層が存在するのかを説明する。
インストールだけしたいなら:github.com/codejunkie99/brain。Claude Code、Cursor、Codex、OpenClaw、Hermes、またはMCPやshellを話せる任意のツールをサポートしている。
プロジェクト全体像
7つのRust crate。5つのadapterディレクトリ。約9000行のコード。143個のregression test。PATHに配置される1つのバイナリ。
brain/
├── crates/
│ ├── brain-types — events, payloads, idempotency keys
│ ├── brain-store — git-backed event log via libgit2
│ ├── brain-index — SQLite FTS5, BM25, projections
│ ├── brain-app — orchestration, two-phase write, catch-up
│ ├── brain-mcp — rmcp stdio server
│ ├── brain-cli — `brain` binary
│ └── brain-tui — full-screen ratatui dashboard
└── adapters/
├── claude-code — MCP config + CLAUDE.md addendum
├── cursor — MCP config + .mdc rule
├── codex — MCP TOML + AGENTS.md addendum
├── openclaw — system-prompt file
└── hermes — system-prompt addendum
本当に重要なインサイト、そしてHarrisonの投稿が私の考えを完全にクリアにしてくれたのは、レイヤリングだ。
- Gitは唯一の信頼できるデータソース。
- SQLiteは一時的なindexで、必要に応じてGitから再構築可能。
- オーケストレーション層は両者の上に配置され、two-phase write操作を処理する。
- adapterは軽量な接続層として、各実行環境が統一されたコアサービスを呼び出せるようにする。
これが意味すること:
- 明日harnessを変えても、何も失わない。
- 明日modelを変えても、何も失わない。
- index全体を失っても、数秒でgitから再構築できる。
本当に価値を蓄積し続けるのは、git logだけ。そしてそれは本質的に、純粋なJSONファイルのディレクトリに過ぎない。
なぜGitが正しいstorage layerなのか
私が試した他のアプローチはすべて致命的な欠陥があった:
- SQLite-only:高速だが、replication、conflict resolution、audit trailを自分で解決する必要がある。
- Markdown files:人間が読むには良いが、プログラムによる解析には向かない。解析速度が遅い。更新操作に競合状態の問題がある。
- DuckDBやPostgreSQL:単一ユーザーのmemoryシナリオには完全にオーバースペックだ。
- Git:各eventが1つのcommit、1つのファイルに対応。監査ログが組み込みで、push/pull同期をサポート、ネイティブなマージメカニズムを持つ。
書き込みの基本単位はcommitで、commit自体がアトミックだ。各eventは永続的に不変のOIDを持つ。
以下はbrain-store/src/repo.rsでのappendの実装だ:
pub fn append_event(&self, draft: EventDraft) -> Result<EventRef, StoreError> {
draft.shallow_validate(SCHEMA_VERSION)?;
let deadline = Instant::now() + Duration::from_secs(2);
let mut delay = Duration::from_millis(25);
loop {
match self.append_event_once(&draft) {
Ok(er) => return Ok(er),
Err(StoreError::Git(ref ge))
if matches!(ge.code(), ErrorCode::Locked | ErrorCode::Modified)
&& Instant::now() < deadline =>
{
std::thread::sleep(delay);
delay = min(delay * 2, Duration::from_millis(200));
continue;
}
Err(e) => return Err(e),
}
}
}
指数バックオフによる再試行ループとidempotency keyの事前検証を組み合わせることで、このメカニズムは複数の並行書き込みシナリオでも安定して動作する。
なぜ検索層にSQLite FTS5を選んだのか
Gitは信頼できるバージョン管理ツールだが、検索速度は遅い。そこで、派生indexを追加した。brain-indexライブラリはSQLite FTS5全文検索indexを維持し、3つの重み付けフィールドを持つ:
- Title:10倍の重み
- Body:ベースライン重み
- Tags:5倍の重み
BM25がランキングを担当する。100000個のeventの規模で、top-5の結果は1ミリ秒以内に返せる。
以下は、brain-index/src/schema.rsで定義された最も重要な2つのテーブルだ:
CREATE TABLE events (
event_id TEXT PRIMARY KEY NOT NULL,
commit_oid TEXT NOT NULL,
event_type TEXT NOT NULL,
subject_kind TEXT NOT NULL,
subject_id TEXT,
chain_id TEXT,
parent_event_id TEXT,
actor_kind TEXT NOT NULL,
actor_id TEXT NOT NULL,
actor_harness TEXT,
layer TEXT NOT NULL,
authority_source_kind TEXT NOT NULL,
authority_score INTEGER,
authority_attested_by TEXT,
signature_state TEXT NOT NULL,
classification TEXT NOT NULL,
time_observed INTEGER NOT NULL,
time_recorded INTEGER NOT NULL,
idempotency_key TEXT,
is_redacted INTEGER NOT NULL DEFAULT 0,
payload_json TEXT NOT NULL
) STRICT;
CREATE VIRTUAL TABLE events_fts USING fts5(
event_id UNINDEXED,
title,
body,
tags,
tokenize = 'unicode61 remove_diacritics 2',
prefix = '2 3 4'
);
重要な詳細:このindexは派生生成される。brain doctor --deepコマンドを実行すれば、Gitから一括でindexを再構築できる。Git履歴が唯一信頼できる核であり、indexはキャッシュとしてのみ存在する。
10種類のevent type
memoryは単一のものではない。preferenceはlessonと等価ではなく、claimは客観的事実と等価ではない。それぞれ異なる検索ルール、異なる可視性セマンティクス、異なる更新戦略が必要だ。
pub enum EventPayload {
Observe(ObservePayload), // "I chose X over Y"
Claim(ClaimPayload), // "warfarin + ibuprofen is dangerous"
Lesson(LessonPayload), // distilled pattern across episodes
Pref(PrefPayload), // "I always use tabs"
SkillEdit(SkillEditPayload), // "I modified skill X for reason Y"
Verify(VerifyPayload), // attestation on a prior claim
Archive(ArchivePayload), // hide without deleting
Redact(RedactPayload), // scrub and roll back projections
Import(ImportPayload), // bulk load from external source
Audit(AuditPayload), // system event
}
型付きmemoryこそが、クエリ可能なmemoryだ。
actor.harnessフィールドは、ツールをまたいだトレーサビリティを実現する鍵だ。Claude Codeが書いたnoteも、Cursorが書いたnoteも、内容上は区別がない。本当に「これがどのツールを使って誰によって書かれたか」を教えてくれるのは、このharnessフィールドだ。
Redactは削除ではなくロールバック
正しいアプローチはロールバックだ。Pref BをRedactする時、projectionはその前のPref Aに復元される。memory版のgit revertのようなものだ。
EventPayload::Redact(r) => {
let pref_keys: Option<(String, String)> = tx.query_row(
"SELECT category, key FROM pref_current WHERE event_id = ?1",
params![target],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
).optional()?;
tx.execute("DELETE FROM pref_current WHERE event_id = ?1", params![target])?;
if let Some((cat, key)) = pref_keys {
let predecessor = tx.query_row(
r#"SELECT event_id, payload_json, time_recorded
FROM events
WHERE event_type = 'pref'
AND is_redacted = 0
AND event_id != ?1
AND json_extract(payload_json, '$.category') = ?2
AND json_extract(payload_json, '$.key') = ?3
ORDER BY time_recorded DESC, event_id DESC
LIMIT 1"#,
params![target, cat, key], |row| { /* ... */ },
).optional()?;
// restore predecessor into pref_current
}
}
隠されたprefilter
シリアライズされた各eventは、gitに書き込まれる前に、RegexSetスキャンを通る。18のパターン、NFKC Unicode正規化を使用し、fullwidth lookalike文字による検出回避を防ぐ。
スキャンプロセスは3段階:
- 生の入力をスキャン。
- zero-width文字を削除して再度スキャン。
- NFKC正規化を実行し、再びゼロ幅文字を削除してから、再スキャン。
構築タイムライン
- 第1週:コアのstore + index + basic CLI。
- 第2週:MCP server。最初に動作するend-to-endシステム。しかしquery rewriterはまだ存在しない。
- 第3週:初めて遭遇した本格的なretrieval failure。その夜、query rewriterを書き上げた。翌朝、システム全体の使いやすさが10倍向上した。
- 第4-5週:Codex review第5-11ラウンド。12個の問題を修正し、30個のregression testを追加。
- 第6週:5つのharnessすべてにadapterを書き上げた。
- 第7週:brain push、brain pull、brain remoteを書いた。全体で30分かかった。本当に仕事をしているのはgitだからだ。
- 第8週:TUIの磨き込み:日別グループ化、tool glyph、filter key。
- 第9週:source-pollution bugに遭遇。すべてのnoteがすべてのqueryにマッチしてしまう問題。
このシステムは魔法ではないし、特に賢いというわけでもない。本当に達成しているのは、一貫性だ。
使用時の注意点
-
Index drift:
brain doctor --deepでgitから一括でindexを再構築可能。 - Source-field FTS pollution:FTSに入るべきフィールドだけをallow-listに登録。
- Detached HEAD writes:open時とappend時の両方でdetached HEADを拒否。
- Concurrent-writer races:指数バックオフ再試行ループ、idempotency-keyの事前チェックを組み合わせる。
- Schema mismatch on upgrade:open時にmismatchを検出したら、古いsqliteファイルを削除してgitから再構築。
- Secret leaked into commit subject:Observe、Lesson、Redactに対してsubject scrubbingを実施。
もう一度やり直すなら
- 初日からretrieval testを書く。
- テストを書く時、ユニットの正しさだけでなく、ユーザー体験を検証する。
- 500行書くごとにadversarial reviewを実施。
- 最初からtyped errorsを使う。
- 最初からtool descriptionをprescriptiveスタイルで書く。
- APIが安定する前にrepoを分割しない。
結論
modelは、より良いものが現れたらいつでも置き換え可能だ。Skillsやprotocolsは、作業方法と一緒に書き換え可能だ。しかしmemoryは代替できない。
memoryを自分の手に握れ。indexを自分の手に握れ。それらをplain fileとgitに保存し、どの会社も奪えない場所に置け。
謝辞
- Harrison Chase: "memory should be open!" — https://x.com/hwchase17/status/2046308913939919232
- Harrison Chase: "Your Harness, Your Memory" — https://www.langchain.com/blog/your-harness-your-memory
- Vivek Trivedi: "The Anatomy of an Agent Harness" — https://www.langchain.com/blog/the-anatomy-of-an-agent-harness
tags: Rust AI Memory MCP SQLite