誰が言ったかを知っている記憶。
人の記憶は人単位で動きます。Bob が何を約束したか、自分が何をすると言ったか。記憶ごとに話者を付ければ、エージェントも同じように覚えます。すべての Tablet・Scroll モデルで。
1 つのチーム、3 つの記憶
1 つのストアが複数の声を混ざらないように保ちます。人を一度登録し、発言のたびに話者を付けて保存し、あとで人単位で尋ねてください。
ストアはもう Bob を知っています。50 人の上限はここ、登録時にのみ数え、store 呼び出しが上限エラーを返すことはありません。
この記憶はもう Bob のものです。検索で返るたびにそう表示されます。
自分の発言も記憶になり、"me" は話者の上限に数えられません。
Bob の言葉だけが返ります。ある人の言葉が別の人の言葉として返ることはありません。
ルールは 3 つ
- "me" はアシスタント自身です。登録もカウントもされません。予約語で小文字のみ有効です。
speaker: "Me"や"ME"は暗黙変換されず、400 invalid_request_errorを返します。 - 上限は登録時に数えます。ストアあたりまず 50 人。超過登録は
400 invalid_request_errorを返し、エラー本文にspeaker_limit: 50が入ります。未登録の名前で store しても400で何も保存されません。未登録の名前で検索をフィルタすると404 not_found_errorです。メッセージ文字列ではなくステータスとフィールドで分岐してください。上限は引き上げる予定です。 - ラベルはすべての読み取りに付きます。検索結果、recall の長期コンテキスト、エングラムの結果のすべてが話者を伴うので、モデルは手にしている言葉が誰のものか常に分かります。検索に speaker を渡せばその人の言葉だけが返り、supersede では話者が引き継がれ、forget では一緒に消えます。
- 名前は Unicode で、どの言語でも使えます。さくら、Иван、하늘 はすべて有効な話者で、帰属の挙動はどの言語でも同一です。マッチングはトリムと Unicode 正規化の後の完全一致なので、
Bobとbobは別人です。名前は 80 文字までです。
# POST /speakers past the limit { "type": "error", "error": { "type": "invalid_request_error", "message": "This store already has 50 registered speakers, ...", "speaker_limit": 50 } } # store with an unregistered name → 400, nothing stored { "type": "error", "error": { "type": "invalid_request_error", "message": "speaker 'Bob' is not registered in this store. Register it first: ...", "speaker": "Bob" } } # search filtered by an unregistered name → 404 { "type": "error", "error": { "type": "not_found_error", "message": "speaker 'Bob' is not registered in this store.", "speaker": "Bob" } }
スコープの注意を二つ。speaker は add / store に付きます。add_turn はやり取りを丸ごと記憶し、人単位のラベルとフィルタは speaker を明示して保存した記憶から得られます。また、セッションパッセージ(expand)は複数の記憶の合成であるためラベルは付きません。speaker フィルタは常に原子単位のラベル付き記憶を返します。 また、保存済みの記憶と意味が十分に近い書き込みは破棄されます。文字列の一致ではなく意味で判定します。その store は status "duplicate" と明示的な note を返し、何も保存せず、話者も付きません。既存の記憶と一点だけ異なる新しい事実(「ピーナッツアレルギー」の次の「甲殻類アレルギー」)も同じ規則で破棄されるため、保存されたと仮定せず status を確認してください。
私たちはこの機能を厳しくテストしています。本文に名前が一切ない記憶を保存し、人単位で想起します。帰属は単語の一致ではなく話者の記録から来るため、どの言語でも同じように動作します。
使い方
mem.add_speaker("Bob", user_id="alice") # once per person; "me" needs no registration
mem.add("Bob said the deadline moved to Tuesday", user_id="alice", speaker="Bob")
mem.add("I promised the summary by Friday", user_id="alice", speaker="me")
hits = mem.search("what did Bob say about the deadline?", user_id="alice", speaker="Bob")
mem.list_speakers(user_id="alice")
mem.remove_speaker("Bob", user_id="alice") # memories stay, the tag goes{ "memories": [
{ "content": "Bob said the deadline moved to Tuesday",
"speaker": "Bob", ... } ] }{ "user_id": "alice",
"speakers": [ { "speaker": "Bob", "memories": 2, "created_at": "2026-07-10T04:20:39Z" } ],
"count": 1, "limit": 50 }一覧はストアが知っている人を、人ごとの記憶数と上限とともに表示します。解除は登録だけを消します。その人の記憶は残り、名前タグだけが外れます。
特定の話者の記憶を読む
by_speaker は、クエリなしで特定の人物の発言を新しい順に返します。"me" はアシスタント自身の発言を返します。ページングは画像と同じカーソル方式で、next_before と next_skip_ids をそのまま返送します。
page = mem.by_speaker("Bob", limit=50)
page["memories"], page["chunks"]| フィールド | 動作 |
|---|---|
| memories | 記憶を新しい順に返します。形式は検索が返すものと同じです。 |
| chunks | それらの記憶の背後にある文単位の断片で、削除で実際に取り除かれる対象です。通常は記憶の件数より多くなります。削除を確定させる前に提示するための値です。points_to_delete としても報告されます。 |
| next_before | next_skip_ids と組で使う次のページ用のカーソルです。記憶はタイムスタンプが重複しうるため、両方が必要です。 |
speaker は保存時に書き込まれたタグであり、本文に対する検索ではありません。話者なしで保存された記憶は検索では見つかりますが、"me" を含め by_speaker では取得できません。話者の一覧取得、閲覧、削除
mem.list_speakers() # who is registered
mem.by_speaker("Bob") # what Bob said, newest first
mem.remove_speaker("Bob") # unregister; the memories stay