Architecture¶
Everything Keel is made of, on one page: where events come from, what happens to them, where they are kept, how agents read and write them, and what runs on a schedule. Read this first; every other page goes deeper into one box of this map.
What Keel is, and what it is not¶
Keel is the context layer for the AI agents inside a product. Domain events from your product go in. A bitemporal context graph per tenant comes out. Every agent reads and writes it through the same five tools. Your domain is declared once, in a cartridge.
| Keel is not | Because |
|---|---|
| An agent framework | Your agents live in LangGraph, the Claude Agent SDK, ADK or your backend. Keel is what they consult. |
| A document or RAG memory | It ingests structured events with time, not text. Ingestion is deterministic: no LLM reads your events. |
| A copy of your database | The graph holds pointers to your rows (ref), their semantics and what agents learned, never whole rows. |
| One flat global memory | Memory has scopes (global, tenant, task, agent), and nothing is promoted to a wider one by accident. |
What happens to one event¶
- Your product emits a domain fact. For example
tire.depth_measuredwithdepth_mm: 2.4. It can leave through an outbox table, CDC, the SDK, a webhook, a queue or a file. - An adapter turns it into an envelope. It checks the fields and that times are timezone-aware UTC. It never interprets the payload.
- The Ingestor dedupes and dispatches it. An
idalready applied is skipped. Otherwise the handler registered for itstyperuns. - The cartridge's handler writes primitives.
upsert_entity,link,open_fact. A new fact closes the one that was current at that instant, so arrival order does not change the result. - An agent asks.
context("tire:T-9001")returns the open facts, the relations and every rule's verdict (tire_critical: true) next to its definition. - The agent writes what it concluded.
annotate(..., scope="task", evidence="inspection:4821"). Keel enforces the memory policy; the agent cannot skip it.
1–2. Getting events in¶
Every source produces the same envelope, and you can combine several for one tenant: the event id dedupes. Details and options for each are in Send your events.
| Source | Use it when | Start it with | Guarantee |
|---|---|---|---|
| Postgres CDC | You would rather not touch the product's code. Each row change becomes <kind>.created\|updated\|deleted. |
keel cdc <cartridge> --dsn ... --map cdc.yaml --setup, or keel serve --cdc cdc.yaml |
The slot moves only after the store commits. Needs wal_level = logical. |
| Typed SDK | Your backend (TypeScript, Python, Go, PHP) emits events from business code, with payload types generated from the cartridge. | keel sdk typescript <cartridge> -o src/keel.ts, then keel.events([emit.tireDepthMeasured({...})]) |
POST /v1/events. Ids are a content hash when you send none, so a retry is a duplicate, not a second event. |
| Outbox table | You already write domain events to a table in the same transaction as the business change. | PostgresTableAdapter(connect, cursor_path=".keel/cursor") |
At-least-once, a seq cursor; restarts resume. |
| Webhook | Something else pushes JSON envelopes over HTTP. | POST /v1/events on keel serve --transport http, or build_app(ingestor, token=...) |
One envelope or a list per request; the sender retries. |
| SQS | You have a queue, or want producer and Keel decoupled. | keel consume <cartridge> --queue <url> --db ... |
At-least-once; a failing message goes to the queue's dead-letter queue. |
| Kafka | Your platform already publishes to a topic. | KafkaAdapter.connect(...) from Python |
At-least-once. No CLI command yet. |
| JSONL file | Development, history backfill, tests, rebuilding the graph. | keel ingest <cartridge> events.jsonl --db ..., keel replay |
Deterministic: the same log gives the same graph. |
| Your own adapter | Any other source. | Any iterable that yields Events (example) |
What you implement; follow() and ack() are optional. |
3. The ingest path and the cartridge¶
The ingest path is deterministic: no network calls, no LLM, no randomness. Replaying a tenant's log rebuilds the same graph, which is why the event log, not the graph, is the truth.
The cartridge is the only domain code you write: a folder with cartridge.yaml and handlers.py. Keel's core knows no entity kind, relation, predicate or event type; all of them come from here.
| Section | What it declares |
|---|---|
entities |
The kinds of things, each with ref to the table it points to. |
relations |
Typed edges between kinds (mounted_on: {from: tire, to: unit}). |
events |
The event catalog: each type, its subject kind and its payload shape. keel validate --events checks a stream against it. |
handlers.py |
One small function per event type: payload in, store primitives out. |
rules |
What "critical", "overdue" or "at risk" means, as a definition and an optional when the tools evaluate. |
memory |
TTL per scope and which scopes need evidence. |
agents |
The agent registry: who may write which scope and see which kinds and relations. |
consolidate |
How episodic memory is summarized into durable facts. |
Business logic lives in rules, not in handlers and not in agents. Rules compose (all, any, not), follow relations (via) and measure durations; counts and aggregates over time stay a definition the agent applies. Build one in Your first cartridge.
4. The bitemporal store¶
Every fact has two time axes. World time (valid_from, valid_to): when it was true. Knowledge time (recorded_at, closed_at): when Keel learned it began and ended. So Keel answers both "is the tire critical?" and "was it critical on October 3, as far as we knew that day?" (as_of, as_known_at). Facts are closed, never overwritten or deleted by a handler, and each carries source, evidence and, when someone corrected it, corrected_by.
The graph is partitioned one subgraph per tenant. No read crosses tenants; the only shared partition is __global__, which every tenant reads and only a privileged annotate(scope="global") writes.
| Backend | For | How |
|---|---|---|
MemoryStore |
Tests, demos, a process that rebuilds from JSONL at start. | MemoryStore(), keel serve --events events.jsonl |
SqliteStore |
The default durable store. One file, nothing to run. | --db keel.db or KEEL_DB=keel.db |
MysqlStore |
Several processes, production. | --db mysql://user:pass@host:3306/keel |
| Your own | Postgres, a graph database, anything else. | Implement the protocol in keel/store/base.py and pass the conformance suite: KEEL_STORE=pkg.module:factory uv run pytest tests/test_store.py |
5. The five tools¶
The public standard. Every agent, through any surface, uses these five and no other. A Tools instance is bound to one tenant (and, with a registry, one agent). Full signatures are in the specification.
| Tool | The question it answers |
|---|---|
context |
What do I know about this subject now, or at as_of? Open facts, relations and rule verdicts. |
history |
How did it change over time? Without a subject: what changed since a watermark. |
search |
Which subjects match? By text, kind, rule, attribute value or meaning (semantic). |
related |
What is it connected to, up to three hops? |
annotate |
Remember what the agent concluded: earned memory, with evidence and a scope. |
Memory scopes and the agent registry¶
| Scope | Who reads it | Who writes it | Typical life |
|---|---|---|---|
global |
Every tenant | A privileged caller only, with evidence | Permanent |
tenant |
That tenant's agents | Any agent allowed to, with evidence | Permanent |
task |
Calls that pass that task id |
Agents working on the task | A TTL, say 14 days |
agent |
Only its author | The agent itself | A short TTL, say 1 day |
The policy is enforced in the tools, never trusted to the agent. With agents: in the cartridge, every call names a declared agent; a missing or unknown id is refused, and each agent sees only the kinds and relations it declares. Authenticating the agent is your host's job: Keel takes the id as given.
6. How agents reach the tools¶
Details in Serve the tools and SDKs.
| Surface | For | Start it with |
|---|---|---|
| Python, in process | An agent that is already Python; no network. | Tools(cartridge, store, tenant="acme", agent="tires-bot") |
| HTTP JSON API | A backend in any language, many tenants from one process. | keel serve --transport http --db ...: POST /v1/{context,history,search,related,annotate}, POST /v1/events, GET /health; headers Authorization: Bearer, X-Keel-Tenant, X-Keel-Agent |
| MCP | Claude Code, Claude Desktop, Codex, any MCP client. | keel serve --tenant acme (stdio), or /mcp on the HTTP server |
| Typed SDKs | TypeScript, Python, Go and PHP backends: the five tools and events() with types from your cartridge. |
keel sdk <language> <cartridge> -o <file> |
| Console | People: browse the graph, try the tools, watch events, download SDKs. | keel serve --transport http --ui, at /ui |
| Semantic search | Finding subjects by meaning; optional. | keel index fills a vector index (Chroma, Weaviate or yours); keel serve --index ... --embedder ... |
Jobs¶
They run outside ingest, from the CLI or cron, so they may call the network or an LLM. Details in Operate.
| Command | What it does | How often |
|---|---|---|
keel expire --consolidate |
Consolidates episodic memory into durable facts, then drops expired task and agent memory. |
Daily |
keel index |
Incremental indexing for semantic search. | Every few minutes |
keel replay |
Rebuilds a tenant's graph from its log, keeping annotations. | After changing handlers |
keel export |
One tenant's audit trail as JSONL, closed facts and provenance included. | On demand |
keel forget |
Erases a tenant for good, or with --before, what closed before a date (retention). |
On demand, or scheduled |
keel usage |
Events ingested and calls per tool, read from the log. | For metering |
Three ways to run it¶
| Mode | What it looks like | Pick it when |
|---|---|---|
| Embedded | pip install keel-context; the Ingestor and the Tools inside your Python service, on SQLite or MySQL. |
Your agents are Python and you want the fewest moving parts. |
| Self-hosted service | The ghcr.io/angelkurten/keel container: keel serve --transport http --db mysql://... --cdc cdc.yaml --ui, plus keel consume if you use SQS, plus cron for the jobs. |
Agents in several languages, or several products sharing one deployment (run it in a container). |
| Hosted | One Keel container and one database per account, with a console to upload or generate the cartridge, set up CDC, issue keys per tenant and try an agent. | You would rather not run it. During the pilots, by invitation: pilot request. |
Rules that do not bend¶
- Events are the truth. The graph is derived and can be rebuilt with
replay. Only annotations live outside the log, and a replay keeps them. - Pointers, not copies. An entity carries
refto its row and the few attributes a tool needs to label it. - Deterministic ingest. No network, no LLM, no randomness in handlers. LLMs belong in jobs or in the agent.
- Facts close. Handlers never delete or mutate a fact.
- One tenant per
Toolsinstance. Cross-tenant reads do not exist. - Five tools. A new capability goes into the semantics of these five before anyone adds a sixth.
- Provenance on every fact.
source,evidence, andcorrected_bywhen someone corrected it.
Your first day¶
- Run the example.
uv sync --extra dev, thenuv run python examples/fleet/agent_minimal.py. Readexamples/fleet/: a complete cartridge with its events and its test. - Pick the tenant and five to ten events. The facts your agents need today, not your whole database. Write twenty real ones to an
events.jsonl. - Write the cartridge.
keel init cartridges/mine, entities withref, the event catalog and one handler per type. Check it:keel validate cartridges/mine --events events.jsonl. - Move business logic into
rules. If an agent computes "overdue", "critical" or "at risk", that rule belongs in the cartridge. - Connect the real source. CDC if you would rather not touch the product; the SDK, an outbox or a webhook for explicit domain events.
- Connect an agent. MCP to try it with Claude; HTTP or the generated SDK for production. Declare it under
agents:with the least it needs. - Schedule the jobs.
keel expire --consolidatedaily and, with semantic search,keel index.
The same path with a time budget per step, and our help: your first pilot in a day.