Korely

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
Write pipeline vs read path, no generative model on the read side

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:

OperationWhat 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

korely architecture 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:

korely sdk korely.search
# 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.

korely sdk korely.get_facts
# 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:

  1. 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/search reuses this vector on every later retrieval, free of charge.
  2. 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, and run_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_of a past date.
  • The graph, the typed entity graph behind the bi-temporal facts that get_context() and get_facts() read, and how fact extraction wires new memories into it automatically.