Architecture: reads without an LLM
Korely is a write-once, read-many memory store: model
work runs only on writes (add(), batch(),
update(), and the direct fact writes
add_fact_triple() and correct_fact()), and every
read is pure data retrieval with no generation step. The store holds
memories with their vectors and the bi-temporal subject-predicate-object
facts extracted from them, whose subjects and objects form the graph. All
of it is indexed at write time, so that
get_context(), get_facts(), and
search() return structured data with no generative model in the loop.
The primary recall path is get_context(): it packs the
current typed facts and the memories most relevant to a query into a
ready-to-prompt block, which is the differentiator over a plain vector
store. For an agent builder this means predictable latency, no
surprise model charges on the read path, and a read-to-write quota ratio
that matches how agents actually behave: many retrievals for every new
memory.
Korely never puts a generative model between your agent and its memory:
get_context() and search() embed the query once,
and get_facts() runs no model at all. No read pays
for a generation or adds a second or two of model latency. The principle is simple:
reads are retrieval, not generation. Your agent already has
a model. It does not need ours to read a database.
This page explains exactly what runs on the read path, what runs on the write path, and why that split makes read quotas at least a hundred times more generous than write quotas.
flowchart TD
A["your agent calls add<br/>POST /v1/memories"] --> B["embed<br/>one vector per memory"]
A --> D["typed-fact extraction<br/>subjects and objects are graph nodes<br/>two-stage contradiction check<br/>supersede, never delete<br/>valid_from / invalid_at stamped<br/>one-sentence summary of the memory"]
B --> P[("persisted store<br/>vectors · graph · facts")]
D --> P
P --> R1
P --> R2
P --> R3
subgraph READ ["READ PATH, no generative model"]
R1["get_facts · get_profile<br/>typed facts read from store<br/>deterministic · zero AI"]
R2["search · get_context<br/>query embedding then<br/>vector similarity + facts"]
R3["get / get_all / users / agents<br/>pure SQL<br/>zero AI"]
end
R1 --> OUT["structured JSON<br/>returned to your agent"]
R2 --> OUT
R3 --> OUT
style READ fill:#0f172a,stroke:#334155,color:#e2e8f0
style P fill:#1e293b,stroke:#475569,color:#e2e8f0
style OUT fill:#052e16,stroke:#166534,color:#bbf7d0
The read path: SQL, vectors you already paid for, and one tiny exception
Korely exposes read operations over the REST API (GET /v1/context,
GET /v1/facts, GET /v1/memories,
POST /v1/memories/search) and mirrors them 1:1 in the SDK. Of
these, all except search and context are pure SQL lookups with
zero AI calls; those two add one embedding of the query:
| Operation | What runs |
|---|---|
GET /v1/context, korely.get_context() | The moat recall path. Embeds the query once, then packs the current typed facts and the memories most relevant to it (up to 10 facts, ranked by similarity and then recency) into a ready-to-prompt block within a token budget. Retrieval, no generation. |
GET /v1/facts, korely.get_facts() | Deterministic filter and sort over the typed fact store. No model call. |
GET /v1/memories/{id}, korely.get(id) | Primary-key lookup. One SQL query. |
GET /v1/memories/{id}/history | The timeline of a single memory: created, updated, deleted, and each fact extracted from it and when that fact was closed. Pure SQL over the memory and its facts. |
GET /v1/memories, korely.get_all() | Paginated SQL with filters. |
GET /v1/users, korely.users() | List end-user IDs seen in your namespace. Pure SQL. |
GET /v1/agents | List active agent IDs. Pure SQL. |
Search is POST /v1/memories/search (SDK:
korely.search(query, ...)). It embeds your query, which costs
a fraction of a hundredth of a cent. Retrieval is semantic vector
similarity (cosine) over the embeddings written at add() time.
That query embedding, and the single one get_context() makes,
are the only model calls on the read surface.
No generative model runs on the read path.
There is no reranker LLM and no answer synthesis. Every read endpoint
returns structured JSON assembled from the store. One thing in the
context block was written by a model, at write time: from the sixth
memory on, get_context() shows each memory as the short
dated sentence about the end user that extraction wrote with its facts,
when it wrote one. The first five memories appear in full. Your agent's own model does the reasoning over what comes back,
which means you choose the model quality, the tone, and the prompt, not us.
Request flow
your agent (its own model)
| ^
tool call | | structured JSON
v | (no LLM runs on the read)
+---------------------------------------------+
| READ PATH |
| |
| search and get_context |
| query embedding, then semantic vector |
| similarity (cosine) over stored vectors |
| |
| get_facts and all other read endpoints |
| pure SQL, zero AI |
+---------------------------------------------+
^
| reads only what writes indexed
+---------------------------------------------+
| WRITE PATH |
| |
| one embedding per memory |
| -> typed-fact extraction (the model), whose |
| subjects and objects are the graph's nodes, |
| with contradiction checking and |
| bi-temporal validity stamps |
| + a one-sentence summary of the memory |
+---------------------------------------------+
^
| korely.add() / POST /v1/memories
your application Worked round trip: a search
Your agent asks about a customer. The call, and the typed objects that come back:
# the call your agent makes
› hits = korely.search("acme renewal objections", limit=3)
# ids and snippets shortened here; a real id is mem_ plus 32 hex characters,
# a snippet is the first 280 characters of the memory
# List[SearchHit]: structured objects, no generation
[
SearchHit(id="mem_9f21ac", score=0.91,
snippet="Main objection is the per-seat price after the team grew to 40.
Dana asked for a usage-based quote before the board meeting..."),
SearchHit(id="mem_b774e0", score=0.86,
snippet="Renewal window opens July 1. Champion: Dana Reyes (VP Ops).
Risk flagged: procurement now requires a security review upfront..."),
SearchHit(id="mem_3c10d8", score=0.79,
snippet="When a customer cites per-seat cost after headcount growth,
lead with the usage tier comparison, not a discount..."),
]
Latency note: the query embedding is the only model call in this round
trip. Everything after it is a semantic vector similarity lookup over the
stored embeddings, returned as structured data. No reranker model runs, no
answer is synthesized. Your agent reads the three snippets, decides that
mem_9f21ac matters, and follows up with
korely.get("mem_9f21ac") (REST: GET /v1/memories/mem_9f21ac),
which is a single SQL query with zero AI calls.
Worked round trip: facts
Typed facts are the fastest read in the system because contradiction
checking already happened at write time. A facts read is a deterministic
filter and sort in the database, with no model call,
however many facts are in the store. GET /v1/facts is available on every
plan, including the free Hobby tier.
# the call your agent makes
› facts = korely.get_facts(entity="Dana Reyes")
# List[Fact]: deterministic filter+sort, no model call
# fields and timestamps shortened here; a real valid_from is a full ISO 8601 timestamp
[
Fact(subject="Dana Reyes", predicate="works_at", object="Acme",
valid_from="2026-02-12", invalid_at=None),
Fact(subject="Dana Reyes", predicate="role_is", object="VP Ops",
valid_from="2026-04-03", invalid_at=None),
# predicate is normalized; the raw verb is kept in predicate_raw
Fact(subject="Dana Reyes", predicate="likes", predicate_raw="prefers",
object="usage-based pricing",
valid_from="2026-06-04", invalid_at=None),
] Latency note: no embedding, no model, no semantic search. The read is deterministic, so two agents issuing the same call get byte-identical output, which makes facts safe to use inside tool chains that need reproducibility. How facts get their validity intervals is covered in temporal facts.
The write path: where the intelligence runs
All of the model work happens once, when a memory is written. A single
korely.add() (REST: POST /v1/memories) triggers the
full pipeline:
- Embedding. The memory is embedded once, as one
768-dimension vector of its text (the first 8,000 characters; facts are
extracted from the whole text).
POST /v1/memories/searchreuses this vector on every later retrieval, free of charge. - Typed-fact extraction. A model reads the memory into
subject-predicate-object facts, in windows of up to 6,000 characters:
one call for most memories, several for a longer one. Each subject has a type (person,
organization, product, place, concept, event), and the subjects and
objects are the nodes of the graph. See
the graph. The facts are
checked against existing facts for contradictions, and
stamped with bi-temporal validity. Conflicts are resolved here, at write
time, so reads never have to arbitrate between a stale fact and a fresh
one. The same call writes a one-sentence summary of what the memory
says about the end user.
get_context()uses it for the memories beyond the first five.
All of this costs a fraction of a cent per memory, and it is all included in the per-memory write price. There are no surprise model charges on top.
Why read quotas are a hundred times more generous
The economics follow directly from the architecture. A write runs an embedding, fact extraction, and the contradiction check. A read runs SQL, plus at most one tiny query embedding. So the pricing mirrors reality: write quotas are sized around the work writes actually do, and read quotas are at least a hundred times more generous, because reads are nearly free to serve.
For agent workloads this is the shape you want. A typical agent reads its memory many times for every time it writes, often dozens of retrievals per new memory. With Korely that ratio costs you almost nothing, and it never pushes you toward caching reads or batching lookups to stay inside a quota. See pricing for the exact numbers per plan.
The API, the vector store, the graph and the embedding model run on our
own infrastructure, EU-hosted. The language model of the write path, the
one that reads a memory into entities and facts and checks new facts
against earlier ones, depends on the project's
region: Google's Gemini
API (United States and other countries where Google has facilities, see
subprocessors) in the default
global region, or
gpt-oss-120b on Scaleway, in Paris, in the eu region. See
Security for what it receives.
See also
- Memory model, how
memories, entities, and facts map to
user_id,agent_id, andrun_id, and when to scope reads to a single user versus the whole agent namespace. - Temporal facts, the
bi-temporal validity model, how contradiction detection runs at write
time, and how to query facts
as_ofa past date. - The graph, the typed
entity graph behind the bi-temporal facts that
get_context()andget_facts()read, and how fact extraction wires new memories into it automatically.