Loop
Source: core/loop/orchestrator.gleam, core/loop/runner.gleam, core/loop/context.gleam, core/loop/compression.gleam, core/loop/error.gleam, core/loop/guardrails.gleam
The loop subsystem is the heart of the agent -- it runs the API-tool conversation cycle. It is split into a pure orchestrator (zero service/plugin imports) and a composition root runner that wires concrete implementations.
Modules
orchestrator.gleam
The pure conversation loop. On each iteration it: builds the system prompt with context warnings, checks for preemptive compression, calls the LLM API, dispatches tool calls, runs guardrails, and recurses. Returns one of four outcomes:
| Variant | Meaning |
|---|---|
Success | Model produced a final text response |
ToolLoopLimit | Max rounds reached; sent a best-effort final prompt |
AwaitingApproval | A tool needs user approval; loop paused |
Failed | Unrecoverable error (non-retryable API error) |
Key function: loop(ctx, history, round, guardrail_state, cooldown_state) -> LoopResult
runner.gleam
Composition root (Blessed Violator #2). Wires concrete service and plugin implementations into a LoopContext and calls the pure orchestrator. Entry points:
run()-- default tool registry, auto-deny approvalsrun_with()-- custom tool list and approval handlerrun_with_chat_fn()-- custom chat function (for testing)
context.gleam
Defines the LoopContext record -- the dependency carrier for the conversation loop. Contains all 7 injected behaviours (chat_fn, logger, context_builder, token_estimator, guardrail_hook, compressor_hook, llm_client), wiring fields (persona, tool_defs, all_tools, conn, user_key, approval_handler), and config values read at construction time.
compression.gleam
Preemptive context compression. Called before each API call when compression_enabled is true. If the context window is at or above compression_trigger_percent (default 70%), it:
- Checks cooldown -- skipped if a recent compression failed
- Splits history into head (protected), middle (compressible), and tail (protected)
- Sends the middle messages to a secondary summarizer LLM call
- Replaces the middle with a single summary message
On failure, enters a 10-minute cooldown. Returns #(compressed_messages, cooldown_state).
error.gleam
Reactive error handler for API failures. When the LLM client returns an error:
- If
should_compress(err)is false, returnsTerminal(unrecoverable) - If true, attempts reactive compression on the current messages
- If compression reduced message count, returns
Retrywith the compressed history - If compression had no effect, returns
Terminal
Returns ErrorResult (either Terminal or Retry), which the orchestrator uses to decide whether to continue or fail.
guardrails.gleam
Tool loop guardrails fired after each tool execution batch. Records success/failure per tool call to a LoopState, then checks escalation thresholds (configurable via env vars). Escalation levels:
None-- no issue, loop continuesWarn-- injects a warning message into history, loop continuesBlockTool-- blocks the offending tool from future calls this turnHaltLoop-- stops the loop immediately withToolLoopLimit
Returns #(history, guardrail_state, halted).
Related Pages
- Request Flow -- End-to-end walkthrough of the conversation loop
- Registry -- Tool registry used during tool dispatch
- Session -- Session handler that invokes the loop
- Blessed Violators -- Why runner.gleam can import services/plugins