Korely

The memory model

Agent memory in Korely is a three-layer, EU-hosted knowledge store. The layer that sets it apart is the bottom one: a durable set of typed (subject, predicate, object) facts that carry validity over time, supersede each other through a two-stage contradiction check, and can be replayed at any past instant with as_of. On top of those facts sits a managed cloud store of memories (notes, transcripts, work-session logs) with semantic vector recall, and a session-scoped view for thread-local working state. You write plain text; extraction, embedding, entity linking, and contradiction detection all happen automatically behind a single add() call. Your agent's primary recall path, get_context(), packs the current facts and the memories most relevant to the query into a prompt-ready block, structured, current knowledge on demand, with no generative model on the read path. For an agent builder this means: never prompt-stuff stale context, never manually reconcile conflicting facts, and never build a separate memory layer from scratch.

Korely stores memory in three layers. One stack behind the REST API, the SDKs, the CLI and the MCP server: every agent that writes and queries it, and every end user who reads, corrects or forgets what an agent remembers, works on the same store. The memory store is the base, and two layers sit on top of it.

flowchart LR
    A["Layer 1, Memory store<br/>add / search / get_context<br/>scoped by user_id"]
    B["Layer 2, Session<br/>run_id tag<br/>thread-local scope"]
    C["Layer 3, Facts<br/>typed triples<br/>valid_from · invalid_at · as_of"]

    A -->|"mined at write time"| C
    A -->|"run_id scopes a slice"| B
    B -->|"facts extracted like any memory"| C
Three-layer memory model: managed cloud store, session scoping, and typed bi-temporal facts
LayerWhat it holdsScopeLifetime
1, Memory storeNotes, transcripts, work-session logs (kind note or session)UserForever (it is the user's data)
2, Session memoryMemories tagged with one run_idSession (a filter on Layer 1)Until deleted, like any memory
3, Cross-session factsTyped (subject, predicate, object) triplesUserBi-temporal: superseded, never silently lost

Layer 1, The memory store

Layer 1 is a managed cloud store in the EU, Postgres with a vector index. Every memory your agent writes lands here, scoped to your account and an end-user namespace. You read and write it over the REST API or the SDK; there is nothing to host yourself. You can read back everything stored for a user, page by page (their memories with get_all, their facts with get_facts(include_invalidated=True), which also returns the facts that ended), and erase it all with one call, delete_all.

For agents, recall over the memory store is semantic vector search:

  • 768-dimension vectors. Every memory is embedded as a 768-dimension vector, so search() finds semantically similar content by cosine distance even when no keyword overlaps. The memory's embedding is computed once at write time; the query is embedded at read time.
  • Entity graph (behind the facts). The typed facts extracted from every memory form a graph: their subjects (typed as people, organizations, products, places, concepts or events) and their objects are the nodes, linked by predicates in 10 families. The graph is what powers the typed facts in Layer 3, it is not a search mode the agent queries directly. Details in The graph.

Your primary recall path is get_context() / GET /v1/context. It embeds the query, picks the current typed facts most relevant to it (up to 10, ranked by similarity and then recency) and the most relevant memories, and packs them into one prompt-ready block within a token budget. To list the facts true now about one end user, call get_profile() (up to 200 facts) or get_facts(), which pages through all of them with limit and offset. Raw search() / POST /v1/memories/search is the secondary path when you want the underlying memory snippets ranked by semantic similarity.

Layer 2, Session memory

Session memory is per-conversation working state. When an agent runs a long thread, a research session, a multi-step refactor, a week-long project, it calls add() with a stable run_id to tag memories to that session. A later search() or get_all() with the same run_id returns only the memories tagged with it. The tag filters those reads and nothing else: search() without run_id and get_context() include these memories, and the facts extracted from them join the end user's facts like those of any other memory.

This is what keeps a thread coherent across context-window resets and client restarts: the agent re-reads its own session memories, picks up the state, and continues.

Layer 3, Cross-session facts

Layer 3 is what most people mean by "agent memory": things the system knows about the user and their world, independent of any one note or conversation. We store them as typed (subject, predicate, object) triples, extracted automatically whenever content is written, and queryable via get_facts() in the SDK or GET /v1/facts in the REST API.

Every predicate belongs to one of 10 families:

FamilyExample triple
preferences(Marco, likes, peach fruit salad)
people(Sara, reports_to, Marco)
places(Marco, lives_in, Bologna)
work(Sara, works_at, Acme GmbH)
ownership(Marco, owns, a 2019 MacBook Pro)
health(Marco, allergic_to, peanuts)
financial(Aurora plan, costs, 50 euro per month)
events(team offsite, scheduled_for, 2026-07-12)
identity(Gordon, renamed_to, Korely)
othercatch-all for valid triples outside the 9 above

Facts are bi-temporal

Every fact carries valid_from and invalid_at. When new information contradicts an existing fact, a two-stage contradiction check invalidates the old one and keeps it as history instead of deleting it. A chain after a price change looks like this:

(Aurora plan, costs, 40 euro per month) valid_from 2026-05-18 invalid_at 2026-06-07
(Aurora plan, costs, 50 euro per month) valid_from 2026-06-07 invalid_at null

By default, GET /v1/facts returns only the facts that are true now: valid_from not in the future, and invalid_at null or still in the future. Pass include_invalidated=true and superseded facts come back too, each carrying its invalid_at timestamp (and, when the fact was replaced through correct_fact, the invalidated_by id of the replacement), so an agent can reason about how a value changed over time, not just what it is now. The full mechanics are in Temporal facts.

Point-in-time queries: the REST API exposes the same data with as-of slicing (GET /v1/facts?as_of=2026-05-15) and a prompt-ready GET /v1/context block. See the API reference.

The human is in the loop

Layer 3 is not a black box the agent maintains behind the user's back. Every fact is a readable triple: get_profile lists them for one end user, correct_fact fixes a wrong one, forget_fact closes one, and delete_all erases a user with an audit record. The dashboard has the same Correct and Forget actions. Forgotten facts are excluded from agent reads by default. We think memory an agent collects about a person should be inspectable by that person, so your product can show it to them. More in Human in the loop.

How a write flows

When an agent calls add() (or POST /v1/memories), the pipeline runs like this:

  1. The memory is stored and embedded (one 768-dimension vector), so search can find it as soon as the call returns; if the embedder gives no vector, a background worker adds it later. The rest runs in the background.
  2. Typed fact extraction pulls (subject, predicate, object) triples into Layer 3 and, in the same call, writes a one-sentence summary of what the memory says about the end user; their subjects and objects are the nodes of the graph.
  3. Each new fact passes the two-stage contradiction check; contradicted facts get invalid_at stamped.

The write call itself returns once the memory is persisted, with facts empty on the immediate response; extraction then runs behind it and the typed facts populate shortly after. You never block on extraction. The write path is where the intelligence runs: the memory's embedding, typed-fact extraction with contradiction checking and bi-temporal validity. It costs a fraction of a cent per memory, all included in your plan.

Here is the same pipeline from the SDK. One add call with raw conversation text in, structured memory out:

korely sdk python
from korely_memory import Korely

korely = Korely(api_key="kor_live_...")

# One write call. Extraction runs asynchronously behind it.
memory = korely.add(
    "Giulia upgraded to the Advanced plan and asked us to stop "
    "calling her. She prefers email follow-ups. She is based in Turin.",
    user_id="customer-giulia-4812",
    agent_id="support-bot",
)

print(memory.id)
mem_8f2c1a
# id shortened here; a real id is mem_ plus 32 hex characters

Seconds later, the same raw sentence exists as a searchable memory and three typed facts, whose subjects and objects are nodes of the graph. One of the facts retires an older preference via the contradiction check (the families are the model's classification):

Memory mem_8f2c1a (embedded for vector search)
Facts (Giulia, has_plan, Advanced) family: financial
(Giulia, likes, email follow-ups) family: preferences
↳ invalidated (Giulia, likes, phone calls) 2026-04-02 → 2026-06-11
(Giulia, lives_in, Turin) family: places

How a read flows

Reads are retrieval, not generation. Most read calls, get_facts(), get_profile(), users(), history() in the SDK, or their REST equivalents, are pure SQL lookups, zero AI calls. get_context() embeds the query once, then picks the current typed facts and the stored memories closest to it. search() embeds the query once, a fraction of a hundredth of a cent. Recall over the memory store is semantic vector similarity (cosine); facts and profile reads are deterministic SQL.

No generative model runs on the read path: no reranker LLM and no answer synthesis. Your agent's own model is the only LLM that reads at query time. In the get_context() block, the first five memories appear in full; the rest appear as the one-sentence summary that extraction wrote about the end user when the memory was stored, when it wrote one. That split is why read quotas are at least a hundred times more generous than write quotas; a read costs us a database query, not an inference call. Facts reads are deterministic, with no model call; search and get_context add one embedding call for the query.

The intelligence runs at write time. Fact extraction and contradiction checks need a model; retrieval does not. The split is intentional: writes are rare and asynchronous, reads are frequent and latency-sensitive. Pay the inference cost once on the way in, never on the way out.

Scoping a read: user_id, agent_id

Reads accept scoping parameters. Filters are additive (AND): omit user_id to read across every end user in the project (add agent_id to narrow it to one agent), and pass run_id to search() or get_all() to limit results to one session. Without run_id, search() covers every session.

ParameterScopes toExample
user_idThe end user your agent serves. End users are unlimited on every tier.customer-giulia-4812
agent_idYour application's namespace. Pass it to narrow a read to one agent. Agents of the same end user share that user's facts, and the wall between customers is the project (each API key belongs to one project).support-bot
run_idOne session or agent run: a tag on writes, and a filter on search() and get_all().2026-06-11-session-03
korely sdk python
# Same client as above. Scope the read to one person and one app.
results = korely.search(
    "contact preferences",
    user_id="customer-giulia-4812",   # one end user
    agent_id="support-bot",             # your application
)

for r in results:
    print(r.score, r.snippet)

0.84  Giulia upgraded to the Advanced plan and asked us to stop calling her. She prefers email follow-ups. She is based in Turin.

Python and Node.js today. See pricing for plan details.

Which call touches which layer

The REST API and SDK give you one surface for all three layers. Which layer each call touches:

SDK method / REST endpointLayerDirection
get_context() / GET /v1/context1 + 3, The current facts and memories most relevant to the query (primary recall)Read
search() / POST /v1/memories/search1, Memory store (semantic vector)Read
get_all() / GET /v1/memories1, Memory storeRead
add() / POST /v1/memories1, Memory store (feeds Layer 3 extraction)Write
update() / PATCH /v1/memories/{id}, delete() / DELETE /v1/memories/{id}1, Memory storeWrite
add(run_id=...) / GET /v1/memories?run_id=...2, Session memory (scoped view of Layer 1)Read / Write
get_facts() / GET /v1/facts3, Cross-session factsRead
add_fact_triple() / POST /v1/facts3, Cross-session factsWrite

Note the asymmetry: facts are extracted automatically from every add() call, the memory is always the traceable source. For explicit fact writes the SDK exposes add_fact_triple() and the REST API exposes POST /v1/facts. See the API reference for the full contract.

EU-hosted: memories, facts, their vectors and the graph are stored on our own infrastructure in the EU, on every tier. To extract facts, the text of each memory goes to the language model of the project's region: Google's Gemini API by default (global), or gpt-oss-120b on Scaleway in Paris (eu). See Security.

End-to-end example: support bot with persistent memory

The following example shows all three layers interacting in a single support session. The agent writes a conversation turn, then reads back structured context for the next turn, including the typed facts that were extracted automatically from earlier sessions.

korely sdk python
from korely_memory import Korely

korely = Korely(api_key="kor_live_...")

# ── Layer 1 write: the agent stores a conversation turn.
# Extraction runs async: typed facts are mined behind the call.
korely.add(
    "Customer-4812 called about her Developer subscription. "
    "She is based in Milan and wants all follow-ups by email. "
    "She is allergic to peanuts (mentioned it while chatting).",
    user_id="customer-4812",
    agent_id="support-bot",
    run_id="session-2026-06-16",   # Layer 2 tag: scopes this turn to the session
)

# ── Layers 1 + 3 read: fetch a prompt-ready context block for the next turn.
# get_context() packs the Layer 3 facts and Layer 1 memories most relevant
# to the query into one block. No generative call on the read path.
ctx = korely.get_context(
    query="preferred contact channel and account tier",
    user_id="customer-4812",
    agent_id="support-bot",
)
print(ctx.context)

# ── Layer 3 read: pull structured facts for an explicit check.
# Returns deterministic SQL rows, not a generated summary.
facts = korely.get_facts(
    user_id="customer-4812",
    agent_id="support-bot",
)

for f in facts:
    print(f.subject, "|", f.predicate, "|", f.object, "|", f.predicate_family)

# Example output (f.predicate is normalized; f.predicate_raw keeps the source verb;
# f.predicate_family is the model's classification):
customer-4812  | lives_in        | Milan           | places
customer-4812  | likes           | email follow-ups| preferences
customer-4812  | has_plan        | Developer       | financial
customer-4812  | allergic_to     | peanuts         | health

The agent wrote one short note, three sentences in a single call. Once the memory's status is ready (see GET /v1/events), its typed facts are in Layer 3; with this text you can expect rows like the ones above, and subjects and objects such as "customer-4812", "Milan" and "Developer" become nodes of the graph. If an earlier contact preference contradicts the new one, the contradiction check closes it. get_context() puts the facts relevant to the query into the prompt block. No post-processing, no prompt engineering to remember what was said three turns ago.

SDK parity: the Node.js SDK exposes the same surface with camelCase method names (getContext, getFacts, addFactTriple, deleteAll, batchStatus). The REST API is at https://api.korely.ai/v1 with Authorization: Bearer kor_live_.... All plans, including hobby (free), share the same three-layer stack.

See also

  • Temporal facts, how valid_from, invalid_at, and the two-stage contradiction check work in detail, with point-in-time as_of queries.
  • The knowledge graph, the entities and typed edges the facts form, and how shared entities connect memories your agent did not explicitly link. Query the neighborhood of an entity with GET /v1/facts?entity=....
  • Human in the loop, how to let end users see, correct, and forget the memory their agents collect about them.