keel

Contracts and checks

Know which layer guarantees what — the compiler, the definition calls, the boot, the runtime, and your own tests each catch a different failure.

Here is an incident shape that hides behind a green build. A wrapper synthesizes a friendly "something went wrong" message when every provider fails — as a normal, successful stream. Downstream code then tells failure from answer by string-matching English sentences. Every layer type-checked. The bug was never in any one function; it was in believing the compiler guaranteed something it never claimed to.

Keel spreads its guarantees across five layers on purpose, and knowing which layer catches what tells you where to look when something refuses:

  • TypeScript checks API shapes while you code,
  • definition calls (defineAgent, asTool, …) check declared relationships when they execute,
  • boot (createRuntime) checks the budget manifest before traffic,
  • the runtime validates every border a live turn crosses,
  • your tests assert your application's behavior — the only layer that checks meaning.

What TypeScript checks

The compiler's flagship job in keel is the outcome. A turn settles as one value of a closed union, and the consequence rides along as a field of the variant:

TurnOutcome — a closed union
completedpayload · proof · deliverable
truncatedat · continuation {rounds, max}
cutby · retryable: true
stoppedby: 'user'
failedcause: Defect · refunded: true
The consequence is a field of the variant — it cannot be forgotten. An unhandled case is a compile error.
ContractWhat it helps you do
TurnOutcome unionnarrow on kind before reading payload or cause — an unhandled case is a compile error
DeliveredTurnOutcome<T>read deliverable.data as T on completed — the close gate minted it, no null check, no cast
BlockReader / BlockWriter unioncheck isWriter before calling apply
zod-inferred block statewrite reducers and readers against the state shape
TurnBudgetsupply the required steps field

The string-matching from the opening story has no keel equivalent: an exhausted chain settles as failed with cause: chain-exhausted and refunded: true — a variant your switch must handle, not a sentence to grep for.

Know the limits too: apply(event, payload) accepts a string and an unknown payload, and a tool's execute receives unknown. Those APIs do not infer event- or tool-specific arguments for you.

What definition calls check

These are executable checks — they run when your code calls the function, which is why the composition root constructs everything at boot. A TypeScript pass alone never executes an unused agent factory.

APIRejected when the call executes
defineBlockno readers are declared
defineAgenta requested reader is missing
defineAgenta tool feeds a block outside writes, or names a missing reducer
defineAgenttools are declared but any chain tier lacks tool support — one text-only fallback is enough to reject
defineToolaccess is omitted, or a long tool has no liveness declaration
asToolthe delegation has no step budget
asToolthe deliverable part is missing from the child's emits, or is ephemeral
defineChainthere are no tiers
defineParta name is duplicated, reserved, or not a valid marker tag

Fixtures are a test-time concern

definePart does not verify fixtures. Call registry.checkFixtures() in a test and assert each result's ok — that is the check that keeps old payloads parseable forever.

What boot and the runtime check

createRuntime refuses to start on a missing budget-manifest entry or a wall-clock ceiling that does not sit under the declared platform ceiling. Once a turn runs, every border is validated where it is crossed:

BoundaryCheck
request → root turnenvelope schema, size cap, ambient scope against the app's declared schema (ambient-invalid), one active root per thread
opening context → modelestimated total plus output reserve fits the window
model → tooltool exists, access is allowed, input parses
tool → blockdeclared write target, schema-valid reducer output
marker → custom partregistered name, schema-valid JSON
turn → callerevery defect settles as exactly one typed outcome, however execution ended

Be precise about what "one typed outcome" covers: every production defect settles as a typed outcome, while framework misuse — a KeelError from a definition or boot check, an unregistered deliverable part — still throws, because it is a bug in your composition, not a runtime condition. Paths that previously escaped as raw exceptions are now typed too: a throwing block reader, repair, or ambient section settles the turn as failed with the render-failure defect, and a throwing bus subscriber is contained rather than propagated.

This is why a perfectly typed tool definition still matters at runtime: the compiler cannot stop a model from inventing an argument. The executor's schema check can — and does, with the reason fed back.

What your application still owns

ResponsibilityHow to cover it
file naming and import directionfollow project conventions in review
truthful prompts and state summariesreview the text; test representative examples
trusted identity and tenancyauthenticate requests before constructing ambient context
safe repeated external effectsdesign tool retries for the operation
answer quality and completenessdomain-specific tests plus live-model evaluation
client compatibilitytest fixtures and client renderers when schemas change

Completed is not correct

A completed outcome proves verifiable work happened — non-empty substance, a validated deliverable. It does not prove the answer is factually right. Types and schemas constrain shape; your tests must check meaning.

Next: Testing — turn expected behavior into executable checks.