Qui l'a dit

Une mémoire qui sait qui l'a dit.

On se souvient par personne : ce que Bob a promis, ce que vous avez dit que vous feriez. Étiquetez chaque souvenir avec un locuteur et votre agent fait pareil, sur tous les modèles Tablet et Scroll.

Les locuteurs sont explicites, comme les magasins. Enregistrez d'abord la personne, puis stockez sous son nom : une faute de frappe ne devient jamais silencieusement une nouvelle personne. Un magasin enregistre jusqu'à 50 personnes pour commencer (nous comptons relever ce plafond), et "me" n'a jamais besoin d'enregistrement ni ne compte.

Une équipe, trois souvenirs

Un magasin garde les voix séparées. Enregistrez une personne une fois, sauvegardez chaque propos sous son locuteur, puis demandez par personne.

addEnregistrez Bob une fois : POST /speakers, ou add_speaker("Bob") dans les SDK.

Le magasin connaît désormais Bob. La limite de 50 se compte ici, à l'enregistrement ; les appels de stockage ne renvoient jamais d'erreur de limite.

BobBob dit que l'échéance passe à mardi. Enregistrez-le avec speaker "Bob".

Le souvenir appartient désormais à Bob : chaque recherche qui le renvoie l'indique.

meVotre assistant promet le résumé pour vendredi. Enregistrez ses propres mots avec speaker "me".

Ses propres mots sont aussi mémorisés, et "me" ne compte jamais dans la limite.

askPlus tard : « qu'a dit Bob sur l'échéance ? » Cherchez avec speaker "Bob".

Seuls les mots de Bob reviennent. Les mots d'une personne ne reviennent jamais comme ceux d'une autre.

Trois règles à retenir

  • "me", c'est l'assistant lui-même. Jamais enregistré, jamais compté. Réservé et en minuscules : speaker: "Me" ou "ME" renvoie 400 invalid_request_error au lieu d'être converti en silence.
  • La limite se compte à l'enregistrement : 50 par magasin pour commencer. Au-delà, l'enregistrement renvoie 400 invalid_request_error avec speaker_limit: 50 dans le corps d'erreur. Stocker avec un nom non enregistré renvoie aussi 400 et ne stocke rien. Filtrer la recherche par un nom non enregistré renvoie 404 not_found_error. Branchez sur le statut et les champs, pas sur le texte du message ; nous comptons relever la limite.
  • Les étiquettes vivent dans chaque lecture. Les résultats de recherche, le contexte long terme de recall et les résultats d'engram portent leur locuteur : le modèle sait toujours à qui appartiennent les mots qu'il tient. Passez speaker à une recherche pour n'obtenir que les mots d'une personne. Un supersede conserve le locuteur ; forget le supprime.
  • Les noms sont en Unicode : toutes les langues fonctionnent. さくら, Иван et 하늘 sont des locuteurs valides, et l'attribution se comporte de la même façon dans toutes les langues. La correspondance est exacte après trim et normalisation Unicode : Bob et bob sont deux personnes. Les noms vont jusqu'à 80 caractères.
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" } }

Deux notes de portée. speaker s'attache à add / store : add_turn mémorise l'échange entier, et les étiquettes par personne et le filtre viennent des souvenirs enregistrés avec un speaker explicite. Et les passages de session (expand) sont des composites de plusieurs souvenirs, donc sans étiquette ; un filtre speaker renvoie toujours des souvenirs atomiques et étiquetés. Et une écriture dont le sens est suffisamment proche d'un souvenir déjà stocké est abandonnée - la correspondance porte sur le sens, pas sur le texte : ce store renvoie status "duplicate" avec une note explicite, n'enregistre rien et n'attache aucun locuteur. Un fait réellement nouveau qui ne varie que par un détail d'un fait existant (« allergie aux crustacés » après « allergie aux arachides ») tombe sous la même règle : lisez status plutôt que de supposer que l'écriture a abouti.

Nous testons cela à la dure : des souvenirs sans aucun nom dans le texte, rappelés par personne. L'attribution vient du registre des locuteurs, pas de la correspondance de mots, donc le comportement est identique dans toutes les langues.

L'utiliser

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 }

La liste montre qui le magasin connaît, avec le nombre de souvenirs par personne face à la limite. Retirer n'efface que l'enregistrement : les souvenirs restent, seule l'étiquette part.

Lire les souvenirs d'une personne

by_speaker renvoie ce qu'une personne a dit, du plus récent au plus ancien, sans requête. "me" donne les mots de l'assistant lui-même. Même pagination par curseur que pour les images : renvoyez next_before et next_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}'
ChampCe qu'il fait
memoriesLes souvenirs, du plus récent au plus ancien. Même forme que ce que renvoie une recherche.
chunksLes fragments au niveau de la phrase derrière ces souvenirs - ce qu'une suppression retirerait réellement. Généralement plus nombreux que les souvenirs eux-mêmes ; à afficher avant toute confirmation de suppression. Également renvoyé sous points_to_delete.
next_beforeCurseur de la page suivante, avec next_skip_ids. Les deux sont nécessaires car plusieurs souvenirs peuvent partager un même horodatage.
speaker désigne ici l'étiquette écrite au moment du stockage, pas une recherche sur le texte. Un souvenir stocké sans locuteur reste accessible par recherche mais jamais par by_speaker, y compris sous "me".

Lister, parcourir et supprimer des locuteurs

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