Inside a turn
Follow model steps, tool calls, automatic feedback, and the growing transcript.
Step 1 · propose
The model requests a tool with invalid arguments.
lookup({ id: 42 })Validate · record
The executor refuses the input and adds the reason as a tool result.
expected id: string → transcript.toolsStep 2 · respond
The next model request includes the earlier call and its feedback.
lookup({ id: "42" }) → validate again
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: completedThe 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
| Event | Feedback path |
|---|---|
| Tool returns successfully | Its serialized result enters the transcript. |
| Unknown tool, denied access, or invalid arguments | Refusal reason enters the tool result. |
| Tool throws after its retries | Default onError: "feedback" returns the error; "fail" ends the turn. |
| Child turn returns | Its structured result or failure enters the caller's transcript. |
| A state byproduct is refused | STATE 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.maxSilenceMsis a real wall-clock watchdog: a tool silent past that ceiling hasctx.signalaborted, and the turn is cut bywatchdoginstead 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.
