memstack について
MemStackは、コーディングエージェントや独自エージェント向けのオープンソースの永続メモリパイプラインです。メモリの保存・検索・要約・整理に対応し、プラグイン式のストレージとLLMを利用でき、Claude CodeとCodexでプロジェクトのメモリを共有できます。
主な機能
- 01セッションをまたぐメモリの保存と検索
- 02古いメモリの要約と整理
- 03関連性・新しさ・重要度による順位付け
- 04プラグイン式ストレージとLLM
- 05Claude CodeとCodex間でのプロジェクトメモリ共有
- 06認証情報の検出とメモリ範囲の分離
活用シーン
- 01コーディングエージェント間でプロジェクト規約を引き継ぐ
- 02Claude CodeとCodexでプロジェクトの決定事項を共有
- 03独自エージェントにメモリパイプラインを追加
- 04検索コンテキストのトークン予算を管理
MCP 対応
MCPサーバーはClaude CodeとCodexに、メモリの保存・検索・取得・削除・統計ツールを提供します。セッション開始フックにより、新しいセッションへプロジェクトの重要なメモリを読み込めます。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
MemStack
AI エージェント向けの永続メモリです。Claude Code と Codex はプロジェクトごとに同じメモリを共有し、独自のエージェントでも同じパイプライン(保存、取得、要約、整理)が利用できます。
ウェブサイトとインストールガイド:memstack.stalewell.com
プロジェクトの仕組みを一方のエージェントに教えれば、もう一方もすでに理解しています。 Claude Code が規約を学習すると、新しい Codex セッションがそれを思い出し、タスクを正しく実行します。
npm install -g @memstack/cli @memstack/mcp better-sqlite3@^11.10.0 # MemStack never installs storage drivers
memstack init # LLM provider (optional) + store
memstack connect claude-code
memstack connect codex
以降のすべてのセッションはプロジェクトの重要なメモリから始まり、どちらのエージェントも、覚えておくように頼んだことを保存します。ハーネスメモリの仕組み。
独自のエージェントを構築していますか?
# Use MemStack in your application
npm install @memstack/core
# Give your coding agents the MemStack skill (-g: every project; omit it for this project only)
npx skills add isiomaC/memstack -g
@memstack/core はランタイム SDK です。Agent Skill は、対応するコーディングエージェントが MemStack を正しく統合し、操作する方法を学ぶためのものです。-g を指定しない場合、スキルは現在のプロジェクトにインストールされます(.agents/skills/ に加え、Claude Code 用の .claude/skills/ リンクが作成されます)。後から npx skills update で更新できます。
課題: AI エージェントは忘れてしまいます。やり取りのたびにゼロから始まります。すべてをコンテキストウィンドウに詰め込む(費用がかかり、遅く、出力品質が低下します)か、エージェントが過去の会話をまったく覚えていない状態になります。
MemStack が行うこと: エージェントと LLM の間に配置される永続メモリパイプラインです。あらゆるやり取りを保存し、関連する情報だけを取得し、古いメモリを要約してトークンを節約し、不要になったものを自動的に整理します。メソッドを一度呼び出すだけで、インフラストラクチャは不要です。
Mem0 に代わるオープンソースの選択肢と考えてください。プラグ可能なストレージ、任意の LLM の利用、ベンダーロックインなしを実現します。
目次
- MemStack を選ぶ理由
- クイックスタート
- ハーネスメモリ(Claude Code と Codex)
- メモリパイプライン
- 実際のユースケース
- メモリタイプのリファレンス
- 取得戦略
- 埋め込み
- アダプター
- API リファレンス全文
- 設定
- 高度な使い方
- LongMemEval-S 検索ベンチマーク
- 開発
- npm への公開
- コントリビューション
- ライセンス
MemStack を選ぶ理由
LLM にあるのはコンテキストウィンドウであり、メモリではありません。 この違いは重要です。
| アプローチ | 問題 |
|---|---|
| すべてをコンテキストに詰め込む | コストは O(n²) です。会話が 100 件あると、トークンは数千に上り、1 回の呼び出しに数ドルかかります。「中間部で見失う」現象により、品質も低下します。 |
| ベクトル DB を直接使う | 類似検索はできますが、要約、整理、最新性の重み付け、重複排除、トークン予算の管理はできません。パイプラインを自分で構築する必要があります。 |
| Mem0 を使う | 独自仕様で、ホスト型 API を利用するクラウド専用です。データの保存先を自分で管理できません。 |
| MemStack を使う | パイプライン全体を備え、あらゆる要素を差し替えられます。データもインフラストラクチャも自分で管理できます。オープンソースです。 |
MemStack が対応する、ベクトル DB だけではできないこと:
- 要約 — 古いインタラクション 100 件を、意味を保ったまま 1 段落に圧縮し、トークンを節約します
- 最新性の重み付け — 最近のメモリをより重視し、MemStack はそれらを上位に並べます
- 重要度のスコアリング — すべてのメモリが同じ重要度ではありません。重要度の高いメモリは整理後も保持されます
- 重複排除 — 同一または類似したメモリは、コンテキストの組み立て時に統合されます
- トークン予算 — LLM の呼び出し前に、
compileContext()で使用するトークン数を確認できます - メモリタイプごとの振り分け — インタラクション、要約、観察を、取得時にそれぞれ異なる方法で扱います
- 自動整理 — 古く重要度の低いメモリを自動的に整理します
クイックスタート
Claude Code と Codex
エージェントハーネス間でメモリを永続化します。Claude Code に伝えた内容を、同じプロジェクト内で Codex が思い出し、その逆も可能です。
npm install -g @memstack/cli @memstack/mcp better-sqlite3@^11.10.0 # MemStack never installs storage drivers for you
memstack init # choose an LLM provider and a store
memstack connect claude-code
memstack connect codex
次に、Claude Code で「このプロジェクトでは Hono を使っていることを覚えておいてください」と伝えます。同じリポジトリで Codex に「このプロジェクトではどのフレームワークを使っていますか?」と尋ねると、Codex は Hono と答えます。仕組みについてはハーネスメモリをご覧ください。
ライブラリとして使う場合
npm install @memstack/core
OpenAI
import { MemStack, OpenAILLMAdapter, OpenAIEmbeddingAdapter, InMemoryStorageAdapter } from "@memstack/core";
const llm = new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! });
const memstack = new MemStack({
llm,
embedding: new OpenAIEmbeddingAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
storage: new InMemoryStorageAdapter(),
});
DeepSeek(埋め込みなし)
DeepSeek はチャット補完を提供しますが、埋め込み API はありません。baseURL を指定して OpenAI 互換の LLM アダプターを使い、埋め込みアダプターは省略してください。取得ではキーワード、最新性、重要度によるランキングにフォールバックします。保存、要約、整理、コンテキストのコンパイルまで、パイプライン全体を引き続き利用できます。
import { MemStack, OpenAILLMAdapter, InMemoryStorageAdapter } from "@memstack/core";
const llm = new OpenAILLMAdapter({
apiKey: process.env.DEEPSEEK_API_KEY!,
baseURL: "https://api.deepseek.com/v1",
defaultModel: "deepseek-flash",
});
const memstack = new MemStack({
llm,
storage: new InMemoryStorageAdapter(),
// No embedding adapter — retrieval uses keyword matching
});
OpenRouter / Together AI / OpenAI 互換 API
同じパターンです。baseURL と defaultModel を変更します。
// OpenRouter
const llm = new OpenAILLMAdapter({
apiKey: process.env.OPENROUTER_API_KEY!,
baseURL: "https://openrouter.ai/api/v1",
defaultModel: "openai/gpt-4o-mini",
});
// Together AI
const llm = new OpenAILLMAdapter({
apiKey: process.env.TOGETHER_API_KEY!,
baseURL: "https://api.together.xyz/v1",
defaultModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo",
});
// Gemini (OpenAI-compatible endpoint)
const llm = new OpenAILLMAdapter({
apiKey: process.env.GEMINI_API_KEY!,
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
defaultModel: "gemini-2.0-flash",
});
保存と取得
// 1. Store what happened
await memstack.memory.store({
actorId: "support-bot-42",
content: "User reports login failing with error 503 on Chrome 125.",
tags: ["login", "bug", "chrome"],
importance: 0.8,
});
// 2. Later, retrieve relevant context
const memories = await memstack.memory.retrieve({
actorId: "support-bot-42",
query: "login error",
strategy: "hybrid",
});
// 3. Assemble an LLM-ready context
const ctx = await memstack.memory.compileContext({
actorId: "support-bot-42",
maxTokens: 2000,
});
const response = await llm.complete({
system: `You are a support bot. Here is what you remember:\n${ctx.systemPrompt}`,
user: "The user is back and still can't log in. What do you do?",
});
console.log(response.text);
// "Based on our history, the user has been experiencing 503 errors on Chrome 125..."
// 4. Every 100 interactions, summarization triggers automatically.
// Old interactions are compressed into a paragraph. Token costs stay flat.
Harness Memory(Claude Code と Codex)
コーディングエージェントはセッション間ですべてを忘れてしまい、学んだことを互いに共有できません。MemStack は Claude Code と Codex にプロジェクトごとのメモリを提供します。一方のエージェントが行った決定は、もう一方のエージェントにも、以降のすべてのセッションにも共有されます。
仕組み
- 保存。 各エージェントは、MCP ハーネスプロファイルを通じて 5 つの MemStack ツール(
memory_store、memory_retrieve、memory_get、memory_delete、memory_stats)を利用できます。「これを覚えておいて」と伝えたときや、エージェントが長期的に有用な事実、決定、好み、ルールを学んだときは、memory_storeを呼び出します。MemStack は LLM にいくつかのトピックタグを付けるよう依頼するため、「Hono を使っている」という情報を、後から「どのフレームワーク?」と尋ねて見つけられます。タグ付けに失敗しても、メモリは保存されます。 - 想起。 セッション開始時のフックが、プロジェクトの最も重要なメモリを新しいセッションごとに読み込むため、エージェントはそれらを把握した状態で開始します。セッション中、エージェントは通常の質問を使って
memory_retrieveを呼び出します。想起はキーワードランキング(ステミングを使った BM25)によりローカルで実行され、LLM を呼び出さないため高速で、オフラインでも動作します。 - スコープ。 メモリは現在のプロジェクトに属します。どこでも適用される好みはグローバル(
scope: "global")として保存でき、すべてのプロジェクトで想起されます。あるプロジェクトが別のプロジェクトのメモリを読み取ったり削除したりすることはできません。 - 安全性。 エージェントには一括操作や破壊的なツールが与えられず、LLM のキーはエージェントの設定ファイルではなく、あなたのみが読み取り可能な
~/.memstack/config.jsonに保管されます。エージェントにはシークレットを保存しないよう指示され、MemStack 側でもチェックされます。認証情報(秘密鍵、クラウドやソースホストのトークン、JWT、接続文字列内のパスワード、password = "..."という代入)を含むメモリは、LLM やストアに渡される前に拒否されます。エラーにはシークレットの種類が示されますが、その値が繰り返されることはありません。このチェックはパターンベースのため、認識できない形式のシークレットは通過します。MEMSTACK_SECRET_POLICY=redactを指定すると、シークレットの代わりに[REDACTED:<kind>]を入れてメモリを保存し、offを指定するとチェックが無効になります。
コマンド
| コマンド | 説明 |
|---|---|
memstack init | LLM プロバイダー(任意)とストアを選択します。実際のリクエストでキーを検証し、~/.memstack/config.json に書き込みます。キーがなくても、トピックタグなしでメモリを保存でき、想起も同様に機能します。非対話モードでは、--provider、--base-url、--model、--api-key-env <VAR>、--no-llm、--store、--path、--url、--yes を指定できます。 |
memstack connect <claude-code|codex> | MemStack をエージェントに登録します。最初にサーバーが動作することを確認し、手順の途中で失敗した場合はすべての変更を元に戻します。--dry-run は変更内容を表示し、--no-hooks と --no-agents-md は該当する部分をスキップします。 |
memstack disconnect <claude-code|codex> | connect が追加したものをすべて削除します。メモリは保持されます。 |
memstack status | 設定、ストレージ、現在のプロジェクト、各エージェントに接続されているものを表示します。 |
memstack doctor | セットアップの問題を診断し、それぞれの修正方法を表示します。--live を指定すると、LLM キーもテストします。 |
memstack memories [query] | このプロジェクトのメモリを一覧表示または検索します。グローバルメモリには --global、メモリの削除には --delete <id> を指定します。 |
memstack project | このリポジトリのプロジェクト ID を表示します。project pin <id> は .memstack.json でプロジェクト ID を固定し、project merge <old-id> は古い ID からメモリを移動します。 |
connect が変更する内容
connect は以下のエントリーのみを変更し、各ファイルを最初にバックアップします。
disconnect は各ファイルを正確に復元します。
| エージェント | ファイル | 変更内容 |
|---|---|---|
| Claude Code | ~/.claude.json | claude mcp add-json で追加される、ユーザースコープの memstack MCP サーバーです。 |
| Claude Code | ~/.claude/settings.json | memstack-mcp hook session-start を実行する SessionStart フックです。 |
| Codex | ~/.codex/config.toml | codex mcp add で追加される memstack MCP サーバーです。 |
| Codex | ~/.codex/hooks.json | SessionStart フックです。Codex は /hooks で一度承認すると、このフックを実行します。 |
| Codex | ~/.codex/AGENTS.md | memstack:begin/memstack:end マーカーで囲まれた短いブロックです。Codex に memory_store でメモリを保存するよう指示します。 |
登録されるコマンドはインストール済みの memstack-mcp で、絶対パスで実行されるため、エージェントがシェルの PATH を使わずに起動した場合でも動作します。読み込むには、Claude Code を再起動するか、新しい Codex セッションを開始してください。
プロジェクト
プロジェクト ID はリポジトリ自体から取得され、何も保存されないため、クローン、ワークツリー、リモートの名前変更、フォルダーの移動、新しいマシン、ストレージの切り替え後も同じ ID が維持されます。
- リポジトリのルートにある
.memstack.jsonの固定 ID (memstack project pin <id>。チームで共有するにはコミットしてください); - それ以外の場合は、リポジトリの最初のコミット;
- shallow clone の場合は、
originリモート; - コミットのないリポジトリでは git ディレクトリ、git 管理外ではフォルダーです。リポジトリの最初のコミット前に保存されたメモリは、新しい ID に自動的に移行されます。
フォークは元のリポジトリと最初のコミットを共有するため、同じストアでは、どちらか一方が memstack project pin を実行するまで、両方でメモリが共有されます。
ストレージ
サポートされているストアならどれでも動作します。memstack init で選択してください。MemStack はストレージドライバーをインストールしません。必要なドライバーを @memstack/mcp と一緒にインストールしてください。
| ストア | インストールするドライバー | 備考 |
|---|---|---|
sqlite | better-sqlite3@^11.10.0 | デフォルトです。ローカルファイル 1 つで、両方のエージェントを同時に安全に使用できます。 |
postgres | postgres@^3.4.9 | 複数のマシン間で共有できます。 |
redis | ioredis@^5.11.1 | |
disk、markdown | なし | 単一プロセスのみです。両方のエージェントで使用している場合、doctor が警告します。 |
ストアを切り替えてもプロジェクト ID は維持されます。memstack export と
memstack import でメモリを移動してください。
トラブルシューティング
まず memstack doctor を実行してください。設定ファイルとその権限、LLM キー、ストレージドライバー、各エージェントの登録、フック、ガイダンスを確認し、サーバーを起動して応答を確認します。
- 「memstack-mcp がインストールされていません」またはドライバーが見つからない: 表示された
npm install -g …コマンドを実行してください。 - Codex がセッション開始時にメモリを読み込まない:
/hooksで Codex の MemStack フックを承認してください。 - Codex が指示しても保存しない:
memstack statusでAGENTS.mdのガイダンスが存在すると表示されることを確認してください。存在しない場合は再接続してください。 - メモリが見つからないように見える:
memstack projectで現在のプロジェクトを確認できます。古い ID のメモリを取り込むには、memstack project merge <old-id>を使用してください。
MCP サーバー自体の詳細:docs/MCP_SETUP.md。
メモリパイプライン
MemStack の中核は、5 段階のパイプラインです。各段階は個別に使用できます。
1. 保存
すべてのエージェントとのやり取りは Memory となり、メタデータによって、その後の取得、要約、整理方法が決まります。
interface Memory {
id: string;
actorId: string; // Who this memory belongs to (user ID, agent ID, session ID)
memoryType: MemoryType; // "interaction" | "summary" | "observation" | "fact" | "reflection" | "preference" | "decision" | "instruction"
content: string; // The actual text
importance: number; // 0-1 — higher = survives pruning, ranks higher in retrieval
emotionalValence: number; // -1 to 1 — for tone-aware retrieval
tags: string[]; // Filter by tag: "bug", "billing", "urgent", etc.
embedding?: number[]; // Computed automatically if embedding adapter is configured
metadata?: Record<string, unknown>; // Your custom fields
expiresAt?: Date; // Auto-pruned after this date
sourceId?: string; // Link back to the originating event
createdAt: Date;
}
// Simple store
await ms.memory.store({
actorId: "agent-7",
content: "Customer asked about refund policy for Q2 purchases.",
tags: ["billing", "refund"],
});
// Batch store — embeddings are batched into one API call for efficiency
await ms.memory.storeBatch([
{ actorId: "agent-7", content: "First interaction" },
{ actorId: "agent-7", content: "Second interaction" },
{ actorId: "agent-7", content: "Third interaction" },
]);
2. 取得
関連する情報を取得します。キーワード、意味(セマンティック)、新しさ、重要度で検索できます。
const memories = await ms.memory.retrieve({
actorId: "agent-7", // Scope to one actor
query: "refund policy", // What to search for
strategy: "hybrid", // How to rank: "recent" | "important" | "semantic" | "hybrid"
limit: 10, // Max results
memoryTypes: ["interaction"], // Only certain types
tags: ["billing"], // Only certain tags
});
戦略の動作:
| 戦略 | 並べ替え基準 | 埋め込みが必要 | 最適な用途 |
|---|---|---|---|
recent | 新しい順 | いいえ | 直近の出来事を把握する |
important | 重要度の高い順 | いいえ | ノイズを除き、重要な情報を残す |
semantic | クエリとのコサイン類似度 | はい | 「X に関する記憶を探す」 |
hybrid | セマンティックと重要度の組み合わせ | はい | 両方の利点を活用する |
埋め込みアダプターがない場合、semantic と hybrid はキーワードマッチングと重要度順にフォールバックします。API コストはかかりませんが、精度は下がります。
3. コンテキストの編成
compileContext() は検索結果を受け取り、LLM 用のシステムプロンプトにまとめます。重複を取り除き、新しさと重要度で並べ替え、LLM を呼び出す前にコストを把握できるよう、トークン数の見積もりも提示します。
const ctx = await ms.memory.compileContext({
actorId: "agent-7",
maxTokens: 2000, // Budget — assembler stops when it hits this
memoryTypes: ["interaction", "summary"],
});
// ctx.systemPrompt:
// ## Important Memories
// - The customer has been attempting login for 3 days. (importance: 0.85)
// - Refund was processed for order #4521 on Jan 12. (importance: 0.72)
//
// ## Recent Interactions
// - Customer asked about refund policy for Q2 purchases.
// - Customer reported login error 503 on Chrome 125.
console.log(ctx.tokenEstimate); // ~280
// Inject into your LLM call
const currentMessage = "The user is asking about their refund status.";
const response = await llm.complete({
system: ctx.systemPrompt,
user: currentMessage,
});
console.log(response.text);
compileContext() は重複の除去、トークン予算の管理、コンテキストの重要度別・新しさ別セクションへの分割を処理します。これを使わない場合、生の検索結果を連結することになり、コンテキストウィンドウがあふれるおそれがあります。
4. 要約
アクターのやり取りが数百件に達すると、検索コストが高くなり、コンテキストが肥大化します。要約では、設定された LLM を使って古いやり取りを1つの段落に圧縮します。
const { summary, deletedCount } = await ms.memory.summarize({
actorId: "agent-7",
olderThan: new Date(Date.now() - 7 * 86400000), // Older than 7 days
skipMostRecent: 10, // Never touch the 10 most recent
targetCount: 50, // Summarize at most 50 memories
memoryTypes: ["interaction"],
keepOriginals: false, // Delete originals after summary
});
// summary.content:
// "Over the past week, the customer reported recurring login failures (error 503)
// on Chrome 125. Multiple troubleshooting attempts including cache clearing and
// password reset were unsuccessful. A refund was processed for order #4521."
console.log(deletedCount); // 47 — 47 interactions compressed into 1 summary memory
自動要約: 設定で summarizationThreshold を設定します(デフォルト:100)。アクターとのやり取りが100回に達するたびに、要約が自動的に実行されます。
警告: keepOriginals: false にすると、要約した記憶が削除されます。元の記憶を要約とともに保持するには、keepOriginals: true を設定します。
カスタム要約プロンプト:
const ms = new MemStack({
llm,
defaults: {
summarizationPrompt:
"You are an enterprise support memory compressor. Highlight: customer name,
product, severity, resolution status, and any open issues.",
},
});
5. 削除
すべての記憶を永久に残す必要はありません。プルーニングでは、価値の低い記憶を削除して、ストレージと検索を高速に保ちます。
// Remove memories older than 30 days
await ms.memory.prune({ type: "byAge", maxAge: 30 * 86400000 });
// Keep only memories above importance 0.3
await ms.memory.prune({ type: "byImportance", minImportance: 0.3 });
// Keep at most 500 memories per actor
await ms.memory.prune({ type: "byCount", maxPerActor: 500 });
// Remove specific types
await ms.memory.prune({ type: "byType", memoryTypes: ["observation"] });
// Custom logic
await ms.memory.prune({
type: "custom",
shouldRemove: (memory) => memory.content.includes("[RESOLVED]"),
});
// Dry run first — see what would be removed
const { wouldPrune, count } = await ms.memory.dryRunPrune({
type: "byAge",
maxAge: 86400000,
});
console.log(`Would remove ${count} memories:`, wouldPrune);
設定で pruneStrategy を指定すると、process() の呼び出しごとに自動でプルーニングできます。
const ms = new MemStack({
llm,
defaults: {
pruneStrategy: { type: "byImportance", minImportance: 0.05 },
},
});
実際のユースケース
サポートエージェント
// detectUrgency and classifyIntent are your own business logic.
// They could be simple keyword matchers, regex, or an LLM call.
function detectUrgency(msg: string): number {
if (msg.match(/urgent|asap|immediately/i)) return 0.9;
if (msg.match(/error|fail|broken/i)) return 0.7;
return 0.5;
}
function classifyIntent(msg: string): string[] {
const tags: string[] = [];
if (msg.match(/bill|refund|charge|payment/i)) tags.push("billing");
if (msg.match(/error|bug|fail|crash/i)) tags.push("bug");
if (msg.match(/login|password|account/i)) tags.push("account");
return tags;
}
// Every customer message becomes a memory
async function handleMessage(customerId: string, message: string) {
await ms.memory.store({
actorId: `customer:${customerId}`,
content: message,
importance: detectUrgency(message),
tags: classifyIntent(message),
});
// Retrieve everything relevant to this customer's history
const ctx = await ms.memory.compileContext({
actorId: `customer:${customerId}`,
maxTokens: 1500,
});
const response = await llm.complete({
system: `You are a support agent. Customer history:\n${ctx.systemPrompt}`,
user: message,
});
return response.text;
}
// Every 100th interaction, old history auto-compresses.
// A customer with 10,000 messages still fits in a $0.02 LLM call.
RAG パイプライン
// Suppose you have documents from your knowledge base
const documents = [
{ text: "Authentication uses JWT tokens with 15-minute expiry.", url: "/docs/auth", section: "security" },
{ text: "Refunds are processed within 5-10 business days.", url: "/docs/billing", section: "billing" },
];
// Index documents as observation memories
for (const doc of documents) {
await ms.memory.store({
actorId: "knowledge-base",
content: doc.text,
memoryType: "observation",
metadata: { source: doc.url, section: doc.section },
});
}
// Query with semantic search
const relevantDocs = await ms.memory.retrieve({
actorId: "knowledge-base",
query: "How does authentication work?",
strategy: "semantic",
limit: 5,
});
const ctx = await ms.memory.compileContext({
actorId: "knowledge-base",
memoryTypes: ["observation"],
});
// Prompt the LLM with retrieved context
const answer = await llm.complete({
system: `Answer using only these documents:\n${ctx.systemPrompt}`,
user: "How does authentication work?",
});
マルチユーザーチャットボット
// Each user gets their own memory space
async function chat(userId: string, message: string) {
await ms.memory.store({
actorId: userId,
content: message,
});
const ctx = await ms.memory.compileContext({
actorId: userId,
maxTokens: 1000,
});
return llm.complete({
system: `You are a friendly assistant. Conversation history with this user:\n${ctx.systemPrompt}`,
user: message,
});
}
// Get stats
const total = await ms.memory.count();
const userCount = await ms.memory.count({ actorId: "user-42" });
記憶タイプのリファレンス
| タイプ | 目的 | 例 |
|---|---|---|
interaction | デフォルト。エージェントとユーザーまたは別のエージェントとの直接的なやり取りです。 | 「ユーザーが請求について質問した。」 |
summary | 古いやり取りを圧縮したものです。summarize() によって作成されます。 | 「3週間で、ユーザーからログイン失敗が5件報告された……」 |
observation | 受動的な知識。事実、ドキュメント、エージェントが知っているものの、やり取りはしていない情報です。 | 「会社の返金ポリシーでは、購入から30日以内に返金できます。」 |
fact | 検証済みの知識。エージェントが確認した個別の事実です。 | 「ユーザーのサブスクリプションプランは Enterprise です。」 |
reflection | 自己生成された洞察。自身の経験についてエージェントが考えたことです。 | 「請求ポリシーを説明しすぎる傾向がある。もっと簡潔にするべきだ。」 |
preference | ユーザーの好みややり方です。 | 「各プルリクエストは、1つの論点に絞って小さくする。」 |
decision | 決定と、その理由があればその理由です。 | 「エッジランタイムのサポートを理由に、Express ではなく Hono を選んだ。」 |
instruction | 継続的に従うルールです。 | 「main に直接コミットしない。」 |
タイプによって検索動作が変わります。compileContext() は interaction と summary を observation とは異なる方法で扱います。タイプを使って「何が起きたか」と「何を知っているか」を分けてください。
検索戦略
目的に応じて、4つの戦略を使い分けます。
// "What just happened?" — most recent first
await ms.memory.retrieve({ actorId: "x", strategy: "recent", limit: 3 });
// "What matters most?" — highest importance, ignoring age
await ms.memory.retrieve({ actorId: "x", strategy: "important" });
// "What relates to this query?" — cosine similarity search (needs embeddings)
await ms.memory.retrieve({ actorId: "x", query: "login bug", strategy: "semantic" });
// "Balance relevance and importance" — semantic + importance blend
await ms.memory.retrieve({ actorId: "x", query: "login bug", strategy: "hybrid" });
戦略の選び方:
- チャットボット、継続中の会話など、時間が重要な場合は
recentを使います - 情報のノイズに対する重要な情報の比率が重要な、長期稼働するエージェントには
importantを使います - RAG、ドキュメント検索、ナレッジベースへのクエリには
semanticを使います - エージェントの記憶全般には
hybridを使います。意味と重要性のバランスが取れています
あらゆるストレージアダプターでのキーワード検索
LexicalRetriever は、埋め込みや LLM の呼び出しを行わずに自然な質問に回答し、どのストレージアダプターでも同じように動作します。指定されたアクターのメモリを retrieve() で読み込み、MemStack 内でコンテンツとタグを対象に、ステミングとプレフィックスマッチングを使った BM25 で順位付けします。一致するものがない場合は、代わりに最も重要なメモリを返します。
import { LexicalRetriever } from "@memstack/core";
const retriever = new LexicalRetriever(storage);
const { hits, fallback } = await retriever.recall({
actorIds: ["project:abc", "global"], // Searched together
query: "What framework does this project use?",
limit: 10, // Max results
maxChars: 8000, // Max total content; the top hit is always returned
});
アクターごとに最大 2,000 件のメモリが順位付けされます(candidateLimit)。アクセス済みとしてマークされるのは、返されたメモリのみです。
埋め込み
埋め込みはセマンティック検索を支えます。埋め込みは任意です。設定しない場合、検索ではキーワードマッチングが使われます。
埋め込みを使用する場合と使用しない場合
埋め込みを使用する場合(embedding アダプターを設定):
import { MemStack, OpenAILLMAdapter, OpenAIEmbeddingAdapter, InMemoryStorageAdapter } from "@memstack/core";
const ms = new MemStack({
llm: new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
embedding: new OpenAIEmbeddingAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
storage: new InMemoryStorageAdapter(),
});
// store() computes a 1536-dim vector automatically
await ms.memory.store({
actorId: "agent-7",
content: "Customer asked about refund policy for Q2 purchases.",
});
// retrieve() with "semantic" or "hybrid" uses cosine similarity
// Query: "refund" finds the refund policy memory even though the word "refund"
// appears differently across stored memories.
const results = await ms.memory.retrieve({
actorId: "agent-7",
query: "how do I get my money back",
strategy: "semantic",
});
// Matches "Customer asked about refund policy" — semantic match, not keyword match.
埋め込みを使用しない場合(embedding アダプターなし):
const ms = new MemStack({
llm: new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
storage: new InMemoryStorageAdapter(),
// no embedding adapter
});
// store() works identically, just no vector computed
await ms.memory.store({
actorId: "agent-7",
content: "Customer asked about refund policy for Q2 purchases.",
});
// retrieve() with "semantic" or "hybrid" falls back to keyword matching
// plus importance/recency sorting. No API costs, no setup required.
const results = await ms.memory.retrieve({
actorId: "agent-7",
query: "refund",
strategy: "hybrid", // falls back to keyword + importance
});
// Still works — finds "refund" via substring match. Less precise for
// paraphrased queries ("money back" won't match "refund").
バッチ埋め込み: storeBatch() はすべてのテキストを 1 回の埋め込み API 呼び出しで送信するため、コストとレイテンシーを削減できます。
// Disable auto-embedding if you only need keyword search
const ms = new MemStack({
llm,
embedding: new OpenAIEmbeddingAdapter({ apiKey }),
defaults: { embedOnStore: false },
});
ベクトルの次元数とモデルの互換性
埋め込みモデルによって、生成されるベクトルの長さは異なります。コサイン類似度を計算できるのは、同じ次元数のベクトル同士だけです。埋め込みモデルを変更すると、既存のベクトルは互換性を失い、新しいベクトルと比較できなくなります。
| アダプター | デフォルトモデル | 次元数 |
|---|---|---|
OpenAIEmbeddingAdapter | text-embedding-3-small | 1536 |
OpenAIEmbeddingAdapter | text-embedding-3-large | 3072 |
CohereEmbeddingAdapter | embed-english-v3.0 | 1024 |
CohereEmbeddingAdapter | embed-english-light-v3.0 | 384 |
CohereEmbeddingAdapter | embed-english-v2.0 | 4096 |
CohereEmbeddingAdapter | embed-multilingual-v3.0 | 1024 |
次元数が一致しない場合: あるモデル(例:1536 次元)でメモリを保存した後、別のモデル(例:1024 次元)に切り替えると、ストレージアダプターは長さの異なるクエリベクトルと保存済みベクトルを受け取ります。次元数の異なるベクトル間のコサイン類似度は定義されていません。結果はストレージバックエンドの動作によって異なります。多くの場合、エラーになるか、空の結果が返されるか、意味のないスコアが生成されます。
推奨事項: ストレージインスタンスごとに埋め込みモデルを 1 つ選び、変更せずに使い続けてください。モデルを切り替える必要がある場合は、新しいストレージインスタンスを作成し、最初から埋め込みをやり直してください。
DeepSeek ユーザー向け: DeepSeek には埋め込み API がありません。LLM として DeepSeek を使用する場合は、次のいずれかが必要です。
- 埋め込みアダプターを省略し、
"recent"または"important"の検索戦略を使用する(API コストはかかりませんが、精度は低くなります) - DeepSeek と別の埋め込みプロバイダーを組み合わせる(例:埋め込みには OpenAI、チャットには DeepSeek を使用する)
アダプター
MemStack はプロバイダーに依存しません。すべての接続境界はインターフェースです。お好みの LLM、埋め込みモデル、ストレージバックエンドを使用できます。
LLM アダプター
summarize() と compileContext() で使用します。OpenAI、Anthropic、Ollama、Groq が組み込みで提供されます。また、baseURL を使えば、OpenAI アダプターをあらゆる OpenAI 互換 API(DeepSeek、Mistral、Gemini、Together AI、Perplexity、Fireworks、xAI など多数)で利用できます。
// OpenAI
import { OpenAILLMAdapter } from "@memstack/core";
const llm = new OpenAILLMAdapter({ apiKey: "..." });
// Any OpenAI-compatible API — just change baseURL
const deepseek = new OpenAILLMAdapter({ apiKey: "...", baseURL: "https://api.deepseek.com/v1" });
const mistral = new OpenAILLMAdapter({ apiKey: "...", baseURL: "https://api.mistral.ai/v1" });
const together = new OpenAILLMAdapter({ apiKey: "...", baseURL: "https://api.together.xyz/v1" });
// Anthropic
import { AnthropicLLMAdapter } from "@memstack/core";
const llm = new AnthropicLLMAdapter({
apiKey: process.env.ANTHROPIC_API_KEY!,
defaultModel: "claude-sonnet-4-5-20250929",
});
// Ollama (built-in)
import { OllamaLLMAdapter } from "@memstack/core";
const llm = new OllamaLLMAdapter({
baseURL: "http://localhost:11434",
defaultModel: "llama3.2",
});
埋め込みアダプター
セマンティック検索で使用します。OpenAI と Cohere が組み込みで提供されます。また、baseURL を使えば、OpenAI アダプターをあらゆる OpenAI 互換の埋め込み API(Together AI、Voyage AI、Jina、Nomic など)で利用できます。
import { OpenAIEmbeddingAdapter, CohereEmbeddingAdapter } from "@memstack/core";
// OpenAI
new OpenAIEmbeddingAdapter({ apiKey: "...", model: "text-embedding-3-small" }); // 1536 dims
// Cohere
new CohereEmbeddingAdapter({ apiKey: "..." }); // embed-english-v3.0, 1024 dims
// Any OpenAI-compatible embedding API
new OpenAIEmbeddingAdapter({ apiKey: "...", baseURL: "https://api.voyageai.com/v1", model: "voyage-3" });
ストレージアダプター
MemStack には、18 種類のストレージアダプター実装が含まれています。12 種類は @memstack/core からエクスポートされ、残りの 6 種類は実験的なソース実装です。Core にランタイム依存関係はなく、データベースクライアントは呼び出し側から渡されます。
サポートレベル:
- 本番環境対応とは、公開パッケージからエクスポートされ、ユニットテストでカバーされており、公開 API の一部としてサポートされていることを意味します。
- 実サービスでの E2E 検証済みとは、アダプターが実際のデータベース実装に対しても
pnpm test:e2eに合格することを意味します。 - モックテスト済みとは、ユニットテストのカバレッジで、稼働中のクラウドサービスではなく、注入されたフェイククライアントを使用していることを意味します。
- 実験的とは、ソースには実装されているものの、公開パッケージからはエクスポートされていないことを意味します。
公開パッケージのエクスポート
組み込み(外部依存なし):
| アダプター | バックエンド | 用途 |
|---|---|---|
InMemoryStorageAdapter | インメモリの Map | テスト、プロトタイピング |
DiskStorageAdapter | ローカルの JSON ファイル | シンプルなローカル永続化 |
MarkdownStorageAdapter | 追記専用の .md ファイル | 人間が読みやすく、git diff で比較しやすく、デバッグに便利 |
HybridStorageAdapter | 任意の 2 つの StorageProviders を組み合わせ | キャッシュ+永続ストレージ、エッジ+永続ストレージ |
リレーショナル/SQL:
| アダプター | バックエンド | ベクトル検索 |
|---|---|---|
PostgresStorageAdapter | PostgreSQL + pgvector | ネイティブ HNSW |
SQLiteStorageAdapter | SQLite (better-sqlite3) | インメモリのコサイン類似度 |
ベクトルデータベース:
| アダプター | バックエンド |
|---|---|
QdrantStorageAdapter | Qdrant |
WeaviateStorageAdapter | Weaviate |
LanceDBStorageAdapter | LanceDB |
MongoDBStorageAdapter | MongoDB Atlas Vector Search |
キャッシュ/KV:
| アダプター | バックエンド |
|---|---|
RedisStorageAdapter | Redis (ioredis) |
グラフ:
| アダプター | バックエンド |
|---|---|
Neo4jStorageAdapter | Neo4j |
実験的(モックテスト済み、またはオプションの E2E 機能が不足)
これらの実装はソースコードへのコントリビューターが利用できますが、公開パッケージの API には含まれていません。
| アダプター | バックエンド | 制約 |
|---|---|---|
TursoStorageAdapter | Turso (libsql) | クラウド専用(Turso アカウントが必要) |
ChromaStorageAdapter | ChromaDB | 埋め込み関数への依存 |
PineconeStorageAdapter | Pinecone | クラウド専用(API キーが必要) |
UpstashStorageAdapter | Upstash Redis + Vector | クラウド専用(API キーが必要) |
Mem0StorageAdapter | Mem0 OSS または Cloud | クラウド専用(API キーが必要) |
ZepStorageAdapter | Zep Cloud または CE | クラウド専用(API キーが必要) |
Pinecone、Upstash、Mem0、Zep、Turso のライブクラウド互換性は未検証です。Chroma の実クライアント E2E テストスイートは、オプションのデフォルト埋め込み関数を利用できない場合にスキップされます。LLM と埋め込みプロバイダーのテストではモックを使用します。実プロバイダーを使ったテストはオプトイン方式で、CI には含まれていません。
バックエンドごとのクイックスタート:
// Postgres
import { PostgresStorageAdapter } from "@memstack/core";
const storage = new PostgresStorageAdapter({ connectionString: "postgres://..." });
// Redis
import Redis from "ioredis";
import { RedisStorageAdapter } from "@memstack/core";
const storage = new RedisStorageAdapter({ redis: new Redis() });
// Markdown (append-only, human-readable)
import { MarkdownStorageAdapter } from "@memstack/core";
const storage = new MarkdownStorageAdapter({ dir: "./memories" });
// Hybrid (Redis cache + Postgres durable)
import { HybridStorageAdapter } from "@memstack/core";
const storage = new HybridStorageAdapter({
cache: new RedisStorageAdapter({ redis: new Redis() }),
durable: new PostgresStorageAdapter({ connectionString: "postgres://..." }),
});
カスタムストレージ:
import type { StorageProvider, MemoryStoreInput } from "@memstack/core";
class MyStorage implements StorageProvider {
async store(input: MemoryStoreInput): Promise<Memory> { /* ... */ }
async get(id: string): Promise<Memory | null> { /* ... */ }
async retrieve(query: MemoryRetrieveQuery, embedding?: number[]): Promise<Memory[]> { /* ... */ }
async count(filter?: MemoryCountFilter): Promise<number> { /* ... */ }
async delete(id: string): Promise<void> { /* ... */ }
async deleteMany(ids: string[]): Promise<number> { /* ... */ }
async storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]> { /* ... */ }
async initialize(): Promise<void> { /* ... */ }
async close(): Promise<void> { /* ... */ }
}
バックエンドの比較
| バックエンド | ベクトル検索 | Touch | ステータス |
|---|---|---|---|
| InMemory | インメモリのコサイン類似度 | はい | ✅ 本番環境対応 |
| Disk (JSON) | キーワード+重要度 | はい | ✅ 本番環境対応 |
| Markdown | キーワード+重要度 | いいえ | ✅ 本番環境対応 |
| Postgres | pgvector HNSW | はい | ✅ 本番環境対応 |
| Redis | RediSearch KNN(自動検出) | はい | ✅ 本番環境対応 |
| Qdrant | ネイティブ ANN | いいえ | ✅ 本番環境対応 |
| Weaviate | BM25+ベクトルのハイブリッド | いいえ | ✅ 本番環境対応 |
| LanceDB | ネイティブ DiskANN | いいえ | ✅ 本番環境対応 |
| MongoDB | Atlas Vector Search | いいえ | ✅ 本番環境対応 |
| Neo4j | Neo4j ベクトルインデックス | いいえ | ✅ 本番環境対応 |
| Hybrid | キャッシュ/永続ストレージに委譲 | 永続ストレージが対応している場合 | ✅ 本番環境対応 |
| SQLite | インメモリのコサイン類似度 | はい | ✅ 本番環境対応 |
API リファレンス全文
MemStack クライアント
import { MemStack } from "@memstack/core";
const ms = new MemStack({
llm: LLMProvider, // Required — for summarization
embedding?: EmbeddingProvider, // Optional — for semantic search
storage?: StorageProvider, // Optional — defaults to InMemoryStorageAdapter
defaults?: {
summarizationThreshold?: number, // Auto-summarize every N process() calls. Default: 100
embedOnStore?: boolean, // Auto-embed on store(). Default: true
pruneStrategy?: PruneStrategy, // Auto-prune during process() (throttled). Default: disabled
pruneInterval?: number, // Run auto-prune every N process() calls. Default: 100
autoImportance?: boolean, // LLM-score importance in process() when not provided. Default: false
autoTags?: boolean, // LLM-extract tags in process() when not provided. Default: false
summarizationPrompt?: string, // Custom prompt for the summarizer
},
hooks?: {
onMemoryStored?: (memory: Memory) => void;
onMemoryPruned?: (ids: string[]) => void;
onSummaryCreated?: (summary: Memory, deletedCount: number) => void;
onError?: (error: Error, context: string) => void;
},
});
自動処理は
store()ではなくprocess()内で実行されます。process()は アクターごとの呼び出し回数を追跡します。要約はsummarizationThreshold回ごとに実行され、 プルーニングは(pruneStrategyが設定されている場合)pruneInterval回ごとに実行されます。store()は低レベルの書き込み処理であり、これらを実行することはありません。
メモリーサブシステム
ms.memory.* からすべてのメソッドにアクセスできます。
// Store
ms.memory.store(input: MemoryStoreInput): Promise<Memory>
ms.memory.storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]>
// Retrieve
ms.memory.retrieve(query: MemoryRetrieveQuery): Promise<Memory[]>
ms.memory.get(id: string): Promise<Memory | null>
// Context assembly
ms.memory.compileContext(options: ContextOptions): Promise<CompiledContext>
// Lifecycle
ms.memory.summarize(options: SummarizeOptions): Promise<{ summary: Memory; deletedCount: number }>
ms.memory.prune(strategy: PruneStrategy): Promise<{ pruned: string[]; count: number }>
ms.memory.dryRunPrune(strategy: PruneStrategy): Promise<{ wouldPrune: string[]; count: number }>
// Management
ms.memory.count(filter?: MemoryCountFilter): Promise<number>
ms.memory.delete(id: string): Promise<void>
ms.memory.deleteMany(ids: string[]): Promise<number>
ms.memory.touch(id: string): Promise<void>
ms.memory.purgeActor(actorId: string): Promise<number>
ms.memory.merge(ids: string[]): Promise<Memory>
ms.memory.stats(actorId?: string): Promise<MemoryStats>
ms.memory.summarizeStream(options: SummarizeOptions): AsyncIterable<{ chunk: string; text: string }>
エクスポート/インポート
永続化、バックアップ、移行のために、状態全体をスナップショットして復元します。
import * as fs from "node:fs";
// Save
const snapshot = await ms.export();
fs.writeFileSync("state.json", JSON.stringify(snapshot, null, 2));
// Restore
const data = JSON.parse(fs.readFileSync("state.json", "utf-8"));
await ms2.import(data);
インポート時には各メモリーの元の createdAt が保持されるため、export → import は損失のない一連の処理となり、バックアップやバックエンド間の移行(例:ディスク → Postgres)にも安全に使用できます。すべてのストレージアダプターは、store()/storeBatch() で指定された createdAt を尊重します。省略された場合は、現在時刻がデフォルトとして使用されます。
ヘルスチェックとクローズ
const status = await ms.health();
// { storage: true, llm: true, embedding: true }
await ms.close(); // graceful shutdown
Harness Memory API
ハーネス機能は、任意のストレージアダプターで動作するHarnessMemoryを基盤としています。これを使って独自のエージェント連携を構築できます:
import { HarnessMemory, defaultRecallNamespaces, projectNamespace } from "@memstack/core";
const memory = new HarnessMemory({ storage, llm });
await memory.remember({
namespace: projectNamespace("acme-api"), // or "global"
content: "This project uses Hono",
kind: "decision", // fact | preference | decision | instruction | observation | ...
source: { harness: "my-agent", project: "acme-api" },
});
const { hits, fallback } = await memory.recall({
namespaces: defaultRecallNamespaces("acme-api"), // project, then global
query: "Which framework do we use?",
limit: 10,
maxChars: 8000,
});
await memory.get(id, namespaces); // null outside namespaces
await memory.forget(id, namespaces); // refuses ids outside namespaces
await memory.stats(namespaces); // { "project:acme-api": 3, global: 1 }
await memory.moveNamespace(from, to); // e.g. when a project's ID changes
rememberはLLMにトピックタグを尋ねます(autoTags: falseで無効化できます)。それ以外のメソッドはLLMを呼び出しません。recallはLexicalRetrieverを使用します。
設定
const ms = new MemStack({
llm: new OpenAILLMAdapter({ apiKey: "..." }),
// Defaults control auto-behavior (all applied during process())
defaults: {
summarizationThreshold: 50, // Summarize every 50 process() calls (default: 100)
embedOnStore: false, // Don't auto-embed — saves API costs
pruneStrategy: { // Auto-clean during process(), throttled by pruneInterval
type: "byAge",
maxAge: 90 * 86400000, // 90 days
},
pruneInterval: 100, // Run the prune check every 100 process() calls (default: 100)
autoImportance: true, // Let the LLM score importance when you don't pass one
autoTags: true, // Let the LLM extract tags when you don't pass any
},
// Hooks for observability
hooks: {
onMemoryStored: (m) => logger.debug("memory:stored", { id: m.id, actor: m.actorId }),
onMemoryPruned: (ids) => logger.info("memory:pruned", { count: ids.length }),
onSummaryCreated: (summary, n) => logger.info("memory:summarized", { count: n }),
onError: (err, context) => logger.error("memory:error", { context, message: err.message }),
},
});
ハーネス設定ファイル
CLIとMCPハーネスプロファイルは、~/.memstack/config.json(または$MEMSTACK_HOME/config.json)を読み込みます。このファイルはmemstack initによって、本人のみが読み取れる権限で書き込まれます:
{
"version": 1,
"llm": { "provider": "openai-compatible", "apiKey": "…", "baseURL": "https://api.deepseek.com", "model": "deepseek-flash" },
"storage": { "type": "sqlite", "path": "/Users/you/.memstack/memstack.db" }
}
環境変数はセクション単位でファイルの設定を上書きします。LLM関連の変数(OPENAI_API_KEY、ANTHROPIC_API_KEY、MEMSTACK_OPENAI_BASE_URL、MEMSTACK_LLM_MODEL)はllmセクション全体を置き換え、MEMSTACK_STORAGEはstorageセクション全体を置き換えるため、キーが別のプロバイダーのURLに送信されることはありません。
高度な使い方
カスタムストレージ
任意のデータベース向けにStorageProviderを実装できます。インターフェースは9つのメソッドで構成されています。完全な仕様については、上記のリファレンスセクションを参照してください。
以下のオプションメンバーは、いずれも必須ではありません:
capabilities: { multiProcess?, textSearch? }では、複数のプロセスでストアを安全に共有できるか、またsearch()がネイティブに実装されているかを宣言します。search(query)はネイティブの全文検索を提供します。textSearchが宣言されている場合はLexicalRetrieverがこれを使用し、そうでない場合はメモリのランク付けを独自に行います。retrieve()は、アクセス済みとしてマークせずにメモリを返すよう、touch: falseを尊重してください。
SQLiteStorageAdapterはWALと5秒のビジータイムアウトを有効にし、複数のプロセスで1つのデータベースファイルを共有できるようにします。変更するにはwalMode: falseまたはbusyTimeoutMsを設定してください。
カスタムLLM/Embedding
任意のサービス向けにLLMProviderまたはEmbeddingProviderを実装できます:
import type { LLMProvider } from "@memstack/core";
class TogetherAIAdapter implements LLMProvider {
async complete(req: { system: string; user: string; model?: string }) {
const res = await fetch("https://api.together.xyz/v1/chat/completions", {
headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify({ model: req.model, messages: [{ role: "system", content: req.system }, { role: "user", content: req.user }] }),
});
const data = await res.json() as any;
return { text: data.choices[0].message.content, tokens: { prompt: data.usage.prompt_tokens, completion: data.usage.completion_tokens, total: data.usage.total_tokens } };
}
}
イベントフック
コードを変更せずにメモリ操作を監視できます:
const ms = new MemStack({
llm,
hooks: {
onMemoryStored: (m) => metrics.increment("memory.stored"),
onSummaryCreated: (_, n) => metrics.gauge("memory.summarized_count", n),
onMemoryPruned: (ids) => metrics.increment("memory.pruned", ids.length),
},
});
LongMemEval-S 検索ベンチマーク
MemStackには、公開されているクリーン済みのLongMemEval-S分割データを使用し、証拠セッションの検索のみを評価するValベンチマークパックが含まれています。ランナーはMemStackのハーネスメモリAPIを通じて各質問のセッションを保存し、ローカルの語彙検索とインメモリストレージを使用します。回答モデルや判定モデルは呼び出しません。このベンチマークの公開コーパスに限り、一時的なインメモリストアでシークレットスキャンが無効になります。このランナーをプライベートデータに使用しないでください。リコール指標はエンドツーエンドのQA精度を表すものではなく、公式のLongMemEval QAリーダーボードと直接比較できません。公開データセットは一般公開されたものであり、非公開の評価セットではありません。
データセットはこのリポジトリに含まれていません。パックを検証した後、Valを使用して、明示的に指定された分割データのみを取得してください:
val benchmark validate benchmarks/longmemeval/manifest.json
val benchmark fetch benchmarks/longmemeval/manifest.json --split public-evaluation
ValはSHA-256で識別されるキャッシュパスを出力します。そのパスをランナーに渡してください。--limitは一部のみを実行する診断用であり、ベンチマーク全体の結果として報告してはいけません:
pnpm benchmark:longmemeval -- --dataset "$HOME/.cache/val/benchmark-packs/objects/d6f21ea9d60a0d56f34a05b609c79c88a451d2ae03597821ea3d5a9678c3a442" --limit 5
pnpm benchmark:longmemeval -- --dataset "$HOME/.cache/val/benchmark-packs/objects/d6f21ea9d60a0d56f34a05b609c79c88a451d2ae03597821ea3d5a9678c3a442"
ランナーはファイルを読み込む前に、宣言された正確なサイズとSHA-256を再確認します。JSONデータセットをストリーミングし、デフォルトのK値1、3、5、10を評価します。また、上流の_abs質問ID接尾辞で示される回答拒否の質問を除外し、証拠セッションのマクロ/マイクロリコール、質問ヒット率、検索レイテンシの中央値/p95を出力します。上流のLongMemEvalの帰属表示とライセンスはマニフェストに記録されています。このパックは公式のLongMemEval結果ではありません。
開発
セットアップとテスト
git clone https://github.com/isiomaC/memstack.git
cd memstack
pnpm install
pnpm test # 407 core tests, no external services needed
pnpm test:packages # 78 package tests after dependency-ordered builds
pnpm test:e2e # 80 pass, 1 optional Chroma skip (requires Docker services)
pnpm test:e2e:run # Start services, run E2E once, preserve failure logs, clean up
pnpm smoke:artifacts # Built core, CLI, MCP, and server black-box checks
pnpm smoke:packages # Pack and install publishable tarballs in a clean project
pnpm smoke:docker # Build and exercise the server image
pnpm verify # Complete local verification pipeline
pnpm test:watch # Watch core tests
pnpm build:all # Build core and all workspace packages
pnpm check:all # Type-check core and all workspace packages
pnpm check:benchmark # Type-check the LongMemEval benchmark runner
CIでは安定したverificationジョブを公開しています。GitHubのブランチ保護で、mainの必須ステータスチェックとしてこのジョブを設定してください。
デバッグ
可観測性にはフックを使用してください。MemStack には組み込みのロギング機能はありません。
const ms = new MemStack({
llm,
hooks: {
onMemoryStored: (m) => console.debug("[memstack] stored:", m.id, m.content.slice(0, 80)),
onMemoryPruned: (ids) => console.debug("[memstack] pruned:", ids.length),
},
});
よくある問題:
| 症状 | 原因 | 対処方法 |
|---|---|---|
CONFIG_ERROR: LLM provider is required | LLM アダプターがありません | 任意の LLMProvider を config に渡してください |
| 検索結果が空 | actorId が正しくない、またはメモリが保存されていません | await ms.memory.count({ actorId }) を確認してください |
| セマンティック検索が機能しない | 埋め込みアダプターがない、または embedOnStore: false です | 埋め込みアダプターを追加するか、strategy: "recent" を使用してください |
| 本番環境でメモリ使用量が多い | InMemoryStorageAdapter を使用しています | Postgres/Redis などに対応する StorageProvider を実装してください |
| 要約の品質が低い | デフォルトのプロンプトが対象ドメインに合っていません | defaults config で summarizationPrompt を使用してください |
実行時の状態を確認する:
// How much data do we have?
const total = await ms.memory.count();
const perActor = await ms.memory.count({ actorId: "user-42" });
// What does one actor's memory look like?
const snapshot = await ms.export();
const actorMemories = snapshot.memories.filter(m => m.actorId === "user-42");
console.log(`User-42: ${actorMemories.length} memories`);
actorMemories.forEach(m => console.log(` [${m.memoryType}] ${m.content.slice(0, 60)} (imp: ${m.importance})`));
npm への公開
# Bump version, then:
pnpm build && pnpm check && pnpm test
npm login
npm publish --access public
@memstack スコープでは --access public が必要です。
貢献
特に必要とされている貢献:
- LLM アダプター:Google Gemini(ネイティブ)、Amazon Bedrock、Vertex AI
- 埋め込みアダプター:ローカル推論(transformers.js、ONNX)
- ベンチマーク:検索品質、レイテンシ、コストの比較
- Python 版:
pip install memstack
github.com/isiomaC/memstack で Issue を作成するか、PR を送ってください。
ライセンス
MIT © MemStack
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。
