keel
v0.3.0-next · production AI applications

TypeScript framework for AI applications

Build AI applications around reliable turns.

Keel changes how you think about building AI applications. Turns become your unit of work, with a clear separation between State, Model, and Context — what your app remembers, what generates its responses, and what the model sees.

Your first Keel appNo API key needed
$ npx @keel-dev/cli@next new my-assistant
$ cd my-assistant
$ npm run dev
Open localhost:4180 to try your assistant in Keel Studio. Send a message, then inspect its turn, context, and output.

The starter uses a scripted model. Connect a provider when you’re ready.

A foundation for your AI application

Scope, execution, and outcomes in one consistent framework.

Building an AI application means coordinating more than a model call. Each request needs the right context, access to tools and state, limits on execution, and a clear result. Keel organizes that work into a turn, giving you one place to define its scope and understand how it ended.

Within a turn, the responsibilities stay distinct: State defines what your app remembers, Model defines how it generates responses, and Context defines what the model can see. Tools perform actions, while a typed wire carries output to your client. You can evolve each part while keeping the same structure for running and inspecting the work.

Three concepts. One application.

Define what your app remembers, what the model sees, and how it responds.

01 · unit: the block

State

Everything that survives a request.

Data lives in blocks: one schema, one writer per turn, named readers, pure reducers, repair on read. The model never writes state — code does, through declared events. The store is async, so a durable adapter is the same interface MemoryStore ships.

02 · unit: the chain

Model

The untrusted text function.

A model is never an instance — it is a chain of provider tiers behaving as one, clamped to the smallest window among them. Tiers stream; text lands on the wire as it arrives. The failure policy is code, not vibes: transient errors retry, provider refusals fall back, exhaustion is a typed defect.

03 · unit: the section

Context

The model’s only window.

The framework assembles opening context once per turn: time, ambient sections, instructions, tools, state views, and history. Tool results grow the transcript; fit is checked before each model step.

How the parts connect · one turn

State → reader views

Saved preferences and notes become text the agent is allowed to read.

Context → opening input

Reader views join instructions, tools, history, and the current request.

Model → next action

The chain produces text or tool calls. Tool results inform the next step.

↳ Tools can update state through declared events. The next turn reads fresh views; this turn receives tool results in its transcript.

From a request to a typed response

Turns bound the work. The wire carries it to your client.

What is a turn?

One caller. One agent. One typed outcome.

A turn is the connection between a caller and a single agent — budgeted, gated, and closed by an outcome. Follow one delegation from a supervisor to see exactly what that means.

A supervisor agent delegates to a researcher agent, opening one turnA node graph. Your app is a small node on the left, connected to a large supervisor node by the root turn T1. The supervisor delegates once, to a researcher node, and that single edge is traced as turn T2: it opens from one caller to one agent, carries a granted budget, and closes with a typed outcome returned to T1. Inside the researcher node, the model loops with a search tool — steps within T2, not new turns. Three further specialist nodes, the analyst, writer, and critic, stay dim until the end, where they light up to show that every edge in the network is another turn of the same shape.T1appyour appT3AnalystT4WriterT5CriticT1Supervisorthe callerT2Researcherthe agentone caller · one agent · one budget · one typed outcome

1. Two agents, nothing running

A supervisor that routes work, and a researcher that can do it. On their own they are only definitions — no work is in flight, and no turn exists yet.

supervisor · researcher

unit: the connection

Turn

You never call a model — you open a turn: a budgeted, gated connection from a caller to an agent that returns a typed outcome for runtime endings. Delegation is a turn inside a turn, with its own explicit budget. The same shape at every scale is what lets the system grow without changing.

unit: the part

Wire

Everything that leaves a turn is a registered, versioned, schema-validated part, streamed as the model streams — clients never parse model prose. Progress is never substance, and the outcome frame cannot be forged. One contract, every consumer.

The harness follows from the structure

Explicit parts and a shared unit of work give the runtime clear contracts to enforce.

Structure → enforcement → evidence

Declare the app.
The runtime supplies the harness.

Separating State, Model, and Context makes the application’s contracts explicit. Running that application as a turn gives Keel a place to enforce those contracts. The harness follows from this structure: the runtime assembles input, checks actions, returns feedback, and determines the outcome.

01 · You declare

Which state an agent may read and update

Named readers · declared writes · reducers

02 · Keel enforces

Enforce the declared access

The runtime renders granted reader views and checks state byproducts against the agent’s declared writes.

Harness · around every turn

ModelTools

Checks before actions · feedback after

03 · You inspect

State renders and updates

Inspect what the model read and which updates were applied or refused.

Runtime events + records → Keel Studio

You define the contracts and limits. Keel provides the runtime that checks them. Answer quality still needs your application’s tests and evaluations.

Explore the harness ↗

Follow a request · illustrative successful turn

“Find the quarterly report”

Give the request a scope

Validate the envelope and ambient scope, then claim the thread. A second root turn cannot own it at the same time.

admit → pass

State
Thread claimed
Context
Not assembled yet
Model
Not called
Wire
No content yet
Inside a turn ↗
gate 01

admit

One root turn per thread, claimed through the store. The envelope and the ambient scope are validated before any spend.

↳ refuses as admit-refused · ambient-invalid · envelope-*

gate 02

fit

A context that cannot fit is refused before the model call — prevention, not recovery. Re-estimated on every step.

↳ refuses as fit-overflow

gate 03

steps

Every connection carries a caller-granted budget — steps, wall-clock, output tokens. No budget, no connection.

↳ refuses as budget-cut · cut · stopped

gate 04

close

Completion requires qualifying content, closed markers, and any declared deliverable contract. It does not prove factual accuracy.

↳ refuses as empty · marker-open · deliverable-missing

Handle every way a turn ends

The outcome’s kind tells you which fields are available. Use an exhaustive switch to handle all five variants, just as in the turn guide. Completion proves the runtime’s content and delivery checks passed; it does not establish answer quality. Refund and retry flags describe policy — your application handles payments and decides whether to retry.

→ outcomes and completion
src/describe-outcome.tsthe full outcome union
import type { TurnOutcome } from "@keel-dev/core";

function describeOutcome(outcome: TurnOutcome): string {
  switch (outcome.kind) {
    case "completed":
      return outcome.payload.text || "Completed with structured content";
    case "failed":
      return `Failed: ${outcome.cause.code}`;
    case "truncated":
      return `Incomplete output at ${outcome.at}`;
    case "cut":
      return `Interrupted: ${outcome.by}`;
    case "stopped":
      return "Stopped by the user";
    default: {
      const unreachable: never = outcome;
      return unreachable;
    }
  }
}

Built for the paths beyond success

Fallback, budgets, and visibility are part of the runtime.

One chain · three possible endings

See what fallback preserves

  1. 01 · Primary tier

    unavailable

  2. 02 · Fallback tier

    answers

  3. 03 · Last tier

    not called

↳ Continue the turn

Before content is emitted, an unavailable tier advances immediately. The next tier receives the same context and tool results.

Retry and fallback ↗
typed outcomes

TurnOutcome distinguishes completed, truncated, cut, stopped, and failed results. An exhaustive switch checks every variant; handle configuration errors and unexpected exceptions at your application boundary.

streaming tiers

Anthropic, OpenAI, and Gemini adapters stream; text reaches the wire as it arrives, and a marker split across chunks still promotes whole. A custom adapter adds stream() next to generate().

fallback that holds

The chain replays the turn's transcript on every step, so a mid-request fallback keeps your tools and every result already earned. Capability never degrades silently — tool-blind tiers are skipped, loudly.

budgets, enforced

Steps, wall-clock time, and output tokens are explicit, caller-granted, and nested. Step and output-token exhaustion produce budget-cut; time ceilings cut the turn with a timeout reason.

a real watchdog

Tools run under an abort signal with a progress channel: each progress emission lands on the wire and resets the silence window; a tool silent past its ceiling is cut instead of hanging the turn.

scoped ambient

The core carries userId and threadId; everything your product knows about a turn lives in a scope you declare with a schema — validated at the admit gate, rendered into the window by your own sections.

deterministic by default

A test double is a ModelAdapter you write — a few lines behind the same public interface as a live provider. The whole app runs offline, keyless, through the real gates.

observable to the event

Every gate decision, model step, tool call, and fallback lands on an event bus. Scrub back through any session in Keel Studio and export its event log as a JSON bug report.

Meet Keel Studio

Run your app locally and inspect the evidence behind each turn.

Watch a turn cross the gates

A generic harness that renders your definitions: the blueprint before any run, then the live turn tree with four gate lights per edge, the streamed output, chain fallbacks, and the assembled window — on one timeline. Export the event log as a JSON bug report.

$ keel dev → http://localhost:4180

turn T2 · researcher
gate admit · pass · budget {steps: 6}
render notes.researcher → “1 note(s) on file.”
tool workspace-search · progress · resets watchdog
say <sources> · promoted mid-stream
gate close · refuse · deliverable-missing
nudge T2.d1 · submit_sources · accepted
outcome completed · proof {chars: 214}

Explore the docs

start with a working app, then explore the concepts and runtime contracts