Skip to Content
DocsConceptsAgent loop

Agent Loop

Every CMDOP chat — local or remote — runs the same loop. Each turn resolves a tool catalogue, sanitises history, calls the LLM, executes tool calls, and decides whether to keep going. This page describes the cycle so you can debug it when something looks off.

The cycle in one diagram

The loop runs on a typed runner that is generic over its dependency and output types, so the same machinery drives every surface (Desktop, CLI, daemon, SDK).

Pseudocode

for turn = 1..maxTurns { tools = active tools if turn == 1 and a tool-category allowlist is set { tools = keep only tools whose tags match the allowlist } tools = tools minus the platform/mode hidden set msgs = sanitize(history) // drop orphan tool calls req = build request(model, msgs, tools, cache breakpoints, temp=0.3) resp = call LLM(req, retries=6, backoff=500ms..32s) history = history + resp.message if resp has no tool calls { return resp.message } results = execute tool calls (concurrently if enabled) history = history + results }

What happens on each turn

1. Inject system context

The runner injects fresh system reminders (NOT new messages — they keep history clean):

  • IdentityI'm CMDOP Operator on <hostname>.
  • Environment info — local OS, timezone, user (suppressed when targeting a remote machine).
  • Target machineyour client is asking from vps-foo, use ask_machine (only when the CLI is scoped to a remote machine via cmdop chat --machine).
  • Critical toolsprefer tools for work (safety net).
  • No-tools honesty — refuse to fabricate output when no tools are available.
  • Inspector links — prefer cmdop:// hrefs in file references.
  • Platform context — platform-specific hints (browser, mobile, vps).

The static prefix is split at a cache-boundary marker so Anthropic prompt caching applies to everything before the marker.

2. Resolve tools

Three filters run in order:

  1. Intent router (default OFF). Re-enable with CMDOP_INTENT_ROUTER=on. When ON, a cheap classifier decides DIRECT vs AGENT. DIRECT skips tool defs (~2000 tokens saved). It is off by default because the savings rarely justify the added latency and the risk of mis-routing a tool-needing turn.
  2. Tool-category router (turn 1 only). When a tool-category allowlist is set, the catalogue is restricted to tools whose tags intersect the allowlist. Core tools are always included. The allowlist is cleared after turn 1.
  3. Hide map. A platform/mode hidden set is consulted last, so it can drop individual tools even if the LLM hallucinates a call later.

3. Sanitize history

The runner drops orphan tool calls — entries where the assistant requested a tool but no matching result followed. This protects providers that reject mismatched call/result pairs.

4. Build the LLM request

Each request carries:

  • The model (an alias or a concrete model).
  • The converted message history.
  • The resolved tool definitions (omitted when no tools are needed).
  • Tool choice set to automatic.
  • A fixed temperature of 0.3.
  • A token budget.
  • Cache breakpoints at the start of the static prefix plus history-based positions.

5. Call the LLM with retry

The provider call is wrapped with 6 attempts and exponential backoff from 500 ms up to 32 s. Streaming is optional; when a stream dies near the deadline, the runner falls back inline to non-streaming. The streaming idle timeout is 2 s per token batch.

6. Execute tool calls

When parallel tool execution is enabled, tool calls run concurrently — handy for ask_machines or multiple read_file calls. Otherwise they execute sequentially.

Each tool result is appended to history with the matching tool_call_id so the next turn sees a clean conversation.

Termination conditions

The loop ends when any of these is true:

  • The LLM response has no tool calls (final answer).
  • The per-agent turn limit is reached (configurable per agent).
  • The parent operation is cancelled.
  • The UI cancel signal fires — it bypasses deadline pressure so a user can kill a long run instantly.

Cancellation has two channels

  • Operation cancellation. Standard cancellation that respects deadlines.
  • UI cancel signal. Driven by a Desktop or TUI button. Honoured immediately even when the operation still has time left.

Both are checked at every loop boundary.

Streaming events

When streaming is enabled, the runner pushes events onto the session’s stream. Consumers see:

  • TOKEN — next text fragment.
  • TOOL_START / TOOL_END — tool execution markers.
  • THINKING — provider thinking marker (when supported).
  • ERROR / CANCELLED — terminal events.

Same event vocabulary appears on ask_machine_stream — see Agent Communication.

Cache boundaries

The runner sets cache breakpoints at index 0 (start of the static system prefix) plus history-based positions. Anthropic providers honour these as cache_control: ephemeral, which dramatically reduces token cost on long sessions.

What can derail the loop

SymptomLikely cause
Agent answers without using toolsIntent router on, classifier ruled DIRECT — disable with CMDOP_INTENT_ROUTER=off
Specific tool never gets calledTool is in the hidden set for this platform / mode
Loop hits max turnsTool keeps reporting partial work; check tool implementations and truncated flags
LLM errors → fallback to textStreaming died near deadline; expected, retried

The runner is the only place that calls the LLM. If you see provider-specific behaviour inside a tool, that tool is ignoring the framework — file an issue.