Why Garnet exists: the FFI tax I refused to keep paying
I wrote Garnet because I got tired of paying the same tax, on every serious agent project, forever.
Here is the tax. You have an agent system. The hot path — embeddings math, mailbox draining, bounded queues, anything throughput-shaped — wants Rust. You write the hot path in Rust because that's the responsible call. The orchestration — the prompt routing, the conditional flow, the "if the model said this, do that, otherwise hand it back," the JSON wrangling at every seam — wants Ruby. Or Python. Or anything where you can think in sentences instead of lifetimes. You write the orchestrator there because that's also the responsible call.
And then you spend your weekend writing the layer in between.
It is always more code than the actual feature. It is always more brittle than either side. It is always where the bugs accrete. It is always where the new contributor gets stuck. It is always the part that someone, eventually, rewrites — and that rewrite always introduces fresh bugs because the previous rewrite had drifted from both sides.
That tax is the bug. Not the languages — they are both fine. The tax is the bug.
The universal answer is "swallow it"
If you complain about the FFI tax in any room with engineers in it, you'll get one of three answers.
"Just write the whole thing in one language." Sure. So write the orchestration in Rust (good luck reading your own prompts after a week) or the hot path in Ruby (good luck with the embedding-math throughput). The universal answer here is to pick the wrong language for half your code on purpose, in the name of unity. It is unsatisfying because it is wrong.
"That's what FFI is for." Yes — and FFI is what the tax is. The answer is the problem.
"Use a managed language with native extensions." Closer. This is Ruby-C, Python-C, Node-native. But the binding still exists; the extension authors still maintain a parallel API; the language interpreter and the native runtime still have separate memory models that the bridge has to reconcile. And every time the interpreter changes, the bridge breaks again.
I sat with this for a while. The thing that finally bothered me was: nobody has questioned that those answers are the only answers. They are presented as if they are properties of the problem. They are not. They are properties of the tools we have collectively agreed to use.
If two execution modes lived inside one grammar — same parser, same type system, same source file, same compile pass — the binding layer would have nothing to do. Because the binding would be the language.
What two modes in one grammar actually looks like
In Garnet, the hot path and the orchestrator share a file. The keyword decides the mode.
module agent {
# managed mode — Ruby velocity
def route(prompt) {
result = try { embed(prompt) } rescue e { return retry_with_fallback(e) }
if score(result) > 0.8 {
narrate(result)
} else {
reflect_and_retry(prompt, result)
}
}
# safe mode — Rust rigor
@safe
fn embed(prompt: String) -> Result<Embedding, EmbedError> {
let tokens = tokenize(prompt)?
let vec = run_model(tokens)?
Ok(Embedding { values: vec })
}
}
The def route reads like Ruby. It uses try/rescue. ARC for memory. Exceptions for errors. You can think in sentences.
The @safe fn embed reads like Rust. Ownership. Result<T,E>. The ? operator. The compiler will refuse to let you use a moved value, alias a mutable borrow, or skip an error.
The interesting line is the one between them. When route calls embed, the language auto-bridges. The Result::Err becomes a managed-mode exception at the seam, caught by rescue. The ownership of vec translates to an ARC handle when the value crosses back. Every crossing is logged in a ModeAuditLog — first-class, inspectable, not a footnote.
The binding has nothing to do. The binding is the language.
That's the hook. The product is what's underneath.
"Rust rigor, Ruby velocity" is the elevator pitch and I'm honest that it's also the marketing hook. The thing that makes Garnet worth building isn't the reconciliation; the reconciliation is the entry door. The thing that makes Garnet worth building is what's behind the door.
Capability propagation. Every Garnet function declares @caps(...) — the OS authority it is permitted to exercise. An empty @caps() is pure computation. @caps(fs) can touch the filesystem. @caps(net, fs) can do both. The CapCaps propagator (currently v3.4.1) enforces this transitively at compile time, so a function with @caps() cannot call a function with @caps(net) without explicitly widening its own declaration. Wasm Component Model is chasing the same idea at module granularity; Garnet does it at function granularity, and the ergonomics matter.
First-class agent memory. memory working / episodic / semantic / procedural are language keywords, not library bolt-ons. The type system and the allocator both know what kind of store each memory is. Bolting these on as libraries (LangChain, LlamaIndex, take your pick) is not the same as language-level support — the type system can't reason about it, the allocator can't tier it, and the audit trail has to be reconstructed every time. Garnet was designed for agent systems from the grammar up.
Ed25519-signed hot-reload. Erlang has hot-reload without signing. Mobile reload-from-server has signing without type-checking. Garnet has both at the language level: actor.reload_signed with a BLAKE3 schema fingerprint and an Ed25519 signature. When a translation rule or a routing policy changes, you don't restart the actor — you reload the new module, and the actor verifies the signature before accepting it. The v0.5 release includes a runnable demo of both the success case and the deliberate mismatch case.
Deterministic signed build manifests. garnet build --deterministic --sign produces byte-identical output across machines. v0.5's CI matrix proves this between ubuntu-latest and macos-latest on every PR. Supply-chain security is the issue of the next decade; Garnet treats it as a language-level guarantee, not a build-system afterthought.
Today's release: v0.5.0, substance over surface
Garnet v0.5.0 publishes today. The headline isn't a number going up; it's that the six blocking slices for v0.5 — LSP MVP, bytecode VM scaffold, parser fuzz harness, signed hot-reload demo, cross-machine determinism CI, and compiler-as-agent advisory mode — all merged with reproducible dogfood evidence. The companion release post, substance over surface, walks each one.
The honesty discipline holds. The MIT/productization pulse from scripts/garnet_mit_readiness_status.py is 71.3% across 19 lanes — up from 55.8%, and now measured at a finer grain that includes the gates v0.4.2 wasn't measuring. Apple Developer ID notarization, Marketplace/OpenVSX publication, Windows/Linux desktop runtime proof, the garnet add manifest spec (S3), and the actor OS-thread bridge (S7) all remain explicitly open and labeled. S4 and S6 have landed, but S6 is a measured policy-benchmark lane, not a production allocator claim. The slice contracts say so in their own statuses.
"Research-grade prototype (v0.x.x) — not production-complete." That line is on the homepage, on the README, in the conformance matrix, and on this page. It is not a hedge. It is part of the contract with anyone evaluating Garnet.
Where this goes
Two things have to be true for Garnet to matter beyond a research artifact.
First, somebody has to build something with it that nobody else could build as easily. The agent-orchestrator template is the seed shape; I'm building Sabbath Echo on top of it — a multi-language narration system where the Researcher, Stylist, Reviewer, and Narrator are all actors with declared capabilities, episodic memories of past passages, and signed hot-reload for changing translation rules. If you're building something where the agent-native + capabilities + signed-build story matters, please tell me what you're trying to do — the show-and-tell discussion thread is where I'm collecting it.
Second, the deferred items have to land. v0.5.1 closes garnet add and garnet fmt. v0.6 opens the package registry conversation in earnest (there's an RFC discussion open with three design options and the constraints I'm holding to). v0.7 is when the bytecode VM stops being a scaffold and starts being a path to production speed. None of that is hand-waved; each is its own evidence-backed slice with a public verification command.
I'm one human. I read every issue, every discussion, every PR comment. The reply latency is real and so is the engagement. If you've ever paid the FFI tax I described — or if you've never paid it because you found a way around it I haven't — the room is now open.
Roll Tide.
— Jon Isaac, Island Development Crew