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().
- Lock the sessionOne turn per session at a time.
- Load history, store the user messageOnly active messages are loaded.
- Build the system promptStable ▸ context ▸ volatile, with cache breakpoints.
- Run session hooks
on_session_startpre_llm_callon_session_start for a new session; pre_llm_call once per turn.
- Check the budget and the cancel tokenDefault budget: 500 model calls.
- Call the model
pre_api_requestpost_api_requestOn failure: api_request_error, then transform_api_error_classification. - Store the assistant message
- Run the tool calls
pre_tool_callpost_tool_calltransform_tool_resultIn parallel unless one is interactive. Results keep their order.
- Finish the reply
transform_llm_outputpost_llm_callWhen the model answers with text and no tool calls. - Store the status
on_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
- Take the session lock. Only one turn runs per session at a time. A second message to the same session waits.
- Load history and store the message. The session’s active messages are loaded, oldest first. The new user message is appended and stored.
- Build the system prompt in three tiers: stable (
SOUL.md), context (frozen for the session), volatile (the current time). See Prompt Tiers. - Run session hooks.
on_session_startfires if the session is new.pre_llm_callfires once per turn. Context it returns is put in front of the user message for the model only; it is never stored. - 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, thentransform_api_error_classification.- Store the assistant message.
- No tool calls? Run
transform_llm_outputandpost_llm_call, and finish. - Otherwise run the tool calls and loop again.
- Finish. After an interrupt,
agent_loop_stoppedfires. The final status is stored, thenon_session_endfires. Like Hermes,on_session_endfires 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:
- Look up the tool by name.
- Fire
pre_tool_callwith the tool name, arguments,riskand call id. - Act on the result:
Block(reason)→ the tool does not run. The model getsBlocked: <reason>as an error result.AskUser(prompt)→ ask the approver. Anything but approval gives the modelNot approved (denied)orNot approved (timed_out).Modify(args)→ the new arguments are merged in before the tool runs.
- Run the tool.
- Fire
post_tool_call. It fires for blocked and failed calls too. - Fire
transform_tool_result, which may replace the result text. - 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:
max_iterations = 50A 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. |