왜 WOS인가

AI 에이전트를 위한 장기 기억.

WOS는 기억 API입니다. 사용자의 기억을 한 번 저장해두고, 매 쿼리마다 관련된 것만 회수해 모델의 프롬프트에 넣습니다.

검색은 순수 의미 기반이며 키워드·BM25 매칭이 없어, 언어가 달라도 검색 품질이 같습니다. 저장량과 무관하게 한 쿼리는 작고 제한된 컨텍스트로 돌아오고, 당신의 저장된 기억 위엔 어떤 모델도 돌지 않습니다.

주요 동작

  • store - 사용자의 기억 하나를 저장합니다.
  • recall - 쿼리에 필요한 기억을 한 번에 회수합니다. 주로 쓰는 호출입니다.
  • search - 저장된 기억에 대한 원시 의미 검색입니다.
  • supersede - 오래된 기억을 갱신하거나 교체합니다.
  • forget - 기억 하나 또는 사용자 전체를 삭제합니다 (GDPR).
왼쪽에서 항목을 선택하세요 각 주제를 자세히 볼 수 있습니다.
모델

세 개의 모델, 하나의 계보.

WOS 모델은 사람이 지식을 지녀 온 방식에서 이름을 땁니다 - Tablet, Scroll, Book. 돌판, 두루마리, 제본된 책. 뒤로 갈수록 엔진이 에이전트를 위해 해내는 일이 늘어납니다.

Tablet

사용 가능
돌에 새기다 · 저장과 회수

가볍고 빠르고 저렴하게 기억을 심고 꺼냅니다. 모든 모델이 위에 쌓이는 토대.

Scroll

사용 가능
두루마리를 펼치다 · LLM이 거드는 회수

언어 모델을 더해 질문을 더 깊이 읽고 더 넉넉한 컨텍스트를 데려와, 흩어진 근거가 하나 모자란 채가 아니라 함께 옵니다.

Book

다음
책을 펴다 · 스스로 길을 찾다

상황에 맞는 기억과 도구를 스스로 골라 펼칩니다. 쓸수록 길을 더 잘 찾습니다.

Tablet 1의 전체 벤치마크 리포트는 벤치마크 페이지에 있습니다.

비용

우리에게 $2, LLM에선 그 몇 배를 아낍니다.

WOS는 매 프롬프트에 전체 히스토리를 욱여넣는 대신, 쿼리당 ~1,200개의 관련 토큰만 LLM에 전달합니다. 그 격차는 막대하고, 히스토리가 커질수록 더 벌어집니다.

1,000 쿼리당 LLM 비용 Tablet 1 기준
사용자 히스토리100K
월 쿼리 수1,000
사용 LLM
45× 더 저렴 - 월 $244 절감
WOS 없이$250.00
WOS 사용$5.50

WOS에 쓴 $1마다 LLM에서 ~$98을 아낍니다. 히스토리가 크거나 모델이 비쌀수록 ROI가 커집니다.

절감이 나오는 곳

  • WOS 없이는 매 프롬프트에 전체 히스토리를 넣습니다 - 100K 토큰 × $2.50/1M = $0.25, 매 쿼리마다 (GPT-4o 입력 단가 기준, Opus급 모델은 약 2배).
  • WOS는 한 번만 적재($2/1M)하고, 이후 각 쿼리는 작은 검색($3/1M × 1,200)과 ~1,200 토큰에 대한 LLM 호출뿐입니다.
  • LLM이 읽는 토큰이 적을수록 비용이 줄고, WOS는 기억이 커져도 그 수치를 일정하게 유지합니다.
컨텍스트 축소 = 히스토리 ÷ 투입 토큰이며 비용 배수가 아닙니다(비용은 위 계산기).  25K → 21× · 100K → 83× · 200K → 167×.
다국어

모든 언어, 같은 정확도.

검색은 순수 의미 기반입니다 - 임베딩만 쓰고 키워드나 BM25 매칭은 전혀 없습니다. 그래서 사용자가 日本語, 中文, Español, English 무엇으로 적어도 검색 품질이 동일합니다.

BM25 같은 어휘 매칭은 특정 언어의 형태 - 형태소, 띄어쓰기, 문자 체계 - 에 맞춰 동작합니다. 여러 언어를 섞어 쓰는 저장소에서는 그만큼 언어별로 검색 품질이 달라진다는 뜻입니다. WOS는 어휘 매칭을 아예 사용하지 않아, 모든 언어가 같은 경로를 지납니다.

한 저장소에 세 언어를 동시에

저장소마다 언어를 정할 필요가 없습니다 - 자유롭게 섞으세요. 아래는 한 사용자의 기억에 일본어·영어·스페인어가 동시에 들어있고, 어떤 언어로 묻든 맞는 기억을 찾아오는 모습입니다. 라이브 API에서 실제로 주고받은 내용입니다:

# one user, three languages stored together
mem.add("彼女はコーヒーより紅茶が好き", user_id="alice")                      # Japanese
mem.add("she works at a design studio in Brooklyn", user_id="alice")       # English
mem.add("A ella le encanta hacer senderismo los sábados", user_id="alice")  # Spanish
실제 결과 - 질문마다 다른 언어의 기억을 찾아냄
"¿Qué bebe ella?"               -> 彼女はコーヒーより紅茶が好き
"what does she do on weekends?" -> A ella le encanta hacer senderismo los sábados
"彼女の仕事は?"                  -> she works at a design studio in Brooklyn

번역 단계도, 언어 감지도, 언어별 설정도 없습니다. 기억과 질문은 언어가 아니라 의미로 배치됩니다 - 맥락과 의미만 같으면 언어가 달라도 찾아냅니다.

여기 세 언어는 지면에 맞춘 것일 뿐 - 애초에 "지원 언어 목록"이라는 게 없습니다. 같은 라이브 테스트가 中文·Русский·العربية 기억으로도 통과했고, 전부 운영 API에서 실측한 것입니다.

키워드를 일부러 금지한 이유

BM25 같은 어휘 점수는 특정 언어를 더 강화하는 성질이 있어, 한 저장소에 여러 언어를 담는 사용에서는 방해가 됩니다. 그래서 엔진에서 완전히 제거했고 코드 리뷰에서 규칙으로 강제합니다: 어휘 점수가 경로에 끼면 언어별로 검색 품질이 달라지기 때문입니다.

LongMemEval은 영어 전용이라 다국어 검색 품질은 측정하지 않습니다. 다국어는 위 데모처럼 라이브 API에서 직접 확인할 수 있습니다.
구조

당신의 기억 위엔 모델이 돌지 않습니다.

저장은 말한 그대로 두고, 엔진은 임베딩으로 검색합니다 - 저렴하고 빠르고 결정론적입니다. 당신의 저장된 기억 위에선 어떤 모델도 돌지 않습니다. Tablet은 모델을 전혀 쓰지 않고, Scroll·Book은 더 나은 결과를 위해 엔진 둘레에 모델을 더하지만, 그 모델은 당신의 질문만 볼 뿐 저장한 것은 보지 않습니다.

  • 결정론적 엔진. 엔진은 같은 쿼리에 같은 기억을 반환합니다 - 그래서 벤치마크 편차가 리더 모델에서만 발생합니다.
  • 대규모에서도 저렴. 저장·검색에 생성 비용이 없어, 기억이 커져도 비용이 모델 사용량이 아니라 저장량을 따라갑니다.

당신의 말, 그대로

쓰기 시점에 언어 모델로 텍스트에서 "사실"을 추출해 다시 쓰는 설계도 흔합니다. 그 설계는 세 가지를 맞바꿉니다: 매 쓰기마다의 생성 비용, 추가 지연, 그리고 원문 대신 모델의 의역이 저장된다는 점. WOS는 반대쪽을 선택했습니다 - 말한 그대로 저장하고, 해석은 읽기 시점에 당신의 LLM이 원문을 들고 하게 합니다.

WOS가 아닌 것: 직접 운영해야 하는 벡터 DB도, 조립해야 하는 RAG 프레임워크도 아닙니다. 당신의 저장된 데이터 위엔 어떤 모델도 돌리지 않습니다 - 그 경로는 순수 임베딩입니다. Scroll과 Book은 더 나은 결과를 위해 언어 모델을 쓰지만, 그 모델은 당신의 질문만 볼 뿐 저장된 기억은 보지 않으며 - 당신의 데이터로 학습하거나 수집하지 않습니다.
근거

67.5%, 재어서 나온 숫자입니다.

BEAM 1M 에서 67.5%입니다. 5회 독립 실행 평균이고(σ 0.22%, 고른 회차 없음), gpt-4.1-mini 가 벤치마크 자체의 판정 프롬프트로 채점했습니다.

같은 벤치마크라도 채점 방식에 따라 점수가 크게 갈립니다. 평가자, 프롬프트, 그리고 검색 계층에 무엇까지 허용하느냐입니다. 저희는 저자 저장소에 실려 있는 평가자로 채점하고, 저자의 판정 프롬프트를 적힌 그대로 쓰며, 시험에 맞추려고 무엇도 바꾸지 않습니다. 하네스와 채점 코드, 리더 프롬프트를 공개하므로 누구든 67.5%를 그대로 재현할 수 있습니다.

측정 프로토콜 한눈에

항목우리 방식
데이터셋BEAM 1M - 대화 35개, 74,630턴, 기억 220만 건, 700문항
채점자gpt-4.1-mini, temperature 0. BEAM 자체의 판정 프롬프트를 그대로 돌립니다. 저자 저장소의 기본값이며 저희가 고른 평가자가 아닙니다
실행5회 독립 실행, 모든 점수 공개, 평균 보고 (σ 0.22%)
리더리더 모델·프롬프트 고정, 원문 그대로 공개

정직하게 유지하는 것: 제3자 채점자, 변경 없이 공개한 리더 프롬프트, 순수 의미 기반 검색, 그리고 최고 회차가 아니라 전 회차 보고. 검색 엔진은 결정론적이라 다시 돌려도 같은 기억이 나옵니다.

더 어려운 벤치로 올라갑니다

우리는 아직 정복하지 못한 가장 어려운 표준 벤치마크 위에서 겨룹니다 - 적힌 점수는 모든 WOS 모델이 세운 최고 기록이고, 더 나은 모델이 나올 때마다 새로 쓰입니다. 94%를 넘기는 순간, 더 어려운 벤치마크로 졸업합니다.

BEAM 1M진행 중
Tablet67.5%
gpt-4.1-mini 평가 · 5회 평균졸업까지 94%
이전 벤치마크 LongMemEval-S 통과
Tablet95.7%
Scroll92.3%
GPT-4o 채점 · 모든 WOS 모델 중 최고졸업까지 94%
전체 리포트 보기
가격

모델당 토큰 단가 둘,
요청당 $0.0001.

100만 토큰 단가에 요청당 $0.0001을 더해, 쓴 만큼만. 구독도, 저장료도, 기억 개수 제한도 없습니다. 에이전트가 쓰고 읽을 때만 내고 - 기억하고 있는 것에는 내지 않습니다.

모델입력 / 1M출력 / 1M
Tablet$2$3사용 가능
Scroll$4$8사용 가능
Book--미정
  • 요청당 $0.0001. 모든 API 호출에 붙는 정액 요금으로, 토큰 사용량에 더해집니다.
  • 저장은 무료입니다. 적재할 때 한 번 내면 보관은 공짜입니다. 개수 제한도, 보관 기간 제한도 없습니다.
  • 보관만 합니다. 학습하지 않고, 사용하지 않고, 보지 않습니다. 에이전트의 기억은 당신의 것 - 저희는 회수할 수 있게 정리만 합니다.
  • Tablet이 이렇게 싼 이유: 엔진이 모델을 돌리지 않아 원가가 임베딩과 디스크지 GPU가 아니기 때문입니다. Scroll·Book은 모델을 더하며, 그 비용이 더 높은 가격에 담겨 있습니다.
보관량에 월 단위로 과금하거나 플랜별로 기억 개수를 제한하는 과금 모델도 있습니다. WOS는 보관량·보관 기간과 무관하게 저장에 과금하지 않습니다.

사용량 티어별 한도 보기 →

개발자

세 번의 호출: 저장, 회수, 답변.

하나의 API. recall() 호출이 단기·장기 기억과 주변 컨텍스트를 한 번의 왕복으로 돌려줘, 프롬프트에 바로 넣을 수 있습니다.

1

저장

add()로 사실과 대화를 저장하세요. 사용자의 말, 어시스턴트 자신의 말(speaker "me"), 이름 붙은 사람의 말 모두 담을 수 있습니다. 들어오는 길에 임베딩되고, LLM 호출은 없습니다.

2

회수

recall()이 단기 + 장기 + 컨텍스트를 한 번에 반환합니다 - 고정 크기의 컨텍스트.

3

답변

그 제한된 컨텍스트를 당신의 LLM에 전달하세요 - 어떤 제공사든, 당신의 키로.

from wontopos import Client
mem = Client(api_key="wos-...")
mem.add("she prefers tea over coffee", user_id="alice")
mem.add("I suggested the jasmine tea", user_id="alice", speaker="me")  # its own words
# one call: short + long + context
ctx = mem.recall("what does alice drink?", user_id="alice")

기억에는 화자가 있습니다. 기본은 사용자의 말이고, speaker "me"는 어시스턴트 자신이 한 말을, "밥" 같은 이름은 사용자 주변 누가 한 말인지를 기억합니다. 그래서 사람 단위로 회수할 수 있습니다.

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

5분 안에 첫 recall.

키 하나, 설치 한 줄, 호출 세 번이면 에이전트에 기억이 생깁니다. 이 페이지의 모든 코드는 실제로 실행해 검증했고, 응답도 실물 그대로입니다.

1

API 키 발급

콘솔에서 키를 만듭니다. wos-live-로 시작하는 155자 키가 한 번만 표시됩니다. 환경변수로 보관하고, 코드에 직접 적지 마세요.

2

설치

pip install wontopos        # Python
npm install wontopos        # TypeScript / JavaScript
cargo add wontopos          # Rust
# curl - nothing to install, just set WOS_API_KEY
# latest: SDK v2.2.32 · MCP v1.0.15
3

저장소 만들고, 저장 & 회수

저장소는 저장·회수의 단위인 user_id입니다. 저장소는 명시적이라 먼저 만들고(아래 호출), 그 안에 저장·회수합니다. 저장 - 적재 시 임베딩, LLM 호출 없음. 회수 - 단기 + 장기 + 문맥을 한 번의 왕복으로.

from wontopos import Client

mem = Client(api_key="wos-live-...", user_id="alice")  # set the store once
mem.create_store()              # create it (stores are explicit)
mem.add("she prefers tea over coffee")  # no user_id needed

# one call → short-term + long-term + context
ctx = mem.recall("what does alice drink?")
import { Client } from "wontopos";

const mem = new Client({ apiKey: "wos-live-...", userId: "alice" });  // set the store once
await mem.createStore();            // create it (stores are explicit)
await mem.add("she prefers tea over coffee");  // no userId needed

// one call → short-term + long-term + context
const ctx = await mem.recall("what does alice drink?");
use wontopos::Client;

let mem = Client::new("wos-live-...").with_user("alice");  // set the store once
mem.create_store(None).await?;            // create it (stores are explicit)
mem.add("she prefers tea over coffee", None, json!({})).await?;

// one call → short-term + long-term + context
let ctx = mem.recall("what does alice drink?", None).await?;
# create the store once - stores are explicit
curl -X POST https://api.wontopos.com/api/v1/memory/collection \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice"}'

# store - embedded on the way in, no LLM call
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":"she prefers tea over coffee"}'

# one call → short-term + long-term + context
curl -X POST https://api.wontopos.com/api/v1/memory/recall \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what does alice drink?"}'
실제 응답 - create_store()
{"user_id": "alice", "status": "created"}
실제 응답 - add()
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}
저장소는 한 번만 지정. 클라이언트에 user_id를 주면 모든 호출이 그걸 써서 매번 안 적어도 됩니다; 호출마다 user_id를 넘기면 그 호출만 덮어씁니다. 저장소는 명시적: 없는 저장소에 저장·회수하면 404 - 먼저 만들어야 합니다. 모든 계정엔 default 저장소가 있어 user_id를 아예 안 줘도 바로 됩니다. 목록·관리는 Stores 참고.

recall()은 네 블록을 돌려줍니다 - short_term(최근 대화), long_term(관련 기억), context(가장 관련된 기억의 주변), 그리고 LLM에게 쓰는 법을 알려주는 instruction. 이 덩어리를 그대로 프롬프트에 넣으면 됩니다.

어떤 언어로든 동작합니다. 영어로 저장하고 한국어·일본어·중국어로 물어도 같은 기억이 나옵니다. 키워드 매칭이 아니라 임베딩 검색이기 때문입니다.

언어별 메서드 전부 보기 →

클라이언트 하나로 설정만 다르게

mem = Client.from_env()                 # reads WONTOPOS_API_KEY
scroll = mem.with_model("scroll-1.2")  # this copy only: another engine
alice  = mem.with_user("alice")       # this copy only: another default store
const alice = mem.withUser("alice");
const scroll = mem.withModel("scroll-1.2");
let alice = mem.with_user("alice");
let scroll = mem.with_model("scroll-1.2");
# curl has no copies — send model and user_id with each request
curl ... -d '{"user_id":"alice","model":"scroll-1.2","query":"…"}'
저장소

저장소 - 만들고, 보고, 지우기.

저장소는 저장·회수의 단위인 user_id - 최종 사용자·에이전트·주제마다 격리된 기억 공간 하나입니다. 저장소는 명시적이라 저장·회수 전에 먼저 만들어야 하고, 아니면 404가 돌아옵니다. 모든 계정엔 default 저장소가 기본으로 있어 만들지 않고도 바로 시작할 수 있습니다.

격리 구조. 계정이 워크스페이스를 갖고, 워크스페이스마다 기억·API 키·사용량이 격리됩니다(결제는 계정 공유). 저장소는 워크스페이스 안에 있어, 같은 워크스페이스의 키는 저장소를 공유하고 다른 워크스페이스끼리는 서로의 기억을 못 봅니다. 계정 → 워크스페이스 → 저장소(user_id) → 기억.
mem.create_store("alice")        # create (idempotent)
mem.list_stores()              # [{"user_id","created_at"}, ...]
mem.delete_store("alice")        # delete the store + all its memories
await mem.createStore("alice");
await mem.listStores();          // [{ user_id, created_at }, ...]
await mem.deleteStore("alice");     // store + all its memories
mem.create_store("alice").await?;
let stores = mem.list_stores().await?;
mem.delete_store("alice").await?;
# create
curl -X POST https://api.wontopos.com/api/v1/memory/collection \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" -d '{"user_id":"alice"}'
# list
curl https://api.wontopos.com/api/v1/memory/collections -H "X-API-Key: $WOS_API_KEY"
# delete (store + all its memories)
curl -X DELETE https://api.wontopos.com/api/v1/memory/collection \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" -d '{"user_id":"alice"}'
실제 응답 - create
{ "user_id": "alice", "status": "created" }   // "exists" if it already did
실제 응답 - list
{ "collections": [
  { "user_id": "default", "created_at": "2026-06-26T02:23:14Z" },
  { "user_id": "alice",   "created_at": "2026-06-26T02:24:01Z" }
], "count": 2 }
없는 저장소 회수 시
{ "error": { "type": "not_found_error",
  "message": "Store 'ghost' does not exist. Create it first with
              POST /api/v1/memory/collection {\"user_id\":\"ghost\"}, then store or recall." } }
사람별로 기억을 나누려면 최종 사용자마다 저장소 하나("alice", "user_42"), 개인 비서라면 default 하나면 됩니다. 코드 없이 콘솔에서도 저장소를 만들고 둘러볼 수 있습니다(Memory ids → Issue). 저장소 삭제는 영구적 - 그 안의 모든 기억이 사라집니다. 저장소 id는 저장 전에 접힙니다. 소문자로 낮추고 [a-z0-9_] 밖의 문자는 _가 되므로 Alice.Smithalice-smith는 같은 저장소입니다. 기존 id와 같은 형태로 접히는 두 번째 id는 조용히 공유되지 않고 409로 거절됩니다. id 자체도 [A-Za-z0-9][A-Za-z0-9._-]{0,63}를 만족해야 하므로 이메일 주소나 비라틴 이름은 저장소 id가 될 수 없습니다. 내부 식별자를 사용하십시오.

저장소 목록과 삭제

mem.list_stores()                # [{"user_id","created_at"}, …]
mem.delete_store("alice")      # the store and every memory in it
await mem.listStores();
await mem.deleteStore("alice");
let stores = mem.list_stores().await?;
mem.delete_store("alice").await?;
curl -X POST   .../api/v1/memory/collections -d '{}'
curl -X DELETE .../api/v1/memory/collection  -d '{"user_id":"alice"}'
리콜 캐싱

반복되는 리콜은 10분의 1 가격으로.

요청당 옵트인하면 WOS가 검색 결과를 쿼리 텍스트 기준으로 캐시합니다. 규칙은 LLM 프롬프트 캐싱과 동일한 프리픽스 방식입니다. 캐시가 살아 있는 동안 반복되거나 이어지는 쿼리는 이전 결과를 재사용하고, 캐시된 부분은 정상 토큰 단가의 10%로 청구됩니다.

태블릿·스크롤 한정. 캐싱은 현재와 미래의 모든 태블릿·스크롤 모델에서 동작합니다. Book은 지원하지 않습니다. Book은 기억 위에서 추론하고 호출 사이에 학습하기 때문에 같은 질문에도 답이 정당하게 달라질 수 있고, 캐시된 결과는 설계상 틀린 답이 됩니다. Book에 cache_control을 보내면 403으로 명확하게 알려드립니다.

대화 하나, 세 턴

에이전트가 기억과 계속 대화할 때 실제로 벌어지는 일입니다. 매 턴, 지금까지의 대화를 쿼리로 보내고 cache_control을 켭니다.

write턴 1 - "앨리스: 나 지난봄에 리스본으로 이사했어."

전체 쿼리를 검색하고 캐시합니다: 입력이 2배 (TTL 5분).

extend턴 2 - 같은 텍스트 + "밥: 거기 날씨는 어때?"

밥의 문장만 임베딩하고 검색합니다. 이전 부분은 0.1배, 새 문장은 2배, 캐시는 이제 그 문장에서 끝납니다.

hit턴 3 - 완전히 같은 쿼리를 다시 (재시도, 새로고침)

엔진 호출이 아예 없습니다. 전부 0.1배: 90% 할인입니다.

요율

동작토큰 과금의미
캐시 쓰기 - TTL 5분첫 요청입니다. 결과는 5분 동안 유지되며, 읽을 때마다 유효시간이 앞으로 밀립니다.
캐시 쓰기 - TTL 1시간첫 요청이며, 한 시간 동안 유지됩니다.
캐시 읽기 - 히트 또는 프리픽스 히트0.1×쓰기 이후의 모든 요청: 캐시된 부분은 정상 토큰 단가의 10분의 1로 계산됩니다.

얼마나 아끼나

구체적인 예시입니다. 에이전트가 3,000토큰짜리 대화를 쿼리로 보내고 5분 안에 10번 반복하거나 이어갑니다. 캐싱이 없으면 정가로 30,000 입력 토큰입니다. 5분 캐시를 켜면 첫 쓰기 6,000(2배) + 아홉 번의 캐시 읽기 약 2,700 = 청구 토큰 8,700, 71% 절약입니다. 대화가 길어질수록 절약도 커집니다.

프리픽스 규칙

매칭은 쿼리의 앞부분 기준입니다. 앞이 그대로이고 뒤에 새 텍스트만 붙으면 캐시된 부분을 재사용하고 새 부분만 검색합니다. 캐시된 텍스트 끝보다 앞에서 무언가 바뀌면 아무것도 재사용할 수 없습니다.

prefix match
cached    [ A B C D E F G ]

○   [ A B C D E F G ] E
✗   [ B C D E F G ] E

히트 - 앞이 그대로이고 E만 새로 들어왔습니다
미스 - 앞이 바뀌어서 전체 쿼리를 다시 검색하고 다시 캐시합니다

기억해야 할 규칙 세 가지

  • 연장하면 새 꼬리까지 다시 캐시합니다. [A B C D E F G] + E 다음에는 캐시가 E에서 끝납니다. 꼬리는 쓰기 요금으로 한 번 계산되고, 다음 턴은 A부터 E까지 전체를 프리픽스로 다시 매칭할 수 있습니다.
  • 요청당 연속된 프리픽스 하나입니다. 하나의 쿼리를 두 개의 캐시 조각으로 나눌 수 없고, 오직 앞부분만 매칭됩니다.
  • 쓰기는 즉시 무효화합니다. store, store-turn, bulk-store, forget, supersede, 저장소 삭제가 일어나면 그 저장소의 캐시는 버려집니다. 캐시된 답이 낡은 기억을 돌려주는 일은 없습니다.

켜는 법

hits = mem.search(
    "...the conversation so far...", user_id="alice",
    cache_control={"ttl": "5m"},   # or "1h"
)
const hits = await mem.search(
  "...the conversation so far...", "alice", 10,
  { cache_control: { ttl: "5m" } },   // or "1h"
);
let hits = mem.search_with(
    "...the conversation so far...", "alice", 10,
    serde_json::json!({"cache_control": {"ttl": "5m"}}),   // or "1h"
).await?;
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":"...the conversation so far...",
       "cache_control":{"ttl":"5m"}}'   # or "1h"
응답 - cache 객체가 무슨 일이 있었는지 알려줍니다
{ "memories": [ ... ],
  "cache": { "status": "hit",              // "write" | "hit" | "extend"
             "ttl": "5m",
             "cache_read_input_tokens": 412,
             "cache_creation_input_tokens": 0 } }

이 기능에 SDK가 꼭 필요한 것은 아닙니다. 캐싱은 HTTP 호출 하나에 붙는 필드 하나라서 어떤 프로그래밍 언어에서든 동작합니다. curl 탭이 만능 레시피이고, Python·TypeScript·Rust SDK는 똑같은 호출을 감싼 편의 도구일 뿐입니다.

캐싱은 워크스페이스 안에서 저장소별, 모델별로 격리되며 기본값은 꺼짐입니다. cache_control을 보내지 않으면 요청은 아무것도 달라지지 않습니다.
누가 한 말인지

누가 한 말인지 아는 기억.

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

화자는 저장소처럼 명시적입니다. 사람을 먼저 등록하고 그 이름으로 저장합니다. 오타가 조용히 새 사람이 되는 일이 없습니다. 저장소당 시작 기준 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"}'
개발자

이미지

기억은 이미지을 담을 수 있습니다. 엔진이 이미지을 색인하므로, 캡션·제목·대체 텍스트가 없어도 어떤 언어의 글 질의로 찾힙니다.

Tablet 2 이상에서 지원합니다. 이미지을 구현하지 않은 엔진은 맨 404 대신 그 사실을 이름으로 알려주므로, 없는 기능인지 없는 기억인지 구분할 수 있습니다. JPEG·PNG·GIF·WebP 를 받습니다.

저장하기

평소의 add 호출에 image 객체를 넘깁니다. content 는 비어도 되며, 그때는 이미지만으로 검색됩니다.

mem.add("at the beach", image={"data": b64})   # caption + image
mem.add("", image={"data": b64})               # the image IS the memory
// the image rides in the 4th argument; the 3rd is metadata
await mem.add("at the beach", undefined, {}, { image: { data: b64 } });
await mem.add("", undefined, {}, { image: { data: b64 } });   // the image IS the memory
let img = json!({"image": {"data": b64}});
mem.add_with("at the beach", None, json!({}), img.clone()).await?;
mem.add_with("", None, json!({}), img).await?;
curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"","image":{"data":"<base64>"}}'

data 는 필수입니다. data:image/jpeg;base64, 접두어와 base64·openssl 이 넣는 줄바꿈은 SDK 가 떼어냅니다.

필드하는 일
data이미지의 base64. 필수입니다. 크기 상한은 SDK 에 박힌 값이 아니라 서버 설정이며, /healthmemory.images.max_bytes 로 알려줍니다.
reference원본 사본이 어디 있는지. 문자열로 저장만 하고 저희가 가져가지 않습니다.
taken_atRFC3339, 보통 EXIF 에서 옵니다. event_date 가 비어 있으면 이 값이 채워서, 올린 때가 아니라 찍은 때 기준으로 정렬됩니다.

찾기

이미지 전용 검색은 없습니다. searchrecall 이 글과 이미지을 함께 순위 매겨 돌려줍니다.

가지고 있는 이미지 다루기

data, mime = mem.get_image(memory_id=mid)
page       = mem.list_images(limit=50)      # page["count"] = store total
mem.forget_image(memory_id=mid, preview=True)
const { bytes, contentType } = await mem.getImage(undefined, mid);
const page = await mem.listImages(undefined, { limit: 50 });
await mem.forgetImage(undefined, mid, { preview: true });
let (bytes, mime) = mem.get_image(None, mid).await?;
let page = mem.list_images(None, 50, None, None).await?;
mem.forget_image(None, mid, true).await?;
# original bytes — the one call on this plane that is not JSON
curl -X POST   .../api/v1/memory/image  -d '{"user_id":"alice","memory_id":"m_1"}'
curl -X POST   .../api/v1/memory/images -d '{"user_id":"alice","limit":50}'
curl -X DELETE .../api/v1/memory/image  -d '{"user_id":"alice","memory_id":"m_1","preview":true}'
호출하는 일
get_image원본 바이트를 (bytes, content_type) 로 돌려줍니다. 형식은 올릴 때 붙은 이름이 아니라 바이트에서 알아냅니다. 이미지이 없는 기억이면 빈 것을 주지 않고 오류를 냅니다.
list_images한 페이지를 최신순으로, 그리고 count 를 함께 줍니다. 페이지 크기가 아니라 저장소 전체 개수입니다. 페이징은 커서라 next_beforenext_skip_ids 를 되돌려주면 됩니다. 이미지이 같은 시각을 가질 수 있어서 둘 다 필요합니다.
forget_image이미지만 지우고 글은 남깁니다. 캡션 없이 저장한 이미지은 그 자체가 기억이라, 그 경우엔 기억도 함께 지워집니다.

forget_imagepreview=True 를 주면 아무것도 바꾸지 않고 memory_kept 만 돌려줍니다. iter_images 는 페이지를 대신 넘깁니다.

이미지 한 장의 값

이미지은 글과 같이 토큰으로 청구합니다. 토큰 = 픽셀 넓이 / 556.7. 긴 변이 1,568 px 을 넘으면 1,568 px 기준으로 세므로, 2,500 px 이미지과 1,568 px 이미지의 값이 같습니다.

이미지세는 크기토큰
700 × 700as sent881
1000 × 1000as sent1,797
1568 × 1568as sent4,417
1920 × 10801568 × 8822,485
2500 × 18751568 × 11763,313
2500 × 25001568 × 15684,417

이미지 한 장 상한은 4,417 토큰입니다. 호출 전에 상한을 잔액에서 잡고, 끝난 뒤 실측값으로 청구합니다. 실측값이 상한을 넘지 않습니다.

크거나 작은 이미지은 400 으로 거절합니다. 저희가 대신 크기를 바꾸지 않습니다. 두 변 모두 700 px 이상, 긴 변 2,500 px 이하여야 합니다. 700 px 아래에서는 임베더가 정액 최소를 받으므로 더 작은 이미지도 저장 원가가 같습니다. 보내기 전에 줄이십시오. 오류에 받은 크기와 필요한 크기가 함께 나옵니다.

몇 장이 돌아오는가

한 응답에 기본 1장, 최대 5장입니다. 5장이면 2만 토큰에 가깝습니다.

필드하는 일
max_images0에서 5. 한 응답이 실을 수 있는 이미지 수입니다. 기본 1. 0 이면 글만 돌아옵니다. 범위를 벗어나면 깎지 않고 거절합니다.
이미지에 붙인 영어 캡션은 영어 질의를 올리고 다른 언어 질의를 떨어뜨립니다. 14개 언어 평균 recall@5 11.4점입니다. 여러 언어로 검색한다면 캡션 없이 저장하십시오.
개발자

verify

verify 는 검색을 여러 번 돌게 합니다. 각 회차는 앞 회차가 돌려준 것을 제외하므로, 두 번째 회차는 첫 회차가 못 닿은 기억에 닿습니다.

Tablet 2 이상에서 지원합니다. 구현하지 않은 엔진에 요청하면 호출 전에 거절되므로, 조용히 아무 일도 하지 않은 회차에 요금이 붙는 일은 없습니다.

searchrecall 에 넣는 0~3 정수입니다. 추가 회차 수라서 3 이면 검색이 네 번입니다. 기본 0.

hits = mem.search("what did I eat", verify=3)
# the SDKs hand back the memories; `verify_used` is on the HTTP response (curl tab)
const hits = await mem.search("what did I eat", undefined, 10, { verify: 3 });
// the SDKs hand back the memories; `verify_used` is on the HTTP response (curl tab)
let hits = mem.search_opts("what did I eat", None, 10,
                            &SearchOpts { verify: Some(3), ..Default::default() }).await?;
// the SDKs hand back the memories; `verify_used` is on the HTTP response (curl tab)
curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what did I eat","verify":3}'
# → {"memories":[…], "verify_used":1}

그 과정에 언어 모델은 돌지 않습니다

회차마다 이미 돌려준 id 를 함께 보내고, 엔진은 그것을 제외하고 그 너머를 찾습니다. 질의를 다시 쓰지 않으므로 같은 요청에 같은 결과가 나오고, 모델 자격증명이 필요 없습니다. 한 번 더 쓸지는 호출자 코드가 정합니다.

보내는 것과 돌아오는 것

호출하는 일
verify0에서 3. 허용할 추가 회차 수입니다. 범위를 벗어나면 조용히 깎지 않고 400으로 거절합니다.
verify_used실제로 수행한 추가 회차 수입니다. 요청한 것보다 적을 수 있습니다.

새로 가져온 것이 없는 회차에서 멈추고, 안 쓴 회차는 청구하지 않습니다. 뒤쪽 회차가 실패하면 그때까지 모은 결과를 돌려줍니다.

첫 회차가 이미 답을 쥐고 있으면 추가 회차가 정확도를 낮출 수 있습니다. LongMemEval-S 의 단일 세션 사용자 유형은 4.2점 떨어집니다. 이득은 첫 검색이 빗나가는 빈도에 비례하므로 큰 저장소에서 큽니다.
부가기능 · Beta

MCP - AI 도구를 위한 기억

WOS의 본체는 API와 SDK입니다. MCP 서버는 그 위의 부가기능입니다. 같은 기억을, 당신이 만들지 않은 도구에 꽂습니다 - Claude Code, Claude Desktop, Cursor.

설치 한 줄이면 에이전트가 기억 도구 9개를 받아 스스로 씁니다. 기억은 계정에 있어 한 도구가 저장한 것을 다른 모든 도구가 회수합니다. SDK로 직접 만든 에이전트도 마찬가지입니다.

이걸로 뭐가 되나

  • 프로젝트를 기억하는 Claude Code. 결정, 버그 픽스, 선호 - 다음 세션에서 다시 설명할 필요 없이 그대로 회수됩니다.
  • ChatGPT에서 시작해 Claude에서 이어가기. 같은 저장소, 같은 기억 - 대화가 처음부터 다시가 아니라 도구를 건너 이어집니다.
  • 내 개인 에이전트도 같은 기억 안에. Claude Code가 배운 걸 SDK 에이전트가 회수하고, 에이전트가 저장한 걸 Claude Code가 되받아 회수합니다.

Claude Code, Claude Desktop, Cursor, Windsurf 등 모든 MCP 호스트에서 동작합니다. ChatGPT는 Actions + OpenAPI 스펙으로 같은 기억에 닿습니다.

설치

claude mcp add wontopos --env WONTOPOS_API_KEY=wos-live-... -- npx -y wontopos-mcp

에이전트는 아홉 개의 툴을 받습니다 - recall · remember · search · update · forget · list_memories · engram · stats · create_store - 각 툴 설명이 언제 쓸지까지 알려줘서 스스로 판단해 씁니다.

부가기능 자체는 무료이며 npm에 공개되어 있습니다. 과금은 이 툴이 호출하는 API 사용량에 평소 요금 그대로 적용됩니다. Node 18 이상과 콘솔에서 만든 API 키가 필요합니다.

개발자 문서 열기

부가기능

OpenAPI 스펙

API의 완전한 기계 판독용 지도 - 모든 엔드포인트, 요청, 응답, 에러.

OpenAPI는 HTTP API를 기계가 읽는 파일로 기술하는 업계 표준 형식입니다.

https://api.wontopos.com/openapi.json

이걸로 뭐가 되나

Postman: File → Import → URL 붙여넣기 - 모든 엔드포인트가 클릭 가능한 콜렉션으로 뜹니다. ChatGPT: GPT를 만들고 Action에 같은 URL을 붙여넣으세요. 코드 생성: openapi-generator -i .../openapi.json -g go로 우리가 안 만든 언어의 클라이언트도 뽑아냅니다.

Postman에 임포트하고, 우리가 안 만든 언어의 클라이언트를 생성하고, ChatGPT Actions를 연결하고, CI에서 계약 검증을 돌리세요. 테스트가 실제 라우트에 고정해 두니 어긋날 수 없습니다.

부가기능

llms.txt

AI가 읽을 수 있는 텍스트 한 장에 담긴 API 전체.

llms.txt는 웹 관습입니다: 사이트 루트에 두는 순수 텍스트 한 장으로, AI에게 제품에 대해 필요한 전부를 알려줍니다.

https://wontopos.com/llms.txt

IDE나 코딩 에이전트에 넣으면 WOS 위에 어떻게 빌드하는지 바로 압니다 - 인증, 엔드포인트, 패턴, 에러까지. 릴리스마다 갱신됩니다.

OpenAPI 스펙과 같은 사실, 다른 청중입니다: 스펙은 도구를 위한 정밀한 구조이고, 이 파일은 AI나 사람이 한 번에 읽는 문서입니다. 둘 다 릴리스마다 함께 갱신됩니다.

Model Context Protocol · Beta

모든 AI 도구 안의 내 기억

명령 한 줄이면 Claude Code, Claude Desktop, Cursor 등 모든 MCP 호스트가 WOS 계정 기반 장기기억을 갖습니다. 통합 코드는 필요 없습니다. 에이전트가 기억 도구 9개를 받아 스스로 판단해 씁니다.

MCP는 베타입니다. 아홉 개의 툴은 지금도 동작하고 테스트돼 있지만, 완성해 가는 동안 표면이 바뀔 수 있습니다. 그 아래의 API와 SDK는 안정적이고 버전 관리됩니다.

설치

Claude Code는 한 줄입니다 (키는 콘솔에서 먼저 발급):

claude mcp add wontopos --env WONTOPOS_API_KEY=wos-live-... -- npx -y wontopos-mcp
# pick which store it remembers into (optional): add --env WONTOPOS_USER_ID=my-project
# ~/.cursor/mcp.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
# .vscode/mcp.json
{ "servers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
# ~/.codeium/windsurf/mcp_config.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
# Claude Desktop and any other MCP host
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }

Add to Cursor →  ·  Add to VS Code →

선택 env: WONTOPOS_USER_ID는 기본 저장소, WONTOPOS_MODEL은 엔진, WONTOPOS_BASE_URL은 셀프호스팅 배포를 지정합니다. WONTOPOS_READ_ONLY=1은 읽기 전용으로 전환합니다(회수·검색·조회만).

저장소를 공유하기 전에 알아둘 것

  • 전용 키를 쓰세요. 키는 워크스페이스를 품고 있어서, MCP 전용으로 만든 키는 연결된 도구들이 닿을 수 있는 범위 자체를 좁힙니다 - 앱 키는 건드리지 않고 콘솔에서 언제든 회전할 수 있습니다.
  • 읽기 전용 모드. WONTOPOS_READ_ONLY=1이면 쓰기 툴이 아예 등록되지 않습니다: 에이전트는 회수·검색·기억 목록·engram 실행·통계 조회만 할 수 있고 저장·갱신·삭제는 불가능합니다. 기억을 소유하는 게 아니라 참고만 해야 하는 에이전트에 맞습니다.
  • 툴 실행 확인은 켜두세요. MCP 호스트는 기본적으로 툴 실행 전에 물어봅니다 - 특히 forget은 켜두세요. 삭제는 그 저장소의 모든 도구에 공유되기 때문입니다.
  • 저장된 것은 키를 가진 모든 도구가 회수할 수 있습니다. 비밀 - API 키, 비밀번호 - 은 절대 기억으로 저장하지 마세요.
  • 회수된 기억은 데이터지 지시가 아닙니다. 툴 설명이 에이전트에게 이걸 명시적으로 말해줍니다. 그래도 자율 에이전트가 따르는 저장소에는 신뢰할 수 없는 제3자 텍스트를 기억으로 저장하지 마세요.
  • 삭제도 공유됩니다. 한 도구에서의 forget·delete_all은 모든 도구에서 사라지는 것입니다.
  • "me"는 그 저장소에 쓰는 에이전트 자신을 뜻합니다. 여러 에이전트가 한 저장소를 공유하면 "me"의 목소리가 섞입니다. 정체성을 구분하려면 에이전트마다 저장소를 따로 지정하세요(WONTOPOS_USER_ID).
  • 과금은 계정 하나로 모입니다. 연결된 모든 도구가 같은 잔액과 속도 한도에서 차감됩니다.

그다음은 대화만 하면 됩니다

you우리 금요일마다 배포하는 거 기억해둬

에이전트가 remember 툴을 호출합니다. 저장소에 영구 저장되어 세션이 끝나도 사라지지 않습니다.

new session우리 언제 배포하지?

새 세션엔 대화 기록이 0입니다. 에이전트가 recall을 호출해 기억으로 답합니다: 금요일.

이렇게 말해보세요

  • "이 레포는 pnpm 써, 기억해둬" → remember가 저장하고, 다음 세션은 이미 알고 있습니다.
  • "지난주에 정한 에러 응답 형식이 뭐였지?" → recall이 그 결정을 컨텍스트로 다시 불러옵니다.
  • "사실 마감이 금요일로 바뀌었어" → 에이전트가 회수한 기억과 어긋난 걸 알아채고 update로 그 기억을 제자리에서 고칩니다.
  • "그거 잘못 기억한 거야, 지워" → 에이전트가 기억 id를 찾아 forget을 호출합니다. 실행 전에 호스트가 확인을 요청합니다.
  • "나에 대해 뭘 기억해?" → list_memories가 저장된 걸 전부 훑어서, 에이전트가 답하거나 정리할 수 있습니다.

특별한 명령어 문법은 없습니다 - 위 예시는 전부 평범한 문장입니다. 에이전트가 각 툴 설명을 읽고 스스로 고릅니다.

아홉 개의 툴

  • recall - 한 번의 호출로 컨텍스트: 최근 턴 + 관련 장기기억. 과거 맥락이 필요할 때 가장 먼저 호출하도록 툴 설명에 명시해 두었습니다.
  • remember - 지속될 사실·결정을 저장. speaker: "me"는 에이전트 자신의 말, 등록된 이름은 그 사람의 말로 남습니다.
  • search - 의미 검색. 사람별 speaker 필터와, 시간·주제로 범위를 좁히는 filters("6월에 뭘 정했지?") — 의미만으로는 좁힐 수 없는 축이 바로 '언제'다.
  • update - 사실이 바뀐 기억을 새 내용으로 대체합니다. 지우지 않고 흔적을 남깁니다.
  • forget - id로 기억 하나를 삭제.
  • list_memories - 저장된 것을 페이지 단위로 훑습니다. "나에 대해 뭘 기억해?"에 답하거나 정리할 때 씁니다.
  • engram - 검색 한 번으로 부족할 때 내장 멀티홉 파이프라인(deep_recall, timeline, gather)을 실행합니다.
  • stats - 저장소에 얼마나 들어 있는지 봅니다. 정리 전에, 그리고 쓰기가 실제로 들어갔는지 확인할 때 씁니다.
  • create_store - 저장소는 명시적입니다. 최종 사용자·프로젝트·에이전트마다 하나씩.

SDK랑 MCP, 뭐가 다르지?

  • SDK는 당신이 짜는 앱 안에 들어갑니다. 언제 저장하고 무엇을 회수할지 당신의 코드가 정확히 결정합니다 - 결정론적이고, 타입이 있고, 버전 관리됩니다. 제품을 만든다면 SDK입니다.
  • MCP는 당신이 만들지 않은 AI 도구에 꽂습니다. 기억을 언제 쓸지는 에이전트가 툴 설명을 보고 판단합니다 - 코드 0줄. Claude Code·Claude Desktop·Cursor에, 혹은 완성된 어시스턴트에 기억을 달 때 맞습니다.

밑은 같은 API, 같은 저장소입니다 - SDK로 만든 앱과 MCP로 붙인 Claude Code 세션이 하나의 기억을 공유합니다. 양자택일이 아니라 표면마다 골라 쓰는 것입니다.

모든 도구를 가로지르는 하나의 기억

기억은 도구가 아니라 계정에 속합니다. ChatGPT(Actions + OpenAPI 스펙)에서 쓴 저장소가 Claude Code에서, 당신의 에이전트에서 그대로 회수되고, 반대도 됩니다. 한 도구에서 시작한 대화가 다른 도구에서 이어집니다.

그리고 하나의 저장소이기 때문에, Claude Code에 붙여 쓰다가 그대로 개인 에이전트와 대화를 이어갈 수 있습니다. 같은 키·같은 저장소의 SDK 에이전트는 Claude Code가 방금 배운 걸 전부 회수하고, 에이전트가 저장한 건 다음 세션의 Claude Code가 회수합니다.

로컬에서 stdio로 돕니다(npx wontopos-mcp). 이 방식에서는 키가 당신의 환경에만 있고 MCP 세션의 일부로 저희에게 전송되지 않습니다. TypeScript SDK를 감싼 것이라 자동 재시도·리다이렉트 거부·키 마스킹이 그대로 적용됩니다.
Model Context Protocol · Beta

Claude Code

대표 경로입니다. 터미널 명령 한 줄이면 모든 세션이 기억과 함께 시작됩니다.

  1. 콘솔에서 API 키를 만드세요. 키는 워크스페이스를 품고 있어서, 키 하나 = 기억 공간 하나입니다.
  2. 서버를 등록하세요. --scope user면 모든 프로젝트에서 쓸 수 있고, 없으면 현재 프로젝트에서만 보입니다.
  3. 확인: Claude Code 안에서 /mcp를 실행하면 wontopos가 툴 9개와 함께 떠야 합니다.
  4. 자동화 팁: CLAUDE.md에 "과거 맥락이 필요하면 wontopos recall을 먼저 호출" 한 줄을 넣으면, 시키지 않아도 매 세션이 기억과 함께 시작합니다.
claude mcp add wontopos --scope user \
  --env WONTOPOS_API_KEY=wos-live-... -- npx -y wontopos-mcp
# pick a store (optional): add --env WONTOPOS_USER_ID=my-project
Model Context Protocol · Beta

Claude Desktop

아래 블록을 claude_desktop_config.json에 넣고(설정 → 개발자 → Edit Config) 앱을 재시작하면 툴 9개가 나타납니다. 참고: 웹·모바일 claude.ai는 원격 MCP 서버가 필요한데 WOS는 아직 제공하지 않습니다 - 데스크톱 앱이 지원 경로입니다.

# claude_desktop_config.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
Model Context Protocol · Beta

Cursor

아래 블록을 ~/.cursor/mcp.json에 넣거나 원클릭 버튼을 누르고 Cursor를 재시작하세요. 에이전트가 툴 9개를 집어 듭니다.

# ~/.cursor/mcp.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }

Add to Cursor →

Model Context Protocol · Beta

VS Code

VS Code(Copilot 에이전트 모드)는 프로젝트의 .vscode/mcp.json에서 MCP 서버를 읽습니다. 아래 블록을 넣거나 원클릭 버튼을 누르세요.

# .vscode/mcp.json
{ "servers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }

Add to VS Code →

Model Context Protocol · Beta

Windsurf

Windsurf(Cascade)는 ~/.codeium/windsurf/mcp_config.json을 읽습니다. 아래 블록을 넣고 리로드하면 같은 툴 9개가 나타납니다.

# ~/.codeium/windsurf/mcp_config.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
Model Context Protocol · Beta

ChatGPT

ChatGPT의 MCP 커넥터는 원격 서버만 받아서, 지금의 지원 경로는 커스텀 GPT의 Action입니다: GPT를 만들고 Action을 추가한 뒤 아래 OpenAPI 스펙 URL을 붙여넣고 API 키를 인증 헤더로 설정하세요. 그러면 그 GPT가 다른 도구들과 같은 기억을 호출합니다.

# GPT → Configure → Actions → Import from URL
https://api.wontopos.com/openapi.json
# Authentication: API Key · Header name: X-API-Key

같은 저장소, 같은 기억입니다. ChatGPT가 Action으로 저장한 걸 Claude Code가 MCP로 회수하고, 반대도 됩니다.

Model Context Protocol · Beta

Gemini CLI

Gemini CLI는 ~/.gemini/settings.json에서 MCP 서버를 읽습니다. 아래 블록을 넣고 CLI를 재시작하면 같은 툴 9개가 거기서도 나타납니다.

# ~/.gemini/settings.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
Python SDK

Python - 모든 메서드, 세 그룹.

쓰고, 읽고, 지우고. 아래 모든 예제는 2026-08-01 라이브 API에서 실제로 실행했고, 응답은 실물 그대로입니다.

pip install wontopos
from wontopos import Client

mem = Client(api_key="wos-live-...")  # or read from an env var

모델 선택

API 키는 어느 기억(당신 계정)을, 모델은 어느 엔진이 그 기억을 읽을지 정합니다. 모든 모델이 기억을 공유하므로, 한 모델로 저장하고 다른 모델로 회수할 수 있습니다. 클라이언트에 기본값을 두고, model=로 호출마다 바꾸세요.

mem = Client(api_key="wos-live-...", model="tablet-1")  # default engine
mem.recall("...", user_id="alice")                  # tablet-1
mem.recall("...", user_id="alice", model="scroll-1")  # or pick a model per call

list_models

카탈로그 - model에 넣을 수 있는 id와 각 모델의 가용 여부. memory: "shared"는 같은 저장소를, "isolated"는 자기 저장소를 씁니다. API 키가 필요 없습니다.

mem.list_models()
실제 응답
[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"},
 {"id": "tablet-2", "name": "Tablet 2", "available": true, "memory": "shared"},
 {"id": "scroll-1", "name": "Scroll 1", "available": true, "memory": "shared"},
 {"id": "scroll-1.2", "name": "Scroll 1.2", "available": true, "memory": "shared"}]

ping

연결과 API 키가 유효한지 한 줄로 확인합니다.

mem.ping()   # True, or raises AuthenticationError / PaymentRequiredError

위 카탈로그는 항상 지금 쓸 수 있는 모델만 보여줍니다 - 다른 id를 넣으면 명확한 에러가 돌아옵니다. 새 모델은 출시되면 자동으로 거기에 나타납니다.

쓰기

add

기억 하나를 저장합니다. 적재 시 임베딩 - LLM 호출이 없어 임베딩 비용만 듭니다.

mem.add("she prefers tea over coffee", user_id="alice")
mem.add("I promised the summary by Friday", user_id="alice", speaker="me")  # its own words - no registration needed
실제 응답
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

add_turn

대화 한 턴(사용자 + 어시스턴트)을 단기·장기 기억에 한 번에 저장합니다.

mem.add_turn("hi", "hello!", user_id="alice")
실제 응답
{"status": "ok"}

speaker

모든 기억에 누가 한 말인지 담을 수 있습니다. 사람은 한 번 등록하고, 그다음부터 이름을 speaker로 넘기세요. "me"(어시스턴트 자신의 말)는 등록이 필요 없습니다. 검색에도 speaker를 주면 그 사람의 말만 회수합니다.

mem.add_speaker("Bob", user_id="alice")  # once per person; "me" needs no registration
mem.add("I promised to send the report on Friday", user_id="alice", speaker="me")
mem.add("Bob said the deadline moved to Tuesday", user_id="alice", speaker="Bob")
mem.search("what did Bob say about deadlines?", user_id="alice", speaker="Bob")
response
[{"content": "Bob said the deadline moved to Tuesday", "speaker": "Bob", ...}]
화자는 저장소처럼 명시적입니다. 사람을 먼저 등록하고 그 이름으로 저장합니다. 오타가 조용히 새 사람이 되는 일이 없습니다. 저장소당 시작 기준 50명까지 등록되고(차차 늘릴 예정), "me"는 등록도 카운트도 필요 없습니다.

add_bulk

긴 텍스트를 한 번에 적재합니다. 서버에서 청크 분할 + 임베딩 - 기존 히스토리 이관에 적합합니다.

mem.add_bulk("Alice moved to Brooklyn in March. She works at a design studio downtown.", user_id="alice")
실제 응답
{"elapsed_secs": 0.154154944, "status": "ok", "stored": 1, "total_chunks": 1}

update

사실이 바뀌었을 때. 옛 기억은 superseded 로 마킹되어 보존되고, 새 기억이 회수에서 그 자리를 차지합니다.

mem.update("576700aa-...", "she switched to coffee this year", user_id="alice")
실제 응답
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
 "old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}

읽기

search

의미 검색, 관련도 순. 순수 임베딩 - 키워드 매칭이 없어 어떤 언어로 물어도 기억을 찾습니다. SDK는 memories 배열을 바로 돌려주며, 아래는 HTTP 원문입니다. 자기 레인이 있는 모델(Scroll 1.2 이상)은 두 레인으로 답하고 SDK가 둘을 합쳐 돌려주므로, 배열이 max_results보다 많을 수 있습니다. 요청한 숫자가 아니라 받은 배열을 기준으로 프롬프트 크기를 잡으십시오.

r = mem.search("what does she drink?", user_id="alice", limit=1)
실제 응답 (HTTP 원문)
[{
   "id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624",
   "content": "she prefers tea over coffee",
   "category": "general",
   "time_bucket": "2026-06",
   "importance": 0.3,
   "similarity": 0.6316057443618774,
   "is_superseded": false,
   "superseded_by": null,
   "created_at": "2026-07-10T04:20:39.688276876Z"
 }]
필드의미
similarity질문과의 임베딩 유사도 (0–1).
is_supersededupdate()로 교체된 기억이면 true.
search_ms서버 검색 소요 시간.

recall

한 번의 왕복으로 LLM에 필요한 모든 것을 돌려줍니다 - 결과를 프롬프트에 그대로 넣으면 됩니다. 저장량과 무관하게 항상 고정 크기입니다.

ctx = mem.recall("what does she drink?", user_id="alice")
실제 응답 (구조 - 목록 축약)
{"short_term":  {"count": 2, "turns": [{"role": "user", "content": "hi", ...}]},
 "long_term":   {"count": 4, "memories": [{"content": "she prefers tea over coffee",
                                           "similarity": 0.63, ...}]},
 "context":     {"count": 4, "around_top_memory": [
                  "[match] she prefers tea over coffee",
                  "[after] Alice moved to Brooklyn in March. ..."]},
 "instruction": "Use short_term for recent context, long_term for relevant
                 past memories, context for surrounding conversation of the
                 most relevant memory."}

history

최근 대화 턴(단기 기억), 오래된 것부터.

turns = mem.history("alice")
실제 응답 (HTTP 원문)
{"count": 2, "turns": [
   {"role": "user",      "content": "hi",     "timestamp": "2026-07-10T04:20:40.989011337Z"},
   {"role": "assistant", "content": "hello!", "timestamp": "2026-07-10T04:20:40.989013416Z"}
 ], "user_id": "alice"}

stats

한 사용자의 기억 통계.

mem.stats("alice")
실제 응답
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}

get

id로 기억 하나를 조회합니다 - add나 list_memories가 돌려준 그 id입니다. 저장한 원문과 메타데이터만 반환하고 벡터는 반환하지 않습니다. 다른 스토어의 id나 삭제·무효화된 기억은 404입니다.

m = mem.get("alice", memory_id="576700aa-...")
response
{"id": "576700aa-...", "content": "she prefers tea over coffee",
 "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}
실제 응답
{"memory": {"id": "8bd090de-...", "content": "the office moved to the seventh floor in June",
  "category": "general", "created_at": "2026-07-31T18:20:30.531518060+00:00", "event_date": null,
  "is_superseded": false, "superseded_by": null}, "user_id": "docs_livetest"}

list_memories

스토어에 저장된 기억을 나열합니다. 저장한 원문과 메타데이터만 반환하고 벡터는 포함하지 않습니다. 커서로 페이지를 넘깁니다 - 응답의 next_cursor를 다음 호출에 넘기면 됩니다.

page = mem.list_memories("alice", limit=100)
response
{"count": 2, "next_cursor": null, "memories": [
   {"id": "576700aa-...", "content": "she prefers tea over coffee",
    "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}
 ]}

iter_memories · export_memories

커서를 직접 관리하지 않고 모든 기억을 순회하거나, 스토어 전체를 한 번에 가져옵니다.

for m in mem.iter_memories("alice"):   # every page, no cursor bookkeeping
    print(m["id"], m["content"])
everything = mem.export_memories("alice")   # the whole store as a list

삭제

delete

기억 하나를 id로 삭제합니다.

mem.delete("alice", memory_id="576700aa-...")
실제 응답
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}

delete_all

한 사용자의 모든 기억을 삭제 - 한 번의 호출, GDPR 대응.

mem.delete_all("alice")
실제 응답
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}

오류와 신뢰성

모든 실패는 타입이 있는 오류입니다. 상황별(rate limit·인증·결제)로 골라 잡거나, 기본 WosError로 한꺼번에 잡습니다.

from wontopos import PaymentRequiredError, NotFoundError
try:
    mem.add("...", user_id="alice")
except NotFoundError:
    mem.create_store("alice")   # store didn't exist yet
except PaymentRequiredError:
    top_up()                       # out of credit - don't retry

rate_limit

호출 직후 남은 한도를 읽어, 한계에 닿기 전에 속도를 늦춥니다.

mem.search("...", user_id="alice")
rl = mem.rate_limit   # {"limit": 150, "remaining": 3, "reset": ...}

search_self

자기기억 모델(Scroll 1.2+)에서 한 번의 호출로 두 갈래를 받습니다: 남이 한 말과 에이전트 자신이 한 말을 따로 돌려주므로, 읽는 쪽이 누가 말했는지 헷갈리지 않습니다.

r = mem.search_self("what did I promise?", user_id="alice")
r["memories"]       # what others said / general memories
r["self_memories"]  # the agent's OWN words (speaker "me")

list_engrams

이 모델이 실행할 수 있는 엔그램과 딜리버리 폼을 서비스에 물어봅니다. 이름을 하드코딩하면 새 엔그램이 나온 순간부터 그 코드에는 영영 보이지 않습니다.

cat = mem.list_engrams()
[e["name"] for e in cat["engrams"]]   # ask, never hard-code

filters

검색을 저장소의 일부로 좁힙니다. 랭킹 전에 걸리므로 필터 안에서의 최선이 나옵니다 - 상위 N 을 걸러낸 것이 아닙니다.

mem.search("what did we decide", user_id="alice", filters={
    "categories": ["work"],
    "event_from": "2026-01-01",   # when it HAPPENED
})
키: categories · event_from / event_to (내용이 언제 일어났나 - metadata.event_date) · time_from / time_to (언제 적재됐나) · min_importance. 목록에 없는 키는 거부가 아니라 버려지므로, 오타는 조용히 검색을 넓힙니다.

idempotency_key

쓰기 하나를 다시 보내도 안전하게 만듭니다. 재시도가 내 쪽에서 일어날 때 씁니다 - 죽었다 다시 돈 작업, 다시 전달하는 큐.

mem.add("she prefers tea", "alice", idempotency_key=f"import:{row.id}")
키는 저장하려는 대상에서 뽑아 만드세요(import:row-42). 상수를 쓰면 안 됩니다 - 서로 다른 두 쓰기에 같은 키를 쓰면 첫 응답이 재생되고 두 번째 쓰기는 조용히 사라집니다. 형식: [A-Za-z0-9._:-] 1~128 자.

with_timeout / with_retries

이미 만들어 둔 클라이언트를 건드리지 않고 호출 지점 하나만 조정합니다. 큰 백필에는 타임아웃이 긴 복제본을, 직접 재시도 루프를 돌 때는 재시도를 끈 복제본을 씁니다.

mem.with_timeout(120).add_bulk(big_blob, "alice")  # this slow call only
mem.with_retries(0).add("...", "alice")              # you retry, not the SDK
TypeScript SDK

TypeScript - 모든 메서드, 세 그룹.

쓰고, 읽고, 지우고. 아래 모든 예제는 2026-08-01 라이브 API에서 실제로 실행했고, 응답은 실물 그대로입니다.

npm install wontopos
import { Client } from "wontopos";

const mem = new Client({ apiKey: "wos-live-..." });

모델 선택

API 키는 어느 기억(당신 계정)을, 모델은 어느 엔진이 그 기억을 읽을지 정합니다. 모든 모델이 기억을 공유하므로, 한 모델로 저장하고 다른 모델로 회수할 수 있습니다. 생성자에 기본값을 두고, withModel()로 호출마다 바꾸세요.

const mem = new Client({ apiKey: "wos-live-...", model: "tablet-1" });  // default
mem.recall("...", "alice");                          // tablet-1
mem.withModel("scroll-1").recall("...", "alice");  // or pick a model per call

listModels

카탈로그 - model에 넣을 수 있는 id와 각 모델의 가용 여부. memory: "shared"는 같은 저장소를, "isolated"는 자기 저장소를 씁니다. API 키가 필요 없습니다.

await mem.listModels();
실제 응답
[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"},
 {"id": "tablet-2", "name": "Tablet 2", "available": true, "memory": "shared"},
 {"id": "scroll-1", "name": "Scroll 1", "available": true, "memory": "shared"},
 {"id": "scroll-1.2", "name": "Scroll 1.2", "available": true, "memory": "shared"}]

ping

연결과 API 키가 유효한지 한 줄로 확인합니다.

await mem.ping();   // true, or throws AuthenticationError / PaymentRequiredError

위 카탈로그는 항상 지금 쓸 수 있는 모델만 보여줍니다 - 다른 id를 넣으면 명확한 에러가 돌아옵니다. 새 모델은 출시되면 자동으로 거기에 나타납니다.

쓰기

add

기억 하나를 저장합니다. 적재 시 임베딩 - LLM 호출이 없어 임베딩 비용만 듭니다.

await mem.add("she prefers tea over coffee", "alice");
await mem.add("I promised the summary by Friday", "alice", { speaker: "me" });  // its own words - no registration needed
실제 응답
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

addTurn

대화 한 턴(사용자 + 어시스턴트)을 단기·장기 기억에 한 번에 저장합니다.

await mem.addTurn("hi", "hello!", "alice");
실제 응답
{"status": "ok"}

speaker

모든 기억에 누가 한 말인지 담을 수 있습니다. 사람은 한 번 등록하고, 그다음부터 이름을 speaker로 넘기세요. "me"(어시스턴트 자신의 말)는 등록이 필요 없습니다. 검색에도 speaker를 주면 그 사람의 말만 회수합니다.

await mem.addSpeaker("Bob", "alice");  // once per person; "me" needs no registration
await mem.add("I promised to send the report on Friday", "alice", { speaker: "me" });
await mem.add("Bob said the deadline moved to Tuesday", "alice", { speaker: "Bob" });
await mem.search("what did Bob say about deadlines?", "alice", 10, { speaker: "Bob" });
화자는 저장소처럼 명시적입니다. 사람을 먼저 등록하고 그 이름으로 저장합니다. 오타가 조용히 새 사람이 되는 일이 없습니다. 저장소당 시작 기준 50명까지 등록되고(차차 늘릴 예정), "me"는 등록도 카운트도 필요 없습니다.

addBulk

긴 텍스트를 한 번에 적재합니다. 서버에서 청크 분할 + 임베딩 - 기존 히스토리 이관에 적합합니다.

await mem.addBulk("Alice moved to Brooklyn in March. She works at a design studio downtown.", "alice");
실제 응답
{"elapsed_secs": 0.154154944, "status": "ok", "stored": 1, "total_chunks": 1}

update

사실이 바뀌었을 때. 옛 기억은 superseded 로 마킹되어 보존되고, 새 기억이 회수에서 그 자리를 차지합니다.

await mem.update("576700aa-...", "she switched to coffee this year", "alice");
실제 응답
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
 "old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}

읽기

search

의미 검색, 관련도 순. 순수 임베딩 - 키워드 매칭이 없어 어떤 언어로 물어도 기억을 찾습니다. SDK는 memories 배열을 바로 돌려주며, 아래는 HTTP 원문입니다. 자기 레인이 있는 모델(Scroll 1.2 이상)은 두 레인으로 답하고 SDK가 둘을 합쳐 돌려주므로, 배열이 max_results보다 많을 수 있습니다. 요청한 숫자가 아니라 받은 배열을 기준으로 프롬프트 크기를 잡으십시오.

const r = await mem.search("what does she drink?", "alice", 1);
실제 응답 (HTTP 원문)
[{
   "id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624",
   "content": "she prefers tea over coffee",
   "category": "general",
   "time_bucket": "2026-06",
   "importance": 0.3,
   "similarity": 0.6316057443618774,
   "is_superseded": false,
   "superseded_by": null,
   "created_at": "2026-07-10T04:20:39.688276876Z"
 }]
필드의미
similarity질문과의 임베딩 유사도 (0–1).
is_supersededupdate()로 교체된 기억이면 true.
search_ms서버 검색 소요 시간.

recall

한 번의 왕복으로 LLM에 필요한 모든 것을 돌려줍니다 - 결과를 프롬프트에 그대로 넣으면 됩니다. 저장량과 무관하게 항상 고정 크기입니다.

const ctx = await mem.recall("what does she drink?", "alice");
실제 응답 (구조 - 목록 축약)
{"short_term":  {"count": 2, "turns": [{"role": "user", "content": "hi", ...}]},
 "long_term":   {"count": 4, "memories": [{"content": "she prefers tea over coffee",
                                           "similarity": 0.63, ...}]},
 "context":     {"count": 4, "around_top_memory": [
                  "[match] she prefers tea over coffee",
                  "[after] Alice moved to Brooklyn in March. ..."]},
 "instruction": "Use short_term for recent context, long_term for relevant
                 past memories, context for surrounding conversation of the
                 most relevant memory."}

history

최근 대화 턴(단기 기억), 오래된 것부터.

const turns = await mem.history("alice");
실제 응답 (HTTP 원문)
{"count": 2, "turns": [
   {"role": "user",      "content": "hi",     "timestamp": "2026-07-10T04:20:40.989011337Z"},
   {"role": "assistant", "content": "hello!", "timestamp": "2026-07-10T04:20:40.989013416Z"}
 ], "user_id": "alice"}

stats

한 사용자의 기억 통계.

await mem.stats("alice");
실제 응답
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}

get

id로 기억 하나를 조회합니다 - add나 list_memories가 돌려준 그 id입니다. 저장한 원문과 메타데이터만 반환하고 벡터는 반환하지 않습니다. 다른 스토어의 id나 삭제·무효화된 기억은 404입니다.

const m = await mem.get("alice", "576700aa-...");
response
{"id": "576700aa-...", "content": "she prefers tea over coffee",
 "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}

listMemories

스토어에 저장된 기억을 나열합니다. 저장한 원문과 메타데이터만 반환하고 벡터는 포함하지 않습니다. 커서로 페이지를 넘깁니다 - 응답의 next_cursor를 다음 호출에 넘기면 됩니다.

const page = await mem.listMemories("alice", { limit: 100 });
response
{"count": 2, "next_cursor": null, "memories": [
   {"id": "576700aa-...", "content": "she prefers tea over coffee",
    "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}
 ]}

iterMemories · exportMemories

커서를 직접 관리하지 않고 모든 기억을 순회하거나, 스토어 전체를 한 번에 가져옵니다.

for await (const m of mem.iterMemories("alice")) console.log(m.id, m.content);
const everything = await mem.exportMemories("alice");

삭제

delete

기억 하나를 id로 삭제합니다.

await mem.delete("alice", "576700aa-...");
실제 응답
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}

deleteAll

한 사용자의 모든 기억을 삭제 - 한 번의 호출, GDPR 대응.

await mem.deleteAll("alice");
실제 응답
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}

오류와 신뢰성

모든 실패는 타입이 있는 오류입니다. 상황별(rate limit·인증·결제)로 골라 잡거나, 기본 WosError로 한꺼번에 잡습니다.

import { NotFoundError, PaymentRequiredError } from "wontopos";
try {
  await mem.add("...", "alice");
} catch (e) {
  if (e instanceof NotFoundError) await mem.createStore("alice");
  else if (e instanceof PaymentRequiredError) topUp();   // out of credit
  else throw e;
}

rateLimit

호출 직후 남은 한도를 읽어, 한계에 닿기 전에 속도를 늦춥니다.

await mem.search("...", "alice");
const rl = mem.rateLimit;   // { limit: 150, remaining: 3, reset: ... }

searchSelf

자기기억 모델(Scroll 1.2+)에서 한 번의 호출로 두 갈래를 받습니다: 남이 한 말과 에이전트 자신이 한 말을 따로 돌려주므로, 읽는 쪽이 누가 말했는지 헷갈리지 않습니다.

const { memories, self_memories } = await mem.searchSelf("what did I promise?", "alice");
// memories = what others said · self_memories = the agent's OWN words

listEngrams

이 모델이 실행할 수 있는 엔그램과 딜리버리 폼을 서비스에 물어봅니다. 이름을 하드코딩하면 새 엔그램이 나온 순간부터 그 코드에는 영영 보이지 않습니다.

const { engrams, forms } = await mem.listEngrams();  // ask, never hard-code

filters

검색을 저장소의 일부로 좁힙니다. 랭킹 전에 걸리므로 필터 안에서의 최선이 나옵니다 - 상위 N 을 걸러낸 것이 아닙니다.

await mem.search("what did we decide", "alice", 10, {
  filters: { categories: ["work"], event_from: "2026-01-01" },  // when it HAPPENED
});
키: categories · event_from / event_to (내용이 언제 일어났나 - metadata.event_date) · time_from / time_to (언제 적재됐나) · min_importance. 목록에 없는 키는 거부가 아니라 버려지므로, 오타는 조용히 검색을 넓힙니다.

idempotencyKey

쓰기 하나를 다시 보내도 안전하게 만듭니다. 재시도가 내 쪽에서 일어날 때 씁니다 - 죽었다 다시 돈 작업, 다시 전달하는 큐.

await mem.add("she prefers tea", "alice", {}, { idempotencyKey: `import:${row.id}` });
키는 저장하려는 대상에서 뽑아 만드세요(import:row-42). 상수를 쓰면 안 됩니다 - 서로 다른 두 쓰기에 같은 키를 쓰면 첫 응답이 재생되고 두 번째 쓰기는 조용히 사라집니다. 형식: [A-Za-z0-9._:-] 1~128 자.

withTimeout / withRetries

이미 만들어 둔 클라이언트를 건드리지 않고 호출 지점 하나만 조정합니다. 큰 백필에는 타임아웃이 긴 복제본을, 직접 재시도 루프를 돌 때는 재시도를 끈 복제본을 씁니다.

await mem.withTimeout(120_000).addBulk(bigBlob, "alice");  // this slow call only
await mem.withRetries(0).add("...", "alice");            // you retry, not the SDK
Rust SDK

Rust - 모든 메서드, 세 그룹.

쓰고, 읽고, 지우고. 아래 모든 예제는 2026-08-01 라이브 API에서 실제로 실행했고, 응답은 실물 그대로입니다.

cargo add wontopos
use wontopos::Client;

let mem = Client::new("wos-live-...");

모델 선택

API 키는 어느 기억(당신 계정)을, 모델은 어느 엔진이 그 기억을 읽을지 정합니다. 모든 모델이 기억을 공유하므로, 한 모델로 저장하고 다른 모델로 회수할 수 있습니다. with_model()로 기본값을 두고, 한 번 더 체이닝하면 그 호출만 바뀝니다.

let mem = Client::new("wos-live-...").with_model("tablet-1");  // default
mem.recall("...", "alice").await?;                       // tablet-1
mem.with_model("scroll-1").recall("...", "alice").await?;  // or pick a model per call

list_models

카탈로그 - with_model에 넣을 수 있는 id와 각 모델의 가용 여부. memory: "shared"는 같은 저장소를, "isolated"는 자기 저장소를 씁니다. API 키가 필요 없습니다.

mem.list_models().await?;
실제 응답
[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"},
 {"id": "tablet-2", "name": "Tablet 2", "available": true, "memory": "shared"},
 {"id": "scroll-1", "name": "Scroll 1", "available": true, "memory": "shared"},
 {"id": "scroll-1.2", "name": "Scroll 1.2", "available": true, "memory": "shared"}]

ping

연결과 API 키가 유효한지 한 줄로 확인합니다.

mem.ping().await?;   // Ok(true), or Err whose .kind() is Auth / PaymentRequired

위 카탈로그는 항상 지금 쓸 수 있는 모델만 보여줍니다 - 다른 id를 넣으면 명확한 에러가 돌아옵니다. 새 모델은 출시되면 자동으로 거기에 나타납니다.

쓰기

add

기억 하나를 저장합니다. 적재 시 임베딩 - LLM 호출이 없어 임베딩 비용만 듭니다.

mem.add("she prefers tea over coffee", "alice", json!({})).await?;
mem.add("I promised the summary by Friday", "alice", json!({"speaker": "me"})).await?;  // its own words - no registration needed
실제 응답
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

add_turn

대화 한 턴(사용자 + 어시스턴트)을 단기·장기 기억에 한 번에 저장합니다.

mem.add_turn("hi", "hello!", "alice").await?;
실제 응답
{"status": "ok"}

speaker

모든 기억에 누가 한 말인지 담을 수 있습니다. 사람은 한 번 등록하고, 그다음부터 이름을 speaker로 넘기세요. "me"(어시스턴트 자신의 말)는 등록이 필요 없습니다. 검색에도 speaker를 주면 그 사람의 말만 회수합니다.

mem.add_speaker("Bob", "alice").await?;  // once per person; "me" needs no registration
mem.add("I promised to send the report on Friday", "alice", json!({"speaker": "me"})).await?;
mem.add("Bob said the deadline moved to Tuesday", "alice", json!({"speaker": "Bob"})).await?;
mem.search_with("what did Bob say about deadlines?", "alice", 10, json!({"speaker": "Bob"})).await?;
화자는 저장소처럼 명시적입니다. 사람을 먼저 등록하고 그 이름으로 저장합니다. 오타가 조용히 새 사람이 되는 일이 없습니다. 저장소당 시작 기준 50명까지 등록되고(차차 늘릴 예정), "me"는 등록도 카운트도 필요 없습니다.

add_bulk

긴 텍스트를 한 번에 적재합니다. 서버에서 청크 분할 + 임베딩 - 기존 히스토리 이관에 적합합니다.

mem.add_bulk("Alice moved to Brooklyn in March...", "alice", "general").await?;
실제 응답
{"elapsed_secs": 0.154154944, "status": "ok", "stored": 1, "total_chunks": 1}

update

사실이 바뀌었을 때. 옛 기억은 superseded 로 마킹되어 보존되고, 새 기억이 회수에서 그 자리를 차지합니다.

mem.update("576700aa-...", "she switched to coffee this year", "alice").await?;
실제 응답
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
 "old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}

읽기

search

의미 검색, 관련도 순. 순수 임베딩 - 키워드 매칭이 없어 어떤 언어로 물어도 기억을 찾습니다. SDK는 memories 배열을 바로 돌려주며, 아래는 HTTP 원문입니다. 자기 레인이 있는 모델(Scroll 1.2 이상)은 두 레인으로 답하고 SDK가 둘을 합쳐 돌려주므로, 배열이 max_results보다 많을 수 있습니다. 요청한 숫자가 아니라 받은 배열을 기준으로 프롬프트 크기를 잡으십시오.

let r = mem.search("what does she drink?", "alice", 1).await?;
실제 응답 (HTTP 원문)
[{
   "id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624",
   "content": "she prefers tea over coffee",
   "category": "general",
   "time_bucket": "2026-06",
   "importance": 0.3,
   "similarity": 0.6316057443618774,
   "is_superseded": false,
   "superseded_by": null,
   "created_at": "2026-07-10T04:20:39.688276876Z"
 }]
필드의미
similarity질문과의 임베딩 유사도 (0–1).
is_supersededupdate()로 교체된 기억이면 true.
search_ms서버 검색 소요 시간.

recall

한 번의 왕복으로 LLM에 필요한 모든 것을 돌려줍니다 - 결과를 프롬프트에 그대로 넣으면 됩니다. 저장량과 무관하게 항상 고정 크기입니다.

let ctx = mem.recall("what does she drink?", "alice").await?;
실제 응답 (구조 - 목록 축약)
{"short_term":  {"count": 2, "turns": [{"role": "user", "content": "hi", ...}]},
 "long_term":   {"count": 4, "memories": [{"content": "she prefers tea over coffee",
                                           "similarity": 0.63, ...}]},
 "context":     {"count": 4, "around_top_memory": [
                  "[match] she prefers tea over coffee",
                  "[after] Alice moved to Brooklyn in March. ..."]},
 "instruction": "Use short_term for recent context, long_term for relevant
                 past memories, context for surrounding conversation of the
                 most relevant memory."}

history

최근 대화 턴(단기 기억), 오래된 것부터.

let turns = mem.history("alice").await?;
실제 응답 (HTTP 원문)
{"count": 2, "turns": [
   {"role": "user",      "content": "hi",     "timestamp": "2026-07-10T04:20:40.989011337Z"},
   {"role": "assistant", "content": "hello!", "timestamp": "2026-07-10T04:20:40.989013416Z"}
 ], "user_id": "alice"}

stats

한 사용자의 기억 통계.

mem.stats("alice").await?;
실제 응답
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}

get

id로 기억 하나를 조회합니다 - add나 list_memories가 돌려준 그 id입니다. 저장한 원문과 메타데이터만 반환하고 벡터는 반환하지 않습니다. 다른 스토어의 id나 삭제·무효화된 기억은 404입니다.

let m = mem.get("alice", "576700aa-...").await?;
response
{"id": "576700aa-...", "content": "she prefers tea over coffee",
 "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}

list_memories

스토어에 저장된 기억을 나열합니다. 저장한 원문과 메타데이터만 반환하고 벡터는 포함하지 않습니다. 커서로 페이지를 넘깁니다 - 응답의 next_cursor를 다음 호출에 넘기면 됩니다.

mem.list_memories("alice", 100, None).await?;
response
{"count": 2, "next_cursor": null, "memories": [
   {"id": "576700aa-...", "content": "she prefers tea over coffee",
    "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}
 ]}

list_all_memories

커서를 직접 관리하지 않고 모든 기억을 순회하거나, 스토어 전체를 한 번에 가져옵니다.

let all = mem.list_all_memories("alice").await?;   // every page, collected

삭제

delete

기억 하나를 id로 삭제합니다.

mem.delete("alice", "576700aa-...").await?;
실제 응답
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}

delete_all

한 사용자의 모든 기억을 삭제 - 한 번의 호출, GDPR 대응.

mem.delete_all("alice").await?;
실제 응답
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}

오류와 신뢰성

모든 실패는 타입이 있는 오류입니다. 상황별(rate limit·인증·결제)로 골라 잡거나, 기본 WosError로 한꺼번에 잡습니다.

use wontopos::ErrorKind;
match mem.search("...", "alice", 10).await {
    Ok(hits) => { /* use hits */ }
    Err(e) if e.kind() == ErrorKind::NotFound => { mem.create_store("alice").await?; }
    Err(e) if e.is_rate_limited() => { /* back off */ }
    Err(e) => return Err(e),
}

rate_limit

호출 직후 남은 한도를 읽어, 한계에 닿기 전에 속도를 늦춥니다.

mem.search("...", "alice", 10).await?;
let rl = mem.rate_limit();   // Some(RateLimit { remaining: Some(3), .. })

search_self

자기기억 모델(Scroll 1.2+)에서 한 번의 호출로 두 갈래를 받습니다: 남이 한 말과 에이전트 자신이 한 말을 따로 돌려주므로, 읽는 쪽이 누가 말했는지 헷갈리지 않습니다.

let r = mem.search_self("what did I promise?", "alice", 10).await?;
// r.memories = what others said · r.self_memories = the agent's OWN words

list_engrams

이 모델이 실행할 수 있는 엔그램과 딜리버리 폼을 서비스에 물어봅니다. 이름을 하드코딩하면 새 엔그램이 나온 순간부터 그 코드에는 영영 보이지 않습니다.

let cat = mem.list_engrams().await?;  // ask, never hard-code

filters

검색을 저장소의 일부로 좁힙니다. 랭킹 전에 걸리므로 필터 안에서의 최선이 나옵니다 - 상위 N 을 걸러낸 것이 아닙니다.

mem.search_with("what did we decide", "alice", 10, json!({"filters": {
    "categories": ["work"], "event_from": "2026-01-01"   // when it HAPPENED
}})).await?;
키: categories · event_from / event_to (내용이 언제 일어났나 - metadata.event_date) · time_from / time_to (언제 적재됐나) · min_importance. 목록에 없는 키는 거부가 아니라 버려지므로, 오타는 조용히 검색을 넓힙니다.

add_idempotent

쓰기 하나를 다시 보내도 안전하게 만듭니다. 재시도가 내 쪽에서 일어날 때 씁니다 - 죽었다 다시 돈 작업, 다시 전달하는 큐.

mem.add_idempotent("she prefers tea", "alice", json!({}), &format!("import:{}", row.id)).await?;
키는 저장하려는 대상에서 뽑아 만드세요(import:row-42). 상수를 쓰면 안 됩니다 - 서로 다른 두 쓰기에 같은 키를 쓰면 첫 응답이 재생되고 두 번째 쓰기는 조용히 사라집니다. 형식: [A-Za-z0-9._:-] 1~128 자.

with_timeout / with_retries

이미 만들어 둔 클라이언트를 건드리지 않고 호출 지점 하나만 조정합니다. 큰 백필에는 타임아웃이 긴 복제본을, 직접 재시도 루프를 돌 때는 재시도를 끈 복제본을 씁니다.

mem.with_timeout(120).add_bulk(big_blob, "alice", "general").await?;
mem.with_retries(0).add("...", "alice", json!({})).await?;
curl

curl - 설치 없이, 같은 메서드.

설치할 SDK가 없습니다 - 어떤 HTTP 클라이언트든 됩니다. 키만 한 번 설정하면 SDK가 감싸는 그 엔드포인트를 그대로 호출합니다. Base URL https://api.wontopos.com, 인증은 X-API-Key, 입출력은 JSON.

# set your key once (never hard-code it)
export WOS_API_KEY="wos-live-..."

쓰기

store

기억 하나 저장. 적재 시 임베딩 - LLM 호출 없음.

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":"she prefers tea over coffee"}'
실제 응답
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

store-turn

대화 한 턴(사용자+어시스턴트)을 단기·장기에 한 번에 저장.

curl -X POST https://api.wontopos.com/api/v1/memory/store-turn \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","user_msg":"hi","assistant_msg":"hello!"}'
실제 응답
{"status": "ok"}

speaker

모든 기억에 누가 한 말인지 담을 수 있습니다. 사람은 한 번 등록하고, 그다음부터 이름을 speaker로 넘기세요. "me"(어시스턴트 자신의 말)는 등록이 필요 없습니다. 검색에도 speaker를 주면 그 사람의 말만 회수합니다.

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":"I promised to send the report on Friday","metadata":{"speaker":"me"}}'

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

supersede

사실이 바뀌면 - 옛 기억은 superseded로 마킹, 새 기억이 회수에서 그 자리를 차지.

curl -X POST https://api.wontopos.com/api/v1/memory/supersede \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","old_memory_id":"576700aa-...","new_content":"she switched to coffee this year"}'
실제 응답
{"new_memory_id": "07e94433-...", "old_memory_id": "576700aa-...", "status": "superseded"}

bulk-store

긴 기록을 한 번에 적재합니다 - 서버에서 청킹하고 임베딩합니다.

curl -X POST https://api.wontopos.com/api/v1/memory/bulk-store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"...a long history...","category":"general"}'
실제 응답
{"elapsed_secs": 0.138589761, "status": "ok", "stored": 1, "total_chunks": 1}

Idempotency-Key

쓰기 하나를 다시 보내도 안전하게 만듭니다. 재시도가 내 쪽에서 일어날 때 씁니다 - 죽었다 다시 돈 작업, 다시 전달하는 큐.

# same key + same body = the FIRST response is replayed, nothing is stored twice
curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: import:row-42" \
  -d '{"user_id":"alice","content":"she prefers tea over coffee"}'
키는 저장하려는 대상에서 뽑아 만드세요(import:row-42). 상수를 쓰면 안 됩니다 - 서로 다른 두 쓰기에 같은 키를 쓰면 첫 응답이 재생되고 두 번째 쓰기는 조용히 사라집니다. 형식: [A-Za-z0-9._:-] 1~128 자.

읽기

search

의미 검색, 관련도 순. 순수 임베딩 - 어떤 언어로 물어도 기억을 찾음.

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 does she drink?","max_results":1}'
실제 응답
{"memories": [{"id": "576700aa-...", "content": "she prefers tea over coffee",
   "similarity": 0.63, "is_superseded": false}], "search_ms": 315, "total_found": 1}

search + filters

검색을 저장소의 일부로 좁힙니다. 랭킹 전에 걸리므로 필터 안에서의 최선이 나옵니다 - 상위 N 을 걸러낸 것이 아닙니다.

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 we decide",
       "filters":{"categories":["work"],"event_from":"2026-01-01","event_to":"2026-06-30"}}'
키: categories · event_from / event_to (내용이 언제 일어났나 - metadata.event_date) · time_from / time_to (언제 적재됐나) · min_importance. 목록에 없는 키는 거부가 아니라 버려지므로, 오타는 조용히 검색을 넓힙니다.

get

store 나 list 가 돌려준 id 로 기억 하나를 읽습니다 - 원문과 메타데이터, 벡터는 없습니다.

curl -X POST https://api.wontopos.com/api/v1/memory/get \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","memory_id":"576700aa-f0e0-4c26-99a0-10e2d5b0d624"}'

list

저장소 전체를 커서 단위로 넘겨 봅니다. 열람이나 내보내기에 씁니다.

curl -X POST https://api.wontopos.com/api/v1/memory/list \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","limit":100}'   # pass next_cursor back for the next page
실제 응답
{"count": 3, "memories": [{"id": "1a1cfc47-...", "content": "...", "category": "general",
   "created_at": "2026-07-31T18:04:51.937117314+00:00", "event_date": null, "is_superseded": false}],
 "next_cursor": "722c08e5-8998-4882-979e-d71995b5b4af", "user_id": "docs_livetest"}

recall

한 번의 왕복으로 단기 + 장기 + 문맥 + instruction. 프롬프트에 그대로 넣으면 됨.

curl -X POST https://api.wontopos.com/api/v1/memory/recall \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what does she drink?"}'
실제 응답 (구조)
{"short_term": {"count": 2, "turns": [...]},
 "long_term":  {"count": 4, "memories": [{"content": "she prefers tea over coffee", "similarity": 0.63}]},
 "context":    {"count": 4, "around_top_memory": ["[match] she prefers tea over coffee"]},
 "instruction": "Use short_term for recent context, long_term for relevant past memories..."}

삭제

forget

id로 기억 하나 삭제, 생략하면 사용자 전체 삭제(GDPR).

curl -X POST https://api.wontopos.com/api/v1/memory/forget \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice"}'  # omit memory_id = delete all
실제 응답
{"memories_deleted": 1, "status": "deleted", "user_id": "alice"}

엔드포인트 + 바디 필드 전부 보기 →

Rust 에만 있는 변형

Python 과 TypeScript 는 이것들을 선택 인자로 받습니다. 안정판 Rust 에는 기본값도 키워드 인자도 없어서, 마무리해야 하는 빌더 대신 각각을 별도 메서드로 뒀습니다.

mem.add_with(text, None, json!({}), extra)   // add + extra body fields
mem.search_opts(q, None, 10, &opts)            // search + verify / max_images
mem.search_with(q, None, 10, extra)            // search + any other field
mem.recall_with(q, None, extra)                // recall + extra
mem.search_self_with(q, None, 10, extra)       // self lane + extra
mem.engram_with(name, q, None, extra)          // engram + extra
mem.update_idempotent(old, new, None, key)     // update + Idempotency-Key
mem.add_turn_idempotent(u, a, None, key)
mem.add_bulk_idempotent(text, None, cat, key)
mem.revisions_page(None, "revised", 20, None, None)
mem.list_all_images(None, None)              // = iter_images, collected

list_all_imagesiter_images 라는 이름으로도 나갑니다. 다른 두 SDK 가 쓰는 이름이라, 그 문서를 보고 온 독자가 먼저 치는 것이 그쪽입니다.

엔그램

Engrams

모델이 호출하는 회수 도구 - 같은 기억 위에서 서로 다른 검색 전략을 씁니다. 하나만 쓰거나 여러 개를 동시에 호출하세요.

지금 사용 가능. 아래 일반 엔그램은 LLM 없는 회수 파이프라인이라 Tablet 1부터 모든 티어에서 돕니다. 회고록·아카이브는 결이 다른 모델 모드라 아래 별도 섹션에서 다룹니다.

엔그램은 계속 추가됩니다 - 목록은 늘어납니다.

회고록 & 아카이브 Scroll 1.2+

이건 호출하는 도구가 아니라 전달 형식입니다. Scroll 1.2 이상에서 호출마다 form: "memoir" 또는 form: "archive"를 고르면, 그냥 검색을 포함한 그 회수에 시간이 그 방식으로 적혀 돌아옵니다.

엔그램 목록 엔그램

Time_awareness Scroll 1.2+

호출마다 고르는 전달 형식. 형식을 지원하는 모델(Scroll 1.2 이상)에서 아무 호출에나 form(memoir 또는 archive)을 넘기면 - 그냥 검색이든 recall이든 엔그램이든 - 그 응답이 그 방식으로 렌더링되어 돌아옵니다. SDK에서는 tz처럼 form 필드로, HTTP에서는 X-WOS-Form 헤더로 넘깁니다. 회고록은 사람이 기억하듯, 아카이브는 정확한 기록 그대로 - 차이는 시간을 적는 방식에서 가장 잘 드러납니다.

회고록

form: "memoir"
사람이 기억하듯 · 서사로

무슨 일이 어떻게 다음으로 이어졌는지, 사람이 떠올리는 흐릿한 시간 감각과 함께 들려줍니다 - 목록이 아니라 경험으로.

아카이브

form: "archive"
기록 그대로 · 정밀한 시각

맞는 것을 정밀한 경과 시간과 절대 앵커로, 기록 그대로 돌려줍니다 - 모델이 바로 읽도록 구조화되어.

이미 저장한 기억을 렌더할 뿐, 기억을 만들지는 않습니다. 기억 하나는 user_id 아래 store / add 호출 한 번 (그 user_id가 곧 그 사람의 저장소). 먼저 저장해야, 그 다음 어떤 회수든 - 아래 그냥 검색 포함 - 시간 태그가 붙어 돌아옵니다. 저장은 Quickstart 참고.
# the memoir form on a plain search — and on recall, the LLM's one-call context
r   = mem.search("what does Alice drink?", user_id="alice", model="scroll-1.2", form="memoir", tz=9)
ctx = mem.recall("what does Alice drink?", user_id="alice", model="scroll-1.2", form="memoir", tz=9)
# every memory's .time reads "a couple weeks ago" (archive → "2 weeks ago (Jun 09)") — the LLM sees human time
// the memoir form on search — and on recall, the LLM's one-call context
const s = await mem.withModel("scroll-1.2").search("what does Alice drink?", "alice", 10, { form: "memoir", tz: 9 });
const ctx = await mem.withModel("scroll-1.2").recall("what does Alice drink?", "alice", { form: "memoir", tz: 9 });
// form on search AND recall — the _with helpers merge extra fields into the body
let s = mem.with_model("scroll-1.2").search_with("what does Alice drink?", "alice", 10, json!({"form": "memoir", "tz": 9})).await?;
let ctx = mem.with_model("scroll-1.2").recall_with("what does Alice drink?", "alice", json!({"form": "memoir", "tz": 9})).await?;
# same X-WOS-Form header on /search, /recall, or /engram/run
curl -X POST https://api.wontopos.com/api/v1/memory/recall \
  -H "X-API-Key: wos-live-..." -H "X-WOS-Model: scroll-1.2" -H "X-WOS-Form: memoir" -H "X-WOS-Timezone: 9" \
  -d '{"user_id":"alice","query":"what does Alice drink?"}'
# every memory comes back with a "time" field; use X-WOS-Form: archive for exact time

tz는 호출자의 UTC 오프셋(시간) - 라 "this morning" 같은 표현과 새벽 4시 경계가 그 사람 현지 시간으로 찍힙니다. 안 주면 UTC, HTTP에선 X-WOS-Timezone 헤더. 지역별 대략값: 미국 동부 -5, 중부 -6, 서부 -8 · 영국 / 리스본 0 · 중부 유럽 +1 · 동유럽 +2 · 인도 +5.5 · 중국 / 싱가포르 +8 · 한국 / 일본 +9 · 시드니 +10. (표준시 기준 - 서머타임이면 일부 지역은 +1, 사용자가 실제로 쓰는 값을 그대로 넣으세요.)

같은 검색, 두 형식 - 기억은 똑같고 time만 달라집니다:

결과 · form: memoir
{ "count": 3, "memories": [
  { "content": "Alice prefers tea over coffee", "time": "a couple weeks ago" },
  { "content": "met Alice at the cafe downtown",  "time": "yesterday afternoon" },
  { "content": "Alice moved to Brooklyn",          "time": "about half a year ago" }
] }
결과 · form: archive
{ "count": 3, "memories": [
  { "content": "Alice prefers tea over coffee", "time": "2 weeks ago (Jun 09)" },
  { "content": "met Alice at the cafe downtown",  "time": "yesterday at 14:00" },
  { "content": "Alice moved to Brooklyn",          "time": "6 months ago (Dec 2025)" }
] }
경과회고록아카이브
3분a few minutes ago3 minutes ago
14분about 15 minutes ago14 minutes ago
30분half an hour ago30 minutes ago
50분about an hour ago50 minutes ago
2시간a couple hours ago2 hours ago, at 13:10
8시간this morning8 hours ago, at 07:10
어제 낮yesterday afternoonyesterday at 14:00
어제 밤last night17 hours ago, at 22:00
2일a couple days ago2 days ago (Tue 15:10)
6일several days ago6 days ago (Fri 15:10)
9일about a week agolast week (Jun 16)
16일a couple weeks ago2 weeks ago (Jun 09)
35일about a month agolast month (May 21)
60일a couple months ago2 months ago (Apr 2026)
180일about half a year ago6 months ago (Dec 2025)
380일about a year agolast year (Jun 2025)
800일a couple years ago2 years ago (Apr 2024)
1500일about 4 years ago4 years ago (May 2022)

위 값은 전부 렌더러의 실제 출력입니다. "어제" 두 줄을 보세요: 회고록은 낮과 밤을 가르지만 - 하루는 잠 한 번 - 아카이브는 시계 시각 하나로 적고 낮·밤을 나누지 않습니다.

모드별 시간 읽는 법

회고록 - 사람이 실제로 말하는 방식. 최근은 비교적 또렷하다가(약 15분 전, 30분 전) 멀어질수록 표현이 넓어집니다 - 2주쯤 전, 반년쯤 전, 몇 년 전 - 기억 자체가 멀수록 풀어지듯. 하루 안에서는 시계 대신 landmark를 씁니다: 오늘 아침, 어젯밤, 어제 오후. 그리고 하루는 달력 한 칸이 아니라 잠 한 번: 경계가 현지 새벽 4시쯤이라, 늦은 밤도 여전히 '오늘 밤'으로 읽히지 벌써 내일이 되지 않습니다.

아카이브 - 정밀, 항상 앵커와 함께. 모든 줄에 정확한 경과 시간 + 모델이 계산할 수 있는 절대 기준이 붙고, 가까울수록 앵커가 촘촘해집니다: 오늘은 시계(8시간 전, 07:10), 이번 주는 요일+시계(2일 전 (화 15:10)), 이번 달은 날짜(지난주 (6월 16일)), 그 너머는 월·연도(6개월 전 (2025년 12월)). 모호함 없이, 틀림 없이.

회고록과 아카이브는 응답의 모든 회수(그냥 검색·recall·엔그램)를 그 방식으로 렌더링합니다. 모델 티어(Tablet → Scroll → Book)이 엔진이 하는 일의 깊이를, 형식(회고록/아카이브)이 시간을 적는 방식을 정합니다. Scroll 1.2 이상에서 사용할 수 있습니다.
엔그램 목록 엔그램

deep_recall

멀티홉 회수. 쿼리로 검색한 뒤 최상위 매치의 내용으로 한 번 더 검색해 단일 검색이 놓칠 연결 맥락을 끌어옵니다. 기억이 서로를 참조할 때(사람 → 프로젝트 → 세부) 가장 좋습니다. 최대 ~12개.

out = mem.engram("deep_recall", "what should I know about Alice?", user_id="alice")
const out = await mem.engram("deep_recall", "what should I know about Alice?", "alice");
let out = mem.engram("deep_recall", "what should I know about Alice?", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"deep_recall","user_id":"alice","query":"what should I know about Alice?"}'
응답
{ "engram": "deep_recall", "hops": 2, "count": 12,
  "memories": [ ... ],
  "usage": { "input_tokens": 200, "output_tokens": 589 } }
토큰 과금 - 호출마다 usage(입력+출력)가 나오며 API 나머지와 같은 토크나이저로 셉니다. 숨은 수수료 없음. 여러 개를 한 번에? 동시에 호출하면 됩니다 - 각 엔그램은 독립 요청.
엔그램 목록 엔그램

timeline

시간순 회수. 관련도가 아니라 사건 발생 시점 기준 최신순으로 정렬해 반환합니다. "언제 X했지", 이력, 순서 질문에. 최대 15개.

events = mem.engram("timeline", "project milestones", user_id="alice")
const events = await mem.engram("timeline", "project milestones", "alice");
let events = mem.engram("timeline", "project milestones", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"timeline","user_id":"alice","query":"project milestones"}'
응답
{ "engram": "timeline", "hops": 1, "count": 15,
  "memories": [ ... ],
  "usage": { "input_tokens": 100, "output_tokens": 736 } }
토큰 과금 - 호출마다 usage(입력+출력)가 나오며 API 나머지와 같은 토크나이저로 셉니다. 숨은 수수료 없음. 여러 개를 한 번에? 동시에 호출하면 됩니다 - 각 엔그램은 독립 요청.
엔그램 목록 엔그램

gather

넓은 수집. 검색 후 상위 매치 주변을 확장해 deep_recall보다 더 넓게 훑습니다. 한 사람·프로젝트·주제 관련된 걸 한 번에 다 모을 때. 최대 ~18개.

related = mem.engram("gather", "everything about Project Atlas", user_id="alice")
const related = await mem.engram("gather", "everything about Project Atlas", "alice");
let related = mem.engram("gather", "everything about Project Atlas", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"gather","user_id":"alice","query":"everything about Project Atlas"}'
응답
{ "engram": "gather", "hops": 4, "count": 18,
  "memories": [ ... ],
  "usage": { "input_tokens": 400, "output_tokens": 637 } }
토큰 과금 - 호출마다 usage(입력+출력)가 나오며 API 나머지와 같은 토크나이저로 셉니다. 숨은 수수료 없음. 여러 개를 한 번에? 동시에 호출하면 됩니다 - 각 엔그램은 독립 요청.
엔그램 목록 엔그램

equilibrium

드리프트 교정. 시맨틱 검색은 대화가 길어질수록 좁아집니다. 쿼리가 현재 상태를 실어 나르므로 같은 상태의 기억이 끌려오고, 다음 턴은 더 그쪽으로 기웁니다. 이 엔그램은 쿼리가 지배하지 못하는 세 축으로 결과를 다시 넓힙니다. 시간에 걸친 분산, 쿼리에서 벗어난 연상, 그리고 스토어에서 실속 있는 부분입니다. 답변이 반복되거나 납작해질 때 쓰십시오. 특정 사실을 찾을 때는 쿼리에 더 가까이 붙는 deep_recall이나 gather가 맞습니다. 최대 12개를 돌려줍니다.

wide = mem.engram("equilibrium", "how have things been lately?", user_id="alice")
const wide = await mem.engram("equilibrium", "how have things been lately?", "alice");
let wide = mem.engram("equilibrium", "how have things been lately?", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"equilibrium","user_id":"alice","query":"how have things been lately?"}'
응답
{ "engram": "equilibrium", "hops": 3, "count": 12,
  "memories": [ ... ],
  "usage": { "input_tokens": 300, "output_tokens": 293 } }
토큰 과금 - 호출마다 usage(입력+출력)가 나오며 API 나머지와 같은 토크나이저로 셉니다. 숨은 수수료 없음. 여러 개를 한 번에? 동시에 호출하면 됩니다 - 각 엔그램은 독립 요청.
엔그램 목록 엔그램

tone_stabilizer

자기 목소리. 대화가 길어지면 어시스턴트가 평소 결에서 벗어납니다. 답이 늘어지고, 보고서처럼 바뀌고, 최근 구간의 분위기를 띱니다. 일반적인 자기 회상은 이를 더 악화시킵니다. 현재 상태와 비슷한 것을 찾기 때문에 가장 최근 발화를 성격인 것처럼 돌려주기 때문입니다. 이 엔그램은 그 구간 이전의 자기 발화를 돌려줍니다. speaker me로 저장된 발화가 있어야 하며, 없으면 추측하지 않고 빈 결과를 돌려줍니다. 최대 10개를 돌려줍니다.

# store the assistant's turns as speaker "me", then pull its own register back
mem.add("I keep answers short unless you ask for detail.", user_id="alice", speaker="me")
mine = mem.engram("tone_stabilizer", "how do I usually answer?", user_id="alice")
await mem.add("I keep answers short unless you ask for detail.", "alice", { speaker: "me" });
const mine = await mem.engram("tone_stabilizer", "how do I usually answer?", "alice");
mem.add("I keep answers short unless you ask for detail.", "alice", json!({"speaker": "me"})).await?;
let mine = mem.engram("tone_stabilizer", "how do I usually answer?", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"tone_stabilizer","user_id":"alice","query":"how do I usually answer?"}'
응답
{ "engram": "tone_stabilizer", "hops": 2, "count": 10,
  "memories": [ { "content": "I keep answers short unless you ask for detail.", "speaker": "me" }, ... ],
  "usage": { "input_tokens": 200, "output_tokens": 442 } }
토큰 과금 - 호출마다 usage(입력+출력)가 나오며 API 나머지와 같은 토크나이저로 셉니다. 숨은 수수료 없음. 여러 개를 한 번에? 동시에 호출하면 됩니다 - 각 엔그램은 독립 요청.
HTTP API

모든 엔드포인트, 하나의 Base URL.

SDK 없이도 됩니다 - 어떤 HTTP 클라이언트든 가능. Base URL https://api.wontopos.com, 인증은 X-API-Key 헤더, 입출력은 JSON. 기억 작업은 POST, 저장소 관리는 /collection에 POST / GET / DELETE. 저장소가 먼저 있어야 하고(Stores 참고), 없으면 404.

헤더

헤더하는 일
X-API-Key모든 호출에 필수입니다. 콘솔에서 발급한 키입니다.
X-WOS-Model선택입니다. 어느 엔진이 답할지 고릅니다. 생략하면 계정 기본값을 씁니다. GET /api/v1/models 로 이 키가 고를 수 있는 모델을 볼 수 있고, 이전 엔진이 못 하는 엔드포인트는 501 과 함께 어떤 모델인지 알려줍니다.
Idempotency-Key쓰기에서 선택입니다. 같은 키에 같은 바디로 보내면 다시 저장하지 않고 첫 응답을 그대로 돌려줍니다 - 아래 주석을 보세요.

엔드포인트

엔드포인트용도바디 필드
POST /api/v1/memory/collection저장소 생성user_id
GET /api/v1/memory/collections저장소 목록(없음)
DELETE /api/v1/memory/collection저장소 + 기억 삭제user_id
/api/v1/memory/store기억 하나 저장user_id · content · metadata? (event_date · speaker) · image?
/api/v1/memory/store-turn대화 턴 저장user_id · user_msg · assistant_msg
POST /api/v1/memory/speakers화자 등록 (명시적, 50명까지)user_id · speaker
GET /api/v1/memory/speakers등록된 화자 목록 + 기억 수user_id
DELETE /api/v1/memory/speakers화자 등록 해제 (기억은 유지)user_id · speaker
/api/v1/memory/by-speaker한 사람이 한 말, 최신순 ("me" = 에이전트 자신)user_id · speaker · limit? · before? · skip_ids?
POST /api/v1/memory/image이미지 기억의 원본 바이트user_id · memory_id
DELETE /api/v1/memory/image이미지만 지우고 글은 남깁니다user_id · memory_id · preview?
/api/v1/memory/images저장소의 이미지, 최신순 (+ 전체 개수)user_id · limit? · before? · skip_ids?
/api/v1/memory/lineage기억 하나의 수정 이력, 오래된 순user_id · memory_id
/api/v1/won/revisions저장소가 얼마나 고쳐졌는지. 무료user_id · include? · limit? · before? · skip_ids?
/api/v1/memory/revisions같은 호출을 memory 주소에서. 무료user_id · include? · limit? · before? · skip_ids?
/api/v1/memory/bulk-store긴 텍스트 적재user_id · content · category? · timestamp?
/api/v1/memory/search의미 검색user_id · query · max_results? · speaker? · cache_control? · filters? · verify? · max_images?
/api/v1/memory/recall단기 + 장기 + 문맥user_id · query · limit? · context_limit?
/api/v1/memory/getid 로 기억 한 건user_id · memory_id
/api/v1/memory/list저장소를 페이지 단위로 열람user_id · limit? · cursor?
/api/v1/memory/history최근 대화user_id
/api/v1/memory/stats기억 통계user_id
/api/v1/memory/supersede바뀐 사실 교체user_id · old_memory_id · new_content
/api/v1/memory/forget하나(또는 전체) 삭제user_id · memory_id? (생략 = 전체 삭제)
GET /api/v1/engram이 모델이 실행할 수 있는 엔그램(없음)
POST /api/v1/engram/run엔그램 하나 실행name · user_id · query · form? · tz?
GET /api/v1/models사용 가능한 모델(없음)
쓰기 요청은 Idempotency-Key 헤더를 받습니다. 같은 키에 같은 본문이면 다시 저장하지 않고 첫 응답을 재생하며(10분), 같은 키에 다른 본문이면 422 로 답합니다. 2xx 만 캐시하므로 실패한 호출은 곧바로 다시 시도할 수 있습니다.
# create the store once (stores are explicit)
curl -X POST https://api.wontopos.com/api/v1/memory/collection \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice"}'

# store a memory
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":"she prefers tea over coffee"}'

# recall - one call, ready for your prompt
curl -X POST https://api.wontopos.com/api/v1/memory/recall \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what does alice drink?"}'
실제 응답 - store
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}
사용량 티어

기능은 모두에게 동일.
티어는 한도만 올립니다.

모든 티어가 같은 엔진을 씁니다 - 같은 recall 품질, 같은 다국어, 모든 메서드. Tier 5까지는 누적 충전액이 쌓이면 신청이나 영업 통화 없이 자동으로 올라갑니다. Enterprise(Tier 6)만 예외입니다.

지출 한도

티어마다 한 달에 쓸 수 있는 금액에 상한이 있습니다. 누적 충전액이 다음 기준에 도달하면 즉시 승급됩니다.

티어누적 충전월 지출 한도
Tier 1$5$100
Tier 2$40$500
Tier 3$200$1,000
Tier 4$400$5,000
Tier 5$1,000$25,000
Tier 6 - Enterprise영업 협의무제한

속도 한도

속도 한도는 계정 단위입니다 - 한 계정의 모든 API 키가 하나의 한도를 공유하며, 티어에 따라 올라갑니다. 넘으면 retry-after 헤더와 함께 429를 돌려주므로, 잠시 물러났다(1초 → 2초 → 4초) 재시도하면 됩니다. 모든 엔드포인트가 멱등에 친화적이라 재시도해도 안전합니다.

티어분당 요청
Tier 1150
Tier 2300
Tier 3600
Tier 41,500
Tier 53,000
Tier 6 - Enterprise영업 협의

Enterprise(Tier 6)는 커스텀 한도, SLA, 전담 지원, 그리고 선택적 self-host 라이선스를 제공합니다 - 문의하세요.

무료 호출

몇몇 엔드포인트는 아예 요금이 붙지 않습니다. Won 아래 모여 있습니다. 가격 대신 한도가 둘 있습니다.

  • 엔드포인트마다 분당 10회. 각자 자기 버킷을 쓰므로, 하나를 다 써도 다른 하나가 줄지 않습니다.
  • 시간당 300회, 전체 공유. 무료 엔드포인트 전부가 계정당 하나의 시간 한도를 나눠 씁니다.

평범하게 쓰면 어느 쪽에도 닿지 않고, 위의 유료 한도와도 섞이지 않습니다.

과금은 사용량 기반입니다: 토큰에 요청당 $0.0001이 더해집니다. Tablet은 입력 100만 토큰당 $2, 출력 100만 토큰당 $3. 저장은 제한 없이 무료입니다. 이렇게 가격을 정한 이유.
에러와 한도

문제가 생겼을 때.

에러는 안정적인 type, 사람이 읽는 메시지, 그리고 문의 시 함께 보낼 수 있는 request_id가 담긴 JSON 봉투로 옵니다.

실제 응답 - 잘못된 키 (HTTP 401)
{"type": "error", "error": {
   "type": "authentication_error",
   "message": "Invalid or revoked API key.",
   "request_id": "063f8b83-eee2-4383-a5cf-11e4bcd29d7c"
 }}
HTTP의미대처
400잘못된 바디 (필드 누락/타입 오류)메시지에 정확한 필드가 나옵니다 - 고치고 재시도.
401잘못됐거나 폐기된 API 키키 확인, 콘솔에서 재발급.
402잔액 부족, 카드 미등록, 또는 티어 상한콘솔에서 충전하거나 카드를 등록하세요. 응답에 balance_centsfloor_cents 가 들어 있어 무엇에 막혔는지 구분됩니다.
404없는 기억·저장소·이미지id 를 확인하세요. get_image 는 기억이 있어도 이미지이 없으면 404 입니다.
409이미 쓰고 있는 이름저장소와 워크스페이스 이름은 계정 안에서 고유합니다 - 다른 이름을 쓰세요.
413바디가 10MB 초과base64 는 원본 파일보다 약 33% 큽니다. 이미지을 줄인 뒤 인코딩하세요.
429요청 한도 초과SDK 가 이미 백오프와 지터로 재시도하며 Retry-After 를 지킵니다. 이 에러를 받았다는 것은 재시도가 소진됐다는 뜻이니, 직접 루프를 감지 말고 동시 요청을 줄이세요.
501이 모델의 엔진이 그 엔드포인트를 구현하지 않음이미지과 수정 이력은 더 새 엔진이 필요합니다. GET /api/v1/models 에서 어떤 모델이 무엇을 지원하는지 볼 수 있습니다.
5xx서버 측 문제백오프 후 재시도하되 무작정 하지는 마세요. 이 API 는 모든 호출이 POST 라 서버가 이미 저장했을 수 있어서, SDK 는 5xx 를 자동 재시도하지 않습니다. 다시 보낼 때는 멱등키를 붙여 중복 저장을 막고, 문의 시 request_id 를 함께 보내주세요.

모든 에러는 WosError 이고, 상태마다 전용 클래스도 있습니다 - BadRequestError, AuthenticationError, PaymentRequiredError, NotFoundError, ConflictError, RateLimitError, ServerError, APIConnectionError. 숫자를 비교하지 말고 다루려는 것을 직접 잡으세요.

# SDK error handling (Python)
from wontopos import Client, WosError, RateLimitError, PaymentRequiredError

try:
    mem.search("...", user_id="alice")
except PaymentRequiredError: ...      # 402 - top up
except RateLimitError: ...            # 429 - the SDK already retried; slow down
except WosError as e: ...            # e.status, e.message, e.request_id
except (ValueError, TypeError): ...    # never left the client

어떤 실수는 저희에게 닿지도 않습니다. API 키, 저장소 id, 멱등키, 이미지은 요청을 보내기 전에 검사하며, 그때는 WosError 가 아니라 ValueErrorTypeError 가 납니다. except WosError 만으로는 안 잡힙니다.

키 안전. 키는 생성 시 한 번만 표시되고 서버에는 해시로만 저장됩니다. 환경변수로 보관하고, 유출 시 콘솔에서 폐기하세요 - 폐기는 즉시 적용됩니다.

요청 한도는 계정 단위로 모든 키가 공유하며, 티어에 따라 올라갑니다 - 사용량 티어 참고. 계정 사용량은 콘솔에 표시됩니다.

개발자

lineage

기억 하나가 고쳐져 온 이력을 오래된 순으로 돌려줍니다. revisions 가 저장소 전체가 얼마나 움직였는지라면, 이쪽은 사실 하나에 무슨 일이 있었는지입니다.

Tablet 2 이상에서 지원합니다. 읽기 전용입니다. revisions 와 달리 기억 내용을 돌려주므로 일반 유료 호출입니다.

체인에 속한 아무 기억의 id 나 넘기면 됩니다. 대체된 판은 지우지 않고 보관하므로, 검색이 현재 사실만 돌려줘도 거슬러 올라갈 수 있습니다.

chain = mem.lineage(memory_id=mid)["chain"]
for step in chain:
    print(step["changed_at"], step["action"], step["content"])
const { chain } = await mem.lineage(undefined, mid);
for (const step of chain) {
  console.log(step.changed_at, step.action, step.content);
}
let r = mem.lineage(None, mid).await?;
for step in r["chain"].as_array().unwrap_or(&vec![]) {
    println!("{} {} {}", step["changed_at"], step["action"], step["content"]);
}
curl -X POST https://api.wontopos.com/api/v1/memory/lineage \
  -H "X-API-Key: $WOS_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","memory_id":"m_9"}'
200
{ "memory_id": "m_9", "count": 3, "truncated": false,
  "chain": [
    { "memory_id": "m_3", "content": "lives in Seoul",
      "created_at": "2026-03-02T…", "changed_at": "2026-06-11T…",
      "action": "replaced", "confidence": 0.94,
      "superseded_by": "m_7", "is_current": false },
    { "memory_id": "m_7", "content": "moved to Busan",   … },
    { "memory_id": "m_9", "content": "Haeundae, specifically",
      "changed_at": null, "superseded_by": null, "is_current": true }
  ] }
필드하는 일
chain판들을 오래된 순으로. 각 판은 기억과 같은 필드에 아래 넷이 더 붙습니다.
changed_at이 판이 대체된 시각(RFC3339). 아직 유효하면 null.
action이 자리에서 무슨 일이 있었는지. 대체본이 이 판과 어떤 관계였는지입니다.
confidence엔진이 그 관계를 얼마나 확신했는지, 0~1.
is_current지금 유효한 판에만 true. 체인당 정확히 하나입니다.
truncated체인이 서비스가 훑는 길이보다 길었으면 true. 돌려준 판들은 여전히 오래된 쪽입니다.

어디에 쓰나

둘입니다. 디버깅 — 이 기억이 왜 지금 이 모양인지 추적합니다. 그리고 어시스턴트가 자기 이력을 보는 것 — 세 번 고쳐진 사실은 한 번 쓰인 사실과 다른 종류이고, 그걸 보여주는 건 체인뿐입니다.

Won

Won 은 기억을 읽는 쪽을 위한 것입니다.

이 API 의 대부분은 기억으로 답합니다. Won 은 기억에 대해 답합니다. 한 저장소가 얼마나 고쳐졌는지, 어디까지 믿어도 되는지입니다. 읽기 전용이고, 무료이며, 검색과 분리되어 있습니다.

Wontopos 는 Won + Topos 이고, 기억이 머무는 하나의 자리라는 뜻입니다. Won 은 그 자리 가운데 기억을 돌려주는 대신 기억에 대해 알려주는 쪽입니다. 이 호출들은 값이 없고, 읽기만 하며, 검색에 손대지 않습니다. 물어도 사용자가 더 내는 것은 없고, 저장된 기억이 달라지지도 않습니다.

지금 올라와 있는 것

지금은 호출 하나입니다.

호출하는 일
POST /won/revisions이 저장소가 쓰인 뒤로 얼마나 고쳐졌는지. 숫자 둘과 그것을 설명하는 문장 둘.

예시 하나

낱개 수가 아니라 비율을 쓰십시오. 40 중 3 과 40 중 30 은 다르게 다뤄야 합니다.

r = mem.revisions()
# {"revised": 3, "total": 40, "counts": "…", "excludes": "…"}

if r["revised"] / r["total"] > 0.1:
    system += "Some of what you remember here has been corrected since."
const r = await mem.revisions();
// { revised: 3, total: 40, counts: "…", excludes: "…" }

if (r.revised / r.total > 0.1) {
  system += "Some of what you remember here has been corrected since.";
}
let r = mem.revisions(None).await?;
let (rev, tot) = (r["revised"].as_f64().unwrap_or(0.0),
                r["total"].as_f64().unwrap_or(1.0));
if rev / tot > 0.1 { /* say so in the system prompt */ }
curl -X POST https://api.wontopos.com/api/v1/won/revisions \
  -H "X-API-Key: $WOS_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice"}'

# → {"user_id":"alice","revised":3,"total":40,
#     "counts":"memories a transform has touched (supersede, update, retract, image removed)",
#     "excludes":"deletions — a deleted memory leaves nothing to count"}

countsexcludes 는 플래그가 아니라 문장으로 돌아옵니다. 호출자가 대개 모델이기 때문입니다. 삭제는 세지 않습니다.

값과 한도

항목
요금없습니다. 무료 호출은 과금 관문을 건너뜁니다. 토큰 요금도, 요청당 요금도 없고, 사용량에도 안 잡힙니다.
분당계정마다, 그리고 엔드포인트마다 분당 10회. 한 엔드포인트의 분을 다 써도 다른 엔드포인트의 분은 안 줄어듭니다.
시간당계정당 시간당 300회이고, 무료 호출 전부가 하나를 나눠 씁니다. 이쪽은 경로를 안 보므로, 무료 엔드포인트가 늘어도 계정 하나가 쓸 수 있는 총량은 안 늘어납니다.
유료 트래픽과의 관계양방향으로 분리돼 있습니다. 이 호출이 검색을 느리게 만들지 않고, 검색이 이 몫을 마르게 하지도 않습니다. 한 계정의 키들은 버킷을 공유하므로 키를 늘려도 한도가 늘지 않습니다.

두 천장 모두 429 로 답하며, Retry-After 에 초 단위 시간이, 메시지에 어느 쪽에 걸렸는지가 들어갑니다.

429 rate_limit_error
Retry-After: 41

{ "error": { "type": "rate_limit_error",
    "message": "This endpoint is free and limited to 10 requests per
                minute, counted per endpoint. Retry in 41s." } }
같은 호출이 /api/v1/memory/revisions 에서도 답합니다. Won 이 생기기 전에 배포된 클라이언트를 위한 것입니다. 같은 핸들러이고 같은 예산이며, 두 번째 몫이 아닙니다. 새로 쓰는 코드는 Won 주소를 쓰십시오.
Won · revisions

저장소가 얼마나 고쳐졌는지

revisionsrevisedtotal 로 답합니다. 저장소에 든 기억 가운데 몇 개가 쓰인 뒤에 바뀌었는지입니다. 중요한 일을 기억에 기대기 전에, 또는 떠올린 사실이 지금 사용자가 하는 말과 어긋날 때 물어볼 만합니다. 열에 셋이 갈아치워진 저장소는 아무도 손대지 않은 저장소보다 덜 믿는 것이 맞습니다.

Tablet 2 이상에서 지원합니다. HTTP API, Python·TypeScript·Rust SDK, 그리고 MCP 도구로 호출할 수 있습니다. 이전 엔진은 501 을 내고, 어떤 모델이 못 하는지 이름으로 알려줍니다.

세기

고쳐 쓴 것을 셉니다 - 대체·수정·철회, 그리고 이미지 삭제입니다.

mem.revisions()
# {"revised": 3, "unrevised": 37, "total": 40, …}
await mem.revisions();
mem.revisions(None).await?;
curl -X POST https://api.wontopos.com/api/v1/won/revisions \
  -H "X-API-Key: $WOS_KEY" -d '{"user_id":"alice"}'
필드
revised변형이 건드린 기억 수입니다.
unrevised쓰인 뒤로 아무것도 건드리지 않은 기억입니다. revised + unrevised 는 항상 total 과 같습니다. 따로 세지 않고 빼서 얻으므로, 동시에 쓰기가 들어와도 셋이 어긋나지 않습니다.
total저장소에 든 기억 수입니다.
counts / excludes플래그가 아니라 문장으로, 그 숫자가 무엇까지 덮는지 적혀 옵니다. 읽는 쪽이 대개 모델이기 때문입니다.

목록 읽기

include 를 주면 개수가 아니라 기억 자체가 옵니다. 빼면 개수만 오고, 그쪽이 싼 호출입니다.

page = mem.revisions(include="revised", limit=20)
page["memories"], page["matched"], page["has_more"]
const page = await mem.revisions(undefined, { include: "revised", limit: 20 });
let page = mem.revisions_page(None, "revised", 20, None, None).await?;
curl -X POST https://api.wontopos.com/api/v1/won/revisions \
  -H "X-API-Key: $WOS_KEY" \
  -d '{"user_id":"alice","include":"revised","limit":20}'
필드하는 일
include"revised" 또는 "unrevised". 다른 값은 개수만 돌려주는 대신 400 으로 거절합니다. 오타 때문에 목록이 조용히 빠지면 빈 저장소와 구분이 안 됩니다.
limit5에서 20, 기본 20. 범위 밖이거나 타입이 틀리면 깎지 않고 거절합니다.
matched이 페이지 뒤에 있는 전체 행 수입니다. 페이지 크기가 아닙니다.
ordered_by서비스가 자기 정렬 기준을 말해줍니다. 저장된 순서로 최신이 먼저이고, 최근에 고쳐진 순서가 아닙니다.
next_before다음 쪽 커서이며 next_skip_ids 와 짝입니다. 둘 다 되돌려주면 되고, id 는 쪽을 넘기며 누적됩니다.
목록은 고쳐진 시각이 아니라 저장된 시각 순입니다. "최근에 고쳐진 것부터" 로 읽으면 페이지를 잘못 읽게 되고, 그래서 응답이 어느 쪽인지 스스로 밝힙니다.
지운 것은 세지 않습니다. 지워진 기억은 셀 것이 남지 않아서, 많이 덜어낸 저장소도 revised 는 낮게 나옵니다. 이 숫자는 얼마나 고쳐졌는지를 말할 뿐, 얼마나 사라졌는지는 말하지 않습니다.
확장

컨텍스트 윈도를 넘어서.

WOS는 어떤 LLM 컨텍스트 윈도보다도 큰 140만 토큰 히스토리에서도 회수하고, 여전히 ~1,470 토큰의 짧은 조각만 돌려줍니다.

에이전트의 기억은 프롬프트에 들어가는 양에 갇히지 않습니다. 전부 보관하고 중요한 것만 회수합니다. 히스토리가 아무리 커져도 마찬가지입니다.

프라이버시

비공개, 그리고 당신의 것.

데이터는 당신의 저장소에 머뭅니다. 저희는 그것으로 학습하지도, 열람하지도, 재사용하지도 않습니다 - 회수할 수 있게 정리만 합니다.

  • BYOK. LLM 키는 요청마다 전달되며 저장되지 않습니다.
  • 격리. 기억은 계정별, 그리고 user_id별로 분리됩니다.
  • GDPR 삭제 & 셀프 호스팅. 한 번의 호출로 사용자를 삭제하고, 원하면 엔진을 자체 환경에서 운영할 수 있습니다.