Skip to content

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:

VariantMeaning
SuccessModel produced a final text response
ToolLoopLimitMax rounds reached; sent a best-effort final prompt
AwaitingApprovalA tool needs user approval; loop paused
FailedUnrecoverable 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 approvals
  • run_with() -- custom tool list and approval handler
  • run_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:

  1. Checks cooldown -- skipped if a recent compression failed
  2. Splits history into head (protected), middle (compressible), and tail (protected)
  3. Sends the middle messages to a secondary summarizer LLM call
  4. 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, returns Terminal (unrecoverable)
  • If true, attempts reactive compression on the current messages
  • If compression reduced message count, returns Retry with 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 continues
  • Warn -- injects a warning message into history, loop continues
  • BlockTool -- blocks the offending tool from future calls this turn
  • HaltLoop -- stops the loop immediately with ToolLoopLimit

Returns #(history, guardrail_state, halted).

  • 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

Built with Gleam on the BEAM/Erlang VM.