Concepts
Architecture: reads without a language model
Korely does its language model work when you write, never when you read. This page shows what runs on each side, and where the write model runs.
When you save a memory, a language model reads it and writes facts. When your agent reads, Korely only looks up what was already written.
Quick start
Install with pip install korely-memory (Python 3.9 or later)
or npm install korely-memory (Node.js 18 or later). Then
get a free API key (Quickstart) and run
export KORELY_API_KEY=kor_live_.... Checked against SDK 0.1.28
(Python) and 0.1.17 (Node.js).
The code saves one memory for the end
user acme-account, waits, then makes three reads that run
no language model.
import timefrom korely_memory import Korely
korely = Korely() # reads KORELY_API_KEY from the environmentkorely.add("Dana Reyes works at Acme. Dana prefers usage-based pricing.", user_id="acme-account")time.sleep(20) # facts usually take about ten seconds
facts = korely.get_facts(user_id="acme-account")hits = korely.search("pricing", user_id="acme-account")block = korely.get_context("How should I pitch the renewal?", user_id="acme-account")
print([(f.subject, f.predicate, f.object) for f in facts])print([(round(h.score, 2), h.snippet) for h in hits])print(block.tokens, block.sources)Output of one run (illustrative: the facts, the score and the ids vary; ids shortened):
[('acme-account', 'works_at', 'Acme'), ('acme-account', 'prefers', 'usage-based pricing')][(0.31, 'Dana Reyes works at Acme. Dana prefers usage-based pricing.')]307 ['fct_...', 'fct_...', 'mem_...']- The note is about Dana, so the model can treat her as the end user and use your
user_idas the subject. Another run can writeDana Reyesinstead. 0.31is the score, closeness in meaning from about 0 to 1. A one-word question like"pricing"can score this low and still find the right memory.307is the size of the block in tokens.block.sourceslists the ids of the facts (fct_) and the memory (mem_) in the block.add()is the write: the language model ran during the wait. The three reads ran no language model.
If facts is empty, read again after a few seconds. Check that
both calls use the same key and the same user_id. A memory can
finish with no facts: then write the fact again as one simple sentence in
the present tense.
Terms
The Glossary defines the words used across the docs. These are the ones this page relies on.
- Read path: the calls under Reads. No language model runs on them.
- Write path: the calls under Writes. The language model runs here.
- Embedding model: a small model on our server that turns a text into an embedding (see Search). It writes no text, so it is not a language model.
- Query embedding: the embedding of your question, made once per
searchorget_contextcall.
The two paths
flowchart TD
A["add (or batch, update)<br/>POST /v1/memories"] --> B["embedding of the memory<br/>normally inside the call"]
B -->|"saved: processing"| S[("store<br/>memories + embeddings<br/>facts, which form the graph")]
B -->|then| D["extraction in the background<br/>about ten seconds<br/>language model writes facts<br/>and checks contradictions"]
D --> S
S --> R1
S --> R2
S --> R3
subgraph READ ["READ PATH, no language model"]
R1["get_facts, get_profile, get,<br/>get_all, history, users,<br/>list_agents<br/>database read, no model"]
R2["search<br/>query embedding, then<br/>similarity over memories"]
R3["get_context<br/>the same, plus<br/>the current facts"]
end
style READ fill:#0f172a,stroke:#334155,color:#e2e8f0
style S fill:#1e293b,stroke:#475569,color:#e2e8f0
Reference
Reads
| Read | REST | SDK Python / Node | Model? |
|---|---|---|---|
| Context block | GET /v1/context | get_context() / getContext() | Embedding model only |
| Search | POST /v1/memories/search | search() / search() | Embedding model only |
| Facts | GET /v1/facts | get_facts() / getFacts() | None |
| Profile of one end user | GET /v1/profile | get_profile() / getProfile() | None |
| One memory | GET /v1/memories/{id} | get() / get() | None |
| List of memories | GET /v1/memories | get_all() / getAll() | None |
| History of a memory | GET /v1/memories/{id}/history | history() / history() | None |
| End users | GET /v1/users | users() / users() | None |
| Agents | GET /v1/agents | list_agents() / listAgents() | None |
Pass user_id to every read when your agent serves more
than one end user. A get_context, get_facts
or search call without user_id reads every end
user of the project. The exception
is a confined key, which reads
only its own end users.
get_contextbuilds the context block. First come up to 10 current facts, chosen by closeness to the question. Between two equally close facts, the one that memories stated more recently, or more often, usually comes first.- Memories in the block fill the rest of the token budget (default 800). The first five appear in full. From the sixth on, each is one dated line: a short summary written at extraction, or the start of the text.
searchreturns the closest memories, each with anid, a score, asnippet(the first 280 characters),user_id,agent_idandmetadata.get_factsfilters facts in the database, by end user, entity or predicate: 50 per page by default, at most 200.get_profilereturns the current facts of one end user, grouped by predicate family, such as work (works_at) or preferences (prefers).getreads one memory and its facts that are true now.historyis the timeline of one memory: created, updated, deleted, each fact extracted from it, and when a fact was closed.userslists the end users with memories you have not forgotten, or with current facts.list_agentslists theagent_idnames used in the project. A name whose data was all forgotten stays listed, with zero counts, until you delete it.
No read sends anything to a language model provider. Each read in the table counts one read in your plan's monthly quota. On every plan the read quota is at least a hundred times the write quota: see Pricing.
Writes
Five calls use a language model:
| Call | REST | What it does |
|---|---|---|
add() | POST /v1/memories | Saves one memory. |
batch() | POST /v1/batch | Saves many memories in one call. |
update() | PATCH /v1/memories/{id} | Rewrites the text of a memory. |
add_fact_triple() | POST /v1/facts | Writes one fact you give, without a memory. |
correct_fact() | PATCH /v1/facts/{id} | Replaces a wrong fact with the right one. |
add, batch and update run the three
steps below. The two fact calls run only step 3.
- Embedding. Inside the call, the embedding model turns the start of the text, up to 8,000 characters, into an embedding. Search matches on that part. Then
add()returns statusprocessing. - Extraction. In the background, usually in about ten seconds, a language model reads the whole text and writes facts. A long text is read in pieces of up to 6,000 characters, one model call each. Each subject gets a type: person, organization, product, place, concept, event, or unknown. The model also writes a short summary sentence about the end user, when the memory says something about them.
- Contradiction check. The database finds earlier facts that could clash, then a language model decides. A contradicted fact is closed, not deleted, unless you erase it. So a read never has to choose between an old fact and a new one.
Steps 2 and 3 take several model calls per memory. They are included in your write count, with no surprise model charges. A memory counts one write per 6,000 characters, rounded up.
Where it runs
On the Cloud, the API, the database, the embeddings and the embedding model run on Korely's servers in Helsinki (Hetzner).
The write language model depends on the project's region:
global(the default): a model from Google (the Gemini API), outside the EU: in the United States and other countries where Google has facilities.eu: gpt-oss-120b on Scaleway, in Paris.
The model receives the text of the memory and the facts to compare. Our Security page gives the details. Subprocessors are the outside companies that process data for us; the list of subprocessors names them.
Details
- A busy server. Under a burst from one account, a read waits its turn, up to 20 seconds for a slot on the account and up to 20 more at the embedder (about 40 seconds at worst). Then
searchanswers503 search_unavailable, andget_contextanswers200withdegraded: true: the block is incomplete: the latest facts, no memories. Retry after a few seconds. Over REST a degradedget_contextstill counts as one read and asearchending in503counts none. Through MCP,searchand an empty degraded block are a tool error (search_unavailable) that counts none.
See also
- Memory model: when to read one end user or the whole project.
- Temporal facts: how facts change over time.
- The graph: how facts connect entities.
- Get context: the context block, line by line.