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):
- Identity —
I'm CMDOP Operator on <hostname>. - Environment info — local OS, timezone, user (suppressed when targeting a remote machine).
- Target machine —
your client is asking from vps-foo, use ask_machine(only when the CLI is scoped to a remote machine viacmdop chat --machine). - Critical tools — prefer 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:
- 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. - 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.
- 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
| Symptom | Likely cause |
|---|---|
| Agent answers without using tools | Intent router on, classifier ruled DIRECT — disable with CMDOP_INTENT_ROUTER=off |
| Specific tool never gets called | Tool is in the hidden set for this platform / mode |
| Loop hits max turns | Tool keeps reporting partial work; check tool implementations and truncated flags |
| LLM errors → fallback to text | Streaming 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.