Runlet
Runlet is a small orchestration language for LLM agents. It lets an agent replace a sequence of individual tool calls with one program that the host can check, execute concurrently, observe, and return as a single structured result.
issues = linear.search_issues({ assignee: user, state: "open" })
pulls = github.search_pull_requests({ author: user, state: "open" })
pulls_with_checks = for pull in pulls limit 8 {
checks = github.checks(pull.number)
return {
number: pull.number,
title: pull.title,
checks
}
}
return {
issues,
pull_requests: pulls_with_checks
}
There is no async or await. References create data dependencies: the
Linear and GitHub searches can start together, each checks lookup starts when
its pull request is available, and limit 8 bounds the fan-out. Only work that
contributes to the returned value runs.
Why a language for tool composition?
A conventional agent loop sends every tool result back through the model. For a multi-step task, that means repeated inference round-trips, a growing transcript full of intermediate data, and asking the model to manually carry state between calls.
Runlet moves that mechanical work into the agent runtime:
model → one Runlet program → many host tool calls → one structured result
The model still decides what should happen. Runlet gives that decision a small, purpose-built execution format instead of making the model supervise every iteration, dependency, retry, and join.
This is especially useful for:
- list-then-detail and other N+1 API access patterns;
- joining, projecting, and enriching structured results;
- independent calls that should run concurrently;
- bounded fan-out across many records;
- read/transform/write workflows;
- retryable operations with an explicit fallback; and
- joining data from built-in tools, MCP servers, and application APIs.
The motivation is borne out by AgentKit's
compose case study.
Its sandboxed Lua composition tool reduced model round-trips, context growth,
and cost for tool-heavy tasks. On the study's initial six-scenario run,
composition reduced cost by 38–77% while maintaining or improving accuracy.
The broader model sweep also found an important boundary: composition was
consistently valuable for N+1 fan-out, but could be counterproductive for
exploratory investigations. Runlet targets the same runtime-enrichment
mechanism with schemas, implicit dataflow, structured concurrency, and an
inspectable execution graph built into the language.
Design goals
| Goal | How Runlet approaches it |
|---|---|
| Make correct concurrency the default | Tool outputs behave like ordinary values; references create graph edges and independent nodes can run together. |
| Keep programs easy for models to produce | The language has bindings, expressions, objects, lists, for, conditionals, boundaries, and return—but no imports, classes, functions, threads, or visible type syntax. |
| Catch mistakes before tools run | The host registers every tool with input and output schemas. The analyzer checks names, calls, projections, operators, branches, and returned values. |
| Avoid accidental effects, keep intended ones | Pure work is lazy and root-reachable: an unused pure computation is pruned with a warning, never dispatched. Statements containing effectful calls are implicit roots — a fire-and-forget write always runs when its block runs. |
| Bound dynamic work | for ... limit N caps active iterations while preserving result order. |
| Make failure handling explicit | boundary retry N { ... } catch err { ... } owns a subgraph, retries retryable failures, and produces a normal fallback value. |
| Let hosts retain control | The embedder supplies the complete tool registry, implementations, schemas, execution policies, and external inputs. |
| Make execution observable | The runtime emits ordered graph events for nodes, dependencies, attempts, failures, recoveries, and results. |
Language tour
Runlet programs are immutable expressions ending in one return:
customer = crm.get_customer(customer_id)
orders = commerce.list_orders({ customer_id: customer.id })
enriched = for order in orders limit 12 {
result = boundary retry 2 {
return risk.score({ customer, order })
} catch err {
return {
status: "unavailable",
code: err.code,
attempt: err.attempt
}
}
return {
id: order.id,
total: order.total,
risk: result
}
}
return {
customer: customer.name,
orders: enriched
}
The main rules are deliberately compact:
- Assignments create immutable bindings.
- A program and every block end with
return. - Objects and lists are ordinary structured values;
{ [expr]: value }computes a property key, andleft + rightmerges two objects shallowly (the right side wins). value if condition else alternativeevaluates only the selected branch;elseis optional and defaults tonull. For larger branches,if cond { ... } else { ... }is the block-bodied expression form — each branch ends withreturn.for item in items limit N { ... }returns an ordered list;skip if conditiondrops an element.fold acc = init for item in items { ... }reduces sequentially; the body'sreturnbecomes the next accumulator.boundary retry N { ... } catch err { ... }turns a failed subgraph into a fallback value;fail(code, message)raises one.- Tool namespaces such as
crm.get_customercome entirely from the host. - A small pure intrinsic library (
text.*,regex.*,list.*,json.*,number.*,time.*) covers data shaping; seeSTDLIB.md.
See examples/ for executable programs and
DESIGN.md for the complete language and runtime semantics.
Embedding Runlet
The Rust crate provides the parser, analyzer, schemas, canonical values, runtime, and execution graph. A host describes its tool surface, connects each descriptor to an implementation, compiles agent-produced source, and executes the resulting program:
use ;
Tool handlers also receive stable operation, dispatch, schema-version, and
attempt context. run_observed exposes the live graph event stream for logs,
traces, dashboards, or an agent UI. .with_prelude() installs the
deterministic intrinsics from STDLIB.md (host registrations of
the same names win), and .retry_backoff(base, factor, cap) configures
exponential backoff between boundary retry attempts.
Enriching an AgentKit runtime
AgentKit provides the surrounding agent loop, model adapters, tools, permissions, MCP integration, reporting, compaction, and task management. Runlet is intended to sit at the composition boundary of that runtime:
- Adapt the AgentKit tool catalog into Runlet
ToolDescriptors. - Expose a
runletcomposition tool to the model alongside the granular tools. - Compile the submitted program against the tools visible for that turn.
- Dispatch Runlet call nodes through AgentKit's existing executor so permissions, approvals, cancellation, and reporting remain authoritative.
- Return only the program's final structured value to the model transcript.
That integration is not part of this repository yet; the current crate provides the executable language model and an in-process threaded executor. The separation is intentional: Runlet plans and explains the work, while the agent host owns capabilities and policy.
Compact results with TOON
Tool composition prevents intermediate results from filling the transcript.
The final result can be made smaller too. The
serde_toon2 crate provides
Serde-compatible Token-Oriented Object Notation, so a host can encode the
structured Runlet result before adding it to the next model turn:
let json = execution.value.presentation_json?;
let value: Value = from_str?;
let model_context = to_string?;
Together, the three projects cover distinct layers of the enrichment story:
- AgentKit runs the model loop and governs tools.
- Runlet composes tool work into a checked execution graph.
serde_toon2encodes the selected result for efficient model context.
Run the project
Run the language examples with the built-in CLI:
Watch a larger concurrent pipeline as a live execution graph:
Run the test suite:
The current implementation includes parsing, schema analysis, canonical
values, lazy root-reachable execution, dynamic branches, bounded concurrent
loops, retry boundaries, and live graph events. Durable journals, recovery,
cancellation, and production executor interfaces remain roadmap work described
in DESIGN.md.
Editor support
editors/vscodeprovides a dependency-free VS Code extension and reusable TextMate grammar.editors/zedprovides a Zed extension backed by Tree-sitter.editors/vimprovides Vim and Neovim syntax support.editors/tree-sitter-runletcontains the Tree-sitter grammar source; generated parser files live only on thetree-sitterbranch.
License
Runlet is available under the MIT or Apache-2.0 license.