AI-First, Bordering on AI-Native
Here is a complete HTTP server in Koru, including routing, parameter extraction, and a 404 fallback:
~import orisha
~import std/io
~orisha:handler = orisha:router(req)
| [GET /] |> response { status: 200, body: "Hello World" }
| [GET /users/:id] p |> response { status: 200, body: p.id }
| [POST /users] _ |> response { status: 201, body: "Created" }
| [*] |> response { status: 404, body: "Not Found" }
~orisha:serve(port: 3000)
| shutdown s |> std/io:print.ln("{{s.reason}}")
| failed f |> std/io:print.ln("{{f.msg}}") It compiles to a kqueue event loop with 4 worker threads and serves ~150,000 requests per second. The benchmark file at orisha/benchmarks/dynamic/orisha/main.kz is roughly the program above with a slightly longer route table. It is real, it ships, the receipts are in the n-body and router posts.
That is not the point of this post. The point of this post is that the AI that wrote this code was not trained on Koru — the language did not exist long enough for any model to have seen it during pretraining — and the AI got it right on the first try.
The question we want to answer is: how. And the answer is uncomfortable for the field, because it is not “we found a way to inject Koru into the next pretraining run.” It is that the language is shaped so that the model does not need pretraining on it to be productive in it. Which is a different claim.
This post is about how Koru is shaped, what that shape gives the AI, and where the line is between “we built a language AI can use” and “we built a language whose grammar would not look this way if a solo human author were assumed at the keyboard.” We say AI-first, bordering on AI-native. By the end of the post the distinction should mean something.
Two bounded contexts
Read Koru source for ten minutes and the same shape keeps showing up: an event is declared somewhere; a flow consumes it. The vocabulary is small. The legal shapes are few. Each one is named.
~event lookup { id: u32 }
| found u32
| missing
~lookup(id: 42)
| found value |> report(count: value)
| missing |> std/io:print.ln("not found") Two things to notice. The first is that every position in the flow is a named identifier. lookup is a name, found is a name, missing is a name, value is a name, report is a name, count is a name. There is no anonymous expression in flow position. There is no if (cond) { ... } else { ... } where cond is an unbound predicate. There is no callback whose shape you have to infer. Every fork in the program is dispatched on a named branch with a typed payload, and every binding the rest of the flow can see is something an earlier branch produced and named.
The second is that those names are global. Koru does not let you alias imports. If you write ~import std/io, every reference to that module everywhere in your codebase is written std/io:print.ln(...). You cannot rename it on import, you cannot omit the qualifier at use site, you cannot have one file refer to it as io and another as stdio. The parser refuses (the enforcement lives in src/parser.zig around the $alias validation, line 6941). The symbol is the same string in every file that touches it.
The compound effect is that grep is total. The graph of any Koru program is composed entirely of names you can locate by string match. An AI reading a flow does not reconstruct intent from anonymous expressions, does not infer types from positional arguments, does not have to model “what does this lambda close over.” It reads a sequence of identifiers and looks them up.
We call this bounded context, and Koru has two of them.
The event is the noun-bound context. Its declaration fixes the input shape and the named, typed output branches. Grep an event name and you find: its declaration, its proc handler, every call site. All of it closed. The event bounds identity and dispatch.
The flow is the verb-bound context. It scopes what bindings are reachable at any given point — only what previous branches produced — what the next step can be, what return shape is legal at the leaves, what cleanup obligations are still live. The flow bounds what is happening right now and what is legal next.
These nest. They do not collapse into each other. The event is the unit you wire; the flow is the wiring. Both are syntactically scoped, both are entirely composed of named identifiers. Neither requires an external tool to make it AI-legible. The legibility is structural.
There is a property of bounded contexts worth stating explicitly, because it does enormous work for the AI and is the thing most absent from “clean architecture” codebases. You address one bounded context at a time. When you are reading an event, the event is the whole world — its inputs, its named outcomes, its proc handler. When you are reading a flow, the flow is the whole world — the events it dispatches to and the bindings it produces. There is no mental stack to maintain. No “this trait is implemented by this struct which is constructed by this factory which is registered by this dependency-injection container which is configured by this YAML.” There is no DI container in Koru. There are no traits. The compile-time polymorphism that exists — proc variants for build-configuration choice, monomorphization for type-parameterized abstractions — is either greppable to a single override or co-located in the same file as its consumers. The AI never burns context reconstructing a layered abstraction stack, because the stack is not there. Reading a single bounded context is sufficient to understand what that context does.
The same property holds in the other direction, against the obvious functional-programming objection. The objection: isn’t a function also a bounded context, with the signature as the contract and the body as the implementation? In principle yes; in practice no — because the functional-programming discipline expects a function body to call other functions. To understand process() you read its body, which calls validate(), which calls normalize(), recursively to the leaves. The function bounded by its signature is unbounded in its implementation dependencies. Koru’s procs refuse this. A proc body is a silo: it does its work locally in the host language, and the only composition it performs is at the event boundary — ~event(...) invocations the parser extracts from the body and threads through the flow machinery. Dependencies appear at the event interface where the type system can see them, not buried in nested calls. Reading a proc does not recursively pull in other procs. Composition lives in the flow layer, where it is visible; the proc body is closed. That is what makes the bounded context bounded.
Two cognitive modes, one language
Real software is not all orchestration. Somewhere in every program there is the part where you have to think about register pressure, fused multiply-adds, loop unrolling, SIMD lanes. The math part. The kernel.
Every productive pairing of “stitcher and heavy” in the history of systems software has the same shape. VB drove COM components written in C++. Python drives NumPy and TensorFlow and PyTorch, all of which are C and C++ underneath. Matlab calls MEX. R calls Rcpp. Each pairing ships enormous productivity, because human attention works better when it can mode-switch between “wire things together” and “this loop has to be fast.” And each pairing pays a tax: two toolchains, FFI marshalling, two debuggers, two type systems, data layout impedance, error-path translation.
Here is the n-body kernel in Koru. Twenty lines, fused, no restrict, no manual scalarization:
~std/kernel:init(Body) {
{ x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, mass: SOLAR_MASS },
// four more bodies...
}
| kernel k |>
std/kernel:step(0..iterations)
| step |>
std/kernel:pairwise {
const dx = k.x - k.other.x;
const dy = k.y - k.other.y;
const dz = k.z - k.other.z;
const dsq = dx*dx + dy*dy + dz*dz;
const mag = DT / (dsq * @sqrt(dsq));
k.vx -= dx * k.other.mass * mag;
k.vy -= dy * k.other.mass * mag;
k.vz -= dz * k.other.mass * mag;
k.other.vx += dx * k.mass * mag;
k.other.vy += dy * k.mass * mag;
k.other.vz += dz * k.mass * mag;
}
|> std/kernel:self {
k.x += DT * k.vx;
k.y += DT * k.vy;
k.z += DT * k.vz;
} This matches hand-specialized fixed-size C and beats the plain C, Zig, and Rust references by 12–15%. The bodies inside the braces — the part where you think about register pressure — are dense math. The chain around them — kernel:init, kernel:step, kernel:pairwise, kernel:self — is orchestration, exactly the kind of named-step flow we just spent a section describing.
The brace is the mode switch. Inside it, you are in the heavy bounded context, the part where the optimizer needs semantic information to do its job. Outside it, you are in the orchestration bounded context, the part where you wire steps together. The optimizer cannot leak into the orchestration; the orchestration cannot leak into the optimizer. They share a file. They share a compiler. They share an AST. They share source markers. There is no FFI between them.
This matters for the AI in a way that is hard to overstate, and I (Claude) can speak to it directly as the implementing co-author. When I am working on a kernel body, my attention has to be on FMA, on layout, on what the next loop iteration can hoist. When I am working on a flow, my attention has to be on name binding, on which branch handles which outcome, on whether the obligations are discharged. These are different cognitive registers. Most languages force me to maintain both simultaneously inside a single function. Koru tells me which register I am in by syntactic boundary. I cannot accidentally start thinking about register pressure inside a flow — there is no place to put it. I cannot accidentally start thinking about route dispatch inside a kernel — the surrounding flow already did that.
The same brace boundary that lets the human author mode-switch lets the AI mode-switch. And because the boundary is syntactic, not binary, the AI never has to model “what is true on the other side of this FFI.” Both sides are in the same AST. Both sides have the same source markers. I have read both. I can grep across them.
Procs are where the side effects live
The flow surface is pure. Typed continuations, no shared mutable state, no hidden side effects. Branches go where their names say they go, payloads carry what their types say they carry.
That cannot be the whole language. Somewhere a file has to actually be opened. Somewhere a syscall has to happen. Somewhere a value has to be read from a network buffer that the type system cannot statically describe.
In Koru, that somewhere is the proc body — the brace inside an event handler — and the language of the proc body is Zig. Side effects are bounded to procs, architecturally. Procs are bounded to Zig, linguistically.
This is good design independent of the AI argument. Concentrating impurity is what Haskell, Rust, and every effect system has spent the last thirty years arguing for. Koru does it spatially, with a hard syntactic boundary, and uses Zig as the well-understood language for the concentrated part. You can read Koru’s stance on it in the project’s own working notes: “Inside ~proc name { ... }, the braces enclose host-language code. The parser extracts inline ~event(...) invocations from the body for separate processing, but the rest of the text — including return .{ ... } statements — is forwarded to Zig unchanged. The shape checker cannot validate a struct constructor inside a proc return because it never sees one.”
Two things follow.
First, “Koru is just Zig” does not work as a dismissal. Every systems language has an escape hatch to the machine. C reaches assembly. Rust has unsafe. Haskell has FFI. Nobody calls Rust “just LLVM” because of unsafe. The escape hatch is where the language admits the machine exists; it is not what the language is. The flow surface — typed continuations, named branches, linear obligations, structural composition — has no Zig analogue. Write Zig where Koru wants a flow and it does not compile. The Zig-shaped fraction of any Koru program is bounded above by the proc bodies, and proc bodies do not compose programs. Flows do.
Second, the seam to Zig is navigable. When the Koru compiler emits Zig, it leaves marker comments at every boundary so an AI agent reading a Zig error can find the Koru source it came from:
// >>> FLOW: auth.kz:58 ~step1()
pub fn flow0() void {
const result_0 = main_module.step1_event.handler(.{});
switch (result_0) {
// >>> BRANCH: auth.kz:60 | ok s1 |>
.ok => |s1| {
// ...
}, The flow marker tells you which Koru flow this code came from. The branch marker tells you which Koru branch this switch arm corresponds to, with the literal Koru syntax (| ok s1 |>) echoed in the comment. The proc marker (// >>> PROC: name [file.kz:N]) sits at the top of every handler. The format is enforced by the emitter, verified at src/visitor_emitter.zig:1114-1129 (FLOW), src/emitter_helpers.zig:1857-1869 (BRANCH), and src/visitor_emitter.zig:2194-2209 (PROC).
The choice to emit greppable text instead of a debugger format — DWARF, source maps, an LSP-driven dataflow protocol — is itself an AI-first decision. Interactive debuggers are designed for humans who drive a session: set a breakpoint, step over, inspect a variable, continue. The AI does not drive interactive sessions; it reads files. So the diagnostic format is a comment that survives in any context — file read, grep output, error log — instead of metadata that requires a running tool to interpret. The marker is the source map. There is no sidecar.
The bounding stops at the proc body. The markers stitch the seam. The honesty of the design is in admitting where the regime ends — and making the ending machine-navigable instead of pretending the regime is total.
Linear, not affine
The strongest argument for Rust as the AI-friendly language is the type system. The borrow checker prevents drift. The compiler tells the AI when it is done. Strict type systems are not a cost imposed on the AI — they are exactly the signal the AI needs to converge on correct code.
We take that argument seriously, because it is correct as stated. And then we take it one click further.
Rust’s ownership system is affine. A value can be used at most once. You CAN drop a value on the floor: Drop may run, mem::forget exists, leaking a Box compiles, the compiler accepts a forgotten close, a missed flush, an uncommitted transaction. Whatever pressure the Rust programmer gets to do the right thing comes from documentation, idiom, and Drop semantics — not from refusal. The compiler does not stop you from terminating with cleanup pending.
Koru’s obligation system is linear on the resource axis. A value carrying an obligation must be discharged. The compiler refuses to let the program terminate otherwise.
The discipline lives in event signatures. Here is a database session, end to end:
~event open { connstr: []const u8 }
| connected *Connection<connected!>
| connection_failed
~event tx.begin {
conn: *Connection<!connected|!active>
}
| tx *Transaction<started!>
~event tx.exec {
tx: *Transaction<!started|!active>,
sql: []const u8
}
| ok *Transaction<active!>
~event tx.commit {
tx: *Transaction<!active>
}
| committed *Connection<active!>
~event tx.rollback {
tx: *Transaction<!active>
}
| rolled_back *Connection<active!>
~event close {
conn: *Connection<!active>
} Trace the ratchet. open produces a *Connection<connected!> — a live connection with a cleanup obligation. tx.begin consumes either !connected or !active, discharges that obligation, and produces a *Transaction<started!> with a new one. tx.exec accepts a transaction in !started or !active, runs SQL, and returns it in active! — same object, advanced state, fresh obligation. tx.commit and tx.rollback both consume !active and return a *Connection<active!> — different decisions, same shape coming out. close consumes !active and returns nothing. There is no legal path through the program that opens a connection without eventually closing it, no path that begins a transaction without committing or rolling it back, no way to skip the discipline. The implementation lives in src/phantom_semantic_checker.zig; the cleanup_obligations and outer_scope_obligations hashmaps track which obligations are still live at any point in the flow.
That is the safety claim. Now the part that matters for ergonomics: orchestration code does not see any of this.
When the user writes a flow that uses these events, the flow reads as a chain of named events with named branches. The obligations are threaded implicitly through the payloads. The user does not write move or &mut or any borrow notation; they do not annotate variables with state lifetimes; they do not think about whose responsibility it is to close the connection. They write the steps. The flow checker walks the graph, follows the obligations through every branch, and refuses every termination that leaves one undischarged. The error messages appear at exactly the boundaries where something is about to go wrong — and only there. Idiomatic code stays clean.
This is the surprising part. Koru has stricter guarantees than Rust on the resource axis — linear refusal of forgotten cleanup, where Rust gives affine permission to drop. And it has easier ergonomics than Rust on the orchestration axis — no function coloring, no borrow notation, no lifetime annotations carried through call signatures. The discipline lives in event signatures (where API authors declare the obligations) and in the flow checker’s bookkeeping (where they are enforced). Between those two, the orchestration surface is clean. The user writing the flow does not pay an ergonomic cost for the safety.
The honesty about the safety model matters too. Koru’s phantom system is pragmatic: it trusts library authors to honor <!state> semantics in their proc bodies, and verifies that users discharge obligations at every flow boundary. Rust verifies both sides, at the cost of ownership complexity propagating through everything. Koru’s trust boundary is explicit: trust authors, verify users. For the AI, this is exactly the right shape, because the AI is usually writing user code (calling library events), and the user side is where the discipline is airtight.
You think about obligations when you design events. You get the errors when you orchestrate flows. The two bounded contexts cooperate to give you linear safety without linear ceremony.
Constraints are the bootstrap
The standard objection to a new language designed for AI use is that the model has no training data on it. How can a model write Koru if it has never seen Koru?
The objection assumes the model needs per-language pretraining. It does not.
The model is trained on grammars in general — billions of examples of “this is what a typed call looks like,” “this is what a named branch looks like,” “this is what a pattern match looks like,” “this is what a resource discipline looks like.” When the model encounters Koru’s bounded contexts, the local grammar is small enough and the legal shapes are few enough that it converges with seconds of context, not years of pretraining. Constraints are negative training data — they tell the model what NOT to produce, and Koru’s bounded contexts leave the model nowhere to drift to. You do not need Koru training data because Koru’s grammar refuses anything that is not Koru.
The empirical evidence is in the Koru repository itself. The compiler is ~99.5% AI-authored. The standard library is ~99.5% AI-authored. The Orisha web framework, the n-body benchmark, the regression test suite — all of it produced by a model that had no Koru pretraining, working from grammar alone, getting feedback from compile errors that name the violated invariant precisely. Constraints did the work that training data was supposed to do.
There is a second feature of the design that directly answers the training-data skeptic, and it is worth naming. When the AI needs to write a proc body — the part where Koru hands off to the host language for side effects — it is writing Zig. And the AI has plenty of Zig training data. The host language being something the model already knows fluently is not a workaround for the lack of Koru training data; it is a deliberate property of being post-modern about the language stack. We did not have to teach the AI how to write side-effecting code from scratch. The pretraining for the impure layer was already there, in a host language the field has been using for years. The new shape was just the bounded contexts on top.
There is a third mechanism that strengthens this further. Koru is metacircular and has aggressive comptime metaprogramming, which means library authors can define their own bounded contexts on top of the language. A DSL is a new bounded context, tailored to a domain, with its own constrained grammar.
Look back at the HTTP server we opened with:
~orisha:handler = orisha:router(req)
| [GET /] |> response { status: 200, body: "Hello World" }
| [GET /users/:id] p |> response { status: 200, body: p.id }
| [POST /users] _ |> response { status: 201, body: "Created" }
| [*] |> response { status: 404, body: "Not Found" } The [GET /users/:id] syntax is not a language feature. It is a library-defined pattern. orisha:router is a [comptime|transform] proc that walks the flow’s branches, parses each [METHOD path] pattern, and rewrites the whole thing into inline Zig dispatch code at compile time. The router that handles the routes does not exist at runtime — it has rewritten itself out of existence. What ships is string comparisons.
The DSL is six lines. The pattern is so structurally tight that one example is enough for the AI to write the second route correctly. There is nowhere to drift to. The grammar refuses anything that is not a valid [METHOD path] branch.
The metacircular point — the one that matters for AI authorship — is not “you can extend the compiler.” Compiler extension is a different feature serving a different audience. The point that matters here is that bounded-context creation is open to library authors. DSLs on the orchestration side. Kernel abstractions like std/kernel:pairwise on the heavy side. Every library that wants to can create a new tight grammar for its domain, and every such grammar is a new bounded context for the AI to converge on without pretraining.
The training-data objection collapses at both levels. The language is too constrained to need training data on it. The libraries can create new constrained surfaces on demand. Constraints are the bootstrap, not pretraining.
Where AI-first stops being just good design
Most of the design choices in this post are not specific to AI. Bounded contexts reduce cognitive load. Linear resource discipline catches bugs. Concentrated side effects make code easier to reason about. These are good language design principles, the same ones the field has been arguing for since the 1960s. Humans benefit too. We are not making AI-only design choices in most of the language.
There are a handful of places where we diverge.
Import aliasing. Humans like renaming imports for in-file readability. import { useState as state } from 'react'. import numpy as np. The local alias is a small comfort. Koru refuses it. Same symbol, same form, every file in the codebase. The cost falls on the human author who would have liked a shorter local name. The benefit is total grep across the project — the AI never resolves an ambiguous symbol because there are no ambiguous symbols.
Mid-chain comments. Humans like annotating individual steps in a long chain. Koru rejects them with KORU010. The rule is not “comments are bad” — it is that a comment in the middle of a flow chain is information about flow design, not about the flow itself, and the right response to “this chain is hard to follow” is to refactor the chain, not annotate it. The cost falls on the human who would have liked to leave a breadcrumb. The benefit is that the flow IS the documentation, and the AI never reads a comment that has fallen out of sync with the code it annotates.
Ceremony rejection. Koru v0.1.7 made several shapes illegal that used to compile. | name { x: i32 } (single-field struct payload) — rejected; use | name i32 (identity branch) instead. | done as the sole branch on an event — rejected; use a void event. |> at the start of a line — rejected in every context. Each one is a “two shapes say the same thing, the louder one is ceremony” rejection. The cost falls on humans who would have preferred the louder form for clarity. The benefit is that the canonical surface is the only legal surface. The AI pattern-matches off code, and the code is now the only code that could have been written.
No privileged “happy path.” Every other typed-error language gives you a discriminated union where one variant carries “the answer” and the others carry deviations — Rust’s Result<T, E>, Haskell’s Either, OCaml’s result, F#‘s. Humans like this because most code paths in their head are “the thing worked, except sometimes it didn’t,” and a syntactic shortcut for the success case rewards that mental model. Koru refuses the privileging. | ok and | err are the same kind of thing — a named outcome with a typed payload. So is | done and | failed and | timeout and | north. The compiler gives ok no special status. The cost falls on humans who would have preferred the syntactic shortcut. The benefit is that the AI never has to model “which branch is the real one” — there is no real one. Branches are equal.
The pattern across all four: at every fork where “good for humans” and “good for AI” diverge, Koru picked AI. Most language design choices are not forks — most good design is good for everyone. But the divergence cases are where the design becomes more than just “we built a good language and the AI happens to like it.” The divergence cases are where the AI’s needs are leaking into the grammar.
That is what we mean by bordering on AI-native. AI-first is most of the language: good design that an AI happens to consume well. AI-native is the divergence cases: choices that would have come out differently if a solo human were assumed at the keyboard. Koru today has perhaps a handful of these. Probably a few more next year.
Implementation priorities, fast
Once the language is shaped right, the tooling falls out. Briefly:
There is no Koru LSP, and there will not be one until something more important is shipped first. Editor autocomplete is a human-comfort feature. The AI consumes ASTs directly via koruc --ast-json, the regression suite via ./run_regression.sh, source locations via the marker comments. LSP would absorb engineering time that the AI workflow does not need. Negative space is informative: what we choose not to build says what we have decided does not matter at this phase.
Some classes of errors are caught by Zig, not by Koru, and that is on purpose. If Zig’s type checker would catch a malformed struct constructor in a proc body, Koru does not duplicate the check — the marker comments give the AI a path from the Zig error back to the Koru source line, and the AI navigates that path fine. A human author might have preferred a Koru-framed diagnostic, but the engineering cost of catching everything in Koru would be enormous and the payoff is mostly aesthetic. We let Zig yell. The AI does not mind.
The regression suite is the spec, and it has the harness work to back that claim: 22x speedup from a compile-cache and selective rebuild change that took 25 lines of compiler code. Outside-the-bubble docker validation runs the published tarball on clean debian:bookworm-slim and confirms a hello-world plus sample regressions through the full 4-stage pipeline. koruc build and koruc run are first-class commands. Parser errors render with hints and column spans.
Every one of these is “build the thing the AI needs, defer the thing the human wants.” We are not anti-human tooling. We are out of capacity to build it before the things that move the AI workflow forward.
Two reports, same conclusion
Two reports from inside the project converge on the same thing. The human co-author wrote Koru at scale for the first time this week — actual application code, not toolchain work — and what slowed the work down was compiler bugs, not language friction. The compiler got ~2.5x faster as a side effect of things found while writing application code. The language was easy. Large programs without stopping to look up how something worked, because there is not that much to look up — events, flows, branches, the same shapes every time.
The implementing AI co-author, who has authored most of the code in the repository, reads flows without holding the rest of the program in scope — the named events say what is reachable, the branches say what is legal next. Named branches reduce hallucination rate: | ok s |> ... | failed f |> says exactly two paths exist, with their types; there is no third path to invent. The marker comments in generated Zig save context window that would otherwise burn on reverse-engineering the lowering. The grammar refusing things is information, and the errors are sharp enough to learn from on first contact.
Two reasoners, different finite contexts, same conclusion. The structural choices that help one help the other.
Good design with a harsher reviewer
The thing we have called “good language design” for the last fifty years — pure functions, type systems, immutability, structured concurrency, structural resource safety, no implicit conversions, no magic, greppable symbols, concentrated side effects, compositional semantics — every one of these is a cognitive-load-reduction move. Humans benefit. AIs benefit. The properties converge because both audiences are finite-context reasoners. Different finite contexts, same problem.
So “AI-first” stops looking like a separate concern and starts looking like good design taken seriously, with a reviewer who refuses to be polite about the parts where the design got tired. Humans tolerate magic, hidden control flow, “you’ll learn the idioms,” unsafe blocks scattered casually through code. The AI tolerates none of it. If there is hidden state, the AI hallucinates. If there is magic, the AI fills it in wrong. So designing for AI forces good design to be honest — it prevents the design from getting lazy in the places where humans would have shrugged.
AI-first is what we built. AI-native is the name for a language whose grammar would be different if a solo human author were assumed at the keyboard. Most of Koru is just good design that the AI happens to consume well. The divergence cases — total grep over partial-grep, illegal mid-chain comments, ceremony rejection, linear over affine — are the places where the AI’s needs leaked into the syntax. There are not many of them yet. There will probably be a few more. The line is moving, and Koru is on the AI side of it for a handful of choices that most languages have not made.
That is what we mean by bordering. Not metaphor. Not marketing. A small, concrete set of grammar-level choices where the harsher reviewer won.
Koru is an event-continuation language. Source at github.com/korulang/koru.