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.
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.
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.
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.
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.
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.
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
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.
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
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
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-*
fit
A context that cannot fit is refused before the model call — prevention, not recovery. Re-estimated on every step.
↳ refuses as fit-overflow
steps
Every connection carries a caller-granted budget — steps, wall-clock, output tokens. No budget, no connection.
↳ refuses as budget-cut · cut · stopped
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 completionimport 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
01 · Primary tier
unavailable
02 · Fallback tier
answers
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.
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.
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().
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.
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.
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.
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.
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.
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
Explore the docs
start with a working app, then explore the concepts and runtime contracts