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 setup
const 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を提供しているため、そちらを利用するのもアリです。