Korely for AI Agents
Memory for AI agents that knows what's still true.
Typed bi-temporal facts, contradictions resolved instead of overwritten, and as_of queries for what was true on any past date.
A Cloud API with data stored on Korely's servers in Helsinki (the Europe region reads your text with Scaleway in Paris; the default region uses a model from Google, outside the EU).
# 1. Connect (reads KORELY_API_KEY from the env)
from korely_memory import Korely
korely = Korely()
# 2. Remember what your agent learns about a user
korely.add("Maria is on the Pro plan, billed yearly.", user_id="maria")
# 3. Later it changes, and Korely resolves the contradiction
korely.add("Maria downgraded to Free.", user_id="maria")
# Facts are extracted after add() returns; korely.events(user_id="maria") says when they are ready
# 4. Pull a prompt-ready block: current facts first, superseded facts left out
ctx = korely.get_context(query="what plan is Maria on?", user_id="maria")
# e.g. ctx.context → "...## Known facts\n- The user subscribes_to Free plan (since ...)"
# (the relation name is chosen by the extractor)
# the Pro fact is invalidated, not deleted // 1. Connect (reads KORELY_API_KEY from the env)
import { Korely } from "korely-memory";
const korely = new Korely();
// 2. Remember what your agent learns about a user
await korely.add("Maria is on the Pro plan, billed yearly.", { user_id: "maria" });
// 3. Later it changes, and Korely resolves the contradiction
await korely.add("Maria downgraded to Free.", { user_id: "maria" });
// Facts are extracted after add() returns; await korely.events({ user_id: "maria" }) says when they are ready
// 4. Pull a prompt-ready block: current facts first, superseded facts left out
const ctx = await korely.getContext({ query: "what plan is Maria on?", user_id: "maria" });
// e.g. ctx.context → "...## Known facts\n- The user subscribes_to Free plan (since ...)"
// (the relation name is chosen by the extractor)
// the Pro fact is invalidated, not deleted # Install the CLI (it ships inside the Python package)
pip install korely-memory
# Remember, then recall: the same store as the SDK and REST
korely add "Maria downgraded to Free." --user-id maria
# Facts are extracted after add returns; korely events --user-id maria says when they are ready
korely context "what plan is Maria on?" --user-id maria
# e.g. ## Known facts
# - The user subscribes_to Free plan (since ...)
# (the relation name is chosen by the extractor)
# Time-travel: what was true back in March?
korely facts --as-of 2026-03-01 --user-id maria # Remember (facts + graph extracted server-side)
curl -X POST https://api.korely.ai/v1/memories \
-H "Authorization: Bearer kor_live_..." \
-H "Content-Type: application/json" \
-d '{"content": "Maria downgraded to Free.", "user_id": "maria"}'
# Recall a prompt-ready block: current facts first, superseded facts left out
curl "https://api.korely.ai/v1/context?query=what+plan+is+Maria+on%3F&user_id=maria" \
-H "Authorization: Bearer kor_live_..." One key authenticates the SDK, CLI, and REST API. Get your key → · Read the docs →
Wire it into the stack you already use
Any agent that speaks REST, the SDK, or the CLI. All integrations →
Proof, not promises
Storing a fact is easy. Returning the current one is where memory breaks.
A fact changes; the question is whether your agent serves the new value or last month's. We measure memory on two public benchmarks, LoCoMo and LongMemEval, and give the method and the limits on the benchmark page.
86.9% vs 79.9%
LoCoMo, Mem0's protocol. With the same reader and judge (gpt-4o-mini), Korely's memory answers better than giving the model the whole conversation.
87.2%
LongMemEval, oracle split: 436 correct of 500 questions, with gpt-4o as reader and judge.
How to read these: one run, an indulgent judge, an older engine than today's. The full method and the limits are on the benchmark page.
How it works
Your agent just chats. Korely keeps it on
what's true now.
Typed facts from plain conversation, contradictions resolved, recalled by any agent, the same store, across every session.
Why Korely
Memory your agent can trust.
Typed facts that resolve over time, so your agent answers from what is true now. The full history and the graph are one query away.
Each number above is how the product works today, not a benchmark score. Check them in the API reference and on the pricing page.
Typed facts, resolved over time, with a graph and an audit trail. One store, four surfaces, free to start.
What Korely does
One memory layer, every shape your agent needs
Semantic vector recall, a typed knowledge graph, and bi-temporal facts, in one store. REST, SDK, and CLI for any agent.
The memory
Memory model
Semantic vector recall over memories, a typed entity graph, and bi-temporal facts, in one store. Contradictions resolved on write.
Time + truth
Bi-temporal facts (valid_from / invalid_at). Point-in-time queries with as_of; contradictions invalidate, never delete.
Retrieval
Semantic vector search over stored memories, plus deterministic lookup of typed facts and entity relations. No generative model on the read path.
Where memories live
A managed cloud store (Postgres + pgvector), stored in the EU (Helsinki). The model step runs in the region you pick per project: Europe (Scaleway) or Global (Google Gemini).
The integration
Agent integration
REST API, Python + Node SDK, and a CLI (it ships inside the Python package).
SDKs
Python and Node.js, plus REST from any language. The CLI installs with the Python package.
Hosting
Stored in our EU cloud (Helsinki) on every tier. The model step runs in the region you pick per project: Europe (Scaleway) or Global (Google Gemini). No overage billing, ever.
Pricing model
Flat, predictable pricing. Graph and temporal facts on every plan, free included.
Trust & control
Efficient by default. Transparent by design. Yours to control.
The primitives a regulated team needs, on every tier, not gated behind an enterprise call.
Efficiency
A context block that fits the budget you set.
Each turn, your agent sends a block of current facts and relevant memories, capped at a token budget you choose, not the whole transcript.
- Current facts first, superseded memories marked as history
- 800 tokens by default, from 50 to 8,000
- No generative model on reads
conversation history
context block
Time travel
Ask what was true on any past date.
Every memory becomes a typed fact with its own validity window. When a value changes, the old one is kept, not overwritten, so you can replay your agent's knowledge as of any moment.
- Typed facts with valid_from and invalid_at
- Changes supersede the old value, never overwrite it
- as_of returns the truth at any past date
Control
Ask to be forgotten, and you really are.
When a user invokes their right to erasure, one call deletes their memories and facts from the live database (within the key’s project), with an audit record that keeps counts, never content. Encrypted backups drop their copy within 35 days. It was never baked into the model, so there is nothing left to unlearn.
- Erase everything about an end user with one call
- Every erasure is logged, so you can prove it
- Korely never trains on your data or bakes it into a model
Use cases
Ten shapes of agent memory
One memory, scoped per agent. Each vertical ships with a code snippet you can copy.
Pricing
Flat, predictable pricing, with the graph included
Prices exclude VAT. Polar adds your country's VAT at checkout.
Changelog
What's new
- API
Korely 1.0.23: a key rotates in one call and lasts at most a year on the Cloud, closing an account takes its id, and a /v1 query parameter the route does not take is a 422
One migration runs when the service starts (it adds three columns to the API keys: the old secret of a rotation, when it ends, and when the current secret was made). Nothing to set, but read what follows. Before upgrading, four things. First, on an install of your own, the image now believes the shipped proxy alone about where a request comes from: FORWARDED_ALLOW_IPS is caddy, the name of the proxy service in docker-compose.yml, where 1.0.22 said * and believed every peer. With the shipped compose, and the installer's setup, there is nothing to do. If a proxy of yours stands in Caddy's place, put FORWARDED_ALLOW_IPS=<its name, address or network> in .env before upgrading: without it every client appears with your proxy's own address (one login lock for everybody, the caps per network shared), the API takes every request for plain HTTP, and /health keeps its details from the callers your proxy relays. A FORWARDED_ALLOW_IPS=* you set yourself is kept. A command: of yours that starts the API with --proxy-headers must drop that flag. Second, a GET of /v1 with a query parameter the route does not take is now a 422 (code invalid_request, one error per parameter, the message naming the one it may mean and every one the route takes). Up to 1.0.22 as_of on the context was ignored and the block answered with the facts true today, and GET /v1/facts?userId=anna answered with every end user of the key. Take the parameter out or fix its spelling (user_id): known_as_of builds the context as the store knew it on a date, and GET /v1/facts?as_of= says what was true on one. The SDKs, the command line, the MCP and the n8n node 0.1.1 send none. Third, DELETE /v1/account wants the account's id typed: ?confirm=<account id>, the tenant_id of GET /admin/whoami with the same key. ?confirm=true, enough up to 1.0.22, is now a 400 confirmation_required that says where the id is, and nothing is deleted. The developer libraries that send true (Python 0.1.29 and earlier, Node 0.1.18 and earlier, and korely delete-account) get that 400 until the new libraries, which come with 1.0.23 and take the id. Fourth, a report or an alert on the audit trail's erase action: from 1.0.23 a memory forgotten is forget and a fact closed with /forget is fact_invalidate, and erase counts erasures only. Rows written before keep erase. To go back to 1.0.22, run alembic downgrade 57a77fbaad61 with the 1.0.23 image first, then start 1.0.22; an old secret still in its overlap stops working at once. What changes. Keys. POST /admin/keys/{id}/rotate gives the same key (id, name, project, permissions, end users) a new secret, shown in that answer only; the old secret works old_key_expires_in_hours more (24 by default, at most 168, 0 ends it at once, never past the key's own end), and old_secret_expires_at says until when, in the answer and in GET /admin/keys. Rotating again ends an earlier overlap, revoking the key ends both secrets, a revoked or expired key or a connected app's is a 409 key_not_rotatable. On the Cloud a key lasts at most 365 days and none for ever: POST /admin/keys with expires_in_days over 365, or null, is a 422 naming the ceiling, and the dashboard offers no Never. Keys made before keep their dates. Adopting an account (signing in to the dashboard with a key made by POST /v1/agents/init) no longer leaves its init key without an end: it lasts 365 days from the adoption. An account still on trial cannot rotate (403 account_not_adopted). Every answer to a key that expires carries the header Korely-Key-Expires-At (ISO 8601, UTC, to the second), on REST, /admin and the MCP door; a key that never expires, and a connected app's, send none. A key made through the service's own code lasts at most 3650 days on an install of your own. The trail. Every event of GET /v1/audit and of the dashboard's Audit log carries api_key_id, the id GET /admin/keys lists, never the key; null for a person signed in to the dashboard, the command line and the worker. The dashboard's lists of memories and facts are filed once per project and end user whose rows they showed (they were one row with the end user asked for, or none), so GET /v1/audit?user_id=anna finds that the screen showed Anna's memories; the rows of one list share meta.call and the Requests page counts the call once. GET /v1/users lists every end user the project holds anything of, as DELETE /v1/users/{id}/memories finds them, with zero counts and last_active null when nothing of theirs is live; a script that read the list as the active end users keeps those with memories or facts above zero. GET /v1/agents with a key confined to some end users lists the agents of their forgotten rows too, with zeros, as a key of the whole project does. Security. A write that an end user's erasure met right after it answered 500 although the memory had been written, and a client that retries a 500 wrote the text again after the erasure; now POST /v1/memories, korely_add, the batch import and PATCH /v1/memories/{id} read the memory back before their commit and answer 201 (or 200) with it, and the erasure, which came after, has erased it. The address the login lock, the sign-in audit row and the caps per network read could be chosen by a container on the compose network, or by a port 8000 published by mistake, with X-Forwarded-For; only the peers FORWARDED_ALLOW_IPS names are believed now. The dashboard. /signup opens an account, and /login signs in: the sign-in page has two views by its address, both titled Korely (the tab said Korely Agents), each linking to the other. With ?plan=developer, team or scale a person who signs in lands on the Plan page with that plan marked; nothing is bought until its own button is clicked. The developer libraries. korely-memory for Python 0.2.0 and its npm release come with 1.0.23: delete_account / deleteAccount / korely delete-account --confirm take the account's id, audit events keep api_key_id, delete receipts keep facts_reopened, the CLI refuses a --user-id or --agent-id a command never sent (exit 2; a script that passed one must remove it), the hooks' reads share 8 seconds and a hex key is masked before a session leaves the machine. The library changes are in the libraries' own changelog. The public specification changes in its version and in the descriptions of GET /v1/users, POST /v1/agents/init and DELETE /v1/account (its confirm is now the account's id), and in the audit event (api_key_id and the forget action).
- API
Korely 1.0.22: a restart no longer holds memories back, an agent's notes keep the search's order, and the dashboard says what failed
One migration runs when the service starts: it adds a column, graph_sync_claimed_by, to agent_memories, and a trigger that clears it whenever a memory's claim changes. Nothing to set. Before and after upgrading, three things. First, going back to 1.0.21 needs the downgrade first: with the 1.0.22 image, run alembic downgrade b6e2d9f4a8c1, then start 1.0.21, because the 1.0.21 image stops at its start on a database at a revision it does not know. Second, DATABASE_URL must reach Postgres directly: not through PgBouncer, or any pooler, in transaction mode, because each process now holds a lock on its own database session (no pooler has been tested, in either mode). The shipped compose connects directly. Third, the image's command ends open requests 5 seconds after a stop (--timeout-graceful-shutdown 5), inside Docker's 10-second stop timeout; if you start the API with a command of your own, add that flag with a value below your stop timeout. What changes. A restart no longer holds the memories being written, and their person, for 15 minutes. Until 1.0.21 a deploy, a rollback or a crash left the memories being written as processing, and every later memory of the same person waited behind them until DRAIN_STALE_MINUTES gave the claim back; nothing was lost. Now each process that drains holds a lock the database keeps alive while the process is connected, and stamps its claims with it. At a stop the worker hands every memory it holds back to the queue at once; after a kill or a crash the next drain finds that no session holds that process's lock and puts its memories back at once. The memory that was being written is written again, one more model call. Still waiting for the timer: a claim with no stamp (a write that waits for its facts, WRITE_ASYNC=false, cut by the restart; a memory claimed by a release before 1.0.22, so the restart that installs 1.0.22 waits once more; a claim in the instant before it is stamped), and every claim when PostgreSQL has no lock to give. Proven by tests with a real process killed while it held a memory; not yet measured on a deploy of the Cloud. In the context block, only an end user's turns are printed as one exchange. Until 1.0.21 memories with no user_id counted as one end user's, so two notes written one after the other, by an agent or through MCP with no ids, read oldest first, and in a 400-token block the newer note, the closer to the question, was cut to a few words or left out (seen on our own team's memory: 5 of 41 memories just written came second). Now a memory with no user_id keeps the search's order, whoever wrote it. An end user's turns are printed as before. If the memories you write with only an agent_id are the turns of one person's conversation, write them with that person's user_id; what is stored is not rewritten. This is not measured with a model reading the blocks: LoCoMo and LongMemEval write every turn with a user_id, and 80 test blocks of their shape are identical to 1.0.21's. The dashboard: only a 401 signs you out; any other failure of the session check (a 500, a 429, a 503, a restart) keeps the session and says why, with Try again and Sign out, and the Cloud also ends the session when its account no longer exists. A 429 or a 503 with Retry-After says when to try again, on your clock. A 403 is shown as a refusal with no Try again. One note per cause when several reads of a page fail together, a note in place of a page that stops while drawing, and the Memory Graph says when the browser has no WebGL 2. The Requests page, the Home chart and GET /admin/requests with its summary and daily views count the calls of keys and apps and the dashboard's changes, no longer the dashboard's own reads (an account no agent had called showed 12 reads, and the two pages disagreed); the plan's counting does not change. Totals and chart both start at midnight UTC of the first day of the range. Project names: a name may be up to 128 characters, counted after the spaces around it are taken off, at create and at rename. A name of 65 to 128 characters used to answer 500 internal_error because its slug did not fit; the slug is now cut to 64 characters, and those names are created. A name of spaces only is now refused with 422 (it was created empty), and a longer name is still a 422 that the dashboard now shows. On the Cloud, adopting a key's account no longer fails with 500 account_not_movable when a renamed project's new slug is one another project still held. install.sh installs no release older than 1.0.9: --version with an older release, and --build or --from-local of one, stop before anything is downloaded; 1.0.9 is the first release published under the repository's present name, and 1.0.8 was never published. Up to 1.0.21 the published releases are 1.0.9, 1.0.10, 1.0.11, 1.0.17, 1.0.18 and 1.0.21; a release's own copy of install.sh checks only that release's files, so another --version stops at once unless you add --unverified, and with --unverified a version never published stops at the download of its files. A webhook URL whose name the resolver could not look up right now (a temporary failure) is refused as before and says host could not be resolved now. korely_search with full_text: the NOTE on a cut result names GET /v1/memories/{id} only for a cut memory; for a cut fact it says only to ask with a smaller limit. The Cloud serves no /openapi.json and no /docs; the API reference is at korely.ai/docs/api-reference and the specification at korely.ai/agents-openapi.json (an install of your own keeps both). The memory API keeps its routes, fields and codes; the public specification changes only in its version and in the wording of two descriptions (erasing an end user and deleting an agent), which now say that they erase every other row kept of that person or agent as well as their memories and facts. Security: nothing changes in what is stored or shown; the new column holds a random number per process, no account data, and the API never returns it.
- API
Korely 1.0.21 (with 1.0.19 and 1.0.20): a write must say whose memory it is
No migration and no new setting, but a program that writes without ids starts failing. Korely 1.0.19 and 1.0.20 reached the Cloud only, and never the public repository. 1.0.20 is 1.0.19 with one function moved, and 1.0.21 is 1.0.20 with three comments rewritten in the code: neither changes an answer of any route or tool, and 1.0.21 adds no endpoint. This entry covers all three, and there is no documentation for 1.0.19 or 1.0.20 apart from it. First, a write must say whose memory it is. POST /v1/memories, each row of POST /v1/batch and POST /v1/facts with neither user_id nor agent_id answer 422 with the code identity_required and store nothing; a batch with one such row is refused whole, the row named (memories[3]). An id of spaces, format characters or combining marks counts as none, and run_id alone is not enough. A key confined to one end user needs nothing. The check comes before the plan's quota and the trial's writes, so a refused write counts against neither. Before upgrading, look for writes that send neither id and give each one a user_id (the person the memory belongs to) or an agent_id (the agent that writes it); a program that writes for one person only can send any stable id. A program that wrote without ids and now sends a user_id starts a second memory: the judge weighs a fact only against the facts of its own end user, so a change written after the upgrade does not close an old fact that had no id. Correct or forget the old one by hand. Nothing stored is rewritten, and memories stored without ids are read, edited, corrected and forgotten as before. Second, MCP writes with no id go to the project's shared memory, where they went before, and the name the assistant gave when it connected is kept on the memory as client and in the audit trail; only a session whose client gave no usable name gets the 422. Several agents that share one memory through MCP should each pass their agent_id. Third, the judge (memory_core 6.9.0): in a memory with no user_id, with or without an agent_id, a new fact is weighed only against the facts of its own entity, not the ten nearest in meaning. Measured on 12 November 2026 on our own team's memory written with no ids (43 memories, one judge model, three runs per version): wrong closures 19 of 41 with 1.0.18 and 12 of 30 with 1.0.19, none between two subjects (7 before). The cost: two spellings of one thing are two subjects until you declare an alias with POST /admin/aliases. A memory with a user_id is judged as before. Fourth, deleting a project never changes the region its data is written in: when the Default project writes in another region and keys or data would move, DELETE /admin/projects/{id} answers 409 project_region_differs and deletes nothing (before, an EU project deleted without erasing moved into the Global Default project). Fifth, an erroneous /authorize request is answered with a 400 in JSON instead of a redirect to the client's address, which closes an open redirect (RFC 9700 4.11.2); nothing was readable through it. Sixth, on the Cloud an agent is counted once per project and name, so one name in two projects is two of the plan's agents; an account that reused a name across projects may be nearer its cap. The SDKs follow: Python korely-memory 0.1.29 (korely init --user-id or --agent-id saves whose memory the command line writes; the SDK itself reads no saved id), npm korely-memory 0.1.18 and n8n-nodes-korely 0.1.1. A write that names no id fails against an older server too, in the CLI and in the n8n node.
Get started
Give your agent memory
that stays true.
Create your account and mint your first key, free to start. Data stored in Helsinki, no card required.