Lemma, the agent.lean mascot: a small microchip robot with a proof checkmark

agent.lean

A tiny, verifiable agent library in Lean 4 that compiles to C.
Write LLM agents with tools in Lean, prove what they may do, and ship them to phones and embedded boards.

License: Apache-2.0 CI Status: v0.1 Lean 4


Status: v0.1 preview. The core, codecs, policies, trace, C ABI, Lean driver, bundle, Swift and Kotlin bindings and the llama.cpp adapter are implemented; the guarantees below are proved in Lean or checked by tests. APIs may still change.

Why

agent.cpp and agent.rs showed that an agent framework can be a small native library instead of a Python runtime. agent.lean takes that idea to Lean 4, which gives two things the others can't:

  • Proofs about agents. Rules such as "send_payment only after confirm_user in the same turn" or "at most 3 web fetches per turn" are theorems enforced by construction, not instructions in a prompt.
  • Portable C output. Lean compiles to C, so the same agent runs on a Raspberry Pi, an industrial gateway, an iPhone or an Android phone, with no interpreter and no garbage-collected VM.

How it works

┌──────────────────────── Lean (pure, proven) ────────────────────────┐
│  step : State → Event → State × List Effect                         │
│  codecs (OpenAI-compatible, Anthropic) · tools · policy monitors    │
└───────────────▲──────────────────────────────────────┬──────────────┘
         events │  (JSON bytes over the C ABI agent.h) │ effects
┌───────────────┴──────────────────────────────────────▼──────────────┐
│  Host: C driver · Lean driver (libcurl) · Swift · Kotlin            │
│  runs HTTP or the in-process local model, executes host tools,      │
│  stores the trace, holds the API keys                               │
└─────────────────────────────────────────────────────────────────────┘
  • Pure core. The whole agent is one deterministic function from state and event to new state and effects. The core performs no IO, so it can be proved about, replayed exactly and embedded anywhere.
  • Host does the IO. Phones use their own networking (URLSession, OkHttp), boards use libcurl. The host only moves bytes; Lean encodes and decodes the provider formats, so every platform behaves the same.
  • Remote and local models. OpenAI-compatible endpoints (OpenAI, Ollama, llama-server, vLLM, LM Studio, OpenRouter) and Anthropic Messages. Local models run in-process through a llama.cpp adapter that the core cannot tell apart from a remote API.
  • Event-sourced. Every event is appended to a trace. State is a fold over it, resume after the OS kills your app is exact, and every recorded session is a regression test.

What is guaranteed

GuaranteeHow
Tools never see malformed inputArguments are decoded into the tool's Lean type or rejected; JSON Schema is derived from the type
Small local models emit valid tool callsThe derived schema becomes a decoding grammar in the local adapter
Every run terminates within its budgetsTheorem over step; sub-agents cannot exceed their parent's remaining budget
Your policies holdTemporal policies compile to monitors with a soundness theorem; violations are refused before any effect
Prompt injection cannot trigger guarded actionsTools label output as untrusted; policies can forbid actions after untrusted input unless confirmed
Replay is exactresume snapshot rest = replay full trace, proved
API keys never reach tracesRequests carry a credential reference; a Secret cannot be serialized
Codecs are correctRound-trip and chunk-split theorems for the JSON and SSE parsers

Quick start

lake build                 # the library (Lean 4.35, pinned in lean-toolchain)
lake test                  # 43 scenario tests, one per spec scenario
scripts/check_proofs.sh    # no sorry; main theorems use only Lean's standard axioms

# run the example assistant through the Lean driver against any OpenAI-compatible endpoint
lake build driver_demo
AGENT_CREDENTIAL_OPENAI=$OPENAI_API_KEY .lake/build/bin/driver_demo \
  --endpoint https://api.openai.com/v1/chat/completions --model gpt-4.1-mini "What is 17 plus 25?"
# or a local model: --endpoint http://127.0.0.1:11434/v1/chat/completions --model <ollama model>

# the portable source bundle: plain C/C++ + CMake, no Lean needed to build it
python3 scripts/bundle.py --root Examples --out bundle/out --extra c/agent_abi.c --extra-include c/agent.h

Write an agent:

import AgentLean
meta import AgentLean.Meta     -- deriving handlers and `policy`, compile time only
open AgentLean AgentLean.Formula

structure TransferArgs where
  to     : String
  amount : UInt32
  memo   : Option String
  deriving ToJson, FromJson, ToolSchema   -- JSON Schema derived from the type

def transfer : Tool TransferArgs Receipt where
  name       := "transfer"
  description := "Send money to a contact"
  executor   := .host "bank.transfer"     -- implemented by the iOS/Android app
  idempotent := false                     -- required: never re-issued after a crash
  sensitive  := true                      -- trace flagged for encryption, hashed on audit export
  pre x      := x.amount > 0

policy payLimit := guard "transfer" "amount ≤ 100" (argNat "amount" (· ≤ 100))
  because "transfers above 100 need a human"
policy confirmBeforePay := requiresBefore "transfer" "confirm_user"
policy noDelete halt := ¬ₚ call! "delete_all"

def assistant : Agent where
  name     := "assistant"
  system   := "You are a careful assistant."
  tools    := #[transfer.erase, confirmUser.erase, Agent.asTool "researcher" "research" "Delegate research"]
  policies := #[payLimit, confirmBeforePay, noDelete]
  budget   := { steps := 64, toolCalls := 10, tokens := 100000 }

initialize registerAgent assistant

See Examples/Agents.lean for the full example.

Drive it from C (Swift, Kotlin and the Lean driver wrap the same five calls):

agent_t *a = agent_new(cfg, cfg_len, NULL);   /* {"agent":"assistant","model":...,"endpoint":...} */
agent_feed(a, user_event, len);
agent_buf_t e;
while (agent_next_effect(a, &e)) {
    /* llm_request → POST e.body to e.url, adding the secret named by e.credential; feed llm_response
       tool_call   → run the host tool; feed tool_result
       ui          → update the UI
       complete    → done */
    agent_buf_free(e);
}
agent_free(a);

Proved and tested

GuaranteeWhere
Policy monitors decide exactly what the formula says over the whole tracemonitor_sound (Policy.lean)
Every run ends within its step budget; a finished run does nothingfinal_done, done_sticky (Run.lean)
A child agent never gets more than its parent has leftallocate_le
Resuming from a snapshot gives the same states and effects as replaying everythingstepAll_append (Trace.lean)
SSE decoding is independent of how the stream is splitSse.feed_chunks
Requests fit the context estimate; system prompt and last user turn are never droppedfit_fits, fit_head, fit_keeps_last_turn
Tool arguments decode into the tool's type or are rejected; schema acceptance = decodingscenario tests
Same behaviour byte for byte on every target and bindinggolden traces in traces/

Measured (v0.1, Apple M-series / Docker)

TargetReference agent, strippedNotes
macOS arm640.97 MBstep ≈ 40 µs, codec ≈ 80 MB/s, cold start < 1 ms, ≈ 7 KB per idle agent
Linux aarch64 (static musl)1.7 MBincludes musl and libc++; step ≈ 55 µs
Linux x86_64 / armv7 / riscv64 (static musl, emulated)1.66 / 1.41 / 1.31 MBconformance + thread tests pass under QEMU
iOS arm64, Android arm64-v8a / x86_64buildsgolden traces pass on the Android emulator (CI) and as Mac Catalyst; iOS device not run

Budgets live in bench/budgets.json and bundle/runtime-surface/; CI fails on regressions.

Targets

ClassTargets
Embedded Linuxaarch64, armv7, riscv64, x86_64 (static, musl)
MobileiOS arm64 + simulator (XCFramework, Swift), Android arm64-v8a + x86_64 (AAR, Kotlin/JNI)
DesktopmacOS, Linux
LaterRTOS/MCU via a pure-C mini-runtime (v2 track)

Bindings are hand-written over each platform's native FFI (Swift C interop, JNI, Kotlin/Native cinterop) and pass the same golden-trace conformance suite as the C ABI.

Layout

PathWhat
AgentLean/the runtime library (imports only Init)
AgentLean/Meta/compile-time deriving handlers and the policy command
AgentLean/Driver.lean, c/agent_curl.cthe Lean-native driver (libcurl, loaded at run time)
c/agent.h, c/agent_abi.cthe C ABI
c/llama/in-process llama.cpp adapter (grammar-constrained tool calls)
bindings/swift, bindings/kotlinSwift (XCFramework/SwiftPM) and Kotlin (JNI/AAR) bindings
scripts/bundle.py, bundle/the portable source bundle generator and runtime shim
traces/golden traces every target must reproduce
Tests/scenario tests, golden generator, axiom report

Roadmap

v0.1 (this): everything in the bootstrap change. Next: a pure-C mini-runtime implementing the recorded runtime surface (RTOS/MCU targets), optional vendored mimalloc, MCP as an untrusted dynamic tool tier, streaming in the local adapter.

Design documents

  • Architecture decisions: docs/adr/ (ADRs 0002–0017)
  • Specs: openspec/specs/, one per capability: agent-core, tools, policies, llm-codecs, trace-replay, host-abi, portable-build
  • Design: design.md of the archived bootstrap change
  • Typed knowledge graph: tg.md/ holds concepts, decisions and constraints as a typed graph, checked with the tg CLI

Out of scope for v1

Runtime-registered or MCP tools, graph/workflow orchestration, token-level control of local inference, long-term memory inside the core (build it as tools), and Python/JS/Rust bindings.

Licence

Apache-2.0.

Meet Lemma

Lemma is a small chip that only does what it can prove. Its antenna listens to the model; the check on its chest is the proof.