mcpField
A
agent-memory

agent-memory

github.com/terminalskillsエージェントワークフロー18340 回インストール3 日前に更新
リポジトリ

agent-memory について

コーディングエージェントにセッションをまたぐ記憶を持たせるためのガイドと実装例です。既存の指示ファイルを優先し、大量の情報や過去の会話にはファイル形式や検索可能な保存先を紹介します。

主な機能

  1. 01エージェントの指示ファイルを優先
  2. 02ファイルベースの記憶構成を解説
  3. 03SQLite の埋め込み検索と ChromaDB の例
  4. 04関連コンテキストの検索と注入を説明
  5. 05記憶の鮮度と機密データの扱いを解説

活用シーン

  1. 01コーディングセッション間でプロジェクト規約を保持
  2. 02過去のサポートチケットを意味で検索
  3. 03エージェントのプロンプトに関連する履歴を追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/terminalskills/skills --skill agent-memory

インストール後、Claude Code で/agent-memoryを入力して読み込まれたか確認します。

同梱ファイル_scores.json

詳細

エージェントのメモリ

概要

コーディングエージェントは、毎回のセッションを空のコンテキストウィンドウで開始します。このスキルを使うと、セッションをまたいで保持されるメモリを与えられます。まずはエージェント自身が読み込むファイルベースのメモリ(CLAUDE.md、AGENTS.md、GEMINI.md)を使い、次に、何千件もの過去のチケットや会話を意味で検索する場合など、指示ファイルに収められない情報のために、自分で管理する小規模なメモリモジュール(Markdown ファイル、埋め込みベクトルを使った SQLite、または ChromaDB)を追加します。

手順

ステップ 0:エージェントがすでに持っているメモリを使う

何かを構築する前に、まずこれを確認してください。指示ファイルは各セッションの開始時に読み込まれます。

エージェント永続的な指示備考
Claude CodeCLAUDE.md(プロジェクト、~/.claude/CLAUDE.md)、CLAUDE.local.md、.claude/rules/*.md@docs/api.md のようにインポートできます。自動メモリはデフォルトで有効です。Claude はメモを ~/.claude/projects/<project>/memory/ に書き込み、MEMORY.md は起動時に読み込まれます(先頭 200 行または 25KB)。/memory で内容を確認したり、切り替えたりできます。CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 で無効にできます。
OpenAI CodexAGENTS.md(~/.codex と、リポジトリのルートから各ディレクトリ)、AGENTS.override.md合計サイズは project_doc_max_bytes(32 KiB)によって制限されます。
Gemini CLIGEMINI.md(~/.gemini/ とプロジェクトディレクトリ)/memory show と /memory reload を使えます。context.fileName を使ってファイル名を変更できます(例:AGENTS.md)。

「エージェントにこちらの規約を覚えさせて」という依頼なら、これらのファイルに書き込むか追記して完了です。以下の方法は、指示ファイルに収めるには大きすぎる、変化が多すぎる、または意味検索が必要なメモリにだけ使ってください。

方法 1:ファイルベースのメモリ(依存関係なし)
memory/
  MEMORY.md          # curated long-term facts, kept short
  2026-10-02.md      # daily session logs
  decisions.md       # key decisions and the reasoning
# MEMORY.md
## Projects
- Billing API: Node 22, Fastify, Postgres 16; deploys from the release branch
## Preferences
- TypeScript strict mode; Vitest, not Jest; pnpm, not npm
## Lessons
- Integration tests need Redis on localhost:6379 (docker compose up redis)
# agent_memory.py
from datetime import datetime, timedelta
from pathlib import Path


class FileMemory:
    def __init__(self, memory_dir: str = "memory"):
        self.dir = Path(memory_dir)
        self.dir.mkdir(parents=True, exist_ok=True)
        self.long_term = self.dir / "MEMORY.md"

    def log_today(self, content: str, section: str = "Notes") -> Path:
        daily = self.dir / f"{datetime.now():%Y-%m-%d}.md"
        if not daily.exists():
            daily.write_text(f"# {daily.stem}\n")
        with daily.open("a") as f:
            f.write(f"\n## {section}\n{content}\n")
        return daily

    def remember(self, key: str, value: str, category: str = "General") -> None:
        text = self.long_term.read_text() if self.long_term.exists() else "# MEMORY.md\n"
        header = f"## {category}"
        if header not in text:
            text += f"\n{header}\n"
        pos = text.index(header) + len(header) + 1
        self.long_term.write_text(text[:pos] + f"- **{key}**: {value}\n" + text[pos:])

    def search(self, query: str, limit: int = 10) -> list[dict]:
        terms = query.lower().split()
        hits = []
        for path in self.dir.rglob("*.md"):
            for n, line in enumerate(path.read_text().splitlines(), 1):
                score = sum(t in line.lower() for t in terms) / len(terms)
                if score:
                    hits.append({"file": path.name, "line": n, "text": line.strip(), "score": score})
        return sorted(hits, key=lambda h: h["score"], reverse=True)[:limit]

    def recent_context(self, days: int = 3) -> str:
        parts = []
        for i in range(days):
            day = datetime.now() - timedelta(days=i)
            f = self.dir / f"{day:%Y-%m-%d}.md"
            if f.exists():
                parts.append(f.read_text())
        return "\n---\n".join(parts)

整理:週に一度、recent_context(days=7) と現在の MEMORY.md をエージェント(またはローカルモデル)に渡し、長期的に有用な事実を統合し、古くなった情報を削除して、各セッションでファイル全体を読み込めるよう短く保つよう依頼します。

方法 2:SQLite と埋め込みベクトル(意味検索、単一ファイル)
npm install better-sqlite3 openai
export OPENAI_API_KEY="sk-proj-..."   # from your secret manager, never committed
// memory-store.ts
import Database from "better-sqlite3";
import OpenAI from "openai";

export class MemoryStore {
  private db = new Database("agent-memory.db");
  private openai = new OpenAI(); // reads OPENAI_API_KEY
  private model = "text-embedding-3-small";

  constructor() {
    this.db.exec(`CREATE TABLE IF NOT EXISTS memories (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      content TEXT NOT NULL,
      category TEXT DEFAULT 'general',
      embedding BLOB NOT NULL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )`);
  }

  private async embed(text: string): Promise<Float32Array> {
    const res = await this.openai.embeddings.create({ model: this.model, input: text });
    return new Float32Array(res.data[0].embedding);
  }

  async store(content: string, category = "general"): Promise<number> {
    const v = await this.embed(content);
    const blob = Buffer.from(v.buffer, v.byteOffset, v.byteLength);
    return Number(this.db.prepare(
      "INSERT INTO memories (content, category, embedding) VALUES (?, ?, ?)"
    ).run(content, category, blob).lastInsertRowid);
  }

  async search(query: string, limit = 5, minScore = 0.3) {
    const q = await this.embed(query);
    const rows = this.db.prepare(
      "SELECT content, category, embedding, created_at FROM memories ORDER BY id DESC LIMIT 5000"
    ).all() as { content: string; category: string; embedding: Buffer; created_at: string }[];
    return rows
      .map((r) => {
        // a Buffer from SQLite can sit at a non-aligned offset inside a shared pool: copy the exact bytes
        const bytes = r.embedding.buffer.slice(r.embedding.byteOffset, r.embedding.byteOffset + r.embedding.byteLength);
        return { content: r.content, category: r.category, created_at: r.created_at, score: cosine(q, new Float32Array(bytes)) };
      })
      .filter((r) => r.score >= minScore)
      .sort((a, b) => b.score - a.score)
      .slice(0, limit);
  }
}

function cosine(a: Float32Array, b: Float32Array): number {
  let dot = 0, na = 0, nb = 0;
  for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] ** 2; nb += b[i] ** 2; }
  return dot / (Math.sqrt(na) * Math.sqrt(nb));
}

これは JavaScript で行をスキャンするため、数千件程度のメモリなら問題ありません。それを超える場合は、インデックス(sqlite-vec 拡張機能または方法 3)を使ってください。ベクトルの保存には、1 つのモデルだけを使ってください。埋め込みモデルを変更すると、すべてを再度埋め込み直す必要があります。

方法 3:ChromaDB(フィルタリングとスケーラビリティ)
python3 -m pip install chromadb      # 1.x; the default embedder downloads a small ONNX model on first use
# chroma_memory.py
from datetime import datetime, timezone
import chromadb


class ChromaMemory:
    def __init__(self, path: str = "./chroma_db", name: str = "agent_memory"):
        self.client = chromadb.PersistentClient(path=path)
        self.col = self.client.get_or_create_collection(
            name, configuration={"hnsw": {"space": "cosine"}}  # older code used metadata={"hnsw:space": ...}
        )

    def store(self, memory_id: str, content: str, category: str = "general") -> None:
        # upsert: the same id overwrites instead of duplicating
        self.col.upsert(ids=[memory_id], documents=[content], metadatas=[
            {"category": category, "created_at": datetime.now(timezone.utc).isoformat()}])

    def recall(self, query: str, n: int = 5, category: str | None = None) -> list[dict]:
        r = self.col.query(query_texts=[query], n_results=n,
                           where={"category": category} if category else None,
                           include=["documents", "metadatas", "distances"])
        return [{"content": d, "category": m["category"], "similarity": 1 - dist}
                for d, m, dist in zip(r["documents"][0], r["metadatas"][0], r["distances"][0])]

    def forget(self, memory_id: str) -> None:
        self.col.delete(ids=[memory_id])

コサイン空間では、distance = 1 - similarity です。複数のマシンやサービスでメモリを共有する必要がある場合は、マネージドベクトルサービス(Pinecone、Qdrant Cloud)が代替手段になります。store と recall の形式は変わりません。

メモリの注入

セッション開始時に MEMORY.md と recent_context() を読み込み、タスクに取りかかる前に recall(task_description) を実行して、上位 3 件または 4 件の結果をシステムプロンプトまたはエージェントの指示ファイルに追加します。ユーザーメッセージには追加しないでください。各メモリの出典を示し、エージェントがどれくらい古い情報か判断できるようにします。

例

例 1:セッションをまたいでプロジェクトの決定事項を記憶する

ユーザープロンプト:「Codex と Claude Code のエージェントが、pnpm を使っていることと、統合テストに Redis が必要なことをすぐ忘れてしまいます。覚えさせてください。」

エージェントは、Codex が読み込む AGENTS.md と、@AGENTS.md をインポートする CLAUDE.md に 2 つの事実を追加するか、/memory を実行して Claude の自動メモリが有効になっていることを確認します。データベースは構築しません。新しいセッションで「テストの実行方法は?」と尋ねると、再度指示しなくても、pnpm test と Redis が前提条件であることを答えます。

例 2:過去のサポートチケットを意味で検索する

ユーザープロンプト:「当社のサポートボットには、解決済みのチケットが 40,000 件あります。顧客が『請求書の PDF が真っ白です』と書いたとき、以前の『請求書のエクスポート結果が空になる』というチケットを見つけてほしいです。」

エージェントは ChromaDB コレクションを作成し、store(f"ticket-{id}", text, category="billing") で各チケットを読み込み、recall("my invoice PDF is blank", n=5, category="billing") を呼び出します。上位の結果には、以前のエクスポート結果が空だったチケット 3 件が類似度スコアと解決内容とともに表示され、ボットは返信でそれらを引用します。

ガイドライン

  • エージェント自身の指示ファイルを優先してください。これらは無料で読み込まれ、プルリクエストで人間が確認します。
  • 常時読み込まれるメモリは短く保ってください(Claude Code は MEMORY.md を約 200 行、Codex は AGENTS.md を 32 KiB 読み込みます)。詳細はトピック別のファイルに記載し、必要に応じて検索してください。
  • メモリファイルやベクトルストアには、シークレット、トークン、個人データを保存しないでください。メモリはプレーンテキストであり、コミットまたは同期されることがよくあります。
  • メモリは古くなります。記録に日付を付け、毎週内容を整理し、事実でなくなったものは削除してください。誤ったメモリは、メモリがないよりも悪いものです。
  • 実際のクエリで想起をテストし、類似度の最低基準を設定して、関連性の低い一致が挿入されないようにしてください。
  • 埋め込みのコストは低額です(text-embedding-3-small は 100 万トークンあたりの料金が設定されています。最新の料金を確認してください)が、埋め込まれたメモリはすべてプロバイダーに送信されます。機密性の高い内容にはローカルの埋め込みモデルを使用してください。
  • 数百件未満のメモリにベクトル検索を使わないでください。Markdown を grep するほうが、デバッグしやすく高速です。

コメント