Quién lo dijo

Memoria que sabe quién lo dijo.

Las personas recuerdan por persona: qué prometió Bob, qué dijiste que harías. Etiqueta cada recuerdo con un hablante y tu agente hará lo mismo, en todos los modelos Tablet y Scroll.

Los hablantes son explícitos, como los almacenes. Registra primero a la persona y luego guarda bajo su nombre: una errata nunca se convierte en silencio en una persona nueva. Un almacén registra hasta 50 personas para empezar (pensamos subirlo), y "me" nunca necesita registro ni cuenta.

Un equipo, tres recuerdos

Un almacén mantiene separadas muchas voces. Registra a una persona una vez, guarda cada comentario con su hablante y luego pregunta por persona.

addRegistra a Bob una vez: POST /speakers, o add_speaker("Bob") en los SDK.

El almacén ya conoce a Bob. El límite de 50 se cuenta aquí, al registrar; las llamadas de guardado nunca devuelven un error de límite.

BobBob dice que la fecha límite pasó al martes. Guárdalo con speaker "Bob".

El recuerdo ahora es de Bob: cada búsqueda que lo devuelve lo indica.

meTu asistente promete el resumen para el viernes. Guarda sus propias palabras con speaker "me".

Lo que dice el asistente también se recuerda, y "me" nunca cuenta para el límite.

askDespués: "¿qué dijo Bob sobre la fecha límite?" Busca con speaker "Bob".

Solo vuelven las palabras de Bob. Las palabras de una persona nunca vuelven como las de otra.

Tres reglas para recordar

  • "me" es el propio asistente. Nunca se registra ni cuenta. Reservado y en minúsculas: speaker: "Me" o "ME" devuelve 400 invalid_request_error en lugar de convertirse en silencio.
  • El límite se cuenta al registrar: 50 por almacén para empezar. Registrar por encima devuelve 400 invalid_request_error con speaker_limit: 50 en el cuerpo del error. Guardar con un nombre sin registrar también devuelve 400 y no guarda nada. Filtrar la búsqueda por un nombre sin registrar devuelve 404 not_found_error. Ramifica por el código y los campos, no por el texto del mensaje; planeamos subir el límite.
  • Las etiquetas viven en cada lectura. Los resultados de búsqueda, el contexto de largo plazo de recall y los resultados de engram llevan su hablante, así que el modelo siempre sabe de quién son las palabras. Pasa speaker en una búsqueda para obtener solo las de una persona. Un supersede conserva el hablante; forget lo elimina.
  • Los nombres son Unicode: cualquier idioma funciona. さくら, Иван y 하늘 son hablantes válidos, y la atribución se comporta igual en todos los idiomas. La coincidencia es exacta tras recortar y normalizar Unicode, así que Bob y bob son dos personas distintas. Los nombres llegan hasta 80 caracteres.
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" } }

Dos notas de alcance. speaker va en add / store: add_turn recuerda el intercambio completo, y las etiquetas por persona y el filtro vienen de recuerdos con speaker explícito. Y los pasajes de sesión (expand) son compuestos de varios recuerdos, así que no llevan etiqueta; un filtro speaker siempre devuelve recuerdos atómicos y etiquetados. Y una escritura cuyo significado se acerca lo suficiente a una memoria ya guardada se descarta: la coincidencia es semántica, no textual. Ese store devuelve status "duplicate" con una nota explícita, no guarda nada y no adjunta hablante. Un hecho genuinamente nuevo que solo varía en un detalle de uno existente ("alergia al marisco" tras "alergia a los cacahuetes") cae bajo la misma regla, así que lea status en lugar de suponer que la escritura se realizó.

Lo probamos de la forma difícil: recuerdos guardados sin nombres en el texto, recuperados por persona. La atribución viene del registro de hablantes, no de coincidencias de palabras, así que se comporta igual en todos los idiomas.

Cómo usarlo

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 lista muestra a quién conoce el almacén con conteos por persona frente al límite. Quitar solo borra el registro: sus recuerdos quedan, solo se va la etiqueta.

Leer las memorias de una sola persona

by_speaker devuelve lo que dijo una persona, las más recientes primero, sin consulta. "me" devuelve las palabras del propio asistente. La misma paginación por cursor que las imágenes: devuelva next_before y next_skip_ids en la llamada siguiente.

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}'
CampoQué hace
memoriesLas memorias, las más recientes primero. La misma forma que devuelve una búsqueda.
chunksFragmentos a nivel de frase que hay detrás de esas memorias - lo que una eliminación borraría realmente. Suele ser mayor que el número de memorias. Muéstrelo antes de que alguien confirme una eliminación. También se informa como points_to_delete.
next_beforeCursor para la página siguiente, junto con next_skip_ids. Hacen falta los dos porque varias memorias pueden compartir la misma marca de tiempo.
speaker aquí es la etiqueta escrita al guardar, no una búsqueda sobre el texto. Una memoria guardada sin hablante es accesible por búsqueda, pero nunca por by_speaker, tampoco bajo "me".

Listar hablantes, recorrerlos y darlos de baja

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