itsmedit/grenat

A programming language for AI agents

Rust

4

140 commits

updated Oct 7, 2026

See the code

See what people are saying

README

Grenat

Ruby's syntax, Rust's speed, agents as first-class citizens.

Grenat is a compiled programming language for building AI agent systems: typed prompts, tools, supervised actor agents, budgets, durable workflows, and an effect system that turns prompt injection into a compile-time error.

prompt summarize(article: String) -> ~Summary using :fast
  user "Summarize: #{article}"
end

agent Researcher
  model :smart
  tools search_web, read_url
  budget usd: 2.00, time: 10.min

  on Research(topic: String) -> ~Report
    run "Investigate #{topic}"
  end
end
  • Specification: SPEC.md
  • A compact reference for LLMs writing Grenat: llms.txt
  • Examples: basics.grn, reviews.grn (native statistics + validated LLM analysis), explorer.grn (a real agent), support_desk.grn (multi-agent, human approval), triage.grn (tests with mocks, evals with an LLM judge), macros.grn (compile-time code generation), usecases/ (twelve agent use cases: support, code review, research, data, documents, a weekly digest, operations, a chat with memory, a team of agents, MCP tools, a knowledge base searched by meaning, meeting minutes from a recording)

A tour in code

Every snippet below passes grenat check, grenat test and grenat fmt --check.

Typed prompts, and answers that must be checked

A prompt is a function a model implements. Its return type becomes a JSON schema, and the ## comments describe the fields to the model. A model's answer is untrusted (~T): it must be checked, approved by a human, or explicitly trusted before it reaches the network, a file, a command, an email or a page — otherwise grenat check fails (E0412).

struct Summary
  title: String           ## 8 words at most
  bullets: Array(String)  ## 3 to 5 key points
end

## Summarizes an article.
prompt summarize(article: String) -> ~Summary using :fast
  user "Summarize:\n#{article}"
end

def headline(article: String) -> String uses llm
  summarize(article).check { |s| s.bullets.size.between?(3, 5) }?.title
end

Tools, agents, budgets and human approval

A tool is a function a model may call; an agent is an actor whose run loop calls the model and its tools until it produces the handler's return type, within a budget. Effects (uses …) are capabilities checked by the compiler, then again at run time.

## Reads a page of the handbook.
tool read_page(name: String) -> String uses fs.read("./handbook")
  File.read("./handbook/#{name}")
end

## Opens a ticket. A human approves it first.
tool open_ticket(title: String) -> Int uses net("tracker.acme.io"), human, env
  approve! "Open “#{title}”?"
  token = Credentials.fetch(:tracker, :token)  # a Secret: never printed, never sent to a model
  Http.post(
    "https://tracker.acme.io/tickets",
    json: {title:},
    headers: {"Authorization" => "Bearer #{token}"},
  ).status
end

agent Support
  model :smart
  tools read_page, open_ticket
  budget usd: 0.50, time: 2.min
  max_turns 12
  instructions "Answer from the handbook only. Open a ticket for bugs."

  on Ask(question: String) -> ~String
    run "Customer question: #{question}"
  end
end

def answer(question: String) -> ~String uses llm, fs.read("./handbook"), net("tracker.acme.io"), human, env
  spawn(Support).ask(Ask(question:))
end

Durable workflows, schedules, HTTP and time

Each step of a workflow is journaled: after a crash, or a human answering days later, the run resumes where it stopped and no model call is billed twice.

def recent_releases(repo: String) -> Array(String) uses net("api.github.com"), env, time
  token = Credentials.fetch(:github, :token)
  res = Http.get(
    "https://api.github.com/repos/#{repo}/releases",
    headers: {"Authorization" => "Bearer #{token}"},
  )
  week_ago = Time.now - 7.days
  res.json.trust!.select { |r| Time.parse(r["published_at"]) > week_ago }.map { |r| r["tag_name"] }
end

workflow weekly_digest(monday: String) uses llm, net("api.github.com"), net("smtp.acme.io"), env, human, time
  tags = step(:fetch) { recent_releases("rust-lang/rust") }
  digest = step(:summarize) { summarize(tags.join(", ")).trust! }
  step(:review) { approve! "Send “#{digest.title}”?" }
  step(:email) do
    Mail.connect(Credentials.fetch(:smtp, :url)).send(
      from: "bot@acme.io",
      to: ["team@acme.io"],
      subject: digest.title,
      body: digest.bullets.join("\n"),
    )
  end
end

every cron: "0 8 * * MON" do  # UTC, run by `grenat serve`
  weekly_digest(Time.today)
end

Routes and streaming

## Answers a customer, three sentences at most.
prompt reply(question: String) -> ~String using :fast
  user question
end

get "/chat" do |req|
  stream do |out|  # Server-Sent Events, as the model writes
    reply(req.params["q"]) { |chunk| out << chunk }
  end
end

Email in

grenat serve reads a mailbox — Gmail, Microsoft 365, any IMAP server — and hands each new email to its handler, then marks it seen or moves it; every field of it is untrusted.

on_email Credentials.fetch(:support, :imap_url), every: 1.minute, move_to: "Done" do |email|
  answer = reply(email.text).check { |a| a.size < 2000 }?
  puts "#{email.attachments.size} attachments, answer ready: #{answer.size} characters"
end

Records, embeddings and search by meaning

struct Passage
  table :passages
  id: Int?
  text: String
  embedding: Vector(1024)
end

migration "001_create_passages" do |db|
  db.migrate("CREATE TABLE passages (id #{db.primary_key}, text TEXT NOT NULL, embedding #{db.vector(1024)} NOT NULL)")
end

def index(parts: Array(String)) uses llm, db
  vectors = embed(:docs, parts)  # one request for many texts
  parts.each_with_index { |text, i| Passage.create(text:, embedding: vectors[i]) }
end

def search(question: String) -> Array(Passage) uses llm, db.read
  Passage.nearest(:embedding, embed(:docs, question), limit: 3)  # pgvector, or brute force on SQLite
end

SSH and SFTP

def restart(server: SshSession) -> Bool uses ssh("api.acme.com")
  server.run(["systemctl", "restart", "shop"]).ok?  # an argument vector: no shell injection
end

def main uses ssh("api.acme.com"), env
  server = Ssh.connect("deploy@api.acme.com", key: Credentials.fetch(:deploy, :ssh_key))
  puts restart(server)
end

Tests that never reach a real model or service

test "only this week's releases" do
  freeze_time("2026-10-05T08:00:00Z") do
    mock_http "GET https://api.github.com/repos/rust-lang/rust/releases", json: [
      {tag_name: "1.95.0", published_at: "2026-10-01T10:00:00Z"},
      {tag_name: "1.94.0", published_at: "2026-08-20T10:00:00Z"},
    ]
    assert_equal ["1.95.0"], recent_releases("rust-lang/rust")
    assert_equal "Bearer test-github-token", Http.requests.last["headers"]["Authorization"]
  end
end

test "the agent reads the handbook, then answers" do
  File.write("./handbook/refunds.md", "Refunds: within 30 days.")
  mock :smart, replies: [
    call(:read_page, name: "refunds.md"),
    "Refunds are possible within 30 days.",
  ]
  assert_equal "Refunds are possible within 30 days.", answer("Can I get a refund?").trust!
end

test "a model answer out of bounds is refused" do
  mock :fast, replies: [{title: "Rust 2.0", bullets: ["only one"]}]
  assert_raises CheckError do
    headline("…")
  end
end

test "the chat streams its answer" do
  mock :fast, replies: ["Hello, Ada"]
  assert_equal "Hello, Ada", request(:get, "/chat?q=hi")["events"].first["data"]
end

test "the passage about refunds is found" do
  mock_embed :docs  # vectors made from the texts' words
  index(["A refund is asked for within 30 days.", "Invoices export to CSV."])
  assert_equal "A refund is asked for within 30 days.", search("How do I get a refund?").first&.text
end

Other doubles: mock_shell, mock_ssh, mock_mcp, mock_transcribe, mock_env, mock_mail(raise: "SMTP down"), mock_credentials, cassette (real calls recorded once, then replayed), with_human(approve_all | deny_all), deliver_webhook, Jobs.perform, Mail.deliveries.

Models and secrets: configured, not coded

# config/models.yml — the first model is the default one
fast:
  provider: anthropic          # anthropic, openai, gemini, mistral, xai, openrouter, groq, deepseek, together, ollama
  name: claude-haiku-4-5
smart:
  provider: openai
  name: gpt-5
docs:
  provider: voyage             # embeddings: voyage, openai, gemini, mistral, ollama
  name: voyage-3.5
  kind: embedding
  dimensions: 1024
grenat credentials edit                    # config/credentials.yml.enc, AES-256-GCM, key in config/master.key
grenat credentials edit --env production   # one per environment, chosen by GRENAT_ENV

Keys are read from the credentials (openai: {api_key: …}), else from the provider's variable (OPENAI_API_KEY…). Anthropic agents use prompt caching by default (cache: true extends it to prompts and conversations).

Command line

grenat new <name>                      a package: grenat.toml, src/, tests/
grenat new --app <name>                an application: database, models, routes, src/app.grn, tests/
grenat generate agent|workflow|record|tool|eval <name> [field:Type…]
                                       a part of the application, with its tests (alias: grenat g)
grenat check [<file.grn>…]             names, types, effects, taint and secrets
grenat run [--log] [--unchecked] [--no-jit] [<file.grn>] [args…]
                                       check, then run `main`
grenat test [<file.grn>…]              the `test` blocks, offline (mocks and cassettes)
grenat eval <file.grn> [name]          the `eval` blocks, against the real models, scored
grenat serve [--listen host:port]      routes, webhooks, schedules, mailboxes, exposed tools and agents, job workers
grenat console [--listen host:port] [--token <token>]
                                       the operations console: approvals, jobs, journals, costs, evals
grenat migrate                         apply the migrations the database has not seen
grenat credentials edit|show [--env <environment>]
                                       the application's encrypted secrets
grenat build [--native] [--release] [<file.grn>] [-o <executable>]
                                       an executable (--native: without the interpreter;
                                       --release: optimized by LLVM)
grenat fmt [--check] <file.grn | dir>… the canonical layout
grenat lsp                             the language server
grenat update                          the latest commits of git dependencies
grenat parse | tokens <file.grn>       the syntax tree, the tokens
grenat --version

setter init                            add a Facetfile to the current package
setter new <name>                      create a facet (a library to share)
setter add <name> ["~> 1.2"]           use a facet from the indexes (or --path <dir>, --git <url> [--tag <tag>])
setter install | update | list         install (and build trusted native facets), update, list
setter publish                         tag this facet's version for the indexes
VariableEffect
ANTHROPIC_API_KEY, OPENAI_API_KEY, …a provider's key, when the credentials have none
GRENAT_LOG=1log every model, tool, HTTP and SSH call, and what the JIT compiled (same as --log)
GRENAT_RECORD=1record every cassette again, with real calls
GRENAT_ENVthe environment (development by default): which credentials
GRENAT_MASTER_KEYthe credentials' key, rather than config/master.key
GRENAT_CONSOLE_TOKENthe token of grenat console (rather than --token)
GRENAT_JIT=0interpret everything (same as --no-jit)
GRENAT_HOMEwhere grenat build finds lib/grenat/libgrenat_{host,standalone}.a
CCthe linker of grenat build (default: cc)

Writing Grenat with an LLM

llms.txt is a reference of about 4,000 tokens written for models: the syntax, the effects, taint, agents, workflows, the standard library, the test doubles and the mistakes to avoid. Every code block in it passes grenat check and grenat test.

It was measured: agents were given two real tasks — a support agent (tools, a ticket opened after human approval, a structured answer) and a weekly release digest (GitHub, a validated summary, email, resumption after a crash without calling the model again) — once in Python with the official Anthropic SDK, once in Grenat with llms.txt as its only documentation, twice each. Every program passes its tests.

Average of 2 runsPythonGrenat
Tokens, support agent79,70043,000 (−46%)
Tokens, weekly digest38,30045,000 (+18%)
Tokens, both tasks118,00088,000 (−25%)
Lines of program, support / digest237 / 20764 / 79

The agent loop, structured answers, approvals and journaled steps are part of the language, so the code a model writes is 2.5 to 3.5 times shorter; where a task is mostly plumbing (HTTP, dates, email), Python's familiarity still pays.

Status

The latest release, v0.1.2, has phases 0 to 11; phases 12 to 15 are on main.

Phase 15 — email in: on_email reads a mailbox (Gmail, Microsoft 365, any IMAP server, over TLS; app passwords or OAuth tokens) under grenat serve, each new email — its PDFs and images ready for a prompt — handled once then marked seen or moved, a failing one retried then flagged, every field untrusted, a crafted email flagged before it is parsed; deliver_email hands a handler messages in tests.

Phase 14 — the gaps LLMs found: a benchmark of models writing Grenat from llms.txt showed what they reach for. Time (Time.parse for ISO 8601, Time.iso, Time.date, Time.weekday, Time.at, Time.now - 7.days, freeze_time in tests), Ruby's everyday methods (s[0, 4], s[1..], flatten, each_slice, reduce(:+), transform_values, format, then…), test doubles (Http.requests to assert what was sent, mock_env, mock_mail(raise:)), and email held to the net effect by the checker.

Phase 13 — what agents need in production: embeddings and search by meaning (embed, Vector(n) fields, nearest, pgvector or brute force), prompt caching (Anthropic breakpoints placed by Grenat, cached tokens in the ledger and the console), streaming (blocks receiving the answer as it is written, Server-Sent Events from routes), and audio (Audio.read, transcribe, audio in prompts).

Phase 12 — libraries in other languages: native facets (Rust code called as ordinary functions, behind a versioned ABI), bridge facets (Ruby or Python functions in a sandboxed process over JSON-RPC — no interpreter embedded), and two official facets: sheets (Excel, OpenDocument, CSV) and html (CSS selectors, links, tables).

Phase 11 — reaching servers: Ssh.connect, commands as argument vectors, upload, download, SFTP (list, read, write, rename…), host keys always verified; proxies for Http (SOCKS5, SOCKS4, HTTP) and for SSH.

Phase 10 — configured, not coded: secrets encrypted per environment as with Rails (grenat credentials edit), as Secret values the language keeps away from models and logs; models of ten providers in config/models.yml, each reached by the right connector with its key found in the credentials — the provider's name is enough.

Phase 9 — agents operated from a browser: grenat console, open source like the rest — approvals waiting for a human, jobs and their workflow journals (retry), what the models cost by agent, workflow and day, eval scores over time, failures and refusals, MCP servers. The runtime records what it shows in the application's database.

Phase 8 — applications of agents, in the language and its toolchain, with no framework on top: routes, records and migrations (SQLite and PostgreSQL), jobs, approvals that wait days for a human in the database, tools and agents served to other programs over MCP and HTTP (expose), and generators — grenat new --app, then grenat generate agent|workflow|record|tool|eval, each part with its tests.

Phase 7 — agents in production, measured by ten real use cases (examples/usecases): an HTTP client, databases, email, a sandboxed Shell, MCP servers, PDFs and images, conversations with long-term memory, the Batch API, schedules and webhooks (grenat serve), and facets — libraries installed by setter from a Facetfile.

Phase 6 — ecosystem: programs of several files and packages (grenat.toml, path and git dependencies, grenat.lock), a language server (grenat lsp), compile-time macros, and release builds optimized by LLVM (grenat build --release).

Phase 5 — production-ready: workflows are durable — each step is journaled, and an interrupted run resumes where it stopped, without paying twice for a model call. Tests never reach a real model: mock gives the model's replies as plain values, cassette records real calls once and replays them. eval measures quality on a dataset, with judge (an LLM as a judge), and fails under a threshold.

Phase 4 — native code: functions over numbers, strings, arrays and structs are compiled to machine code by a Cranelift JIT when the program loads — fib(35) runs in 0.05 s, about 1.7× Rust with the same overflow semantics, 200× faster than the interpreter. Objects are reference counted, Perceus style: no garbage collector, no leak, in-place updates of uniquely owned values. grenat build compiles a program ahead of time into a standalone executable. Tasks are M:N green threads: 100,000 concurrent tasks fit in ~1 GB on a few OS threads. Agents are actors (one message at a time, deadlocks detected, supervision with restarts), and parallel_map and race run truly in parallel. Before running anything, grenat checks names, types, effects and taint: an unvalidated model answer that reaches the network is a compile-time error.

Install

macOS or Linux, with Homebrew:

brew install itsmedit/grenat/grenat

Any Linux with glibc (Ubuntu 20.04+, Debian 11+, Fedora, RHEL 9, Amazon Linux 2023…) or macOS, without a package manager — the install script puts grenat and setter in ~/.grenat (in /usr/local as root), checks the archive's SHA-256, and adds them to your PATH:

curl -sSL https://github.com/itsmedit/grenat/releases/latest/download/install.sh | sh
# options: sh -s -- --version v0.1.1 | --prefix DIR | --no-modify-path | --uninstall

A fresh EC2 instance (Amazon Linux 2023), for instance:

sudo dnf install -y gcc                 # the C linker `grenat build` uses (run, test and serve need none)
curl -sSL https://github.com/itsmedit/grenat/releases/latest/download/install.sh | sh
exec $SHELL -l                          # a new shell, with grenat on the PATH
grenat new --app hello && cd hello && grenat test

apt or dnf, with the packages of a release:

curl -LO https://github.com/itsmedit/grenat/releases/download/v0.1.2/grenat_0.1.2_amd64.deb
sudo apt install ./grenat_0.1.2_amd64.deb                  # Ubuntu, Debian (arm64: _arm64.deb)
sudo dnf install https://github.com/itsmedit/grenat/releases/download/v0.1.2/grenat-0.1.2-1.x86_64.rpm   # Fedora, RHEL, Amazon Linux (aarch64: .aarch64.rpm)

Docker — the official image, for amd64 and arm64, with a C linker for grenat build:

docker run --rm -v "$PWD":/app ghcr.io/itsmedit/grenat test
docker run --rm -v "$PWD":/app -p 3000:3000 ghcr.io/itsmedit/grenat serve --listen 0.0.0.0:3000
# your application's image
FROM ghcr.io/itsmedit/grenat:0.1.2
COPY . /app
CMD ["serve", "--listen", "0.0.0.0:3000"]

From the sources (Rust, and a C linker: Xcode's command line tools on macOS):

cargo install --locked --path crates/grenat_cli && cargo install --locked --path crates/grenat_setter
cargo build --release -p grenat_host -p grenat_standalone     # the libraries `grenat build` links
mkdir -p ~/.cargo/lib/grenat && cp target/release/libgrenat_{host,standalone}.a ~/.cargo/lib/grenat/

Alpine (musl) is not supported by the binaries: use a glibc distribution, or the Docker image.

Try it

cargo build
target/debug/grenat run examples/basics.grn            # the core language, no LLM
target/debug/grenat run --log examples/fib.grn        # native code: see what the JIT compiled
target/debug/grenat run --log examples/objects.grn    # strings, arrays, structs, natively
target/debug/grenat build examples/objects.grn && ./objects   # a standalone executable (needs `cc`)
target/debug/grenat build --native examples/objects.grn        # without the interpreter: ~0.5 MB
target/debug/grenat build --native --release examples/fib.grn  # optimized by LLVM (needs clang)

export ANTHROPIC_API_KEY=sk-ant-…            # or, in an application: grenat credentials edit
target/debug/grenat run --log examples/explorer.grn crates/grenat_parser        # a real agent
target/debug/grenat run examples/support_desk.grn examples/tickets.jsonl        # multi-agent + approval

target/debug/grenat new --app desk && cd desk # an application: database, models, routes, tests
grenat generate agent triage                 # a part and its tests (also workflow, record, tool, eval)
grenat generate record doc text:String "embedding:Vector(1536)"   # a record searched by meaning
grenat migrate && grenat test && grenat serve
grenat console                               # its operations console: http://127.0.0.1:4000

target/debug/grenat new hello && cd hello    # a package: grenat.toml, Facetfile, src/, tests/
setter add http_tools                        # a facet (library) from an index, like a gem
grenat run && grenat test                    # in a package, no file to name

target/debug/grenat check examples/*.grn     # names, types, effects, taint
target/debug/grenat fmt examples             # canonical layout (--check: only report)
target/debug/grenat test examples/triage.grn # `test` blocks: mocks and cassettes, never a real model
target/debug/grenat eval examples/triage.grn # `eval` blocks: the real model, scored on a dataset
cargo test                                   # ~950 tests: unit, integration, CLI, HTTP, MCP, SSH, JIT, build
scripts/test-linux.sh                        # the same suite on Linux, in Docker

Official facets

Two facets ship with Grenat, in facets/: native code the application trusts explicitly, built by setter install.

# Facetfile
facet "sheets", path: "../grenat/facets/sheets", native: true
facet "html", path: "../grenat/facets/html", native: true
require "sheets"
require "html"

def main uses fs.read, fs.write, net("acme.io")
  orders = Sheets.records("orders.xlsx", sheet: "2026").trust!          # .xlsx, .xls, .ods, CSV
  big = orders.select { |o| o["total"].to_f > 1000.0 }
  Sheets.write_csv("big_orders.csv", Sheets.table(big, ["id", "customer", "total"]))

  page = Http.get("https://acme.io/pricing").body
  prices = table_records(page, "table.prices")                           # untrusted, as the page
  puts prices.size
end

See facets/sheets and facets/html.

Native facets

A facet can ship Rust code, as a gem ships C: a crate depending on grenat_ext, whose exported functions Grenat calls as ordinary ones. The application trusts it explicitly — it runs outside Grenat's sandbox — and setter install builds it and writes its declarations:

/// Reads a sheet: a line per row, cells separated by commas.
#[grenat_ext::export(effects = "fs.read")]
pub fn read_sheet(path: String) -> Result<Vec<Vec<String>>, String> { … }
# Facetfile
facet "sheets", "~> 0.1", native: true

# generated: native def read_sheet(path: String) -> ~Array(Array(String)) uses fs.read
def main uses fs.read
  puts read_sheet("sales.csv").trust!.size
end

Types and effects are checked like any call's; the result is untrusted unless the function is pure, and no secret is ever handed to native code (see SPEC.md, phase 12).

Bridge facets

A facet can also ship Ruby or Python functions — no interpreter is embedded in Grenat: the facet's server is a separate process, speaking JSON-RPC 2.0 on its standard input and output, with a helper library Grenat ships (standard library only):

# bridge/server.rb, started by `[bridge] command = ["ruby", "bridge/server.rb"]` in grenat.toml
require "grenat/bridge"

Grenat::Bridge.export(:slug, params: {title: :string}, returns: :string, pure: true) do |title:|
  title.downcase.gsub(/[^a-z0-9]+/, "-")
end

Grenat::Bridge.run
# Facetfile
facet "texts", "~> 0.1", bridge: true

# generated: native def slug(title: String) -> String pure
def main
  puts slug("Hello, World")
end

In Python, @export on an annotated function, then run() (from grenat_bridge import export, run). The server runs sandboxed in the facet's directory — a clean environment, no network unless a function declares net; a relative path it is given resolves there, so pass it absolute ones — one per facet, kept alive, each call within a timeout; types, effects, taint and secrets are checked as for native code (see SPEC.md, phase 12).

Editors

grenat lsp is a language server (diagnostics as you type, formatting, hover, go to definition, symbols). In Neovim:

vim.filetype.add({ extension = { grn = "grenat" } })
vim.api.nvim_create_autocmd("FileType", { pattern = "grenat", callback = function()
  vim.lsp.start({ name = "grenat", cmd = { "grenat", "lsp" } })
end })

Layout

CrateRole
grenat_lexertokens, interpolation, heredocs, ## doc comments
grenat_astsyntax tree
grenat_parserrecursive descent + Pratt, diagnostics with error recovery
grenat_llmmodel providers: the catalog, Anthropic's Messages API and Chat Completions (OpenAI, Gemini, Mistral, Ollama…), streaming, prompt caching, embeddings (those and Voyage), transcriptions (OpenAI's, uploaded as multipart/form-data) and audio in prompts (OpenAI, Gemini); mocks, fake embeddings and transcripts, cassettes and a scripted provider for tests
grenat_typeschecker: names, types, effects, ~T taint, secrets (E0100–E0500)
grenat_codegenCranelift: typing, liveness (Perceus), translation, boundary; JIT and object files; LLVM IR for release builds
grenat_runtimereference-counted strings, arrays and records called by native code
grenat_driverload, check and run a program (shared by the CLI and built executables)
grenat_hoststatic library linked into the executables of grenat build
grenat_standalonestatic library linked into grenat build --native executables
grenat_reportdiagnostic rendering, in the file each error points into
grenat_dbdatabases: SQLite (embedded) and PostgreSQL behind one interface; vectors (pgvector, or bytes searched by brute force)
grenat_mcpthe Model Context Protocol: a client (stdio and HTTP), and the server side of expose
grenat_sshSSH and SFTP: host keys verified, commands quoted, SOCKS5 proxies, a blocking API; a real server in process for tests
grenat_imapIMAP over TLS (implicit or STARTTLS): password or OAuth (XOAUTH2) logins, unseen messages by UID, flags and moves, MIME parsed into what on_email gives; a server in process for tests
grenat_servetriggers: cron schedules, calendar arithmetic, webhook signatures, the HTTP server of grenat serve, streamed responses
grenat_generategrenat new --app and grenat generate: an application's parts, with their tests
grenat_opsthe operations store: jobs, approvals, model calls, events, eval runs, workflow journals
grenat_consolegrenat console: pages and actions of the operations console, and who may use it
grenat_configthe application's configuration: encrypted credentials per environment, config/*.yml
grenat_settersetter: creates, adds, installs and publishes facets (libraries)
grenat_packagegrenat.toml, require, facets (Facetfile, versions, indexes, trusted native code and bridges), path and git dependencies
grenat_extthe SDK of native facets: Rust functions exported to Grenat behind a versioned JSON ABI, and their manifest
grenat_ext_macros#[grenat_ext::export] and #[derive(GrenatType)]
grenat_nativenative facets, Grenat's side: building a facet's library, loading it (ABI checked), declaring and calling its functions
grenat_bridgebridge facets, Grenat's side: a facet's Ruby or Python functions served by a process over JSON-RPC 2.0, described, declared and called
grenat_sandboxsandboxed processes (Shell.run, bridges): an argument vector, a clean environment, no network unless allowed
grenat_fmtthe formatter
grenat_lspthe language server
grenat_macrosmacro expansion: templates of declarations
grenat_greenM:N green threads: scheduler, green locks, channels, timers
grenat_interpinterpreter: values, evaluation, prompts, agents, budgets, taint, capabilities, workflows, test doubles, evals
grenat_clithe grenat binary

External dependencies: ureq (HTTP + rustls), serde_json, toml, yaml-rust2, rusqlite (SQLite, compiled in), postgres, russh (SSH), imap and mail-parser (email in, over rustls with ring), lettre (email out), and Cranelift for native code.

License

Your choice of MIT or Apache 2.0.

itsmedit/grenat

A programming language for AI agents

Rust

4

140 commits

updated Oct 7, 2026

See the code

See what people are saying

README

Grenat

Ruby's syntax, Rust's speed, agents as first-class citizens.

Grenat is a compiled programming language for building AI agent systems: typed prompts, tools, supervised actor agents, budgets, durable workflows, and an effect system that turns prompt injection into a compile-time error.

prompt summarize(article: String) -> ~Summary using :fast
  user "Summarize: #{article}"
end

agent Researcher
  model :smart
  tools search_web, read_url
  budget usd: 2.00, time: 10.min

  on Research(topic: String) -> ~Report
    run "Investigate #{topic}"
  end
end
  • Specification: SPEC.md
  • A compact reference for LLMs writing Grenat: llms.txt
  • Examples: basics.grn, reviews.grn (native statistics + validated LLM analysis), explorer.grn (a real agent), support_desk.grn (multi-agent, human approval), triage.grn (tests with mocks, evals with an LLM judge), macros.grn (compile-time code generation), usecases/ (twelve agent use cases: support, code review, research, data, documents, a weekly digest, operations, a chat with memory, a team of agents, MCP tools, a knowledge base searched by meaning, meeting minutes from a recording)

A tour in code

Every snippet below passes grenat check, grenat test and grenat fmt --check.

Typed prompts, and answers that must be checked

A prompt is a function a model implements. Its return type becomes a JSON schema, and the ## comments describe the fields to the model. A model's answer is untrusted (~T): it must be checked, approved by a human, or explicitly trusted before it reaches the network, a file, a command, an email or a page — otherwise grenat check fails (E0412).

struct Summary
  title: String           ## 8 words at most
  bullets: Array(String)  ## 3 to 5 key points
end

## Summarizes an article.
prompt summarize(article: String) -> ~Summary using :fast
  user "Summarize:\n#{article}"
end

def headline(article: String) -> String uses llm
  summarize(article).check { |s| s.bullets.size.between?(3, 5) }?.title
end

Tools, agents, budgets and human approval

A tool is a function a model may call; an agent is an actor whose run loop calls the model and its tools until it produces the handler's return type, within a budget. Effects (uses …) are capabilities checked by the compiler, then again at run time.

## Reads a page of the handbook.
tool read_page(name: String) -> String uses fs.read("./handbook")
  File.read("./handbook/#{name}")
end

## Opens a ticket. A human approves it first.
tool open_ticket(title: String) -> Int uses net("tracker.acme.io"), human, env
  approve! "Open “#{title}”?"
  token = Credentials.fetch(:tracker, :token)  # a Secret: never printed, never sent to a model
  Http.post(
    "https://tracker.acme.io/tickets",
    json: {title:},
    headers: {"Authorization" => "Bearer #{token}"},
  ).status
end

agent Support
  model :smart
  tools read_page, open_ticket
  budget usd: 0.50, time: 2.min
  max_turns 12
  instructions "Answer from the handbook only. Open a ticket for bugs."

  on Ask(question: String) -> ~String
    run "Customer question: #{question}"
  end
end

def answer(question: String) -> ~String uses llm, fs.read("./handbook"), net("tracker.acme.io"), human, env
  spawn(Support).ask(Ask(question:))
end

Durable workflows, schedules, HTTP and time

Each step of a workflow is journaled: after a crash, or a human answering days later, the run resumes where it stopped and no model call is billed twice.

def recent_releases(repo: String) -> Array(String) uses net("api.github.com"), env, time
  token = Credentials.fetch(:github, :token)
  res = Http.get(
    "https://api.github.com/repos/#{repo}/releases",
    headers: {"Authorization" => "Bearer #{token}"},
  )
  week_ago = Time.now - 7.days
  res.json.trust!.select { |r| Time.parse(r["published_at"]) > week_ago }.map { |r| r["tag_name"] }
end

workflow weekly_digest(monday: String) uses llm, net("api.github.com"), net("smtp.acme.io"), env, human, time
  tags = step(:fetch) { recent_releases("rust-lang/rust") }
  digest = step(:summarize) { summarize(tags.join(", ")).trust! }
  step(:review) { approve! "Send “#{digest.title}”?" }
  step(:email) do
    Mail.connect(Credentials.fetch(:smtp, :url)).send(
      from: "bot@acme.io",
      to: ["team@acme.io"],
      subject: digest.title,
      body: digest.bullets.join("\n"),
    )
  end
end

every cron: "0 8 * * MON" do  # UTC, run by `grenat serve`
  weekly_digest(Time.today)
end

Routes and streaming

## Answers a customer, three sentences at most.
prompt reply(question: String) -> ~String using :fast
  user question
end

get "/chat" do |req|
  stream do |out|  # Server-Sent Events, as the model writes
    reply(req.params["q"]) { |chunk| out << chunk }
  end
end

Email in

grenat serve reads a mailbox — Gmail, Microsoft 365, any IMAP server — and hands each new email to its handler, then marks it seen or moves it; every field of it is untrusted.

on_email Credentials.fetch(:support, :imap_url), every: 1.minute, move_to: "Done" do |email|
  answer = reply(email.text).check { |a| a.size < 2000 }?
  puts "#{email.attachments.size} attachments, answer ready: #{answer.size} characters"
end

Records, embeddings and search by meaning

struct Passage
  table :passages
  id: Int?
  text: String
  embedding: Vector(1024)
end

migration "001_create_passages" do |db|
  db.migrate("CREATE TABLE passages (id #{db.primary_key}, text TEXT NOT NULL, embedding #{db.vector(1024)} NOT NULL)")
end

def index(parts: Array(String)) uses llm, db
  vectors = embed(:docs, parts)  # one request for many texts
  parts.each_with_index { |text, i| Passage.create(text:, embedding: vectors[i]) }
end

def search(question: String) -> Array(Passage) uses llm, db.read
  Passage.nearest(:embedding, embed(:docs, question), limit: 3)  # pgvector, or brute force on SQLite
end

SSH and SFTP

def restart(server: SshSession) -> Bool uses ssh("api.acme.com")
  server.run(["systemctl", "restart", "shop"]).ok?  # an argument vector: no shell injection
end

def main uses ssh("api.acme.com"), env
  server = Ssh.connect("deploy@api.acme.com", key: Credentials.fetch(:deploy, :ssh_key))
  puts restart(server)
end

Tests that never reach a real model or service

test "only this week's releases" do
  freeze_time("2026-10-05T08:00:00Z") do
    mock_http "GET https://api.github.com/repos/rust-lang/rust/releases", json: [
      {tag_name: "1.95.0", published_at: "2026-10-01T10:00:00Z"},
      {tag_name: "1.94.0", published_at: "2026-08-20T10:00:00Z"},
    ]
    assert_equal ["1.95.0"], recent_releases("rust-lang/rust")
    assert_equal "Bearer test-github-token", Http.requests.last["headers"]["Authorization"]
  end
end

test "the agent reads the handbook, then answers" do
  File.write("./handbook/refunds.md", "Refunds: within 30 days.")
  mock :smart, replies: [
    call(:read_page, name: "refunds.md"),
    "Refunds are possible within 30 days.",
  ]
  assert_equal "Refunds are possible within 30 days.", answer("Can I get a refund?").trust!
end

test "a model answer out of bounds is refused" do
  mock :fast, replies: [{title: "Rust 2.0", bullets: ["only one"]}]
  assert_raises CheckError do
    headline("…")
  end
end

test "the chat streams its answer" do
  mock :fast, replies: ["Hello, Ada"]
  assert_equal "Hello, Ada", request(:get, "/chat?q=hi")["events"].first["data"]
end

test "the passage about refunds is found" do
  mock_embed :docs  # vectors made from the texts' words
  index(["A refund is asked for within 30 days.", "Invoices export to CSV."])
  assert_equal "A refund is asked for within 30 days.", search("How do I get a refund?").first&.text
end

Other doubles: mock_shell, mock_ssh, mock_mcp, mock_transcribe, mock_env, mock_mail(raise: "SMTP down"), mock_credentials, cassette (real calls recorded once, then replayed), with_human(approve_all | deny_all), deliver_webhook, Jobs.perform, Mail.deliveries.

Models and secrets: configured, not coded

# config/models.yml — the first model is the default one
fast:
  provider: anthropic          # anthropic, openai, gemini, mistral, xai, openrouter, groq, deepseek, together, ollama
  name: claude-haiku-4-5
smart:
  provider: openai
  name: gpt-5
docs:
  provider: voyage             # embeddings: voyage, openai, gemini, mistral, ollama
  name: voyage-3.5
  kind: embedding
  dimensions: 1024
grenat credentials edit                    # config/credentials.yml.enc, AES-256-GCM, key in config/master.key
grenat credentials edit --env production   # one per environment, chosen by GRENAT_ENV

Keys are read from the credentials (openai: {api_key: …}), else from the provider's variable (OPENAI_API_KEY…). Anthropic agents use prompt caching by default (cache: true extends it to prompts and conversations).

Command line

grenat new <name>                      a package: grenat.toml, src/, tests/
grenat new --app <name>                an application: database, models, routes, src/app.grn, tests/
grenat generate agent|workflow|record|tool|eval <name> [field:Type…]
                                       a part of the application, with its tests (alias: grenat g)
grenat check [<file.grn>…]             names, types, effects, taint and secrets
grenat run [--log] [--unchecked] [--no-jit] [<file.grn>] [args…]
                                       check, then run `main`
grenat test [<file.grn>…]              the `test` blocks, offline (mocks and cassettes)
grenat eval <file.grn> [name]          the `eval` blocks, against the real models, scored
grenat serve [--listen host:port]      routes, webhooks, schedules, mailboxes, exposed tools and agents, job workers
grenat console [--listen host:port] [--token <token>]
                                       the operations console: approvals, jobs, journals, costs, evals
grenat migrate                         apply the migrations the database has not seen
grenat credentials edit|show [--env <environment>]
                                       the application's encrypted secrets
grenat build [--native] [--release] [<file.grn>] [-o <executable>]
                                       an executable (--native: without the interpreter;
                                       --release: optimized by LLVM)
grenat fmt [--check] <file.grn | dir>… the canonical layout
grenat lsp                             the language server
grenat update                          the latest commits of git dependencies
grenat parse | tokens <file.grn>       the syntax tree, the tokens
grenat --version

setter init                            add a Facetfile to the current package
setter new <name>                      create a facet (a library to share)
setter add <name> ["~> 1.2"]           use a facet from the indexes (or --path <dir>, --git <url> [--tag <tag>])
setter install | update | list         install (and build trusted native facets), update, list
setter publish                         tag this facet's version for the indexes
VariableEffect
ANTHROPIC_API_KEY, OPENAI_API_KEY, …a provider's key, when the credentials have none
GRENAT_LOG=1log every model, tool, HTTP and SSH call, and what the JIT compiled (same as --log)
GRENAT_RECORD=1record every cassette again, with real calls
GRENAT_ENVthe environment (development by default): which credentials
GRENAT_MASTER_KEYthe credentials' key, rather than config/master.key
GRENAT_CONSOLE_TOKENthe token of grenat console (rather than --token)
GRENAT_JIT=0interpret everything (same as --no-jit)
GRENAT_HOMEwhere grenat build finds lib/grenat/libgrenat_{host,standalone}.a
CCthe linker of grenat build (default: cc)

Writing Grenat with an LLM

llms.txt is a reference of about 4,000 tokens written for models: the syntax, the effects, taint, agents, workflows, the standard library, the test doubles and the mistakes to avoid. Every code block in it passes grenat check and grenat test.

It was measured: agents were given two real tasks — a support agent (tools, a ticket opened after human approval, a structured answer) and a weekly release digest (GitHub, a validated summary, email, resumption after a crash without calling the model again) — once in Python with the official Anthropic SDK, once in Grenat with llms.txt as its only documentation, twice each. Every program passes its tests.

Average of 2 runsPythonGrenat
Tokens, support agent79,70043,000 (−46%)
Tokens, weekly digest38,30045,000 (+18%)
Tokens, both tasks118,00088,000 (−25%)
Lines of program, support / digest237 / 20764 / 79

The agent loop, structured answers, approvals and journaled steps are part of the language, so the code a model writes is 2.5 to 3.5 times shorter; where a task is mostly plumbing (HTTP, dates, email), Python's familiarity still pays.

Status

The latest release, v0.1.2, has phases 0 to 11; phases 12 to 15 are on main.

Phase 15 — email in: on_email reads a mailbox (Gmail, Microsoft 365, any IMAP server, over TLS; app passwords or OAuth tokens) under grenat serve, each new email — its PDFs and images ready for a prompt — handled once then marked seen or moved, a failing one retried then flagged, every field untrusted, a crafted email flagged before it is parsed; deliver_email hands a handler messages in tests.

Phase 14 — the gaps LLMs found: a benchmark of models writing Grenat from llms.txt showed what they reach for. Time (Time.parse for ISO 8601, Time.iso, Time.date, Time.weekday, Time.at, Time.now - 7.days, freeze_time in tests), Ruby's everyday methods (s[0, 4], s[1..], flatten, each_slice, reduce(:+), transform_values, format, then…), test doubles (Http.requests to assert what was sent, mock_env, mock_mail(raise:)), and email held to the net effect by the checker.

Phase 13 — what agents need in production: embeddings and search by meaning (embed, Vector(n) fields, nearest, pgvector or brute force), prompt caching (Anthropic breakpoints placed by Grenat, cached tokens in the ledger and the console), streaming (blocks receiving the answer as it is written, Server-Sent Events from routes), and audio (Audio.read, transcribe, audio in prompts).

Phase 12 — libraries in other languages: native facets (Rust code called as ordinary functions, behind a versioned ABI), bridge facets (Ruby or Python functions in a sandboxed process over JSON-RPC — no interpreter embedded), and two official facets: sheets (Excel, OpenDocument, CSV) and html (CSS selectors, links, tables).

Phase 11 — reaching servers: Ssh.connect, commands as argument vectors, upload, download, SFTP (list, read, write, rename…), host keys always verified; proxies for Http (SOCKS5, SOCKS4, HTTP) and for SSH.

Phase 10 — configured, not coded: secrets encrypted per environment as with Rails (grenat credentials edit), as Secret values the language keeps away from models and logs; models of ten providers in config/models.yml, each reached by the right connector with its key found in the credentials — the provider's name is enough.

Phase 9 — agents operated from a browser: grenat console, open source like the rest — approvals waiting for a human, jobs and their workflow journals (retry), what the models cost by agent, workflow and day, eval scores over time, failures and refusals, MCP servers. The runtime records what it shows in the application's database.

Phase 8 — applications of agents, in the language and its toolchain, with no framework on top: routes, records and migrations (SQLite and PostgreSQL), jobs, approvals that wait days for a human in the database, tools and agents served to other programs over MCP and HTTP (expose), and generators — grenat new --app, then grenat generate agent|workflow|record|tool|eval, each part with its tests.

Phase 7 — agents in production, measured by ten real use cases (examples/usecases): an HTTP client, databases, email, a sandboxed Shell, MCP servers, PDFs and images, conversations with long-term memory, the Batch API, schedules and webhooks (grenat serve), and facets — libraries installed by setter from a Facetfile.

Phase 6 — ecosystem: programs of several files and packages (grenat.toml, path and git dependencies, grenat.lock), a language server (grenat lsp), compile-time macros, and release builds optimized by LLVM (grenat build --release).

Phase 5 — production-ready: workflows are durable — each step is journaled, and an interrupted run resumes where it stopped, without paying twice for a model call. Tests never reach a real model: mock gives the model's replies as plain values, cassette records real calls once and replays them. eval measures quality on a dataset, with judge (an LLM as a judge), and fails under a threshold.

Phase 4 — native code: functions over numbers, strings, arrays and structs are compiled to machine code by a Cranelift JIT when the program loads — fib(35) runs in 0.05 s, about 1.7× Rust with the same overflow semantics, 200× faster than the interpreter. Objects are reference counted, Perceus style: no garbage collector, no leak, in-place updates of uniquely owned values. grenat build compiles a program ahead of time into a standalone executable. Tasks are M:N green threads: 100,000 concurrent tasks fit in ~1 GB on a few OS threads. Agents are actors (one message at a time, deadlocks detected, supervision with restarts), and parallel_map and race run truly in parallel. Before running anything, grenat checks names, types, effects and taint: an unvalidated model answer that reaches the network is a compile-time error.

Install

macOS or Linux, with Homebrew:

brew install itsmedit/grenat/grenat

Any Linux with glibc (Ubuntu 20.04+, Debian 11+, Fedora, RHEL 9, Amazon Linux 2023…) or macOS, without a package manager — the install script puts grenat and setter in ~/.grenat (in /usr/local as root), checks the archive's SHA-256, and adds them to your PATH:

curl -sSL https://github.com/itsmedit/grenat/releases/latest/download/install.sh | sh
# options: sh -s -- --version v0.1.1 | --prefix DIR | --no-modify-path | --uninstall

A fresh EC2 instance (Amazon Linux 2023), for instance:

sudo dnf install -y gcc                 # the C linker `grenat build` uses (run, test and serve need none)
curl -sSL https://github.com/itsmedit/grenat/releases/latest/download/install.sh | sh
exec $SHELL -l                          # a new shell, with grenat on the PATH
grenat new --app hello && cd hello && grenat test

apt or dnf, with the packages of a release:

curl -LO https://github.com/itsmedit/grenat/releases/download/v0.1.2/grenat_0.1.2_amd64.deb
sudo apt install ./grenat_0.1.2_amd64.deb                  # Ubuntu, Debian (arm64: _arm64.deb)
sudo dnf install https://github.com/itsmedit/grenat/releases/download/v0.1.2/grenat-0.1.2-1.x86_64.rpm   # Fedora, RHEL, Amazon Linux (aarch64: .aarch64.rpm)

Docker — the official image, for amd64 and arm64, with a C linker for grenat build:

docker run --rm -v "$PWD":/app ghcr.io/itsmedit/grenat test
docker run --rm -v "$PWD":/app -p 3000:3000 ghcr.io/itsmedit/grenat serve --listen 0.0.0.0:3000
# your application's image
FROM ghcr.io/itsmedit/grenat:0.1.2
COPY . /app
CMD ["serve", "--listen", "0.0.0.0:3000"]

From the sources (Rust, and a C linker: Xcode's command line tools on macOS):

cargo install --locked --path crates/grenat_cli && cargo install --locked --path crates/grenat_setter
cargo build --release -p grenat_host -p grenat_standalone     # the libraries `grenat build` links
mkdir -p ~/.cargo/lib/grenat && cp target/release/libgrenat_{host,standalone}.a ~/.cargo/lib/grenat/

Alpine (musl) is not supported by the binaries: use a glibc distribution, or the Docker image.

Try it

cargo build
target/debug/grenat run examples/basics.grn            # the core language, no LLM
target/debug/grenat run --log examples/fib.grn        # native code: see what the JIT compiled
target/debug/grenat run --log examples/objects.grn    # strings, arrays, structs, natively
target/debug/grenat build examples/objects.grn && ./objects   # a standalone executable (needs `cc`)
target/debug/grenat build --native examples/objects.grn        # without the interpreter: ~0.5 MB
target/debug/grenat build --native --release examples/fib.grn  # optimized by LLVM (needs clang)

export ANTHROPIC_API_KEY=sk-ant-…            # or, in an application: grenat credentials edit
target/debug/grenat run --log examples/explorer.grn crates/grenat_parser        # a real agent
target/debug/grenat run examples/support_desk.grn examples/tickets.jsonl        # multi-agent + approval

target/debug/grenat new --app desk && cd desk # an application: database, models, routes, tests
grenat generate agent triage                 # a part and its tests (also workflow, record, tool, eval)
grenat generate record doc text:String "embedding:Vector(1536)"   # a record searched by meaning
grenat migrate && grenat test && grenat serve
grenat console                               # its operations console: http://127.0.0.1:4000

target/debug/grenat new hello && cd hello    # a package: grenat.toml, Facetfile, src/, tests/
setter add http_tools                        # a facet (library) from an index, like a gem
grenat run && grenat test                    # in a package, no file to name

target/debug/grenat check examples/*.grn     # names, types, effects, taint
target/debug/grenat fmt examples             # canonical layout (--check: only report)
target/debug/grenat test examples/triage.grn # `test` blocks: mocks and cassettes, never a real model
target/debug/grenat eval examples/triage.grn # `eval` blocks: the real model, scored on a dataset
cargo test                                   # ~950 tests: unit, integration, CLI, HTTP, MCP, SSH, JIT, build
scripts/test-linux.sh                        # the same suite on Linux, in Docker

Official facets

Two facets ship with Grenat, in facets/: native code the application trusts explicitly, built by setter install.

# Facetfile
facet "sheets", path: "../grenat/facets/sheets", native: true
facet "html", path: "../grenat/facets/html", native: true
require "sheets"
require "html"

def main uses fs.read, fs.write, net("acme.io")
  orders = Sheets.records("orders.xlsx", sheet: "2026").trust!          # .xlsx, .xls, .ods, CSV
  big = orders.select { |o| o["total"].to_f > 1000.0 }
  Sheets.write_csv("big_orders.csv", Sheets.table(big, ["id", "customer", "total"]))

  page = Http.get("https://acme.io/pricing").body
  prices = table_records(page, "table.prices")                           # untrusted, as the page
  puts prices.size
end

See facets/sheets and facets/html.

Native facets

A facet can ship Rust code, as a gem ships C: a crate depending on grenat_ext, whose exported functions Grenat calls as ordinary ones. The application trusts it explicitly — it runs outside Grenat's sandbox — and setter install builds it and writes its declarations:

/// Reads a sheet: a line per row, cells separated by commas.
#[grenat_ext::export(effects = "fs.read")]
pub fn read_sheet(path: String) -> Result<Vec<Vec<String>>, String> { … }
# Facetfile
facet "sheets", "~> 0.1", native: true

# generated: native def read_sheet(path: String) -> ~Array(Array(String)) uses fs.read
def main uses fs.read
  puts read_sheet("sales.csv").trust!.size
end

Types and effects are checked like any call's; the result is untrusted unless the function is pure, and no secret is ever handed to native code (see SPEC.md, phase 12).

Bridge facets

A facet can also ship Ruby or Python functions — no interpreter is embedded in Grenat: the facet's server is a separate process, speaking JSON-RPC 2.0 on its standard input and output, with a helper library Grenat ships (standard library only):

# bridge/server.rb, started by `[bridge] command = ["ruby", "bridge/server.rb"]` in grenat.toml
require "grenat/bridge"

Grenat::Bridge.export(:slug, params: {title: :string}, returns: :string, pure: true) do |title:|
  title.downcase.gsub(/[^a-z0-9]+/, "-")
end

Grenat::Bridge.run
# Facetfile
facet "texts", "~> 0.1", bridge: true

# generated: native def slug(title: String) -> String pure
def main
  puts slug("Hello, World")
end

In Python, @export on an annotated function, then run() (from grenat_bridge import export, run). The server runs sandboxed in the facet's directory — a clean environment, no network unless a function declares net; a relative path it is given resolves there, so pass it absolute ones — one per facet, kept alive, each call within a timeout; types, effects, taint and secrets are checked as for native code (see SPEC.md, phase 12).

Editors

grenat lsp is a language server (diagnostics as you type, formatting, hover, go to definition, symbols). In Neovim:

vim.filetype.add({ extension = { grn = "grenat" } })
vim.api.nvim_create_autocmd("FileType", { pattern = "grenat", callback = function()
  vim.lsp.start({ name = "grenat", cmd = { "grenat", "lsp" } })
end })

Layout

CrateRole
grenat_lexertokens, interpolation, heredocs, ## doc comments
grenat_astsyntax tree
grenat_parserrecursive descent + Pratt, diagnostics with error recovery
grenat_llmmodel providers: the catalog, Anthropic's Messages API and Chat Completions (OpenAI, Gemini, Mistral, Ollama…), streaming, prompt caching, embeddings (those and Voyage), transcriptions (OpenAI's, uploaded as multipart/form-data) and audio in prompts (OpenAI, Gemini); mocks, fake embeddings and transcripts, cassettes and a scripted provider for tests
grenat_typeschecker: names, types, effects, ~T taint, secrets (E0100–E0500)
grenat_codegenCranelift: typing, liveness (Perceus), translation, boundary; JIT and object files; LLVM IR for release builds
grenat_runtimereference-counted strings, arrays and records called by native code
grenat_driverload, check and run a program (shared by the CLI and built executables)
grenat_hoststatic library linked into the executables of grenat build
grenat_standalonestatic library linked into grenat build --native executables
grenat_reportdiagnostic rendering, in the file each error points into
grenat_dbdatabases: SQLite (embedded) and PostgreSQL behind one interface; vectors (pgvector, or bytes searched by brute force)
grenat_mcpthe Model Context Protocol: a client (stdio and HTTP), and the server side of expose
grenat_sshSSH and SFTP: host keys verified, commands quoted, SOCKS5 proxies, a blocking API; a real server in process for tests
grenat_imapIMAP over TLS (implicit or STARTTLS): password or OAuth (XOAUTH2) logins, unseen messages by UID, flags and moves, MIME parsed into what on_email gives; a server in process for tests
grenat_servetriggers: cron schedules, calendar arithmetic, webhook signatures, the HTTP server of grenat serve, streamed responses
grenat_generategrenat new --app and grenat generate: an application's parts, with their tests
grenat_opsthe operations store: jobs, approvals, model calls, events, eval runs, workflow journals
grenat_consolegrenat console: pages and actions of the operations console, and who may use it
grenat_configthe application's configuration: encrypted credentials per environment, config/*.yml
grenat_settersetter: creates, adds, installs and publishes facets (libraries)
grenat_packagegrenat.toml, require, facets (Facetfile, versions, indexes, trusted native code and bridges), path and git dependencies
grenat_extthe SDK of native facets: Rust functions exported to Grenat behind a versioned JSON ABI, and their manifest
grenat_ext_macros#[grenat_ext::export] and #[derive(GrenatType)]
grenat_nativenative facets, Grenat's side: building a facet's library, loading it (ABI checked), declaring and calling its functions
grenat_bridgebridge facets, Grenat's side: a facet's Ruby or Python functions served by a process over JSON-RPC 2.0, described, declared and called
grenat_sandboxsandboxed processes (Shell.run, bridges): an argument vector, a clean environment, no network unless allowed
grenat_fmtthe formatter
grenat_lspthe language server
grenat_macrosmacro expansion: templates of declarations
grenat_greenM:N green threads: scheduler, green locks, channels, timers
grenat_interpinterpreter: values, evaluation, prompts, agents, budgets, taint, capabilities, workflows, test doubles, evals
grenat_clithe grenat binary

External dependencies: ureq (HTTP + rustls), serde_json, toml, yaml-rust2, rusqlite (SQLite, compiled in), postgres, russh (SSH), imap and mail-parser (email in, over rustls with ring), lettre (email out), and Cranelift for native code.

License

Your choice of MIT or Apache 2.0.