Built for agents: fast, quiet feedback
Lumina is meant to be written by coding agents as well as by people. An agent needs the same thing a careful human does: to hear early, before a customer does, that something will not work. It also needs the message to be short, because every extra line costs context. The goal is Rust's rigor without Rust's chatter.
Early, quiet feedback
lumina verify app.lumina parses and type-checks without generating code, and answers in tens of milliseconds on a small file, so it never slows down an edit loop (measurements). lumina build produces the native binary. lumina agent-guide prints a version-matched prompt that teaches an agent the language. Testing, linting, fixing and explaining error codes are there too, with JSON output where a machine reads it. Agents working on this repository also get skills for the language implemented.
Same mistake, two compilers
A typical agent slip: adding an enum variant and forgetting one match.
enum Shape {
Circle { r: Float }
Rect { w: Float, h: Float }
Square { s: Float }
}
function area(s: Shape): Float = match s {
Shape.Circle(r) => 3.14 * r * r,
Shape.Rect(w, h) => w * h
}
$ lumina verify shapes.lumina
error: shapes.lumina:7:40 failed verification
error[E2011]: non-exhaustive match on `Shape` — missing variants: Shape.Square
note: `match` expression missing cases for some values
help: Add arms for all variants or use `_` wildcard
explain: run `lumina explain E2011` for the full explanation and fix (also `lumina help E2011`)
rustc reports the same mistake (E0004) in about three times as many lines, with source excerpts, the enum definition and a ready-to-paste todo!() arm. The table uses real output from both compilers:
| Mistake | Lumina | rustc |
|---|---|---|
| wrong argument type | E4002, names parameter and both types, hint, source excerpt at the call, also inside nested calls and string interpolation | E0308 with excerpts and the function definition, about twice as long |
missing match arm | E2011, names the missing variant | E0004, suggests the missing arm as code |
use after move into a take parameter | E5106 at the use site, suggests reading first or .clone() | E0382, also marks where the move happened and suggests .clone() as code |
T!E assigned to a plain T | E2001, hint: unwrap with ? or match | E0308, suggests .expect(...), which panics |
Result silently dropped | E2100 is an error | only a warning; the program compiles |
| call without a capability argument | E4008 from lumina verify and lumina build, at the call with a source excerpt | no equivalent; any Rust function may do I/O |
Lumina's messages are shorter and point at the safe fix. Rust's are longer but show more context. Lumina's note: and help: lines speak to the specific mistake in progress, and a short source excerpt shows the offending line implemented.
Signatures tell an agent what code can do
- Errors are in the type.
Integer!ParseErrorcannot be used as anInteger(E2001), and ignoring it is an error (E2100). See Errors & Types. - Effects are parameters. File, network and clock APIs are methods on capabilities such as
FileSystem, received as parameters.FileSystem {}cannot be conjured inside a function, and a call that leaves the capability out is rejected (E4008) bylumina verifyandlumina build. Ambient built-ins such asread_file(path)draw a warning (W106) that names the matching capability call, herefs.read(path)in progress. Reading a signature tells an agent what the function can reach. See Capabilities. - Effects cannot leak. An algebraic effect that reaches
mainwithout a handler stops compilation (E9003). Model calls made through theLlmeffect are such an effect, so code that uses it shows up in the types (see Interop & Stdlib). Direct calls such ascallLlm, the agent loop and the built-inhttp_*functions do not carry it, so the types do not yet show every path to a model in progress. - Exhaustive
match, as above, and const by default: reassigning a plain binding is E3009 with amuthint. See Mutability.
Other mistakes the compiler stops before the program runs:
- Reading a field of a
T?without unwrapping in progress. - A wrong argument type in any nested call, including inside string interpolation (E4002).
- Use after send, for every value and not only resources in progress.
Diagnostics stay actionable: info-level lints stay quiet on good code in progress, a runtime crash points at the program's own source location in progress, and a failed assert shows the expression and the values implemented.
Agents inside your program
The second story runs the other way: a Lumina program hosts an agent at run time. The program loads Lumina source that the agent wrote, compiles it through the JIT and calls it, so an agent can program its own extensions. Module.loadSource(src, caps:) loads a module, module.call runs one of its functions, and Module.eval evaluates an expression implemented.
function main(console: Console): Integer {
// Code an agent just wrote, granted no capabilities.
src = "function score(a: Integer, b: Integer): Integer { return a * 10 + b }"
match Module.loadSource(src, caps: []) {
Ok(ext) => {
n: Integer = ext.call("score", [4, 2])?
console.println(`score = {n}`)
},
Err(e) => console.println(`rejected: {e}`)
}
// Code that asks for a FileSystem it was not granted is refused at load time.
spy = "function main(fs: FileSystem): Integer { fs.read(\"/etc/hosts\") return 0 }"
match Module.loadSource(spy, caps: []) {
Ok(_) => console.println("loaded"),
Err(_) => console.println("refused: FileSystem not granted")
}
return 0
}
score = 42
refused: FileSystem not granted
The same rules that bound ordinary code bound the agent's code:
- Capabilities limit its reach. Loaded code touches only the
FileSystemorNetworkthe host grants it in progress. - Outbound actions wait for a human.
guardedActionperforms theApprovaleffect before a tool runs, and a program that leavesApprovalunhandled does not compile (E9003). No tool call goes around the handler in progress. - Network access is narrowed to named hosts.
ScopedNetwork.allow(hosts:, ports:)gives the agent aNetworkthat reaches only those hosts and ports in progress. - CPU and memory are capped.
ResourceLimitsruns a module call in a child process with CPU and memory limits in progress. - The operating system fences it in. Loaded modules run under an OS isolation level you choose, with a wall-clock limit and a hard stop for a runaway call designed.
To build your own agent, start from the harness loop in stdlib/agent/: runAgent drives the model-and-tools loop, a tool registry describes and dispatches the tools, runAgentWith talks to OpenAI or Anthropic with retries, a budget and a policy, and runAgentSeamed replays scripted replies so the loop can be tested without a network in progress. examples/ai_effects/agent_e2e_flow.lumina runs it end to end.
Next: Comparison