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.

curl -fsSL ecko.sh/install | sh

A server in a file

import std.http

type Reply = Answer { body: String }
    | Escalate { reason: String }

# The HTTP server is in the binary. There is no framework to install.
http.serve(8080, fn(req) {
    match [req.method, req.path] {
        ["POST", "/support"] => {
            reply = ai[Reply] "Handle this support email: {req.body}"
            match reply {
                Answer(body) => http.json({ ok: true, body: body })
                Escalate(reason) => http.json({ ok: false, reason: reason })
            }
        }
        _ => http.not_found()
    }
})

                

Try other examples in the playground

Runtime and types

Why Ecko?

Four reasons. The first is measured: one program, one machine, four runtimes.

Cold start · hello world to first byte out
  • 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 later
    

    reference 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 lsp for 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 lsp and gets the same server.

Setting up IDEs that support LSPcommand ecko · argument lsp · files *.ecko
// 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.

Python + SDK parse · retry
# shape exists only at runtime
r = client.responses.create(
  model="…", input=prompt,
  response_format=schema
)
data = json.loads(r.output_text)
sentiment = validate(data)
Ecko 3 lines · typed
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.

  1. Typed output

    Ask for a type, get that type.

    ai[Report] turns the reply into a Report and 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}

  2. Mock mode

    AI code you can test.

    Set no API key and ai returns 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

  3. 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 check3 files

$ ecko check billing.ecko triage.ecko inbox.ecko

  1. 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

  2. triage.ecko:3

    non-exhaustive-match

    match over Priority does not cover: P2

  3. 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.

curl -fsSL ecko.sh/install | sh

Homebrew: brew install ecko-lang/tap/ecko · binaries and checksums →