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:

MistakeLuminarustc
wrong argument typeE4002, names parameter and both types, hint, source excerpt at the call, also inside nested calls and string interpolationE0308 with excerpts and the function definition, about twice as long
missing match armE2011, names the missing variantE0004, suggests the missing arm as code
use after move into a take parameterE5106 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 TE2001, hint: unwrap with ? or matchE0308, suggests .expect(...), which panics
Result silently droppedE2100 is an erroronly a warning; the program compiles
call without a capability argumentE4008 from lumina verify and lumina build, at the call with a source excerptno 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

Other mistakes the compiler stops before the program runs:

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:

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