誰の発言か

誰が言ったかを知っている記憶。

人の記憶は人単位で動きます。Bob が何を約束したか、自分が何をすると言ったか。記憶ごとに話者を付ければ、エージェントも同じように覚えます。すべての Tablet・Scroll モデルで。

話者はストアと同じく明示的です。先に人を登録し、その名前で保存します。タイプミスが静かに新しい人になることはありません。ストアあたりまず 50 人まで登録でき(順次拡大予定)、"me" は登録も数えられることもありません。

1 つのチーム、3 つの記憶

1 つのストアが複数の声を混ざらないように保ちます。人を一度登録し、発言のたびに話者を付けて保存し、あとで人単位で尋ねてください。

addBob を一度登録します。POST /speakers、SDK では add_speaker("Bob") です。

ストアはもう Bob を知っています。50 人の上限はここ、登録時にのみ数え、store 呼び出しが上限エラーを返すことはありません。

BobBob が締め切りは火曜に延びたと言います。speaker "Bob" で保存します。

この記憶はもう Bob のものです。検索で返るたびにそう表示されます。

meアシスタントが金曜までに要約を送ると約束します。自分の言葉は speaker "me" で保存します。

自分の発言も記憶になり、"me" は話者の上限に数えられません。

ask後日:「Bob は締め切りについて何と言ってた?」speaker "Bob" で検索します。

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 正規化の後の完全一致なので、Bobbob は別人です。名前は 80 文字までです。
errors - verbatim
# 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
await mem.addSpeaker("Bob", "alice");  // once per person; "me" needs no registration
await mem.add("Bob said the deadline moved to Tuesday", "alice", { speaker: "Bob" });
await mem.add("I promised the summary by Friday", "alice", { speaker: "me" });
const hits = await mem.search("what did Bob say about the deadline?", "alice", 10, { speaker: "Bob" });
await mem.listSpeakers("alice");
await mem.removeSpeaker("Bob", "alice");  // memories stay, the tag goes
mem.add_speaker("Bob", "alice").await?;  // once per person; "me" needs no registration
mem.add("Bob said the deadline moved to Tuesday", "alice", json!({"speaker": "Bob"})).await?;
mem.add("I promised the summary by Friday", "alice", json!({"speaker": "me"})).await?;
let hits = mem.search_with("what did Bob say about the deadline?", "alice", 10, json!({"speaker": "Bob"})).await?;
mem.list_speakers("alice").await?;
mem.remove_speaker("Bob", "alice").await?;  // memories stay, the tag goes
curl -X POST https://api.wontopos.com/api/v1/memory/speakers \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","speaker":"Bob"}'   # once per person

curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"Bob said the deadline moved to Tuesday","metadata":{"speaker":"Bob"}}'

curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what did Bob say about the deadline?","speaker":"Bob"}'

curl "https://api.wontopos.com/api/v1/memory/speakers?user_id=alice" -H "X-API-Key: $WOS_API_KEY"

curl -X DELETE https://api.wontopos.com/api/v1/memory/speakers \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","speaker":"Bob"}'   # memories stay, the tag goes
response
{ "memories": [
    { "content": "Bob said the deadline moved to Tuesday",
      "speaker": "Bob", ... } ] }
GET /speakers
{ "user_id": "alice",
  "speakers": [ { "speaker": "Bob", "memories": 2, "created_at": "2026-07-10T04:20:39Z" } ],
  "count": 1, "limit": 50 }

一覧はストアが知っている人を、人ごとの記憶数と上限とともに表示します。解除は登録だけを消します。その人の記憶は残り、名前タグだけが外れます。

特定の話者の記憶を読む

by_speaker は、クエリなしで特定の人物の発言を新しい順に返します。"me" はアシスタント自身の発言を返します。ページングは画像と同じカーソル方式で、next_beforenext_skip_ids をそのまま返送します。

page = mem.by_speaker("Bob", limit=50)
page["memories"], page["chunks"]
const page = await mem.bySpeaker("Bob", undefined, { limit: 50 });
let page = mem.by_speaker("Bob", None, 50, None, None).await?;
curl -X POST https://api.wontopos.com/api/v1/memory/by-speaker \
  -H "X-API-Key: $WOS_KEY" \
  -d '{"user_id":"alice","speaker":"Bob","limit":50}'
フィールド動作
memories記憶を新しい順に返します。形式は検索が返すものと同じです。
chunksそれらの記憶の背後にある文単位の断片で、削除で実際に取り除かれる対象です。通常は記憶の件数より多くなります。削除を確定させる前に提示するための値です。points_to_delete としても報告されます。
next_beforenext_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
await mem.listSpeakers();
await mem.bySpeaker("Bob");
await mem.removeSpeaker("Bob");
mem.list_speakers(None).await?;
mem.by_speaker("Bob", None, None, None, None).await?;
mem.remove_speaker("Bob", None).await?;
curl -X GET    .../api/v1/memory/speakers   -d '{"user_id":"alice"}'
curl -X POST   .../api/v1/memory/by-speaker -d '{"user_id":"alice","speaker":"Bob"}'
curl -X DELETE .../api/v1/memory/speakers   -d '{"user_id":"alice","speaker":"Bob"}'