kleene
Kleene — relational algebra for recursive model calls.
Write declarative SQL. Compile joins, recursion, predicates and aggregation
into an execution graph of language-model calls, recursive sub-sessions and
tool calls. Plan and cost that graph, run it, and query the trace with the
same SQL. Watch it happen in a terminal UI.
SELECT candidate
FROM possibilities
WHERE VERIFY(candidate)
AND NOT EXISTS (
SELECT 1 FROM counterexamples ce
WHERE REFUTE(candidate, ce.text)
);
VERIFY is one model call per distinct candidate. The NOT EXISTS is an
anti-semi-join that stops on the first refuting counterexample. EXPLAIN
tells you how many calls that is before you spend them.
Install
Pick one of the three. Prebuilt binaries cover macOS (Apple silicon, Intel),
Linux (x86_64, aarch64) and Windows (x86_64; a zip on the
releases page, see
docs/CLI.md).
# curl: detects OS and architecture, verifies the SHA-256, installs to ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/MarcusElwin/kleene/main/install.sh | sh
# Homebrew: the repository is its own tap
brew tap MarcusElwin/kleene https://github.com/MarcusElwin/kleene
brew trust MarcusElwin/kleene # Homebrew 7 asks once for third-party taps
brew install kleene
# From source (compiles DuckDB the first time, about ten minutes)
cargo install --git https://github.com/MarcusElwin/kleene kleene
KLEENE_VERSION=v0.1.0 pins the installer to a tag and
KLEENE_INSTALL=/usr/local/bin changes the destination; the Homebrew
formula is Formula/kleene.rb. Details and the other
cargo variants are in docs/CLI.md.
Check that the engine works without a model:
kleene --version
kleene repl -c "SELECT 1 + 1 AS two" # prints a one-row table: two = 2
Full install, provider and command reference: docs/CLI.md.
Quickstart
1. Point it at a model. The wizard asks which providers to use and stores
the keys owner-readable under ~/.config/kleene/; environment variables win
over the file when both are set:
kleene setup # Anthropic, OpenAI, or a compatible endpoint (Ollama, vLLM, a gateway);
# optionally Brave, Tavily, Exa or Linkup for web_search
export ANTHROPIC_API_KEY=sk-ant-... # or just the environment: routed root/worker/proxy/judge by default
export OPENAI_API_KEY=sk-... # or any OpenAI-compatible endpoint (OPENAI_BASE_URL, OPENAI_MODEL)
2. Run it. kleene alone opens the terminal UI: one scrolling stream
with a prompt. Type a task, press Enter, watch the model's reply stream in,
its SQL run and the answer arrive. /sql, /trace and /board query the
engine from the same prompt and /setup opens the wizard without leaving
it. Or drive it from the shell:
kleene # the UI, prompt bar focused
kleene run @demos/oolong/task.txt --context demos/oolong/corpus.txt --budget-calls 60
kleene trace "SELECT depth, role, outcome, turns, calls, dollars FROM trace_sessions ORDER BY started_at"
kleene run streams the reply as it is written, highlights the SQL, shows a
spinner while statements execute and prints the results and the final
relation as tables.
The model receives the context as a table ctx(ordinal, text) and writes
CallSQL turn by turn: it can SELECT over the context, define prompt
functions, call tools with CALL, delegate with rlm(...) and
spawn(...), and finish with FINAL. Every model call is memoised, so
running the same task again is free. Everything lands in
.kleene/run.duckdb under the current directory.
| First open | A run mid-stream |
|---|
| |
| Slash commands | /setup wizard |
|---|
| |
Example runs
Every example below is in demos/ or
tasks/. They need a provider unless noted; a second run
of the same demo costs nothing because every call is memoised.
Just kleene. No flags. The first open shows a welcome card and a
prompt; the first thing worth typing is /setup if no key is configured
yet. Then describe a task and press Enter. The model's reply streams in,
each sql fence runs as it lands, every statement shows its rows and its
cost, and the answer arrives as a relation. A task typed here has no
--context; the model reads files through the workspace tools (read,
chunks, grep) rooted at the current directory.
$ kleene
> Which project consumed the most hours in total? The notes are in demos/oolong/corpus.txt
── turn 1 ─────────────────────────────────────────────────────── 0 calls · $0.0000
No context table was given, so the notes come in through the workspace tools.
CREATE TABLE ctx AS
SELECT c.ordinal, c.text FROM read('demos/oolong/corpus.txt') r CROSS JOIN LATERAL chunks(r.text, 200) c;
SELECT COUNT(*), MIN(ordinal), MAX(ordinal) FROM ctx;
60 | 0 | 59
1 row
── turn 2 ────────────────────────────────────────────────────── 67 calls · $0.1100
Only some notes mention hours. A cheap proxy filters those, then a worker extracts project and hours as JSON.
CREATE TABLE hours AS
SELECT ordinal, llm_json('Extract project and hours from: ' || text, '{"project":"string","hours":"number"}') AS h
FROM ctx WHERE mentions_hours(text);
28 rows
↳ worker cc3970 d1 Resolve which project 'the migration' refers to in notes 14, 22 and 41
│ ── turn 1 ──────────────────────────────────────────────────── 0 calls · $0.0000
│ SELECT ordinal, text FROM ctx WHERE ordinal IN (14, 22, 41);
│ FINAL FROM (SELECT 'Osprey' AS project);
│ ■ final · 1 turns · 2 calls · 3100 tok · $0.0040
── turn 3 ──────────────────────────────────────────────────────────────── writing
The hours table has 28 rows across six projects. Summing per project and picking the top one:
FINAL FROM (SELECT h.project, SUM(h.hours) AS total_hours FROM hours GROUP BY 1 ORDER BY 2 DESC LIMIT 1);
Child sessions nest under the turn that opened them, with their own turns
and a footer. Afterwards the same prompt queries what just happened:
> /trace SELECT depth, role, outcome, turns, calls, dollars FROM trace_sessions
> /sql SELECT COUNT(*) FROM hours
> /plans
The commands the popup lists:
| Command | Does |
|---|
/sql <statement> | run one CallSQL statement yourself in an interactive session |
/trace <sql> | query the store: trace_*, memo, tasks, evals |
/board | the continual loop's task board |
/runs, /follow <run>, /cancel | live runs on this daemon; show one by (the tail of) its id; cancel the one being followed |
/plans | show or hide EXPLAIN plans under statements (also Ctrl-P) |
/theme [flavour] | next Catppuccin flavour, or mocha, macchiato, frappé, latte (also Ctrl-T) |
/setup | add or change API keys: model providers and web search; the daemon reloads them |
/mcp [add <name> <cmd> ...] | the MCP servers and their tools; add or remove one in mcp.json |
/skills [name] | the loaded skills, or one in full |
/new | start a new conversation; until then each task continues the last one (the model is told the earlier tasks and answers, and finds all of them in the conversations table) |
/clear, /quit | clear the stream (Ctrl-L); detach, the daemon and its runs keep going (Ctrl-C) |
Tab completes a command, Up/Down walk the input history, PageUp/PageDown
scroll the stream. Runs started against the same daemon from another client
(kleene tui --run, kleene attach --run) appear in the stream too.
Long context: partition and map. Sixty dated meeting notes about six
projects. The root peeks at ctx, partitions it, maps rlm over the
partitions and aggregates the children's answers.
kleene run @demos/oolong/task.txt --context demos/oolong/corpus.txt --budget-calls 60
kleene trace "SELECT depth, role, outcome, turns, calls FROM trace_sessions ORDER BY started_at"
Repository question with reviewer agents. The root reads the codebase
with files, grep and read, declares a read-only reviewer agent and
spawns one per hypothesis with CROSS JOIN LATERAL spawn(...).
kleene run @demos/repo-review/task.txt --workspace . --max-depth 1
kleene trace "SELECT s.role, st.sql, st.calls FROM trace_statements st JOIN trace_sessions s USING (session) ORDER BY st.started_at"
The planner, without spending anything. EXPLAIN needs no provider: it
prints the plan space, the chosen join order and the as-written cost beside
it. See demos/planner.
kleene explain "SELECT c FROM candidates WHERE llm_bool('Is ' || c || ' a real place?')"
kleene repl < demos/planner/three_way.sql # join ordering over call predicates
kleene repl < demos/planner/cascade_and_beam.sql # a proxy cascade and a beam of width five
Watch it live.
kleene tui --run @demos/oolong/task.txt --context demos/oolong/corpus.txt
Keep it learning overnight, and measure it. State is tables in the
store, so stopping and starting again resumes.
kleene learn run --tasks 20 --generators puzzle,corpus # resumable continual loop
kleene learn report # the morning query
kleene bench run tasks/terminal --mode frozen # a task pack, one mode
kleene bench report # accuracy and cost per pack and mode
Resume a run that hit its turn cap.
kleene run "..." --max-turns 5
kleene resume <run-id> --max-turns 10
How it works
One turn of a session
A session is a loop. The harness sends the model a cached system prefix
(the CallSQL rules, the catalog, the budget) plus the transcript; the model
replies with CallSQL in one fence; the harness parses it, annotates every
operator with the calls it implies, prices the plan, refuses it if the
remaining budget cannot pay, executes it, renders the rows back into the
transcript, and repeats until the model writes FINAL.
sequenceDiagram
autonumber
participant M as Model
participant H as Harness (kleene-harness)
participant S as kleene-sql
participant A as kleene-algebra
participant X as kleene-exec
participant L as LiveSink
participant D as DuckDB (kleene-store)
H->>M: system prefix + transcript
M-->>H: CallSQL in one sql fence
H->>S: parse, validate, resolve names and types
S-->>H: LogicalPlan
H->>A: annotate with call kinds, run rewrite rules
A-->>H: CallPlan with rows, calls, tokens, dollars
alt estimate exceeds remaining budget
H-->>M: refused, with the plan
else within budget
H->>X: execute(plan)
loop every scalar, table, tool or child call
X->>L: call
L->>D: memo lookup
alt hit
D-->>L: cached result
else miss
L->>L: route, call provider or tool, price, charge budget
L->>D: memo store, TraceEvent
end
L-->>X: rows
end
X-->>H: batches
H->>D: persist session
H-->>M: rendered rows + footer (calls, tokens, dollars, remaining)
end
Note over M,H: repeat until FINAL
kleene repl and kleene explain run the same path without a model turn
around it. Details: the path of one statement.
Delegation: rlm and spawn
Child sessions are the same loop one level deeper, with a role, a budget
slice and their own table namespace. rlm(q, ctx) opens one child per input
row; spawn('reviewer', task) opens a child with that agent's tools and
budget. A child's FINAL comes back to the parent as a row.
sequenceDiagram
participant R as Root session (depth 0)
participant P as Provider
participant C1 as Child 1 (depth 1, worker)
participant C2 as Child 2 (depth 1, worker)
R->>P: turn: peek at ctx, partition it
P-->>R: CREATE TABLE parts AS SELECT ...
R->>P: turn
P-->>R: SELECT * FROM parts CROSS JOIN LATERAL rlm(question, chunk)
par one child per row, concurrently
R->>C1: task + chunk, budget slice
C1->>P: turns until FINAL
C1-->>R: (answer, detail, session)
and
R->>C2: task + chunk, budget slice
C2->>P: turns until FINAL
C2-->>R: (answer, detail, session)
end
R->>P: turn: aggregate the children's answers
P-->>R: FINAL FROM (SELECT ...)
Budgets have calls, tokens, dollars, depth and wall clock; a child gets the
parent's remaining slice intersected with its role's budget and its spending
rolls up. Details: sessions and delegation.
Processes: CLI, daemon and TUI
kleene run, repl, explain, learn and bench run the harness in
one process. The TUI talks to an engine daemon over a Unix socket and a
JSONL protocol with cursors, so a client that reconnects resumes from where
it left off and kleene attach can watch the same events headless.
sequenceDiagram
participant U as kleene (TUI)
participant Dm as kleene daemon
participant Hs as Harness + store + provider
U->>Dm: connect .kleene/daemon.sock (starts one if none listens)
U->>Dm: Subscribe { after: cursor }
Dm-->>U: Hello, replayed Events
U->>Dm: StartRun (a task typed at the prompt)
Dm->>Hs: run
Hs-->>Dm: streamed text, trace events
Dm-->>U: CallDelta ... TurnFinished (reply, SQL, rendered results)
U->>Dm: /sql → Submit, /trace and /board → Query
Dm-->>U: Table
U->>Dm: /setup → Reload (re-read the keys)
Dm-->>U: RunFinished
Details: processes and
kleene tui.
Crates
flowchart TB
CLI[kleene<br/>CLI binary]
TUI[kleene-tui<br/>ratatui client, setup wizard]
DAEMON[kleene-daemon<br/>Unix socket, JSONL protocol]
HARNESS[kleene-harness<br/>sessions, turn loop, LiveSink, learn, bench]
EXEC[kleene-exec<br/>operators, semi-naive recursion, concurrent calls]
ALGEBRA[kleene-algebra<br/>call kinds, cost model, rules, EXPLAIN]
SQL[kleene-sql<br/>sqlparser to LogicalPlan]
LLM[kleene-llm<br/>Anthropic, OpenAI-compatible, router, replay]
TOOLS[kleene-tools<br/>files, grep, shell, git, web_search ...]
STORE[kleene-store<br/>DuckDB: tables, memo, trace, sessions]
TRACE[kleene-trace<br/>TraceEvent, sinks]
CORE[kleene-core<br/>Value, Schema, Catalog, Budget, CallKind]
CLI --> TUI
CLI --> DAEMON
CLI --> HARNESS
TUI --> DAEMON
DAEMON --> HARNESS
HARNESS --> EXEC
HARNESS --> LLM
HARNESS --> TOOLS
HARNESS --> STORE
HARNESS --> TRACE
EXEC --> ALGEBRA
ALGEBRA --> SQL
SQL -.-> CORE
EXEC -.-> CORE
LLM -.-> CORE
TOOLS -.-> CORE
STORE -.-> CORE
TRACE -.-> CORE
Every crate depends on kleene-core and nothing depends on the binary.
The full table of crates, what each owns and its key types is in
docs/ARCHITECTURE.md.
- A planner for calls. Predicates that call a model have a cost the
optimizer can see: join order, conjunct order, semi-joins for
EXISTS,
cascades through cheap proxies and beam-limited recursion are rewrite
rules with a cost model. EXPLAIN shows the estimate before the spend.
- Budgets as semantics. Calls, tokens, dollars and depth are dimensions
of a budget that child sessions inherit and slice; a statement the
remaining budget cannot pay for is refused with its plan.
- Learning as tables. What the harness learns (playbook SQL, ratings,
generator dials, sampled selectivities) is rows in DuckDB: inspectable,
replay-gated and revertible.
Commands
| Command | Does | Reference |
|---|
kleene | the terminal UI: one stream, a prompt, slash commands; type a task to run it | docs |
kleene setup | pick providers and store their keys; /setup in the TUI does the same without leaving it | docs |
kleene run <task> | drive a model through the SQL turn loop to FINAL, streamed to the terminal | docs |
kleene resume <run-id> | continue a run that hit its turn cap | docs |
kleene repl [-c SQL] | CallSQL against the store, interactive or scripted | docs |
kleene explain <sql> | the call plan and its cost, without executing | docs |
kleene trace <sql> | DuckDB SQL over the trace, memo and session tables | docs |
kleene tui | the same UI over the engine daemon, optionally starting a run on connect | docs |
kleene attach | the same, headless: every event as a JSON line |
Every flag: kleene <command> --help, or docs/CLI.md.
Documentation
The website at kleene.sh renders the documents below,
with a technical overview and
the benchmarks as pages; its source is
site/.
| Read this | For |
|---|
docs/CLI.md | install, provider setup, environment variables, every command and flag, troubleshooting |
docs/DIALECT.md | the CallSQL language: the relational core, sessions, model calls, tools, delegation, planner rules |
docs/ARCHITECTURE.md | the crates, the path of a statement, sessions, , , , |
Benchmark results
What kleene bench has measured on real models so far. Three modes run
the same packs on the same model, tools and budget:
| Mode | What it is | Compare it with |
|---|
learning | Kleene with the playbook shown and adopted | frozen, to see what the playbook adds |
frozen | Kleene with the playbook off, the control | plain, to see what the SQL abstraction adds |
plain | a tool-calling agent, no SQL, same provider and tools | the baseline |
Pass counts the tasks the pack's oracle accepted. Cost is the solver's
own calls at list prices, without the replay evals a learning run pays
to gate its playbook (about four dollars in five of a learning run's
real spend on Opus). The first table is the one to read; the cost table,
the full per-cell table and the pass-rate-against-cost plots for the
hard packs fold out below it. Filterable charts of the same rows are on
kleene.sh/benchmarks, the reading is in
the write-up, and what
each number measures is in docs/BENCHMARKS.md.
Runs so far: Claude Opus 5.5 on 27 September 2026 (the four original
packs), 6 October (coding, memo-rubric) and 10 October (logbook-hard);
Claude Haiku 4.5 on 1 and 6 October; GPT-6 Luna on 5 October over all
seven packs. Claude Sonnet 5.5 judged every memo. logbook-hard is
capped at ten tasks for every model, and Luna's learning run on it
stopped after three. The block below is generated from the CSVs under plots/ by
kleene bench results (the command is in
docs/BENCHMARKS.md); a new
run is a new CSV and a re-run, never a hand edit.
Pass rate, one column per model; each cell reads learning / frozen / plain, the best mode in bold, – where that mode has not run.
| Pack | Tasks | Claude Opus 5.5 | Claude Haiku 4.5 | GPT-6 Luna |
|---|
coding | 12 | 100% / 100% / 100% | 17% / 17% / 33% | 83% / 92% / 75% |
finance-hard | 20 | 100% / 100% / 100% | 75% / 65% / 95% | 40% / 40% / 30% |
finance-synthetic | 20 | 100% / 100% / 100% | 95% / 100% / 100% | 75% / 70% / 100% |
legal-hard | 20 | 95% / 100% / 100% | 35% / 50% / 55% | 35% / 15% / 20% |
legal-synthetic | 20 | 100% / 100% / 100% | 85% / 70% / 75% | 75% / 90% / 100% |
logbook-hard | 10 | 100% / 100% / 50% | 10% / 10% / 20% | 33% / 70% / 30% |
memo-rubric | 20 | 20% / 50% / 50% | 55% / 40% / 40% | 90% / 40% / 55% |
oolong-hard | 20 | 100% / 100% / 80% | 35% / 30% / 30% | 35% / 45% / 0% |
Models: Claude Opus 5.5 is claude-opus-5-5, Claude Haiku 4.5 is claude-haiku-4-5-20251001, GPT-6 Luna is gpt-6-luna.
| Pack | Tasks | Claude Opus 5.5 | Claude Haiku 4.5 | GPT-6 Luna |
|---|
coding | 12 | $0.142 / $0.109 / $0.110 | $0.039 / $0.072 / $0.065 | $0.0017 / $0.0019 / $0.0023 |
finance-hard | 20 | $0.086 / $0.081 / $0.172 | $0.077 / $0.144 / $0.196 | $0.011 / $0.041 / $0.0021 |
finance-synthetic | 20 | $0.031 / $0.042 / $0.0036 | $0.015 / $0.022 / $0.0064 | $0.0004 / $0.0005 / $0.0001 |
legal-hard | 20 | $0.173 / $0.163 / $0.094 | $0.160 / $0.156 / $0.063 | $0.0029 / $0.0024 / $0.0011 |
legal-synthetic | 20 | $0.130 / $0.085 / $0.0054 | $0.015 / $0.036 / $0.0080 | $0.0007 / $0.0016 / $0.0001 |
logbook-hard | 10 | $0.143 / $0.145 / $1.48 | $0.595 / $0.513 / $0.458 | $0.062 / $0.023 / $0.0074 |
memo-rubric | 20 | $0.062 / $0.063 / $0.028 | $0.018 / $0.065 / $0.018 | $0.0010 / $0.0015 / $0.0003 |
oolong-hard | 20 | $0.086 / $0.115 / $0.695 |
| Pack | Mode | Model | Tasks | Pass | $/task | Total $ | Calls/task | Tokens/task | Seconds/task |
|---|
coding | learning | Claude Opus 5.5 | 12 | 12/12 (100%) | $0.142 | $1.70 | 8.0 | 88,984 | 35.2 |
coding | learning | Claude Haiku 4.5 | 12 | 2/12 (17%) | $0.039 | $0.473 | 2.7 | 29,910 | 26.5 |
coding | learning | GPT-6 Luna | 12 | 10/12 (83%) | $0.0017 | $0.020 | 8.2 | 42,094 | 26.2 |
coding | frozen | Claude Opus 5.5 | 12 | 12/12 (100%) | $0.109 | $1.31 | 8.6 | 76,533 | 36.9 |
coding | frozen | Claude Haiku 4.5 | 12 | 2/12 (17%) | $0.072 | $0.868 | 4.6 | 48,375 | 43.9 |
coding | frozen | GPT-6 Luna | 12 | 11/12 (92%) | $0.0019 | $0.023 | 12.1 | 55,286 | 32.6 |
coding | plain | Claude Opus 5.5 | 12 |
Status
Released: the latest tag is on the releases page with prebuilt
binaries, and install.sh, Homebrew and cargo install all install it.
Every milestone of the plan is on main.
| Milestone | Shipped |
|---|
| M0 scaffold | workspace, CI, kleene --version |
| M1 relational core | CallSQL parser and executor, recursive CTEs by semi-naive evaluation, DuckDB store, differential tests against DuckDB |
| M2 call algebra | llm_* and prompt-defined functions, expand, CALL tools with volatility fences, memo, batching (BATCH n), budgets, EXPLAIN, Anthropic, OpenAI-compatible and (feature gateway) Open Responses adapters, router, record and replay |
| M3 RLM harness | the session loop, rlm, spawn, CREATE AGENT, persistence and resume |
| M4 daemon and TUI | JSONL protocol with cursors, cancel, detach and reattach, headless attach |
| M5 planner | join ordering over call predicates, cascades, beam recursion, sampled selectivity, budget refusal |
| M6 continual loop | tasks, generators, oracles, ratings, curriculum, replay-gated playbook and function refinement (learn refine) |
| M7 benchmarks | task packs, bench in learning, frozen and plain-agent modes, LAB import, bench plot, the write-up |
Since M7: the rename to Kleene, kleene setup and the config file, streamed
replies, the one-stream Catppuccin TUI with slash commands and /setup,
web_search over Brave, Tavily, Exa or Linkup, the curl installer, the
Homebrew formula and the release workflow. Then the coding loop: a finish
check that refuses FINAL while the tests fail (--check, a pack task's
check), patch with fuzzy matching and a diff, lines ranges, ranked
search, the turns table that folds old results out of the prompt, the
plan table the UI renders, skills (SKILL.md, six built in, /skills),
project instructions from AGENTS.md, MCP servers as catalog tools
(mcp.json, /mcp), and a plain baseline on native tool calling.
Not yet, in the order they are planned:
- Harder packs. The first real runs (Claude Opus 5.5, 27 September
2026) solved every task of every pack in every mode, so they compare cost
only: the playbook cuts calls per task (oolong 4.9 to 3.9, legal 12.7 to
10.6), the replay gate costs more than it saves at twenty tasks a pack,
and the one-call tool-calling baseline is cheapest wherever the context
fits in a prompt. The table and the reading are in
the write-up; the
per-task rows, the report and the plots are under
plots/. The harder
packs (coding, memo-rubric, logbook-hard) do separate on accuracy:
on Haiku 4.5 (1 October 2026) Kleene solves 2/12 coding steps and 8/20
memos frozen, 11/20 memos learning, against 8/20 for the plain agent;
see section 4.1.
- Engine gaps listed in the write-up:
proxy thresholds calibrated from a sample instead of declared, and the
learned cost model persisted as versioned tables rather than sampled per
session.
Developing
git clone https://github.com/MarcusElwin/kleene && cd kleene
cargo build # DuckDB compiles once, ~10 min
cargo test --workspace --all-features
cargo clippy --workspace --all-targets --all-features -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
CLAUDE.md holds the working rules (interfaces first, typed errors, the
flag shapes that keep DuckDB from rebuilding), CI runs in
.github/workflows/ci.yml and
.github/workflows/release.yml publishes
tagged releases. Written in Rust, model-agnostic with no SDK
and no gateway required, MIT licensed.
Why "Kleene"
Stephen Cole Kleene (1909–1994) gave computation three of its load-bearing
ideas, and this project leans on all three.
- The Kleene star turns "one step" into "any number of steps". Kleene
introduced
a* in 1951 to describe the "regular events" a McCulloch–Pitts
nerve net can recognise [1]; it is the closure of a under
repetition, and it is exactly what a recursive CTE computes when it runs a
term to its fixpoint. Aho and Ullman later showed that relational algebra
needs precisely such a least-fixpoint operator to express transitive
closure at all [4], which is why CallSQL has WITH RECURSIVE.
- The Kleene fixed-point theorem says how to reach that closure: start
from nothing and apply the step until nothing changes. The construction
is the "first recursion theorem" of Introduction to Metamathematics
[2], and in its database form it is semi-naive evaluation
[5]: each round sees only the previous round's delta, which is
how the executor runs a recursive term and where a
LIMIT k inside it
becomes a beam.
- The recursion theorem shows a program can refer to itself without
paradox. Kleene proved it in 1938 as a lemma about ordinal notations
[3]; it is what a session does when it opens a child session with
rlm(...), the same loop one level deeper with a slice of the budget.
That is this project in three theorems. A model call is a step; SQL gives
it joins, predicates and aggregation; recursion with a beam gives it search;
the planner prices the closure before it is computed. The engine is named
for the mathematician who showed that closure is a thing you can compute,
and the dialect keeps its own name, CallSQL, because the SQL is where the
calls are. (The name kleene is taken on npm by an unrelated JavaScript
library and free on crates.io; see docs/RESEARCH.md.)
- S. C. Kleene, "Representation of Events in Nerve Nets and Finite
Automata", RAND RM-704 (1951); in Automata Studies, Princeton
University Press, 1956, pp. 3–41.
- S. C. Kleene, Introduction to Metamathematics, North-Holland, 1952,
§66 (the first recursion theorem; the least-fixed-point construction).
- S. C. Kleene, "On Notation for Ordinal Numbers", Journal of Symbolic
Logic 3(4), 1938, pp. 150–155 (the second recursion theorem).
- A. V. Aho and J. D. Ullman, "Universality of Data Retrieval Languages",
POPL '79, pp. 110–119 (relational algebra plus a least fixpoint).
- F. Bancilhon and R. Ramakrishnan, "An Amateur's Introduction to
Recursive Query Processing Strategies", SIGMOD '86, pp. 16–52
(semi-naive evaluation).
The research the design draws on beyond Kleene, from recursive language
models to SQL as a model interface and the complexity results the planner
relies on, is collected in docs/RESEARCH.md.