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.
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.
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.
Le souvenir appartient désormais à Bob : chaque recherche qui le renvoie l'indique.
Ses propres mots sont aussi mémorisés, et "me" ne compte jamais dans la limite.
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"renvoie400 invalid_request_errorau 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_erroravecspeaker_limit: 50dans le corps d'erreur. Stocker avec un nom non enregistré renvoie aussi400et ne stocke rien. Filtrer la recherche par un nom non enregistré renvoie404 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 :
Bobetbobsont deux personnes. Les noms vont jusqu'à 80 caractères.
# 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{ "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 }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"]| Champ | Ce qu'il fait |
|---|---|
| memories | Les souvenirs, du plus récent au plus ancien. Même forme que ce que renvoie une recherche. |
| chunks | Les 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_before | Curseur 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