Skip to main content

The agent loop

Engine.Run starts a background loop and returns a *Run handle. Use the handle's event channel to follow the run and answer permission requests. The loop calls the model, runs tools, records their results, and repeats until the session ends.

The event stream​

Run.Events() returns every observable event in order and closes when the run ends. Range over the channel to follow a session from start to finish.

Events you will see, roughly in order:

EventWhen it fires
session.initOnce, before the first turn
turn.startBeginning of each LLM turn
message.deltaStreaming text fragments from the model
tool.callA tool call is about to execute
tool.result.availableA completed result is safe to present for this call
tool.resultThe authoritative result for the model and durable history
permission.askThe loop has paused for approval
resultThe run ended; includes usage and the stop reason

The sequence for a single turn with one tool call looks like this:

Turn structure​

Each turn follows this sequence:

  1. Check limits. The run ends if it has a stop reason, is cancelled, or has reached its token budget.
  2. Compact when needed. If the request approaches the context limit, the loop compresses persisted history before the next model call. See Context limits and compaction.
  3. Call the model. Mecatl streams the response and retries eligible failures that occur before output becomes visible.
  4. Run tools. Mecatl dispatches requested tools, records their results, and starts another turn.
  5. Finish or continue. Meaningful text without tool calls ends the run. An empty or reasoning-only response receives up to MaxNoProgressNudges continuation prompts before ending with StopNoProgress. Cancellation, refusal, truncation, and failure end immediately.

A tool error does not abort the run. It becomes an error result fed back to the model, which can retry or choose a different path.

Retrying a failed model step​

A terminal result can include retry_disposition (unknown, retryable, or permanent) and stream_progress (unknown, precommit, visible, or complete). Missing fields do not indicate that retry is safe.

Use gRPC RetryStart or POST /v1/sessions/{id}/retry to repeat an eligible step without adding a user message. Automate only bounded retries marked retryable and precommit. Require a person to decide after visible output.

Read-parallel / mutate-serial dispatch​

When one turn requests multiple tools:

  • Consecutive read-only calls are authorized in order, then run concurrently.
  • A completed read-only call emits tool.result.available when its result is safe to present. These live events can arrive in completion order, so each tool card can settle independently.
  • Mutating calls and calls with unknown safety run alone.
  • The dispatcher releases canonical tool.result events after the read batch's decisions finish, in the model's original call order. These results feed the model and durable history.

A canonical result confirms an identical available result without changing the card. If cancellation changes a previously available result to a synthetic error, the canonical result replaces the card's displayed payload. A client that misses the transient availability event, including after reconnect, uses its canonical result to settle the same call.

The dispatcher enforces these rules; clients do not need to coordinate calls.

Permission pause/resume​

When a tool call requires human or policy approval, the loop pauses and emits a permission.ask event. The event carries an askID, the tool name, the arguments, and the reason the call was flagged.

Your client returns one of three verdicts:

  • Allow once runs the tool without learning a rule.
  • Allow always runs the tool and learns a session-scoped allow rule so the same pattern will not ask again in this session.
  • Deny returns a permission error to the model, which can adapt.

An abandoned request fails closed. Mecatl registers the request before emitting the event, so an early verdict is not lost.

gRPC clients send ResumeApproval on the Converse stream. HTTP clients use POST /v1/sessions/{id}/controls/resolve-ask with the run's run_id; see the HTTP and SSE reference.

Context limits and compaction​

Before each turn, Mecatl estimates the complete model request and compacts persisted history when it approaches the selected model's context window. A separate run-token budget can end the session between turns.

Context windows owns the resolution order, compaction strategies, thresholds, token-budget behavior, and mecatui context meter.

Cancellation and terminal states​

Every run ends in exactly one of three terminal states:

StateMeaning
completedThe model or a limit ended the run cleanly. Stop reasons include end_turn, budget, no_progress, and max_turns.
cancelledRun.Cancel() was called, or the context was cancelled
failedAn unrecoverable error (e.g. the LLM provider returned an error that exhausted retries)

Run.Cancel() ends the run as cancelled, either during dispatch or at the next turn boundary. Restart closes incomplete tool calls with error results so the conversation remains valid.

The service repairs incomplete tool calls before recovering a failed, cancelled, or abandoned run. An awaiting session keeps its pending approval. See Start and resume sessions for the client workflow and Session continuity for persistence and crash recovery.

Next steps​

  • Permissions and posture for how the permission rule engine and model-based guardrails work.
  • Hook system for lifecycle hooks that fire before and after tool calls, prompts, and sessions.
  • Extension points to replace providers, stores, policies, and other capabilities.