# shape exists only at runtime r = client.responses.create( model="…", input=prompt, response_format=schema ) data = json.loads(r.output_text) sentiment = validate(data)
The programming language where ai returns a type.
Thirty-five standard library modules, go-styled package management, and one 23 MB binary that starts in milliseconds. No JIT warmup, no GC pauses.
Or the same curl installer as Linux. Apple Silicon only.
Unpack and put ecko.exe on your PATH. All platforms →
Runtime and types
Why Ecko?
Four reasons. The first is measured: one program, one machine, four runtimes.
-
ecko
2.5ms
-
python
9.2ms
-
node
19.0ms
-
ruby
43.6ms
Median of 100 runs after 10 warmups. Ecko v0.20.0, python 3.12.3, node 24.5.0, ruby 3.2.3, on an AMD Ryzen 9 5950X under Linux.
A bytecode compiler and a stack VM, with no tier that has to get hot - so the thousandth run costs what the first one did, and CLIs, serverless handlers and dev reloads are up before your finger leaves the key.
-
No garbage-collection pauses
Nothing sweeps the rug out from under a request, and there is no pause budget to tune.
fn handle(body) { n = len(body) n } # body drops at return - no GC pause laterreference counted · freed at scope exit
-
Concurrency without the ceremony
Parallel work is something you write, and bindings are immutable unless you say
mut.reviews = ["shipped fast", "broke twice"] scores = pmap(reviews, fn(r) { ai[Int] "Rate 1 to 10: {r}" })channel() · async fn · await · pmap()
-
The call returns a type
Ask for a type and get that type back. With no API key the same call answers from a mock.
type Lead = Hot { company: String, seats: Int } | Demo lead = ai[Lead] "Acme wants 40 seats"ai is a keyword · typed return
Editor support
The language server ships with it.
A language server ships inside the binary - ecko lsp.
Diagnostics, completion, hover, and go-to-definition in any editor
that speaks LSP, with first-party extras where they exist.
-
VS Code
A first-party extension: the TextMate grammar, snippets, and a client that launches
ecko lspfor you. -
Cursor
The same extension from Open VSX, which is where Cursor and the other VS Code forks install from.
-
JetBrains
A first-party plugin for IntelliJ, PyCharm, GoLand and the rest - free Community editions included, through LSP4IJ.
-
Zed & the rest
No plugin needed. Any editor that speaks LSP points at
ecko lspand gets the same server.
// Zed - settings.json
{
"lsp": {
"ecko": { "binary": { "path": "ecko", "arguments": ["lsp"] } }
},
"languages": {
"Ecko": { "language_servers": ["ecko"] }
}
}
// JetBrains, without the plugin: LSP4IJ, command ecko, argument lsp
Keep ecko on your PATH - or give the
absolute path. Highlighting works without it; smart editing
waits for the server. Full editor setup →
Against Python and an SDK
Three lines, typed.
In Python, one AI call means a client, a schema, a JSON parser, and retry glue you get to maintain. In Ecko, the type you declared does that work. You write the three lines that matter.
type Sentiment = Positive | Neutral | Negative mood = ai[Sentiment] "I love this" # the runtime validates and retries, giving Positive{}
=> Positive{}
Typed calls, mocks, contracts
Everything is built in.
Typed calls, mock mode, and contracts are language features, not a framework you bolt on and keep in sync by hand. Nothing to fall out of step, nothing to babysit.
-
Typed output
Ask for a type, get that type.
ai[Report]turns the reply into aReportand retries when it doesn't fit. What lands in your code is a struct you can use, not a string to parse and second-guess.type Report = Report { city: String, verdict: String } report = ai[Report] "compare Cairo and Oslo"
=> Report{city: [AI Mock] city, verdict: [AI Mock] verdict}
-
Mock mode
AI code you can test.
Set no API key and
aireturns deterministic, schema-valid values. That's how every example on this page runs offline - and how you test AI code in CI at all.# no ECKO_API_KEY, no network, no spend count = ai[Int] "how many planets?"
=> 42
-
Contracts
Bad answers stop at the call.
Boolean contracts are checked on every call. The natural-language form is judged by a model, so it catches what a type can't. The long-term goal is a compiler that proves them at build time.
@requires(len(text) > 0) @ensures("result is a valid ISO-8601 date") fn deadline(text) = ai "Extract the deadline from: {text}"
Mock mode has no model to judge the string, so it passes through.
Checks and grants
Hard to misuse.
Below is a real run over three files, each carrying one real
mistake. A careful reviewer might catch these on a good day.
ecko check catches them every time, exits nonzero, and
CI refuses the merge.
$ ecko check billing.ecko triage.ecko inbox.ecko
-
billing.ecko:3
unwrapped-credential
os.env("STRIPE_KEY") reads a credential but is not wrapped - use secret(os.env("STRIPE_KEY")) so it can't leak into logs, prompts, or output
-
triage.ecko:3
non-exhaustive-match
match over Priority does not cover: P2
-
inbox.ecko:3
arity-mismatch
'classify' needs at least 2 argument(s), got 1
$ echo $? 1 nonzero, so the pipeline stops
Capabilities
A package declares what it needs; you decide whether to grant
it. Authority only narrows as you go down the tree, so a
dependency three levels deep can't reach net
unless every step above it did. Grants are path-scoped: a
logging package can write its own directory and nothing else.
{ "grant": ["fs:write:./logs"] }
Compatibility
A frozen corpus of programs with golden output runs on every build, so changing behaviour takes a deliberate breaking decision, a changelog entry, and a rebless - never a surprise. Deprecated syntax goes through deprecate, migrate, and remove, with a version gate so nothing disappears overnight.
ecko fix --migrate --check ./src
Install it in one command.
Homebrew: brew install ecko-lang/tap/ecko ·
binaries and checksums →