The Agent Loop
The agent loop is what fisk-ai run drives. It calls the Anthropic model, runs the tools the model asks for, feeds the results back, and repeats until the model gives a final answer or a budget stops it.
Where it lives
internal/agent: one-time setup in agent.go, the loop itself in runner.go, the reporting contract in events.go. The single model call with its per-call timeout is util.CallLLM in internal/util/llm.go.
Setup, then loop
agent.Run does all the one-time work before the loop starts (agent.go:119): load and validate tools, inject the built-in tools, import any remote tools, build the Anthropic tool params, construct the confirm gate, build the client, seed the conversation, compute the resume fingerprint, and open or resume the journal. It then constructs a runner and calls its loop. The runner splits its state in two: infrastructure that is rebuilt from config on every start or resume, and mutable conversation state that is resumable. That split is what makes a run suspendable.
How one iteration runs
The loop runs while the iteration count is below max_iterations (runner.go:368).
- Check for suspend Only at the loop boundary, before the iteration index is consumed, never mid-tool, so the conversation is always left coherent (`runner.go:384`).
- Call the model Under a per-call timeout derived from `call_timeout`. `util.CallLLM` wraps the single request in its own context (`llm.go:16`).
- Journal the turn The assistant response is appended to the conversation and journaled before any tool runs, so a crash mid-batch resumes without re-paying for the call (`runner.go:444`).
- Decide terminality A turn with no tool-use blocks, and not a paused turn, is the final answer and ends the run. Otherwise the tool calls are executed (`runner.go:470`).
- Run tools and feed back Each tool result is journaled as it completes, then all results are appended as one user message that becomes the next iteration's input (`runner.go:494`).
Budgets
Two different token caps apply. defaultMaxOutputTokens (8192, or 16384 with thinking) bounds a single response so a call stays under the non-streaming ceiling (agent.go:39). The configured llm.budget.max_tokens bounds the whole run. The run budget is a soft cap checked after each call but before that turn’s tools run, so an over-budget turn incurs no further tool side effects (runner.go:490). All four token tiers, input, output, cache read, and cache create, are summed so the cap measures total throughput.
max_iterations bounds the loop count and call_timeout bounds each call. The defaults are 200000 tokens, 50 iterations, and 120 seconds.
Load-bearing decision
The assistant turn is journaled before any tool executes, and each tool result is journaled the instant that tool finishes. This ordering is what gives the durability guarantee in Sessions and Resume: a clean suspend is exactly-once and a crash repeats at most one tool call.
Reporting, not rendering
The loop never draws anything. It emits typed callbacks through the Events interface, and the caller decides how they look. The same callbacks back both the line UI and the full-screen UI, which keeps the loop free of terminal concerns. Tool dispatch distinguishes local, remote, built-in, and memory tools so each is traced correctly.
Next
Continue to Safety and Human in the Loop for the guardrails around each tool call.