Turn lifecycle
A turn starts when a user message arrives at the chat handler. The handler routes to a skill, picks a model tier, exposes a scoped tool catalog, and runs the model in a tool calling loop until the model stops, the budget runs out, or a pause point is written.
Overview
- Entry
POST /api/chat/v2- Handler
src/app/api/chat/v2/route.ts- Engine
src/lib/skills/engine.ts- Default model
- Allegro, escalates to Forte for reasoning-heavy skills
- Turn budget
- Declared per skill in the manifest (3 to 16)
- Streaming
- Server-sent events to the chat surface
A turn is the smallest unit of work in Ultron. It is one round trip of input to the model, one set of tool calls executed, and one set of outputs persisted. A skill almost always spans more than one turn because each tool call costs a round trip.
The handler is structured to make the path explicit. Read the file top to bottom and you see the same five steps every request takes: authenticate, route, pick a tier, build tools, run the loop. Each step is its own function and each one is testable in isolation.
End to end trace
What runs between the user pressing send and the assistant streaming back. Times are typical for an Allegro turn with two tool calls.
Router
The router decides which skill runs and which persona owns the turn. It runs once per turn at the very top of the handler.
Routing is cheap and explicit. If the user types a slash command, the router shortcuts to the persona it names. Otherwise it asks a quick Minuet-tier call to classify the message. If the conversation has a sticky persona from a prior turn, the router prefers a skill inside that persona before looking elsewhere.
Model tier selection
Each skill declares a default tier. The router may override based on message length, attachment count, or a difficulty hint.
| Tier | When it runs | Latency budget | Cost |
|---|---|---|---|
| Minuet | Routing decisions, memory judging, short responses | 200ms p50 | Cheapest |
| Allegro | Default skill execution | 1.4s p50 first turn | Mid |
| Forte | Reasoning heavy skills, long planning | 3s p50 first turn | Highest |
Tool catalog
Every turn exposes a scoped set of tools. The catalog is built at the start of the turn and frozen until the turn ends.
tools manifest field. The chat handler enforces the allow-list before dispatching, even if the model attempts to call an unlisted tool.The tool calling loop
The core loop runs in src/lib/skills/engine.ts. Each iteration is one turn.
- 01Send the promptBuild the message array from compressed history plus the current user message. Attach the tool catalog. Stream the response from the model.
- 02Inspect stop reasonThe model returns one of
end_turn,tool_use,max_tokens, orstop_sequence. Onlytool_usecontinues the loop. - 03Execute the tool callsFor each tool use, dispatch into the registry. Provider modules win first. Native handlers second. External servers third. Errors are caught and returned as tool error results so the model can self-correct.
- 04Check for pauseIf any tool returned the
__APPROVAL_REQUIRED__marker, write a pause point row, set the conversation state to paused, and exit the loop. The turn resumes when the user approves. - 05Loop or finalizeAppend tool results to the message array and send the next request. Repeat until stop reason is
end_turnor the turn budget reaches zero.
Stop conditions
Four ways the loop can terminate. The state at exit is recorded on the assistant message.
| Condition | Trigger | Conversation state after |
|---|---|---|
| end_turn | Model returned a final assistant message | idle |
| pause | wait_for_approval tool was called | paused |
| budget_exhausted | Turn count reached maxTurns | idle, with a warning attached |
| error | Unrecoverable tool or model error | failed |
Persistence
What gets written to the database at the end of the turn.
The chat handler persists every assistant message, every tool call, and every tool result. Tool results are persisted as their own message rows with role tool so the next turn can reconstruct the exact context the model saw. Streaming is purely a UI affordance; the source of truth is the database.
File map
Glossary
- Turn
- One iteration of the tool calling loop. Begins with a model request and ends with a stop reason.
- Stop reason
- How a model response ended. One of end_turn, tool_use, max_tokens, stop_sequence.
- Tool catalog
- The list of tool-call schemas exposed to the model on this turn. Built from provider modules, native handlers, and external servers.
- Turn budget
- The maximum number of turns a skill can take before the loop exits. Declared per skill.
- Tier
- A model class: Minuet, Allegro, Forte, or Plinth. Resolved to a concrete model id at request time.
- Stream
- Server sent events sent to the chat surface while the loop runs. Streaming is presentation only.