Claudio.Agent (Claudio v0.7.0)
View SourceStateless agent loop utility for tool-calling workflows.
Runs the tool-calling loop: send request → check for tool uses → execute handlers → append results → repeat until done.
This is a pure function — no GenServer, no process. State management (conversation history, persistence) is the caller's responsibility.
Example
alias Claudio.{Agent, Messages.Request, Tools}
# Define tools and handlers
weather_tool = Tools.define_tool("get_weather", "Get weather", %{
"type" => "object",
"properties" => %{"location" => %{"type" => "string"}},
"required" => ["location"]
})
handlers = %{
"get_weather" => fn %{"location" => loc} ->
{:ok, "72°F and sunny in #{loc}"}
end
}
# Build request
request = Request.new("claude-opus-5-5")
|> Request.add_message(:user, "What's the weather in SF?")
|> Request.add_tool(weather_tool)
|> Request.set_max_tokens(1024)
# Run the agent loop
{:ok, response, messages} = Agent.run(client, request, handlers)
# response is the final Response struct
# messages is the full conversation history (for continuing later)Errors
run/4 returns {:error, reason, last_response, messages} — a 4-tuple, not
{:error, reason} — so a failed run is resumable: messages is the history up to the
failure (tool results already executed included) and last_response the previous
successful response (nil on the first call).
A tool handler that raises, throws or exits does not abort the run: the failure is
reported to the model as a tool result with is_error: true (e.g. "Tool error: boom")
and the loop continues, so the model can retry or explain. Use :on_tool_call to observe
it — the callback receives {:error, message} as the result. A handler that returns
anything other than {:ok, content} / {:error, reason} is a programming error and
raises ArgumentError instead.
Client toolsets
Calls from Request.add_computer_toolset/2 / add_browser_toolset/2 carry a
toolset_name and go only to the handler keyed by it, as fn member, input -> … end
(e.g. %{"computer" => fn "screenshot", _ -> {:ok, [image_block]} end}); a plain tool
with the same name as a member never receives them. Results echo toolset_name. If an
action fails, the toolset's later actions in that turn are not run and are answered
with the documented halt text (Claudio.Tools.halt_result/1).
Programmatic tool calling and pauses
A response container is carried to the next request (required to continue calls made
from code execution). A pause_turn is resumed automatically by resending the
assistant turn; it counts toward :max_turns.
Compaction
A stop_reason: :compaction reply (Request.request_compaction/2, or
Request.add_compaction/2 with pause_after_compaction: true) is continued with
Request.apply_compaction/2 — the history becomes the summary block and the on-demand
compaction field is cleared — and the loop calls the model again with no user turn.
It counts toward :max_turns. A failed compaction (content: nil) keeps the history.
Options
:max_turns— Maximum model calls (default: 10):on_tool_call— Optional callbackfn tool_use, result -> :ok endfor logging/observability. Not called for client-toolset actions skipped by a batch halt (they never ran).
Summary
Functions
Runs the agent loop until the model stops requesting tools or max_turns is reached.
Types
@type run_result() :: {:ok, Claudio.Messages.Response.t(), [map()]} | {:error, :max_turns_exceeded, Claudio.Messages.Response.t(), [map()]} | {:error, term(), Claudio.Messages.Response.t() | nil, [map()]}
Functions
@spec run(Req.Request.t(), Claudio.Messages.Request.t(), handlers(), keyword()) :: run_result()
Runs the agent loop until the model stops requesting tools or max_turns is reached.
max_turns caps the number of model calls. With max_turns: 2 the model is called at
most twice — the initial call plus one tool-result (or pause_turn) follow-up; if the
second call still requests tools or pauses, the loop stops with :max_turns_exceeded.
To resume a programmatic run from there, read the container from last_response.container
(it is not in messages).
Returns {:ok, final_response, messages} on success, where messages is the
full conversation history including all tool calls and results.
Returns {:error, :max_turns_exceeded, last_response, messages} if the loop
doesn't converge within max_turns model calls. When last_response stopped on
:tool_use, the last message in messages is that assistant turn with its tool calls
not yet executed: get them with Claudio.Tools.extract_tool_uses(last_response),
add their results as a user turn, and resume.
Returns {:error, reason, last_response, messages} if an API call fails — messages is
the history up to the failed call (tool results already executed included) and
last_response the previous successful response (nil on the first call), so the run
can be resumed.
Raises ArgumentError if a handler returns anything other than {:ok, content} or
{:error, reason} — a programming error, reported with the handler's name.