Skip to content
Platform3 min read

The agent loop

How one turn becomes many model steps, tool calls and a checked result — and where the limits are.

Updated Nov 6, 20253 min readEdit this page
On this page

A chat completion answers once. An agent turn keeps going: the model plans, calls tools, reads what came back, and decides whether the goal is met. Lumo implements that loop on top of the Vercel AI SDK, with every step visible in the transcript.

What one turn contains#

A turn starts with the user's message and its attachments, plus the system prompt and the tool catalogue. It ends when the model produces a final answer, the user presses stop, or a configured cap is reached.

  1. Assemble context: conversation, page or selection attachments, file references and the enabled tool list.
  2. Call the model and stream its parts: reasoning, text, and zero or more tool calls.
  3. Execute every tool call, in parallel when the model issued several, and append typed results.
  4. Feed the enlarged context back and repeat from step 2 until the model answers without a tool call.

The step budget#

The default budget is unbounded. A browsing task can legitimately need dozens of steps, and any fixed cap becomes a reply that stops mid-task and asks the user to type "continue".

  • 0 means no cap and is the default. It is not a step count.
  • Custom caps below MIN_MAX_STEPS (10) are raised, because a lower cap would truncate almost every real task.
  • 20 and 60 are offered as presets for users who want a predictable ceiling.
The only budget checktypescript
export function stepAllowed(step: number, maxSteps: number): boolean {  return maxSteps === 0 || step < maxSteps;}

Tool execution#

  • The tool catalogue is assembled per request from every connected MCP server, so disabling a server removes its tools on the next step.
  • Each call is rendered as a collapsible card with its input, status and output; failures open by default.
  • Images returned by a tool are stripped from later prompts and replaced with a caption, so base64 payloads cannot consume the context window.
  • Tool calls that were still running when a turn was interrupted are shown as interrupted, not as an infinite spinner.

Context and compaction#

The context ring estimates what the next request will cost, not what the conversation has cost. When the estimate crosses the model's window, old turns are folded into a checkpoint that is replayed as a summary; the original messages stay readable in the transcript.

SignalSourceWhat it changes
Context ringPrompt estimate + live provider usageHow close the next request is to the window
Compaction checkpointOlder turns folded into a summaryWhat is replayed to the model
Step usagePer-step token countsThe usage breakdown on an assistant message
Stop reasonProvider finish reasonWhether the reply is marked interrupted

The transcript is the source of truth

Compaction changes the prompt projection, never the stored conversation. A user can always scroll back to the messages that were summarised.

Stopping and resuming#

  • Stop aborts the in-flight request and settles every open tool call.
  • Retry re-sends the turn that failed, with bounded attempts and backoff.
  • Regeneration creates a new variant of an assistant message; earlier variants remain switchable.

Providers are part of the loop#

The loop is protocol-agnostic. Three wire protocols are supported, plus a vendor profile for OpenCode that derives its endpoints and per-conversation session identifier.

ProfileProtocolEndpointNotes
OpenAI ChatChat CompletionsPOST {baseUrl}/chat/completionsPortable default; works with most gateways.
OpenAI ResponsesResponsesPOST {baseUrl}/responsesReasoning and built-in tool shapes.
AnthropicMessagesPOST {baseUrl}/messagesClaude models, vision and effort control.
OpenCodeAll threeDerived from the vendorClient header and stable session affinity.

OpenCode is a vendor type, not a protocol

If you use an OpenCode endpoint, set the provider vendor to OpenCode. It derives the protocol path, sends the identifying client header and a per-conversation session ID. A custom provider pointed at the same path omits those headers, so OpenCode rejects the request.

Reasoning is a per-model setting

Reasoning effort is stored per model and omitted when unset, so a provider default is never overridden by an empty value.