import claude/agent/config.{type AgentConfig} import claude/agent/tool_runner import claude/messages.{RequestParams} import claude/types/content.{type ContentBlock, ToolUse} import claude/types/error.{type ApiError} import claude/types/message.{ type Message, type MessageParam, Assistant, BlockContent, MessageParam, ToolUseStop, } import gleam/list import gleam/option.{Some} /// The result of a successful agent loop run. pub type AgentResult { AgentResult( messages: List(MessageParam), final_message: Message, iterations: Int, total_input_tokens: Int, total_output_tokens: Int, ) } /// Errors that can occur during the agent loop. pub type AgentError { ApiCallFailed(ApiError) MaxIterationsReached(messages: List(MessageParam), iterations: Int) } /// Events emitted during the agent loop execution. /// /// These events allow callers to observe the progress of an agent run, /// whether via a callback function or via BEAM message passing. pub type AgentEvent { /// Agent started processing Started(session_id: String) /// Assistant sent a response (may include tool calls) AssistantResponse(message: Message) /// Tool execution started ToolExecuting(tool_name: String, tool_id: String) /// Tool execution completed ToolCompleted(tool_id: String, result: Result(String, String)) /// Agent finished successfully Done(result: AgentResult) /// Agent encountered an error Failed(error: AgentError) } /// Extract tool-use blocks from a list of content blocks. pub fn extract_tool_calls(content: List(ContentBlock)) -> List(ContentBlock) { list.filter(content, fn(block) { case block { ToolUse(..) -> True _ -> False } }) } /// Build a user message containing tool results. /// /// Each result is a tuple of (tool_use_id, Ok(content) | Error(error_message)). pub fn build_tool_results_message( results: List(#(String, Result(String, String))), ) -> MessageParam { let blocks = list.map(results, fn(result) { let #(id, outcome) = result case outcome { Ok(content) -> content.tool_result(tool_use_id: id, content: content, is_error: False) Error(err) -> content.tool_result(tool_use_id: id, content: err, is_error: True) } }) MessageParam(role: message.User, content: BlockContent(blocks)) } /// Run the agent loop. /// /// Starts with the given prompt, sends messages to the API, and automatically /// handles tool-use responses by executing tools and feeding results back. /// Continues until the model stops with a non-tool-use reason or max_iterations /// is reached. pub fn run( config: AgentConfig, prompt: String, ) -> Result(AgentResult, AgentError) { let initial_messages = [message.new_user(prompt)] event_loop( config, list.reverse(initial_messages), 0, 0, 0, fn(_) { Nil }, ) } /// Run the agent loop with an existing message history. /// /// Like `run` but allows passing in an existing conversation history. pub fn run_with_messages( config: AgentConfig, messages_list: List(MessageParam), ) -> Result(AgentResult, AgentError) { event_loop( config, list.reverse(messages_list), 0, 0, 0, fn(_) { Nil }, ) } /// Recursive agent loop that emits events at each step. /// /// This is the single canonical implementation of the agent loop. /// Both `agent.run` (with a no-op callback) and `actor.run_with_events` /// delegate to this function. /// /// The `on_event` callback is called at each notable step. Pass `fn(_) { Nil }` /// for a silent loop, or a real callback for event-driven usage. pub fn event_loop( config: AgentConfig, messages: List(MessageParam), iteration: Int, total_input: Int, total_output: Int, on_event: fn(AgentEvent) -> Nil, ) -> Result(AgentResult, AgentError) { // Guard: check max iterations case iteration >= config.max_iterations { True -> { let err = MaxIterationsReached( messages: list.reverse(messages), iterations: iteration, ) on_event(Failed(error: err)) Error(err) } False -> { // Call the API case messages.create(RequestParams( config: config.client, model: Some(config.model), max_tokens: Some(config.max_tokens), messages: list.reverse(messages), tools: config.tools, system: config.system, tool_choice: config.tool_choice, thinking: config.thinking, )) { Error(api_error) -> { let err = ApiCallFailed(api_error) on_event(Failed(error: err)) Error(err) } Ok(response) -> { // Emit assistant response event on_event(AssistantResponse(message: response)) let new_input = total_input + response.usage.input_tokens let new_output = total_output + response.usage.output_tokens // Convert assistant response to MessageParam and append let assistant_params = message.content_to_params(response.content) let assistant_msg = MessageParam(role: Assistant, content: BlockContent(assistant_params)) let updated_messages = [assistant_msg, ..messages] // Check stop reason case response.stop_reason { Some(ToolUseStop) -> { // Extract tool calls let tool_calls = extract_tool_calls(response.content) // Emit ToolExecuting events for each tool call list.each(tool_calls, fn(block) { case block { ToolUse(id: id, name: name, ..) -> on_event(ToolExecuting(tool_name: name, tool_id: id)) _ -> Nil } }) // Execute tools let results = tool_runner.execute_concurrent( tool_calls: tool_calls, tools: config.tools, timeout_ms: config.tool_timeout_ms, ) // Emit ToolCompleted events list.each(results, fn(result) { let #(id, outcome) = result on_event(ToolCompleted(tool_id: id, result: outcome)) }) let tool_results_msg = build_tool_results_message(results) let next_messages = [tool_results_msg, ..updated_messages] // Recurse event_loop( config, next_messages, iteration + 1, new_input, new_output, on_event, ) } _ -> { let result = AgentResult( messages: list.reverse(updated_messages), final_message: response, iterations: iteration + 1, total_input_tokens: new_input, total_output_tokens: new_output, ) on_event(Done(result: result)) Ok(result) } } } } } } }