Wer es gesagt hat

Gedächtnis, das weiß, wer es gesagt hat.

Menschen erinnern sich nach Person: was Bob versprochen hat, was du zugesagt hast. Gib jeder Erinnerung einen Sprecher und dein Agent tut dasselbe, auf jedem Tablet- und Scroll-Modell.

Sprecher sind explizit, wie Stores. Registriere die Person zuerst, speichere dann unter ihrem Namen: ein Tippfehler wird nie still zu einer neuen Person. Ein Store registriert zunächst bis zu 50 Personen (wir planen mehr), und "me" braucht nie Registrierung und zählt nie.

Ein Team, drei Erinnerungen

Ein Store hält viele Stimmen auseinander. Registriere eine Person einmal, speichere jede Äußerung unter ihrem Sprecher und frage später nach Person.

addRegistriere Bob einmal: POST /speakers, oder add_speaker("Bob") in den SDKs.

Der Store kennt Bob jetzt. Das 50er-Limit wird hier bei der Registrierung gezählt; Speicheraufrufe liefern nie einen Limit-Fehler.

BobBob sagt, die Deadline sei auf Dienstag gerutscht. Speichere es mit speaker "Bob".

Die Erinnerung gehört jetzt Bob: jede Suche, die sie zurückgibt, sagt das auch.

meDein Assistent verspricht die Zusammenfassung bis Freitag. Speichere seine eigenen Worte mit speaker "me".

Auch die eigenen Worte werden erinnert, und "me" zählt nie gegen das Limit.

askSpäter: "Was hat Bob zur Deadline gesagt?" Suche mit speaker "Bob".

Nur Bobs Worte kommen zurück. Die Worte einer Person kommen nie als die einer anderen zurück.

Drei Regeln zum Merken

  • "me" ist der Assistent selbst. Nie registriert, nie gezählt. Reserviert und kleingeschrieben: speaker: "Me" oder "ME" liefert 400 invalid_request_error statt stiller Umdeutung.
  • Das Limit zählt bei der Registrierung: zunächst 50 pro Store. Darüber hinaus liefert die Registrierung 400 invalid_request_error mit speaker_limit: 50 im Fehlerbody. Speichern mit unregistriertem Namen liefert ebenfalls 400 und speichert nichts. Suche mit unregistriertem Namen im Filter liefert 404 not_found_error. Verzweige auf Status und Felder, nicht auf den Meldungstext; wir planen, das Limit anzuheben.
  • Labels leben in jeder Leseoperation. Suchergebnisse, der Langzeit-Kontext von recall und Engram-Ergebnisse tragen ihren Sprecher — das Modell weiß immer, wessen Worte es hält. Mit speaker in der Suche kommen nur die Worte einer Person zurück. Ein supersede behält den Sprecher; forget entfernt ihn.
  • Namen sind Unicode: jede Sprache funktioniert. さくら, Иван und 하늘 sind gültige Sprecher, und die Zuordnung verhält sich in jeder Sprache identisch. Der Abgleich ist exakt nach Trim und Unicode-Normalisierung, also sind Bob und bob zwei Personen. Namen sind bis 80 Zeichen lang.
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" } }

Zwei Randnotizen. speaker gehört zu add / store: add_turn merkt sich den ganzen Austausch, Personen-Labels und der Filter kommen aus Erinnerungen mit explizitem speaker. Und Sitzungspassagen (expand) sind Komposite mehrerer Erinnerungen, tragen also kein Label; ein speaker-Filter liefert immer atomare, gelabelte Erinnerungen. Und ein Schreibvorgang, dessen Bedeutung einer gespeicherten Erinnerung nahe genug kommt, wird verworfen - entscheidend ist die Bedeutung, nicht der Wortlaut: ein solcher store liefert status "duplicate" mit expliziter note, speichert nichts und hängt keinen Sprecher an. Auch eine wirklich neue Tatsache, die sich nur in einem Detail von einer vorhandenen unterscheidet ("Schalentierallergie" nach "Erdnussallergie"), fällt unter dieselbe Regel - lesen Sie also status, statt anzunehmen, der Schreibvorgang sei angekommen.

Wir testen das auf die harte Tour: Erinnerungen ohne Namen im Text, abgerufen pro Person. Die Zuordnung kommt aus dem Sprecher-Register, nicht aus Wortabgleich, und verhält sich daher in jeder Sprache gleich.

So nutzt du es

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 }

Die Liste zeigt, wen der Store kennt, mit Erinnerungszahl pro Person gegen das Limit. Entfernen löscht nur die Registrierung: die Erinnerungen bleiben, nur das Namenslabel geht.

Die Erinnerungen einer einzelnen Person lesen

by_speaker gibt zurück, was eine Person gesagt hat, neueste zuerst, ohne Suchanfrage. "me" liefert die eigenen Äußerungen des Assistenten. Gleiche Cursor-Paginierung wie bei Bilder: Geben Sie next_before und next_skip_ids zurück.

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}'
FeldWas er tut
memoriesDie Erinnerungen, neueste zuerst. Gleiche Struktur, wie eine Suche sie zurückgibt.
chunksFragmente auf Satzebene hinter diesen Erinnerungen - was ein Löschvorgang tatsächlich entfernen würde. Üblicherweise größer als die Anzahl der Erinnerungen und vor einer Bestätigung anzuzeigen. Wird auch als points_to_delete gemeldet.
next_beforeCursor für die nächste Seite, zusammen mit next_skip_ids. Beides ist nötig, weil Erinnerungen denselben Zeitstempel haben können.
speaker ist hier die beim Speichern geschriebene Markierung, keine Suche über den Text. Eine ohne Sprecher gespeicherte Erinnerung ist über die Suche erreichbar, aber nie über by_speaker, auch nicht unter "me".

Sprecher auflisten, durchsehen und entfernen

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"}'