Katari is a language for writing orchestration logic for AI agents. You write a Katari program; the compiler (Haskell) lowers it to an intermediate representation (IR) that a persistent runtime server (TypeScript) executes and manages. Parallelism and concurrency are first-class, and the runtime persists every step of a run — so programs can run for a long time, park on a human question, and recover from a crash without losing their place.
Status: 0.1 — released, pre-1.0. The language, toolchain, and runtime are usable today and releases are versioned, but the API surface is not frozen: minor versions may still ship breaking changes, each recorded in the release notes. Pin the versions you deploy (the CLI's lockfile and the runtime image tag both exist for exactly this).
The project site is katari-lang.dev. To use the CLI without building from source, install the published binary from npm:
npm install -g @katari-lang/cliBuilding from source (for contributors, or on an unsupported platform) is under Getting started.
Agents are Katari's unit of execution — think "function". They call each other (a delegation),
fan out across threads with parallel, and escalate a request that has no handler in scope out
of the run as an open question a human answers from the console or the CLI.
// A tiny taste: agents delegate, `parallel for` fans work out across threads, and a `request`
// with no handler in scope escalates to a human — the run parks until the question is answered.
@"Ask a human to weigh in; the run waits until this question is answered."
request ask(question: string) -> string
@"Review one source by asking a human what stands out in it."
agent review(source: string) -> string with ask {
let note = ask(question = f"What stands out in ${source}?")
f"${source}: ${note}"
}
@"Review every source in parallel, then join the findings into one report."
agent main(sources: array[string]) -> string with ask {
let notes = parallel for (let source in sources) {
next review(source = source)
}
string.join(parts = notes, separator = "\n")
}
The with ask in each signature is the effect row: it tracks that these agents may perform the
ask request. Because nothing handles ask, it escalates — the runtime shows the blocked
delegation tree on the run page, and katari answer (or the console's inbox) supplies each reply.
For a full tour — data types and match, stateful inline handlers, typed errors, the FFI, and
more — see examples/playground and its
README.
- Agents, effects, and handlers. An agent is a function with a JSON schema for its input and
output. A
requestdeclares an effect; ause handlerblock implements it. Effect rows (with ask,with prelude.throw[T]) are inferred from an agent's body when omitted, and checked against it when written. - Delegation and escalation. Calling an agent is a delegation; performing a request with no
handler in scope is an escalation that surfaces as an open question, answerable from the
console or with
katari answer. - First-class parallelism.
parallel forandparallel [...]fan work out so each element runs on its own thread. - Durable execution. The runtime persists run state, so programs can run for a long time, wait on external input, and recover from failure. Uploading a program creates an immutable snapshot you can roll back to.
- Agents as AI tools. Schema derivation (
reflection.get_metadata) and dynamic dispatch (reflection.call_agent), plus a typed JSON boundary (json.validate[T]), let an AI loop pick and call agents as tools with arguments validated against their schemas. - MCP integration. Consume any MCP server's tools as agents with
use mcp.provide— a scoped provider whose tools live exactly as long as the block that opened them, enforced by the type system; publish your own agents as an MCP server withmcp.serve; generate typed.ktrbindings from a live server withkatari mcp pull; and authorize outbound servers with OAuth by answering the authorization escalation the run pauses on (from the console orkatari answer). - Typed errors.
prelude.throw[T]raises a typed, catchable error the signature tracks;panicis the runtime's separate failure channel, also catchable. - First-class files. File blobs flow through agents and the FFI in both directions, with an MCP image bridge for multimodal flows.
- Partial application.
scale(factor = 2.0, value = _)fixes some arguments now and yields a residual agent that takes only the remaining holes. finally. Arms instance finalizers (Go-defer-like) that run at a run's terminal.- Inbound webhooks.
webhook.inboundmints a public URL and turns each POST into a validated callback call. - Tooling. A CLI (
katari), an LSP server and VSCode extension, a TypeScript FFI, and an admin web console with a run trace and delegation tree.
This is a monorepo of Haskell and TypeScript packages.
| Path | What it is |
|---|---|
haskell/compiler |
The Katari compiler: source (.ktr) to IR, plus the standard library |
haskell/project |
Project configuration and dependency handling |
haskell/lsp |
Language Server Protocol implementation (editor features) |
haskell/cli |
The katari command-line interface |
typescript/runtime |
The runtime server: executes IR and persists run state |
typescript/port |
FFI library for interacting with the runtime from TypeScript |
typescript/bundle |
Bundler for FFI code (invoked by the CLI during apply) |
typescript/mcp |
The katari-mcp helper used by katari mcp pull |
typescript/cli |
npm wrapper around the Haskell katari binary |
typescript/admin-web |
Web console for managing the runtime |
typescript/vscode |
VSCode extension |
typescript/types |
Shared TypeScript types |
docs |
Design notes and reference |
examples/playground |
A runnable tour of the standard features |
The standard library lives in haskell/compiler/stdlib/prelude
(string, array, math, json, http, file, mcp, webhook, reflection, …).
- Stack (Haskell toolchain)
- pnpm (Node package manager)
- Docker (the runtime uses PostgreSQL and S3-compatible storage)
pnpm install
pnpm run build # builds Haskell (stack) and TypeScript (pnpm) packages
pnpm run typecheck # typecheck both toolchains
pnpm run test # run Haskell and TypeScript testsOther useful scripts: pnpm run format, pnpm run lint. See package.json for the full list.
pnpm run build also builds the katari CLI. Put it on your PATH with stack install (then use
katari directly, as the examples below do), or run it in place from the repo as
stack exec katari -- <command>.
The compile-only commands need no running server:
mkdir my_project && cd my_project
katari init # scaffold a new project (package name defaults to the directory name)
katari check # compile and report diagnostics
katari build # compile to IR JSONDeploying and running a program needs a live runtime. Bring one up locally, then deploy and run:
# From the repo root: start PostgreSQL + storage and the runtime + web console.
pnpm run dev
# In your project directory:
katari apply # compile the locked closure, bundle, and deploy a snapshot
katari run main.main # start the entry agent and wait for its resultRun pnpm run dev:down to stop the local services. The scaffolded project's own README explains
setting KATARI_API_KEY (how the CLI authenticates with the runtime), and
examples/playground/README.md is a fully worked end-to-end
session. Run katari --help for the full command list (apply, run, status, cancel,
answer, ls, env, file, mcp, project, …).
katari.lock records the exact closure your project compiles against, and it is the only thing
check, build and apply read — none of them touches the registry. Four commands write it:
katari add PKG # declare a dependency in katari.toml, and re-lock
katari remove PKG # undeclare it, and re-lock
katari update # re-pin [dependencies].snapshot to the registry's newest cut, and re-lock
katari update SNAPSHOT # ... or to a named cut ("staging" for the mutable candidate set)
katari lock # re-lock what katari.toml already declares, and nothing elseTo see what there is to add, katari ls packages lists the pinned snapshot's whole set alongside
which of them this project already has:
PACKAGE VERSION STATUS
ai 0.4.0 added
google_common 0.2.0 in closure
web 0.2.0
Everything else reads. If you hand-edit katari.toml — moving the snapshot pin, adding an
[overrides] entry — check, build and apply refuse until you run katari lock, naming
what disagrees. A green check earned against a closure the manifest no longer asks for would be a
wrong answer, not a warning worth printing.
Because the lock is frozen at lock time, snapshot = "staging" is safe to pin even though the
staging set itself is mutable: two applys from the same commit deploy the same closure, and you
take a newer staging only when you ask for one.
- katari-lang.dev/docs — the quickstart, the five-chapter
tutorial, concepts, and guides. The same docs are served to AI coding agents as an MCP server
at
katari-lang.dev/mcp. katari-lang/examples— complete, deployable example projects, one per use case (a release monitor, a Slack standup bot, a two-desk community concierge, a Gmail-to-calendar butler), each pinned to a published registry snapshot and compiled in CI.docs/— dated design notes covering the runtime domain model, the IR, generics inference, composability and reflection, MCP integration, and more.examples/playground— one project, several independently runnable modules, each demonstrating a core feature.
The flow above is the local one. Before you put a runtime on a network, read:
docs/deploying.md— the operations guide: how to supply the two keys without leaking them into a task definition, whyKATARI_SECRET_KEYmust be kept forever (and how to rotate it), database TLS, the single-instance requirement and what it means for rolling deploys, and what a program is allowed to reach over the network.SECURITY.md— the trust model, how to report a vulnerability, and the limitations that are known rather than undiscovered.
The short version: KATARI_API_KEY is equivalent to shell access on the runtime host, because deploying a
snapshot runs code there. Treat it that way.
MIT. See LICENSE.