keel
Turns

Inside a turn

Follow model steps, tool calls, automatic feedback, and the growing transcript.

  1. Step 1 · propose

    The model requests a tool with invalid arguments.

    lookup({ id: 42 })
  2. Validate · record

    The executor refuses the input and adds the reason as a tool result.

    expected id: string
    → transcript.tools
  3. Step 2 · respond

    The next model request includes the earlier call and its feedback.

    lookup({ id: "42" })
    → validate again
Feedback is automatic. The model still needs remaining budget to act on it.

A turn is a loop, not necessarily one provider response. The model can ask for a tool, receive its result, and make another decision within the same turn. You do not need to open another turn to return normal tool feedback.

Assemble once, then build the transcript

At opening, Keel assembles the agent's system sections, prompt, skills, allowed tools, state reader views, selected history, and current brief. The current-turn transcript starts empty.

After a model step, its text and calls enter the transcript. Executed tool results follow them. The next model request includes that transcript alongside the opening context and brief. When the tier streams, the step's text does not wait for the step to finish — deltas land on the wire as they arrive, with partially formed markers held back so a structured part promotes whole.

Root turn T1
  step 1: model → lookup({ id: 42 })
          tool → input rejected: id must be a string
  step 2: model → lookup({ id: "42" })
          tool → matching record
  step 3: model → answer informed by that record
  close:  completed

The model receives feedback automatically; it may still choose not to correct its call. Another response requires remaining execution budget.

Know what reaches the next step

EventFeedback path
Tool returns successfullyIts serialized result enters the transcript.
Unknown tool, denied access, or invalid argumentsRefusal reason enters the tool result.
Tool throws after its retriesDefault onError: "feedback" returns the error; "fail" ends the turn.
Child turn returnsIts structured result or failure enters the caller's transcript.
A state byproduct is refusedSTATE COMMIT REFUSED (block): reason is appended to the tool's own result — the model reacts to it; under onError: "fail" it fails the turn.

Byproduct refusals are never silent: a commit naming an undeclared block, an unknown reducer, or a writer already taken this turn is reported in the result the model reads next step. See Failure and recovery for the supported correction paths.

Run tools under the executor's control plane

Each tool call receives its typed input, the ambient context, and a context object: execute(input, ambient, ctx) where ctx is { signal: AbortSignal; progress: (data) => void }.

  • ctx.progress(data) emits a progress part on the wire and resets the liveness window — the heartbeat that keeps the watchdog satisfied.
  • liveness.maxSilenceMs is a real wall-clock watchdog: a tool silent past that ceiling has ctx.signal aborted, and the turn is cut by watchdog instead of hanging.
  • Every call is additionally capped by the turn's remaining time budget — a tool without liveness still cannot out-sleep the wall clock.

Long-running tools should pass ctx.signal to their IO so cancellation and the watchdog actually free the work.

Inspect a transcript in an adapter

A custom adapter receives a GenerateContext on each call. For example, inside its generate(ctx) implementation:

for (const item of ctx.transcript) {
  if (item.role === "tools") {
    for (const result of item.results) {
      console.log(result.tool, result.result);
    }
  }
}

Shipped provider adapters translate the transcript into provider messages. They also replay it when a chain falls back, so earlier tool results remain available to the next tier. An adapter can additionally implement stream(ctx); when present, the runtime prefers it and forwards text deltas live instead of after the full step.

Distinguish a fresh result from a fresh state view

The opening state views are not automatically rendered again after every tool call. A successful state update can change the store while the original rendered text remains in the opening context. Return the information needed for the next decision through the tool result. A later turn assembles its own state views again.

Keep the loop bounded

One model step can request several tool calls; the executor runs them in order until a hard failure (onError: "fail"), a cut, or a stop ends the turn — later calls in that step do not run. The chain can also retry or fall back within a step. Neither is equivalent to starting a new turn.

Before each model step, Keel estimates the growing request against its input allocation and window. Oversized results can therefore cause fit-overflow before the next provider request. Continuation and delivery nudges have separate recovery bounds; see Budgets and cancellation.

Inspect in Studio

In CONTEXT, inspect the opening views. In MODEL, follow a tool call, its result, and the next model step. For a correction flow, verify the rejected operation never executed and that the later corrected call did.

Next: Outcomes and completion.