The Agent Loop
On this page
A turn is one model call plus the tool calls it makes; kli runs turns back to back until the model replies without asking for a tool. That is the agent loop: each turn ends by deciding, from the model's own reply, whether to run another. It is why a chain of reads, runs, and edits is not one model call, and why a tool that fails rarely stops the whole task.
A turn, step by step
A turn is the unit kli repeats. Each turn runs five steps in order.
Seal the context projection. Before kli calls the model, it takes an immutable snapshot of the conversation so far. The live session can change while a request is in flight; the snapshot cannot. The model always sees a fixed view of the messages, frozen at the moment the request went out. The turn can also carry ephemeral context that rides this one snapshot and is never written back, so per-turn information reaches the model without becoming part of the permanent log.
Stream the model response. kli sends the sealed snapshot and reads the reply as it arrives. Text and reasoning come back incrementally. When the stream ends, kli has the full assistant message: prose, and zero or more tool calls the model wants to make.
Execute the tool calls serially. Tool calls run one at a time, in order, not in parallel. After each call kli checks two things: whether you asked it to stop, and whether you typed a new instruction while it worked. Serial execution keeps the order of effects predictable, so a write that a later call depends on has already happened.
Append to the session log. The assistant message goes into the log, and so does each tool's result. The log is the conversation: it is what the next snapshot is built from. By the time the turn ends, the model's request and every answer to it are recorded together.
Loop until there are no tool calls. This is the decision that makes kli autonomous. If the response asked for at least one tool call, kli runs another turn so the model can react to the results it just got. If the response asked for none, the model is done, and the loop stops and goes idle. The model controls its own length: it keeps the loop alive by calling tools, and ends it by answering in plain text.
Why the loop, and not a single call
A single model call can plan an edit, but it cannot see whether the edit worked. The loop closes that gap. The model calls a tool, the result lands in the log, and the next turn shows the model what actually happened. A failed test becomes input, not a dead end. This is why kli can run a build, read the real error, and fix the line that caused it: each turn feeds the last turn's results back in.
The snapshot-then-stream order matters here. Because the context is sealed before the call, anything that happens during the call — a tool result appended, an edit you make on the side — does not change the request already in flight. It lands in the log and enters the next snapshot instead. Every request the model sees is internally consistent, and new information always arrives at a turn boundary rather than mid-thought.
Why a failed tool comes back as a result
When a tool fails, kli does not abort the turn. It turns the failure into a tool result marked as an error and appends it to the log like any other result. The next turn shows that error to the model, which can read it and try something else.
There is a hard reason this is the only safe choice. The model's request to call a tool is already in the log before the tool runs. A tool call with no answer leaves the conversation in a broken shape that model providers reject: once a tool call is recorded, the request that follows it must contain that call's result, or the provider refuses every further turn over that conversation. So an exception escaping a tool would not just lose one result — it would wedge the whole session. kli closes that by guaranteeing every tool call gets a result, even when the tool throws, even when the model invents a tool that does not exist. The error is recorded as the result, and the loop continues.
The same rule covers the case where the model calls a tool with arguments that are not valid JSON. Rather than invoke the tool with empty arguments and let it fail as a missing-parameter error — which would mislead the model about what went wrong — kli returns a result that names the real cause: the arguments did not parse. The model gets an accurate error and can correct it on the next turn.
The model wrote the tool call, so the model is the party that can fix it. By feeding errors back as results instead of unwinding the turn, kli lets the model debug its own work the same way it does anything else: read what happened, decide what to do next.
What stops a loop
Three things end a run. The model can stop it by returning a turn with no tool calls — the normal finish. You can stop it: if you ask kli to abort, the in-flight request is cancelled and the agent settles without starting another turn. And a turn that hits an error the loop cannot turn into a tool result (a failure in the model call itself, not in a tool) ends the run in an error state, where a higher layer can retry it or surface it to you. A failed tool is none of these. It is data the model gets to act on.
Related
- Sessions as a Tree — the append-only record each snapshot is built from
- Steer a Running Turn — typing to kli while a turn is running
- Capabilities and Fault Barriers — how a tool call can be denied, which also comes back as a result