Claudio.Agent (Claudio v0.7.0)

View Source

Stateless 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 callback fn tool_use, result -> :ok end for 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

handler()

@type handler() :: (map() -> result()) | (String.t(), map() -> result())

handlers()

@type handlers() :: %{required(String.t()) => handler()}

result()

@type result() :: {:ok, String.t() | [map()]} | {:error, String.t()}

run_result()

@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

run(client, request, tool_handlers, opts \\ [])

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.