The loop engine and tools¶
A loop agent gives a model a set of tools and lets it work toward an outcome within hard
budgets. Tools are other agents, other workflows, or allow-listed MCP tools; their schemas are
derived from contracts, never hand-written.
- id: resolve_ticket
description: Resolve a support ticket using the available tools
engine: loop
inputs: { ticket: Ticket }
outputs: { resolution: str }
budget: { max_iterations: 8, max_tokens: 40000, max_tool_calls: 12, timeout: 2m } # all required
loop:
model: default
steering: artifact://support-steering@4 # text or a steering artifact
skills: [artifact://refund-policy@2]
tools:
- agent: lookup_order # off-spine agent in this workflow
- workflow: issue_refund # another workflow as a child run
approval: { when: "args.amount > 200", required_group: support_manager }
- mcp: artifact://zendesk@3
allow: [add_comment, set_status] # required allow-list
max_calls: 3
supervisor: { rules: [...], judge: artifact://resolution-judge@1 }
guardrails: { tools: [artifact://no-secrets-in-observations@1] }
routes: { resolved: reply, escalate: human_queue, error: ops_alert,
max_iterations: human_queue, budget_exceeded: human_queue,
timeout: human_queue, aborted: ops_alert }
Tool sources¶
| Source | Schema | Execution |
|---|---|---|
agent: <id> |
The agent's inputs → parameters, description → tool description |
The runner executes that agent with the model's arguments; its own routes are ignored |
workflow: <name> |
The workflow's trigger.input and description |
A child run with its own journal, gates and budget; its tokens count toward the parent's max_tokens; nesting capped by policy.max_depth (default 2) |
mcp: artifact://x |
The server's tools/list, filtered by allow |
Through the artifact's connection; each allowed tool's schema hash is pinned in tuvl.lock |
The model's arguments are validated against the tool's schema before execution; invalid arguments
return to the model as an error observation (tool_args_invalid) and count as an iteration. Tool
results stay inside the loop — only the declared outputs reach the run context.
Per-tool governance¶
| Field | Meaning |
|---|---|
approval |
required, or { when: <TEL over args>, required_group? } — the loop pauses at that call |
max_calls |
Per-tool cap, on top of budget.max_tool_calls |
Each tool's effect is derived (the agent's, the child workflow's strongest, or the MCP hints) and
shown to the model and in Insight. Write/destructive MCP calls are at-least-once; V015 asks for an
approval or compensate.
In-loop approval¶
- The model requests a call that matches
approval. - The runner journals the full loop state and the pending call, and the run becomes
waiting_approval. - An approver (
approvals:decide, plus the tool'srequired_group) sees the exact arguments and approves, edits them (re-validated) or rejects with a message:POST /api/approvals/{id}{ decision: approve | edit | reject, edited_args?, message? }. - The run is re-queued; approve/edit executes the call, reject returns the rejection to the model.
Outcome¶
The final turn must return the outputs schema plus outcome: enum[<normal route keys>]; the
outcome is the signal. A bad parse gets one repair attempt, then error (parse_error). Budgets end
the loop with max_iterations, budget_exceeded or timeout; the supervisor or an operator abort
ends it with aborted.
Supervisor¶
spec.supervisor applies to every loop agent unless the agent sets loop.supervisor.
supervisor:
rules:
- { when: tool_repeated, count: 3 } # same call, same args, N times (optional tool:)
- { when: budget_fraction, gt: 0.8 } # a budget more than 80 % used
- { when: iteration_reached, gte: 6 }
- { when: effect_count, effect: write, count: 3 } # more than N write calls
judge: artifact://resolution-judge@1 # evaluates the trajectory
every_n_iterations: 1
on_violation: pause # abort | pause | steer
on_judge_error: ignore # ignore | pause | abort
steer_message: Stop repeating lookups; decide with what you have.
The supervisor reads the journal and writes control flags on the run record, so it works across
workers. A pause waits for an operator (POST /api/runs/{id}/resume); steer injects a message
(operators can also steer with POST /api/runs/{id}/steer).
Guardrails¶
guardrails.tools lists guardrail/judge artifacts applied to tool observations before the model
sees them; guardrails.output checks the final result (guardrail_violation).