The Turn Loop

What CommonAgent does between receiving a message and returning a reply, step by step.

A turn starts with one user message and ends with a reply or a stop. In between, the agent calls the model, runs any tools the model asks for, and calls the model again, until the model answers with text. CommonAgent::run_turn follows Hermes’s run_conversation().

  1. Lock the sessionOne turn per session at a time.
  2. Load history, store the user messageOnly active messages are loaded.
  3. Build the system promptStable ▸ context ▸ volatile, with cache breakpoints.
  4. Run session hookson_session_startpre_llm_callon_session_start for a new session; pre_llm_call once per turn.
Loop until the model answers with text
  1. Check the budget and the cancel tokenDefault budget: 500 model calls.
  2. Call the modelpre_api_requestpost_api_requestOn failure: api_request_error, then transform_api_error_classification.
  3. Store the assistant message
  4. Run the tool callspre_tool_callpost_tool_calltransform_tool_resultIn parallel unless one is interactive. Results keep their order.
  1. Finish the replytransform_llm_outputpost_llm_callWhen the model answers with text and no tool calls.
  2. Store the statuson_session_endcompleted · interrupted · budget_exhausted · failed · skipped

To watch a real turn with tools, child agents and an approval go through these steps, see Message Flow.

Step by step

  1. Take the session lock. Only one turn runs per session at a time. A second message to the same session waits.
  2. Load history and store the message. The session’s active messages are loaded, oldest first. The new user message is appended and stored.
  3. Build the system prompt in three tiers: stable (SOUL.md), context (frozen for the session), volatile (the current time). See Prompt Tiers.
  4. Run session hooks. on_session_start fires if the session is new. pre_llm_call fires once per turn. Context it returns is put in front of the user message for the model only; it is never stored.
  5. Loop:
    • Stop if the iteration budget is spent or the turn was cancelled.
    • pre_api_request → model call → post_api_request. On failure: api_request_error, then transform_api_error_classification.
    • Store the assistant message.
    • No tool calls? Run transform_llm_output and post_llm_call, and finish.
    • Otherwise run the tool calls and loop again.
  6. Finish. After an interrupt, agent_loop_stopped fires. The final status is stored, then on_session_end fires. Like Hermes, on_session_end fires at the end of every turn, not only when a session closes.

Each tool call

Tool calls from one model response run in parallel, unless one of them is interactive; then they run one at a time. Results go back to the model in the order the model asked for them.

For each call:

  1. Look up the tool by name.
  2. Fire pre_tool_call with the tool name, arguments, risk and call id.
  3. Act on the result:
    • Block(reason) → the tool does not run. The model gets Blocked: <reason> as an error result.
    • AskUser(prompt) → ask the approver. Anything but approval gives the model Not approved (denied) or Not approved (timed_out).
    • Modify(args) → the new arguments are merged in before the tool runs.
  4. Run the tool.
  5. Fire post_tool_call. It fires for blocked and failed calls too.
  6. Fire transform_tool_result, which may replace the result text.
  7. Store the tool call, and its approval if there was one.

Stopping a turn

Every turn takes a CancellationToken. Cancelling it stops the turn between steps, and also stops a model call that is in flight. Nothing partial is saved from a cancelled model call. The turn ends with status interrupted, and agent_loop_stopped fires with reason: "cancelled".

In CommonOS, the dashboard’s stop button and Dispatcher::stop (Hermes’s /stop) cancel the running turn of a session. Child agents get a child token, so stopping the parent stops its children too.

The budget

max_iterations caps the number of model calls in one turn. The default is 500, as in Hermes. Set it per profile in config.toml:

config.toml
max_iterations = 50

A turn that runs out ends with status budget_exhausted.

Statuses

Status Meaning
running The turn is in progress.
completed The model answered with text.
interrupted The turn was cancelled.
budget_exhausted The turn used all its model calls.
failed A model call failed. The reply is Model call failed: <error>.
skipped A gateway hook blocked or handled the message, so no turn ran. Only replies carry this status.

Edit this page on GitHub

Esc