누가 한 말인지

누가 한 말인지 아는 기억.

사람의 기억은 사람 단위로 움직입니다. 밥이 뭘 약속했는지, 내가 뭘 하기로 했는지. 기억마다 화자를 달아주면 에이전트도 똑같이 기억합니다. 모든 태블릿·스크롤 모델에서 동일하게 동작합니다.

화자는 저장소처럼 명시적입니다. 사람을 먼저 등록하고 그 이름으로 저장합니다. 오타가 조용히 새 사람이 되는 일이 없습니다. 저장소당 시작 기준 50명까지 등록되고(차차 늘릴 예정), "me"는 등록도 카운트도 필요 없습니다.

한 팀, 기억 세 조각

저장소 하나가 여러 사람의 목소리를 섞이지 않게 지킵니다. 사람을 한 번 등록하고, 말이 나올 때마다 화자를 달아 저장한 뒤, 사람 단위로 물어보세요.

add밥을 한 번 등록합니다. POST /speakers, SDK에서는 add_speaker("Bob")입니다.

이제 저장소가 밥을 압니다. 50명 한도는 여기 등록에서만 세고, store 호출은 한도 에러를 반환하지 않습니다.

Bob밥이 마감이 화요일로 밀렸다고 말합니다. speaker "Bob"으로 저장합니다.

이제 이 기억은 밥의 것입니다. 검색에 돌아올 때마다 그렇게 표시됩니다.

me어시스턴트가 금요일까지 요약을 보내주기로 약속합니다. 자기 말은 speaker "me"로 저장합니다.

자기가 한 말도 기억이 되고, "me"는 화자 한도에 포함되지 않습니다.

ask나중에: "밥이 마감 뭐라고 했지?" speaker "Bob"으로 검색합니다.

밥이 한 말만 돌아옵니다. 한 사람의 말이 다른 사람의 말로 둔갑하는 일은 없습니다.

규칙은 세 개예요

  • "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하면 함께 지워집니다.
  • 이름은 유니코드라 어떤 언어든 됩니다. さくら, Иван, 하늘 전부 유효한 화자이고, 귀속 동작은 모든 언어에서 동일합니다. 매칭은 트림과 유니코드 정규화 후 정확 일치라 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_before다음 쪽 커서이며 next_skip_ids 와 짝입니다. 기억이 같은 시각을 가질 수 있어서 둘 다 필요합니다.
여기서 speaker 는 저장할 때 붙인 태그이지 본문을 뒤지는 검색이 아닙니다. 화자 없이 저장한 기억은 검색으로는 닿지만 by_speaker 로는 "me" 를 포함해 절대 안 나옵니다.

화자 목록·훑기·해제

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