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).

  1. 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`).
  2. 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`).
  3. 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`).
  4. 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`).
  5. 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`).
call modelunder timeouttool_use?final answerreturnrun toolsfeed results backnoyesloop
One iteration. The dashed edge feeds tool results back into the next call; the token budget is checked on that edge before any further tools run.

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.