korely-memory
The Python SDK for Korely Agents: memory for AI agents, with bi-temporal typed facts and contradiction checking built in.
A typed, zero-dependency client over the Korely REST API. Every method maps 1:1 onto an endpoint, so anything you can do with curl you can do here, and the JSON shapes in the API reference are the attribute shapes you get back. All the intelligence (embeddings, entity and typed-fact extraction, contradiction checking, bi-temporal validity) runs server-side, so your install stays small and your process stays light.
Install
pip install korely-memory
Python 3.9 or later. The optional MCP server (pip install 'korely-memory[mcp]')
needs Python 3.10 or later, because the mcp package does.
Quickstart
from korely_memory import Korely
korely = Korely(api_key="kor_live_...", region="eu")
# or read the key from the environment (KORELY_API_KEY)
korely = Korely(region="eu")
# Remember: the write path extracts facts and resolves contradictions
korely.add("Dana prefers TypeScript with strict mode", user_id="dana")
korely.add("Dana switched to Rust", user_id="dana")
# Recall the raw memories, ranked by meaning. Both come back: memories are
# kept as written, it is the facts extracted from them that get superseded.
for hit in korely.search("preferred language", user_id="dana", limit=5):
print(hit.id, hit.score, hit.snippet)
# One-call, prompt-ready context for your LLM. Its "Known facts" are the
# current ones, so the TypeScript fact, once superseded, is not among them.
ctx = korely.get_context(query="what language should I use?", user_id="dana",
token_budget=800)
messages = [{"role": "system", "content": f"You are helpful.\n\n{ctx.context}"}]
Methods
Every method wraps exactly one REST endpoint.
| Method | Endpoint |
|---|---|
add(content, *, agent_id=, user_id=, run_id=, metadata=, timestamp=) |
POST /v1/memories |
search(query, *, user_id=, agent_id=, run_id=, metadata=, limit=) |
POST /v1/memories/search |
get_all(*, user_id=, agent_id=, run_id=, limit=, offset=) |
GET /v1/memories |
get(memory_id) |
GET /v1/memories/:id |
update(memory_id, *, content, expected_updated_at=) |
PATCH /v1/memories/:id |
delete(memory_id) |
DELETE /v1/memories/:id |
delete_all(*, user_id) |
DELETE /v1/users/:user_id/memories |
history(memory_id) |
GET /v1/memories/:id/history |
users(*, agent_id=, limit=, offset=) |
GET /v1/users |
list_agents(*, limit=, offset=) |
GET /v1/agents |
delete_agent(agent_id) |
DELETE /v1/agents/:agent_id |
get_facts(*, subject=, entity=, predicate=, predicate_family=, include_invalidated=, as_of=, …) |
GET /v1/facts |
add_fact_triple(subject, predicate, object, *, user_id=, valid_from=, tense=, …) |
POST /v1/facts |
correct_fact(fact_id, *, subject=, predicate=, object=) |
PATCH /v1/facts/:id |
forget_fact(fact_id, *, at=) |
POST /v1/facts/:id/forget |
get_profile(*, user_id, agent_id=, as_of=) |
GET /v1/profile |
get_context(*, query, user_id=, agent_id=, token_budget=) |
GET /v1/context |
events(*, user_id=, status=, limit=) |
GET /v1/events |
batch(memories) |
POST /v1/batch |
batch_status(job_id) |
GET /v1/batch/:id |
AsyncKorely has the same methods, awaitable.
add(..., timestamp="2026-01-15") backfills the past: facts extracted inherit
the timestamp as their valid_from, so as_of point-in-time queries reflect
when things were true, not when they were ingested. Each item of batch() takes
the same timestamp key, so a migration keeps its real dates:
korely.batch([
{"content": "Franco signed up on the Pro plan.", "user_id": "franco", "timestamp": "2026-01-15"},
{"content": "Franco downgraded to Free.", "user_id": "franco", "timestamp": "2026-06-20"},
])
A timestamp that is not an ISO 8601 date or datetime refuses the whole batch
with a 422 naming the item (memories[1].timestamp), before anything is queued.
list_agents() / delete_agent(agent_id) manage your agent namespaces: call
list_agents() after an agent_cap_exceeded error to reuse an existing
agent_id, or delete_agent() to purge a throwaway one. The page's total
counts the namespaces of this key's project; used counts the cap slots taken
across the account, which is what the 403 compares with cap. delete_agent()
raises NotFoundError for a name this project does not use, and its receipt's
slot_freed says whether the slot is free now (it is not while another project
of the account still uses the name).
delete_all(user_id=) answers with memories_deleted and facts_deleted, the
rows physically erased. memories_forgotten and facts_invalidated carry the
same numbers under their old names and are deprecated.
correct_fact() returns the new fact, whose invalidated lists every fact the
correction superseded (the corrected one, plus any the contradiction check
closed). A correction that restates the fact as it already stands supersedes
nothing: the same fact comes back, reconfirmed, with invalidated == [].
get_facts() returns a list of Fact that also carries .total, the number of
facts matching the filters across all pages, so offset knows when to stop.
get_all(), get_facts(), users(), list_agents() and events() take a
limit up to 200.
Bi-temporal facts
The differentiator: typed (subject, predicate, object) facts with validity over
time. Ask what was true on any date.
# Current state
facts = korely.get_facts(entity="Northwind Hosting")
print(facts[0].object) # 50 euro per month
print(facts[0].invalid_at) # None: active
# Point-in-time: what did we believe on June 1?
facts = korely.get_facts(entity="Northwind Hosting", as_of="2026-06-01")
print(facts[0].object) # 40 euro per month
Scoping
Three identifiers, three levels of scope, the same everywhere (SDK, REST, MCP):
agent_id: your application or agent (one namespace per product surface)user_id: your end user (free-form string; unlimited on every tier)run_id: one session or run (sub-scope inside a user)
korely.add("Asked to be contacted on Slack", agent_id="support-bot", user_id="customer-4812")
results = korely.search("contact preference", user_id="customer-4812")
Always pass
user_idon reads in multi-tenant products. Filters are additive (AND); a search withoutuser_idspans every end user in the namespace.
Error handling
Every error the server answers with is an APIError carrying the stable code
and the message of the REST error envelope ({"code", "message"}), so you can
branch on err.code; a self-hosted install that answers FastAPI's detail is
read the same way, and err.body keeps the response as it came. The common
statuses also have their own subclass. Everything subclasses KorelyError,
which is also what a client-side problem raises (no key, a connection error, a
timeout).
import time
from korely_memory import Korely, AuthenticationError, NotFoundError, QuotaExceededError
korely = Korely(api_key="kor_live_...")
try:
memory = korely.get("mem_8f2c1a")
except AuthenticationError:
raise # 401: check or rotate the key
except NotFoundError:
memory = None # 404: forgotten or never existed
except QuotaExceededError as err: # 429
if err.retry_after is None:
raise # monthly quota used up: nothing to wait for
time.sleep(err.retry_after) # rate limit: wait as long as the server said
memory = korely.get("mem_8f2c1a")
| Exception | Status | Typical code |
|---|---|---|
AuthenticationError |
401 | invalid_key |
NamespaceForbiddenError |
403 | agent_cap_exceeded, missing scope |
NotFoundError |
404 | not_found |
StaleWriteError |
409 | stale_write |
QuotaExceededError (.retry_after) |
429 | rate_limit_exceeded (has retry_after), quota_exceeded (monthly, retry_after is None) |
APIError |
any other, and the base of all of the above | invalid_request (422), search_unavailable / model_unavailable (503, safe to retry) |
The SDK does not retry on its own.
MCP server
pip install 'korely-memory[mcp]' # Python 3.10+
korely-mcp is a stdio MCP server with four tools (korely_get_context,
korely_add, korely_search, korely_get_facts), the same four the hosted
server at https://api.korely.ai/agent/mcp offers, with the same arguments
(korely_add takes timestamp) and the same fact lines: a fact whose end date
is still to come reads [until 2027-01-01], not [superseded ...], and dates
are UTC days. It reads the key from KORELY_API_KEY or from the file
korely init saved.
Links
MIT licensed.
Metadata
Release files for korely-memory 0.1.15
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| korely_memory-0.1.15.tar.gz | 50.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| korely_memory-0.1.15-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 87.4 kB
Release files / korely_memory-0.1.15.tar.gz
| Download URL | korely_memory-0.1.15.tar.gz |
|---|---|
| Size | 50.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1abdaef909b84a35b0d130299185bcb90b29dde14e00fed3be9a20160cf218d6
|
|
BLAKE2b-256 checksum How to use checksums |
785a4ef930aef5a14dc5a0e7267433e338e2b55b24b4e9353b6ff77d3abbe83d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.16
|
Release files / korely_memory-0.1.15-py3-none-any.whl
| Download URL | korely_memory-0.1.15-py3-none-any.whl |
|---|---|
| Size | 36.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
06bd1c3707bc5dece573b3bca2ac81c21f74822f6fa68cc74779193535cef352
|
|
BLAKE2b-256 checksum How to use checksums |
4692bb16cc459b432b80f907f3bb66b6a674b03a599c23eead2592356cb653a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.16
|