Packages
claude_gleam
1.0.0
Gleam SDK for the Anthropic Claude API with agentic tool-use loop and BEAM concurrency
Current section
Files
Jump to
Current section
Files
src/claude/agent.gleam
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)
}
}
}
}
}
}
}