Agent blueprints (agent.leviath)
An agent is a directory with an agent.leviath file, a TOML blueprint describing a
multi-stage workflow graph. The agent catalog has seven
complete ones worth stealing from.
New to this? Build your first agent walks through writing one stage by stage; this page is the reference for every field it uses.
Start from a scaffold rather than a blank file:
lev create my-agent
cd my-agent
lev run . --task "Your task here"A blueprint needs very little to run: a name, an entry stage, and one stage with a prompt. Everything else on this page is opt-in from there. A fuller one looks like this (machine-checkable against the published blueprint schema):
[agent]
name = "coder"
version = "0.2.0"
description = "Analyze, implement, and review with graph-based recovery"
entry_stage = "analyze"
[tool_permissions] # global defaults; per-stage overrides allowed
read_file = "allow"
write_file = "ask"
bash = "ask"
[stages.analyze]
mode = "autonomous"
model = { models = [
{ provider = "anthropic", model = "claude-sonnet-4-6" },
{ provider = "openai", model = "gpt-5.4-mini" },
] }
available_tools = ["read_file", "list_dir"]
required_tools = [] # human-in-the-loop tools kept in an unattended run
max_iterations = 15
system_prompt = """Understand the task and produce a short implementation plan."""
[stages.analyze.transitions.implement]
hint = "Plan ready, begin implementation"The run loop
Within a stage, an agent runs a tight loop (infer, act on tool calls, repeat) until the model signals it's done or a transition fires:
flowchart LR
I["Infer<br/>(stage model)"] --> T{"Tool calls?"}
T -->|yes| X["Execute tools<br/>route output to regions"]
X --> I
T -->|no| D{"Transition?"}
D -->|hint / error / stuck| N["Next stage"]
D -->|none, done| E["Finish"]
N --> I
Lifecycle
A run moves through a handful of states the dashboard and API report on:
stateDiagram-v2 [*] --> Starting Starting --> Running Running --> WaitingInput: prompt, or holding for children WaitingInput --> Running: answered, or children done Running --> Paused: lev pause Paused --> Running: lev resume Running --> Complete Running --> CompleteInteractive: done, still accepting messages Running --> Error: unrecoverable error Running --> Cancelled: lev cancel Complete --> [*] CompleteInteractive --> [*] Error --> [*] Cancelled --> [*]
These are the exact RunStatus values the dashboard and API
report. CompleteInteractive means every required stage finished but the agent is still
accepting messages.
WaitingInput covers two very different situations: a run stopped on a prompt somebody has
to answer, and a run parked while its own sub-agents or
fan-out workers get on with it. The second needs nothing from you.
lev ps tells them apart, so reach for it before concluding a
run is stuck.
Stages and models
Each stage gets its own model (an ordered provider/model fallback list: the first configured
provider wins), tools, iteration cap, and context layout. Transitions form a
graph: linear by default, or branch on conditions like error and stuck.
[stages.analyze.model]
allow_user_default = true # fall back to the user's default model, else fail closed
models = [
{ provider = "anthropic", model = "claude-sonnet-4-6" },
{ provider = "openai", model = "gpt-5.4-mini" },
]
request_timeout_secs = 120 # per-stage inference wall-clock cap
[stages.analyze.model.parameters] # free-form, passed through to the provider
temperature = 0.2
max_tokens = 8000Model selection is per stage, and only per stage. Two mistakes here are quiet ones. A top-level
[model] block parses and is read by nothing, and a stage naming no model takes the host default
without saying so. lev validate reports both. See
every stage should name its own model.
Which tools a stage gets
available_tools lists what the stage may call.
required_tools is the exception to the unattended cut. A --yolo run drops
every tool that waits on a person, and this is where a stage names the ones it wants kept anyway. Every entry must also
appear in available_tools.
Naming a tool here also settles the blocking-tool-in-autonomous-stage lint for it, since listing
it is how you say you meant it. See
human-in-the-loop tools.
Context regions
[context.regions] defines the memory layout. There are nine region kinds (the default is
temporary); see Structured context for what each one does. Budgets come in
three forms:
[context.regions.codebase]
kind = "compacting"
budget = "35%" # ceiling as a share of the model's context window
max_tokens = 60000 # absolute guard-rail the percentage never exceeds
min_tokens = 4000 # absolute floor on small context windows
[context.regions.task]
kind = "pinned"
max_tokens = 2000 # bare max_tokens alone = fixed absolute budgetPercentages are ceilings, not allocations. They may sum past 100%, because regions rarely all
fill at once. With a percentage, max_tokens caps and min_tokens floors the resolved value;
without one, max_tokens is the fixed budget. Compacting regions also take
threshold_tokens, the fill level that triggers compaction.
A stage can override the whole layout for just itself with [stages.<name>.context.regions]. The
per-stage layout applies when the stage is entered, and uses the same syntax:
[stages.plan.context.regions.constraints]
kind = "pinned"
budget = "10%"A region the stage leaves out is hidden, not destroyed. It keeps its contents, is left out of that stage's prompt, and comes back with everything in it as soon as a later stage declares it again. That is what makes this usable for narrowing: a compute stage need not carry a large data preview through every one of its calls, and a summary stage further on can still read it.
conversation, tool_results and final_output are always visible, whatever a stage declares.
The first two hold the typed tool-call turns the next stage's own turns attach to, and an answer
submitted early has to survive to the end.
Seed commands
A region can be seeded before the run starts:
[context.regions.codebase]
kind = "compacting"
seed = { command = "git ls-files" }Seeds run at spawn before any approval prompt, confined to the workdir and routed through the entry stage's sandbox, time- and size-capped.
Warning
A seed command runs a shell command before you approve anything, so it must be covered by
[safe_commands] to run at all. lev validate
prints every seed a blueprint will run; review them for third-party blueprints. Refuse with
--no-seed-commands or [security] allow_seed_commands = false.
Read paths
An agent that needs to read beyond its workdir, for run archives, design docs, or sibling directories, declares them:
[read_paths]
allow = ["~/.leviath/runs", "../shared-docs", "glob:~/design-docs/**"]The declarations do nothing on their own: the user's config must grant them, they are
read-only, and every access is checked against the symlink-resolved real path. Run
lev validate to see which of them the config on this machine actually grants. See
Security for the grant stanzas and the full matching rules.
How the coding agent verifies its work
The bundled coder agent decides what "done" means before it starts, rather than judging it at
the end. Its entry stage is discover: before planning anything, the agent classifies the
project's testing story and writes a workflow region ending in three literal lines that later
stages execute verbatim:
BASELINE: <command to run BEFORE any edit>
VERIFY: <command to re-run after each change>
DONE WHEN: <the completion bar, including "no regressions vs baseline">The baseline is captured before the first edit, so "a test that was already failing" and "a test
I broke" are distinguishable. Each change re-runs VERIFY and compares against the baseline, and
the run is only done when DONE WHEN holds, not when "most tests pass". Regions that carry this
state are marked required = true; if one is empty when a stage needs it, the workflow routes
back through discovery instead of guessing. Projects with no tests at all are handled explicitly:
the plan must include building verification (a smoke test to write and run), stated plainly
rather than invented.
Tracking files the agent touches
[context.file_tracking] keeps a running list of what the agent has read and written, in its own
region, so a later stage knows what has already been looked at.
[context.file_tracking]
region = "files" # default "files"
track_reads = true # default true
track_writes = true # default true
max_file_tokens = 4000 # cap on how much of one file is trackedCatching an agent going in circles
[repetition_detection] watches for an agent making the same call over and over, or reading without
ever writing. When it sees one, it writes a [System] note into the agent's conversation telling it
what it is doing and to try something else.
It nudges, it does not intervene. The run keeps going either way, the stage does not fail, and no
transition fires. If you want a loop like this to actually route somewhere, use a stuck edge in
stages. The two work well together: the nudge gives the agent a
chance to correct itself, and the edge catches it if it does not.
[repetition_detection]
enabled = true # default
max_repeat_calls = 3 # default; identical tool call, back to back
max_readonly_streak = 10 # default; read-only calls with no modification in betweenWho does the summarizing
A compacting region summarizes rather than evicting, and something has to write
that summary. By default it is Sonnet on Anthropic, whatever the stage itself runs on, because a
summary is cheap work that does not need the stage's model:
[compaction]
provider = "anthropic" # default
model = "claude-sonnet-4-6" # default
max_summary_tokens = 2000 # default
temperature = 0.2 # default
system_prompt = "..." # optional; replaces the built-in summarizer promptPoint it at a provider you have configured if you do not use Anthropic. A run whose compaction
provider is not registered loses compaction rather than failing, so an unset [compaction] on an
OpenAI-only machine quietly stops summarizing. lev doctor reports which providers are
registered.
Discovering tools mid-run
By default a stage advertises a fixed tool set resolved at spawn, and a tool that appears later is
invisible to it. dynamic_tools opts an agent in to re-advertising:
[agent]
dynamic_tools = trueWith it on, a script tool written into the run's own tools/ directory becomes callable on the
next inference. Off (the default) is the safer choice, since it means an agent cannot grow its own
capabilities mid-run.
Handing context to a sub-agent
[[transforms]] maps one blueprint's regions onto another's when a parent spawns a child, so the
child starts with the parent's findings under its own region names.
[[transforms]]
from_blueprint = "researcher"
to_blueprint = "reviewer"
[[transforms.mappings]]
from_region = "findings"
to_region = "source_material"
transform = "direct" # direct | summarize | extract
[[transforms.mappings]]
from_region = "conversation"
to_region = "brief"
transform = "summarize"extract additionally takes fields to pull named pieces out. See
Sub-agents.
Validate before you run
lev validate . # check the graph, and what the blueprint leaves unsaid
lev validate . --deny-warnings # for CI: warnings fail too
lev test . # run the blueprint's tests/ cases (real API calls)
lev test . --dry-run # parse and report them without calling a providerBeyond the graph, lev validate reports the fields whose absence quietly changes what a run does.
That covers a stage with no model block, a tool name that matches nothing, and an autonomous stage
offering a tool that waits for a person. Errors exit non-zero, warnings do not, notes never can. The
CLI reference lists every check. The daemon logs the same findings
when a run spawns, so a blueprint nobody validated still says what is wrong with it.