Skip to main content
Glama

Keel

ci PyPI docs DOI

Stop rebuilding the pipeline for every agent.

Live demo · Docs · Hosted console (beta) · Why: We built four agents and four copies of our customer context

Keel is a context layer for the AI agents inside a product: a vertical SaaS, a consumer app, an internal tool. Domain events from your product go in; a bitemporal context graph per tenant comes out, whatever your unit is (an end-customer, a household, a team); every agent you build reads and writes it through the same five tools, over MCP, HTTP or a typed SDK.

You describe your vertical once, in a cartridge (ontology, event catalog, semantic rules, memory policies). Keel's core knows nothing about your domain. Your second agent connects in a day instead of weeks, because there is no second pipeline to write.

Try it in a minute, with an example cartridge and its events:

pip install keel-context
keel init fleet --example fleet          # also crm, clinics, finance
keel context fleet fleet/events.jsonl unit:MXK-4821

Status: 0.38.0, alpha. Container: ghcr.io/angelkurten/keel. The five tools (plus query, an optional read-only SQL sixth, off by default) over MCP and an HTTP JSON API (/v1), Postgres, webhook, Kafka and SQS adapters, semantic search, consolidation, an agent registry and SDKs for TypeScript, Python, Go and PHP, on the in-memory store, SQLite (--db keel.db, stdlib, no server to run), MySQL (--db mysql://...), Postgres (--db postgresql://...) or DuckDB (from Python).

Terminal demo: validate the stream against the cartridge, ingest, ask for a tire's context with the rule's verdict, run an agent with no LLM

The problem

Every new agent in a product repeats the same work: extract the customer's data, normalize it, decide what "critical" means, keep its own memory, expose its own tools to the model. Four agents, four copies of the customer's context, four definitions of "critical", four memories that never talk.

Existing memory layers (Zep, Mem0, AgentCore Memory, Neo4j's MKG) ingest text and documents and model memory per user or per agent. None of them start from domain events, and none model the tenant your product already has: the end-customer of a B2B SaaS, the household of a consumer app, the team of an internal tool.

Related MCP server: Graphiti Memory MCP Server

How Keel is different

  1. Cartridges. A vertical is declared in YAML plus a few ten-line handlers, or none for an event that only maps fields. The core stays generic. Write a cartridge for fleets, clinics, logistics, or your own product; a tenant that brings data of its own extends it with kinds of its own, without a deploy.

  2. Events in, not documents. Keel ingests structured facts with time: your Postgres tables through CDC, an outbox, a typed SDK, webhooks, Kafka, SQS. Ingestion is deterministic and cheap. The graph is exact because it is born from events, not inferred from text.

  3. Every fact carries its provenance. Source, valid_from, valid_to, recorded_at, and who corrected it. Audit-ready by construction.

  4. One graph per tenant. One subgraph per end-customer, household or team, with its own permissions and expiry.

Five tools, any agent (and an optional sixth)

Tool

The question it answers

context

What do I know about this subject now, or at a past moment? Open facts, relations and rule verdicts.

history

How did it change over time, and when did we learn it?

search

Which subjects match? By text, kind, rule, attribute value or meaning.

related

What is it connected to, up to three hops?

annotate

Remember what an agent or a person concluded, with evidence and a scope.

query (optional)

Off unless enabled (keel serve --query): read-only SQL over views generated from the cartridge, to filter, join, count or aggregate. The store binds it to one tenant.

Every parameter is in the specification. query never sees another tenant, agent or task memory, or proposals, whatever SQL the agent writes (how).

search(mode="semantic") finds subjects by meaning: ids in the query first, then a dense and a lexical leg fused, then an optional reranker. Where vectors live and which model makes them are adapters you choose: in memory, Chroma, Weaviate, any OpenAI-compatible embeddings endpoint, or your own vector database and model (how, example).

Expose them over MCP (keel serve) and point any agent framework at them. Declare agents: in the cartridge and keel serve --agent <id> binds a server to one of them: what it may read and write is enforced by the layer, not by the agent.

Quick start

uv sync --extra dev
uv run keel validate examples/fleet --events examples/fleet/events.jsonl
uv run keel ingest examples/fleet examples/fleet/events.jsonl
uv run keel context examples/fleet examples/fleet/events.jsonl unit:MXK-4821

Then write your own cartridge: see docs/first-cartridge.md (20 minutes). Settings (database, secret, embeddings keys) go in a .env: copy .env.example (reference).

In your own product it is a library: uv add keel-context (or pip install keel-context); the import is import keel. From another language, generate a typed client from your cartridge: keel sdk typescript|python|go|php examples/fleet -o <file>, with a small runtime per language (see docs/sdks.md).

Pilot it with us

Run Keel on your product's events with your agents, with our help: your first pilot in a day is the path, with a time budget per step. Open a pilot request, book 30 minutes at calendar.app.google/3oKUSqXTQPYmjya56, or write to angel@angelkurten.com.

Connect a real event stream

Point an MCP client at the five tools:

uv sync --extra mcp
uv run keel serve examples/fleet --tenant acme-logistics --events examples/fleet/events.jsonl

With Claude Code, one command registers it and the agent sees exactly five tools:

claude mcp add keel -- uv run --directory $PWD keel serve examples/fleet --tenant acme-logistics --events examples/fleet/events.jsonl

Codex CLI (OpenAI) works the same way from ~/.codex/config.toml; the four read tools are annotated read-only, so it runs them without prompting. Both clients answered the same questions through the same five tools: see serve the tools. A backend, or many tenants from one process, use the same five as a JSON API: keel serve --transport http (/v1/context, /v1/events, ... with OpenAPI; tenant and agent per request).

Send your product's events from wherever they already are (how, for each):

  • Postgres CDC, no code in your product: keel cdc <cartridge> --dsn postgresql://... --map cdc.yaml --db keel.db --setup follows your own tables; each row change is an event.

  • The typed SDK, from your backend: keel sdk typescript <cartridge> -o src/keel.ts, then keel.events([emit.tireDepthMeasured({...})]) (also Python, Go, PHP).

  • An outbox table: PostgresTableAdapter polls it in order with a cursor, so restarts resume.

  • A webhook: POST /v1/events on keel serve --transport http, or build_app(ingestor, token=...).

  • SQS or Kafka: keel consume --queue <url>, or KafkaAdapter from Python; at-least-once, acknowledged after each commit.

  • A .jsonl file, for backfills and tests: keel ingest <cartridge> events.jsonl --db keel.db.

keel export <tenant> --db keel.db writes one tenant's whole audit trail as JSONL, closed facts and provenance included; keel import restores it, as it was, into an empty tenant of any store.

Architecture

Keel architecture: sources, adapters, deterministic ingest, bitemporal store, cartridge, five tools, jobs, surfaces and agents

Every box, the options for each and when to pick which: docs/architecture.md.

Design rules the core enforces:

  • Events are the truth. An append-only log; the graph is derived and can be replayed. What agents and people wrote is kept across a replay.

  • Pointers, not copies. The graph holds references to your rows, the semantics, and earned memory. Never a copy of your tables.

  • Explicit time. Facts close, they are not overwritten.

  • Scopes, not a flat store. global > tenant > task > agent. Nothing is promoted to a wider scope by accident. Agent memory is read only by the agent that wrote it; task memory tied to a task id only by calls that pass that task (context(task=), history(task=)); global memory lives in a reserved partition every tenant reads and no event writes.

Repository layout

keel/            core: envelope, handlers, store, scopes, cartridge loader, ingest, tools, cli
cartridges/      your own cartridges (empty here)
examples/        fleet (reference), crm, finance and clinics: four domains, one core, no core change between them;
                 each a cartridge, its events and its test; fleet also has a minimal agent and a multi-agent workflow
sdk/             SDK runtimes: typescript, go, php (the Python client ships in keel)
evals/           benchmarks: Jira questions (the paper), TDBench, semantic search, scale and efficiency
docs/            site sources (mkdocs.yml at the root): architecture, first-cartridge, ingest, serve, sdks, operate, SPEC
tests/

Roadmap

A summary; the full plan, with what each release shipped, is in ROADMAP.md.

Shipped:

  • Event envelope, handler registry, deterministic ingest with dedupe by event id and replay

  • Bitemporal store: in memory, SQLite, MySQL, Postgres and DuckDB, behind a store protocol with a conformance suite

  • Memory scopes with TTL, evidence policy, expiry and consolidation into durable facts (keel expire, keel consolidate)

  • The five tools over MCP (stdio and HTTP) and a JSON API, with an agent registry (people included)

  • Adapters: Postgres outbox, webhook, Kafka, SQS

  • Rules that compose, follow relations and measure durations

  • Transaction time (as_known_at), changes since a watermark, task ids, idempotent writes, proposals, conflicts, traces

  • SDKs generated from the cartridge: TypeScript, Python, Go, PHP

  • Bulk ingest (a million events in under a minute and a half on MySQL)

  • Semantic search: hybrid, ids first, rerankers, pluggable vector index and embedder, with an eval bench

  • Postgres CDC: follow your own tables through logical replication (0.28.0)

  • query, the optional sixth tool: read-only SQL generated from the cartridge, bound to one tenant by the store (0.35.0)

  • Agents created at runtime per tenant (0.37.0); tenant extensions: kinds and events each tenant brings, declarative and additive, without a deploy (0.38.0)

Open:

  • Real agent queries from pilots in the semantic-search eval bench

  • Cartridge catalog, hosted Keel, enterprise features (2027)

Paper

Keel: one deterministic context layer for product agents (PDF on Zenodo, source in docs/paper/). On 387 questions over three real Jira projects, with three agents, Keel leads the product's own API by 21 to 32 points on tenant-wide questions and is cheapest per correct answer; the harness and every result row are in evals/jira/.

@misc{kurten2026keel,
  author    = {K{\"u}rten, Angel},
  title     = {Keel: one deterministic context layer for product agents},
  year      = {2026},
  publisher = {Zenodo},
  doi       = {10.5281/zenodo.23571090},
  url       = {https://doi.org/10.5281/zenodo.23571090}
}

License

Apache 2.0. The event envelope, the cartridge format and the five tools are the standard; they will always be free.

Available Tools

5 tools
annotateC

Record what you learned (value: any JSON). scope tenant or global needs evidence; key: an idempotent retry; status="proposed": awaits review.

NameRequiredDescriptionDefault
byNo
keyNo
taskNo
scopeNotask
valueYes
detailNoack
statusNo
subjectYes
evidenceNo
predicateYes

TDQS

C2.4/5.0
Behavior1/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotation idempotentHint=false directly contradicts the description's claim that 'key: an idempotent retry.' This is a serious inconsistency that could mislead an agent about retry safety. The description also does not clarify permissions, review effects, or what happens to existing annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is short and front-loads the action, but the semicolon-separated fragments are cryptic rather than efficient. Some details earn their place, yet the lack of structure makes it harder to parse than a well-formed sentence.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 10-parameter write tool with no output schema, the description is far from complete. It omits most parameter semantics, does not explain the write/review lifecycle beyond status='proposed,' and contains an idempotency contradiction that undermines trust in the definition.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% across 10 parameters, so the description must compensate, but it explains only value, scope, key, and status. Critical parameters like subject, predicate, evidence, by, task, and detail are left undocumented, and required subject/predicate receive no semantic explanation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description says 'Record what you learned,' which gives a verb but an ambiguous resource; it does not clarify that this stores an annotation/knowledge claim, nor does it distinguish the tool from siblings like context, history, search, or related. The parenthetical '(value: any JSON)' hints at structured storage but leaves the core purpose vague.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives some conditional usage rules: tenant/global scopes require evidence, key enables idempotent retries, and status 'proposed' awaits review. However, it never states when to use annotate versus alternatives such as context or history, nor does it clearly mark when not to use it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

contextB
Read-only

What is true about a subject (':' or a list) now or at as_of: facts with since, relations, rule evaluations. as_known_at: as Keel knew it then. depth=1 adds neighbours. detail: brief (default), concise, full (audit). task: its memory.

NameRequiredDescriptionDefault
onlyNo
taskNo
as_ofNo
depthNo
detailNobrief
excludeNo
subjectYes
as_known_atNo

TDQS

B3.3/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

readOnlyHint already tells the agent this is a safe read, so the bar is lower; the description still adds real behavior — depth=1 pulls in neighbours (a cost/scope signal), detail levels run up to 'full (audit)', and as_known_at yields a historical view. However, nothing is said about result size, truncation, or how only/exclude narrow output, which is exactly the kind of behavioral detail this tool needs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The core question is front-loaded in the first clause, and the remainder is a compact parameter gloss with no filler. It is so compressed that a few fragments ('task: its memory') become hard to parse, which slightly undercuts the terseness.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For an 8-parameter tool with no output schema, the description covers the query and most filtering semantics but omits any statement of return shape and says nothing about two of the parameters. An agent could call it, but would be guessing about output size and the meaning of `only`/`exclude`.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must carry the load, and it does for most of it: subject format ('<kind>:<id>' or a list), as_of, as_known_at, depth semantics, and the detail enum with its default. It leaves `only`, `exclude`, and `task` opaque ('its memory' is cryptic), so it compensates well but not completely for the 8 undescribed parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific question the tool answers — 'What is true about a subject now or at as_of' — and enumerates the payload (facts with `since`, relations, rule evaluations), which pins down the resource better than the bare name 'context' does. It implies a current-state/temporal-query role that distinguishes it from a logged `history`, though it never explicitly names its siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gestures at when the tool applies ('now or at as_of', 'as Keel knew it then'), but there is no explicit guidance on choosing it over `history`, `search`, `related`, or `annotate`. The rest of the text is parameter gloss rather than usage policy, and no when-not conditions are given.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

historyB
Read-only

Facts over time of a subject, oldest first; no subject + since/until: every change in the tenant then (predicate: one field). was: the value replaced (null: unset; absent: a first value). as_known_at: as Keel knew it then; changed_since: recorded since a watermark. limit keeps the latest (0: all). detail: concise or full.

NameRequiredDescriptionDefault
taskNo
limitNo
scopeNo
sinceNo
untilNo
detailNoconcise
subjectNo
predicateNo
as_known_atNo
changed_sinceNo

TDQS

B3.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

readOnlyHint=true already covers safety, and the description adds genuine behavioral context beyond it: default result ordering (oldest first), that limit retains the *latest* entries and that limit=0 returns everything, and the null-vs-absent semantics of the replaced value. What is missing is any note on pagination or the result shape, which would matter given no output schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The text is short and every clause carries information, but it is a semicolon-chained run-on with no lead verb and heavy domain jargon ('Keel', 'watermark', 'tenant then'), so it is not front-loaded and requires re-reading. Density here buys brevity at the cost of parseability.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 10-parameter, zero-coverage, no-output-schema tool, the description covers most of the tricky temporal semantics, which is more than most. It still leaves two parameters (task, scope) undocumented and misstates the detail enum, and it never says what a returned record looks like, which the agent needs since no output schema exists.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must carry all ten parameters, and it explains subject, since, until, predicate, as_known_at, changed_since, limit and detail. It leaves 'task' and 'scope' (agent/task/tenant/global) entirely unexplained, and its 'detail: concise or full' contradicts the three-value enum that also allows 'text'; 'was' is described but is not actually an input parameter, which is confusing.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening clause 'Facts over time of a subject, oldest first' describes the resource and the ordering, but never supplies a verb – the agent must infer that this is a read/query operation. It partially differentiates scope ('no subject + since/until: every change in the tenant') but none of the sibling tools (context, search, related) are named or contrasted.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is conditional guidance for one branch – omitting subject while supplying since/until yields a tenant-wide change feed. However there is no explicit when-to-use-this vs. search/context, no statement of prerequisites or cost, and the relationship between as_known_at and changed_since is only hinted at, leaving the agent to infer which one applies.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 5 tool updatesv0.34.0
    • First observedannotate
    • First observedcontext
    • First observedhistory
    • First observedrelated
    • First observedsearch

TDQS

B3/5.0

Scored across 5 tools

Disambiguation3/5

The core split (context = current facts, history = facts over time, search = find subjects, annotate = write, related = neighbours) is mostly clear, but the boundaries blur: `search` with related_to/via and `context` with depth=1 both do what `related` does, and `context` vs `history` differ only by temporal framing. The descriptions do enough to steer an agent, but these are real overlaps rather than clean separations.

Naming Consistency3/5

All names are single lowercase tokens, which is tidy, but the grammatical convention is mixed: `search` and `annotate` are verbs while `context`, `history`, and `related` are nouns/adjectives. `context` in particular is a vague name for a read operation, so the set is readable but not predictable from a pattern.

Tool Count5/5

Five tools is well within the healthy range and each maps to a distinct capability of a temporal fact store: point-in-time read, timeline read, discovery, neighbourhood traversal, and write. Nothing feels padded or missing at the count level.

Completeness3/5

Read coverage is strong (current, historical, search, relations) and `annotate` covers ingestion, but there is no way to retract, correct, or supersede a fact, and no tool for managing the rules that `context` and `search` evaluate against. Agents hitting bad data or needing rule definitions will dead-end.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides AI agents with operational customer context, including typed revenue objects, persistent state, scoped tools, and human-in-the-loop handoffs through MCP, REST, and CLI.
    15 npm
    13
    Apache 2.0
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI agents to manage and query a temporally-aware knowledge graph memory, supporting episode tracking, entity relationships, and semantic search via MCP tools.
    1
    -
  • A
    license
    A
    quality
    B
    maintenance
    Enables AI agents to store and retrieve provenance-aware facts with source, age, and boundaries, supporting search, read, propose, and local telemetry via MCP tools.
    4
    81 npm
    Apache 2.0