Korely

A research agent that builds on every session

You're three weeks into researching "EU AI Act compliance for our SaaS" with an agent. A stateless agent opens session 5 by asking what you're researching, re-derives the obligations you already pinned down in session 2, and has no idea you overturned one of them after the legal review. You spend the first ten minutes re-grounding it.

With Korely in the loop, session 5 can open already knowing the obligations you flagged, which questions are still open, and which earlier conclusion was overturned and why, as long as each was written down. Each session's findings accumulate into typed facts; the contradiction judge can close the old answer instead of letting it pile up; and as_of replays what was true on any date (known_as_of: what the memory had learned by that date). This cookbook walks the pattern at the call level. The words end user, fact, as_of and context block are in the Glossary.

The snippets use the Python SDK (pip install korely-memory). The same calls work over the REST API and the Node SDK.

The scoping: one project, many sessions

A research thread is one user_id (here the research project; it is the "end user" whose memory this is), and each session is a run_id. Findings written under any session accumulate under the project, so get_context and get_facts see all of them; run_id is there when you want to search or list a single session's memories (search(..., run_id=...), get_all(run_id=...)).

from korely_memory import Korely
korely = Korely(api_key="kor_live_...", region="eu")
PROJECT = "proj-eu-ai-act"
SESSION = "session-2026-06-12" # one run_id per working session

1. Session start: load everything known so far

Before the first turn of a new session, assemble the project's state into a prompt-ready block. get_context returns the active facts most relevant to the query (up to ten) plus the most relevant memories, already fitted to a token budget, in one call. For every active fact, call get_facts or get_profile. No generative model runs on the read path; the query is embedded on Korely's own server.

ctx = korely.get_context(
query="EU AI Act obligations, open questions, decisions so far",
user_id=PROJECT,
token_budget=1200,
)
# ctx.context -> a Markdown block to drop into your system prompt
# ctx.tokens -> how many tokens it used
# ctx.sources -> the fact and memory ids it drew from

Abridged, the block reads:

{
"context": "_The facts below are a compact profile of the user; the memories are the verbatim source of truth. ..._\n\n## Known facts\n- hiring module qualifies_for Annex III research exemption (since 2026-06-05)\n\n## Relevant memories\n_A time in square brackets after a relative expression ... is the day it was said._\n- [2026-05-28] Session 2 conclusion: Article 6 high-risk registration applies to our hiring module.\n- [2026-06-05] Legal review: the hiring module qualifies for the Annex III research exemption, so Article 6 high-risk registration does not apply.",
"tokens": 344,
"sources": ["fct_...", "mem_...", "mem_..."]
}

This is real output for two memories written with backdated timestamps (the first on 2026-05-28, the second on 2026-06-05), with the ids shortened. Only the second memory produced a fact. Both memories appear under Relevant memories, each with its date, so the model sees the old conclusion next to the newer one and the dates tell it which is current. A fact makes the current answer easy to read; the dated memory text stays the evidence.

The agent now opens the session already grounded. No "remind me what we're working on."

Session 5

You re-open the research thread

  • Three weeks in
  • Resolves to user_id "proj-eu-ai-act"

Korely

get_context(query=...)

  • Active facts + relevant findings, every session
  • Assembled, not re-generated

Turn 1

Agent is already grounded

  • Knows the open facts
  • Sees both dated conclusions

2. During the session: search the back-catalogue

When the agent needs a specific past finding, search the project's memories. Scoped to the project, ranked by relevance, no model in the path beyond the query embedding.

hits = korely.search(
"annex III exemption hiring module",
user_id=PROJECT,
limit=5,
)
# each hit: {id, score, snippet, user_id, agent_id, metadata}

3. On a finding: write it down (any session)

When the agent reaches a conclusion, store it. Tag the session with run_id; it still accumulates under the project.

korely.add(
"Legal review: the hiring module qualifies for the Annex III research "
"exemption, so Article 6 high-risk registration does not apply.",
user_id=PROJECT,
run_id=SESSION,
timestamp="2026-06-05", # when the legal review concluded
)

Fact extraction runs server-side: this finding becomes typed (subject, predicate, object) triples a few seconds after the write returns, ready at the next get_facts / get_context.

4. When a finding overturns an earlier one

Session 2 concluded "Article 6 high-risk registration applies." The June 5 legal review written in step 3 contradicts it. You don't write supersede logic: the contradiction detector (a prefilter, then a model judge) looks for the earlier fact and, when it closes it, the old conclusion isn't deleted, it gets an invalid_at timestamp. The closure is a model's judgement and it can miss, most often when the new finding is stated as a consequence of another claim, as it is here. In the real run above, the earlier conclusion produced no fact to close. When a fact is open next to the one that overturns it, both are served. A single-valued attribute that changes value (a status, a plan, an owner) is the case the judge closes most reliably.

facts = korely.get_facts(user_id=PROJECT)
# returns what is valid now. If the judge closed the Art. 6 "applies" fact, it is gone
# from this list. If it did not, both facts are here: read the memories in
# get_context, or pass include_invalidated=True to see what was closed.

5. Point in time: what was true back then?

This is the part a vector store can't do. Replay the project's facts as it stood on any date, before the legal review, say, for an audit trail or to understand how a conclusion changed.

# what was true before the June 5 legal review
before = korely.get_facts(user_id=PROJECT, as_of="2026-06-04")
# what the memory had learned by June 4 (ingestion time, not event time)
known = korely.get_facts(user_id=PROJECT, known_as_of="2026-06-04")
# the full point-in-time profile, grouped by family
snapshot = korely.get_profile(user_id=PROJECT, as_of="2026-06-04")

as_of="2026-06-04" returns the facts that were valid on that date, so the "Article 6 applies" conclusion is there. It shows a difference from the current read only when the judge closed that fact on June 5; if it stayed open, both reads return it. The history is intact; you're just choosing which moment to read from.

known_as_of is the other axis: when the memory learned a fact, not when the event happened. The findings in this walkthrough are written with backdated timestamps, so known_as_of="2026-06-04" returns an empty list for them. It returns facts only for memories that were actually written on or before that date.

Why not dump the transcripts into a vector DB?

  • Staleness. A transcript store holds both "Article 6 applies" (session 2) and "Article 6 does not apply" (session 3), and you're trusting the LLM to pick the right one every turn. get_facts returns only what's valid now, and the judge closes many superseded facts for you. Closure is a model's judgement, so for a fact that matters, check with include_invalidated=True.
  • Token cost. Re-feeding five sessions of transcript is thousands of tokens every turn, growing forever. A get_context block stays under the token budget you set, 800 by default, however long you've been researching.
  • No point in time. A vector store has no concept of "what was true on June 4". Facts with start and end dates do.

One honest caveat: korely.add runs fact extraction asynchronously, so a finding's typed facts land a few seconds after the write returns, ready (usually after about ten seconds) for the next session's get_context, not inside the same call. Write findings fire-and-forget during the session; read the assembled state at the start of the next one.

Where to go next

Temporal facts explains supersession and as_of under the hood; get context is the call in step 1; the memory-as-a-tool cookbook hands these same operations to the model as callable tools.