Mastraでチャットに紐づくメタデータを保存する方法
senpai.nvimというNeovimのAIプラグインをMastraを使って開発しています。その際に調べた「Mastraでのチャット(スレッド)に紐づくメタデータの保存・取得方法」について、備忘録を残しておきます。
senpai.nvimの紹介については別の記事Neovimの履歴管理ができるAIプラグインsenpai.nvimの使い方に書いています。
※ 今回の記事ではMastraのClient SDKについては触れていません。
メタデータってどんな時に必要?
ユーザーにモデルを選ばせるタイプのアプリであれば「プロバイダー名・モデルID」を保存しておけば、スレッドを復元する時に同じモデルをセットできます。同様にシステムプロンプトの記録・復元にも便利です。つまるところ、後から使う予定があるデータをメタデータに入れておきます。
ちなみに、Mastraでは各スレッドの初回のメッセージ送信時にタイトルが自動的に生成されます。つまりタイトルの管理は自力でやる必要は無いです。変に苦労しないようにしましょうね(戒め)。
Mastraのメタデータ管理のしくみ
メタデータの保存にはMemoryを使います。
次のコードはUsing Agent Memoryより引用です。
import { Agent } from "@mastra/core/agent";import { Memory } from "@mastra/memory";import { openai } from "@ai-sdk/openai";
// Basic memory setupconst memory = new Memory();
const agent = new Agent({ name: "MyMemoryAgent", instructions: "You are a helpful assistant with memory.", model: openai("gpt-4o"), memory: memory, // Attach the memory instance});Memoryの実体はSQLの操作です。デフォルトだと、libSQLを使ってローカルにmemory.dbのようなファイルに保存されます。もちろん外部のDBにも保存できるようですが今回は割愛します。
メタデータの保存のタイミング
メッセージのやりとりが終わった後にチャットのタイトルなどが生成され、メモリへ保存されます。メタデータの保存はその後に発火されるonFinishというフックで処理するとよいでしょう。
streamのオプションにonFinishを渡します(generateでも同じオプションがあります)。
const agentStream = await agent.stream([userMessage], { threadId: command.thread_id, resourceId: "senpai", onFinish: async () => { // ここでメタデータを保存 },});updateThreadを使う
メモリに対してupdateThreadを実行します。次のコードは筆者が実際に使った例です。
// 初回メッセージのみメタデータを保存する場合の分岐const thread = await memory.getThreadById({ threadId: command.thread_id });let isFirstMessage = false;if (thread == null) { isFirstMessage = true;}
const agentStream = await agent.stream([userMessage], { threadId: command.thread_id, resourceId: "senpai", onFinish: async () => { if (isFirstMessage) { // タイトルを取得して渡さないと初期化される const thread = await memory.getThreadById({ threadId: command.thread_id, }); await memory.updateThread({ id: command.thread_id, title: thread.title, metadata: { provider: command.provider, system_prompt: command.system_prompt ?? "", }, }); } },});最初にgetThreadByIdを呼んでいるのはメタデータの保存を「スレッドの初回メッセージのみ」に限定しているためです。そういう縛りが必要ではない場合、分岐不要です。updateThreadの呼び出しにはタイトルも必須のため、onFinishの中でもgetThreadByIdを呼んで取得しています。
このupdateThreadはドキュメントには載っていなかったため、どこかのバージョンで変更や削除されるかもしれません。バージョンアップの際は要注意です。
保存したメタデータを取得する
保存したメタデータはgetThreadByIdで取得できます。
const result = await memory.getThreadById({ threadId: thread_id,});戻り値の型はStorageThreadType | nullです。次が実際に筆者が使った時の例です。
{ "id": "test-2025-04-14-1741", "resourceId": "senpai", "title": "I'll analyze the content of the src/presentation/hello.ts file in Japanese.", "metadata": { "provider": { "name": "openrouter", "model_id": "anthropic/claude-3.7-sonnet" }, "system_prompt": "" }, "createdAt": "2025-04-14T08:41:36.325Z", "updatedAt": "2025-04-14T08:41:36.325Z"}保存できる情報
実装を見ると、JSON.stringifyで変換されています。つまり、JSONで使えない値は入れられません(たとえばfunctionなど)。KVSみたいな感じですね。
型はRecord<string, unknown>です。メタデータを取得したらバリデーションしておくとよいでしょう。
以上、Mastraにおけるメタデータの扱い方でした。
アップデートで変わったりするものなので最新の情報はMastraのドキュメントや実装をご覧ください。
というのが今までの定石でしたが、MastraがドキュメントのMCP@mastra/mcp-docs-serverを提供しているため、そちらを利用するのもアリです。