Packages

Gleam SDK for the Anthropic Claude API with agentic tool-use loop and BEAM concurrency

Current section

Files

Jump to
claude_gleam src claude@agent.erl
Raw

src/claude@agent.erl

-module(claude@agent).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/claude/agent.gleam").
-export([extract_tool_calls/1, build_tool_results_message/1, event_loop/6, run/2, run_with_messages/2]).
-export_type([agent_result/0, agent_error/0, agent_event/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type agent_result() :: {agent_result,
list(claude@types@message:message_param()),
claude@types@message:message(),
integer(),
integer(),
integer()}.
-type agent_error() :: {api_call_failed, claude@types@error:api_error()} |
{max_iterations_reached,
list(claude@types@message:message_param()),
integer()}.
-type agent_event() :: {started, binary()} |
{assistant_response, claude@types@message:message()} |
{tool_executing, binary(), binary()} |
{tool_completed, binary(), {ok, binary()} | {error, binary()}} |
{done, agent_result()} |
{failed, agent_error()}.
-file("src/claude/agent.gleam", 50).
?DOC(" Extract tool-use blocks from a list of content blocks.\n").
-spec extract_tool_calls(list(claude@types@content:content_block())) -> list(claude@types@content:content_block()).
extract_tool_calls(Content) ->
gleam@list:filter(Content, fun(Block) -> case Block of
{tool_use, _, _, _} ->
true;
_ ->
false
end end).
-file("src/claude/agent.gleam", 62).
?DOC(
" Build a user message containing tool results.\n"
"\n"
" Each result is a tuple of (tool_use_id, Ok(content) | Error(error_message)).\n"
).
-spec build_tool_results_message(
list({binary(), {ok, binary()} | {error, binary()}})
) -> claude@types@message:message_param().
build_tool_results_message(Results) ->
Blocks = gleam@list:map(
Results,
fun(Result) ->
{Id, Outcome} = Result,
case Outcome of
{ok, Content} ->
claude@types@content:tool_result(Id, Content, false);
{error, Err} ->
claude@types@content:tool_result(Id, Err, true)
end
end
),
{message_param, user, {block_content, Blocks}}.
-file("src/claude/agent.gleam", 124).
?DOC(
" Recursive agent loop that emits events at each step.\n"
"\n"
" This is the single canonical implementation of the agent loop.\n"
" Both `agent.run` (with a no-op callback) and `actor.run_with_events`\n"
" delegate to this function.\n"
"\n"
" The `on_event` callback is called at each notable step. Pass `fn(_) { Nil }`\n"
" for a silent loop, or a real callback for event-driven usage.\n"
).
-spec event_loop(
claude@agent@config:agent_config(),
list(claude@types@message:message_param()),
integer(),
integer(),
integer(),
fun((agent_event()) -> nil)
) -> {ok, agent_result()} | {error, agent_error()}.
event_loop(Config, Messages, Iteration, Total_input, Total_output, On_event) ->
case Iteration >= erlang:element(9, Config) of
true ->
Err = {max_iterations_reached, lists:reverse(Messages), Iteration},
On_event({failed, Err}),
{error, Err};
false ->
case claude@messages:create(
{request_params,
erlang:element(2, Config),
{some, erlang:element(3, Config)},
{some, erlang:element(4, Config)},
lists:reverse(Messages),
erlang:element(6, Config),
erlang:element(5, Config),
erlang:element(7, Config),
erlang:element(8, Config)}
) of
{error, Api_error} ->
Err@1 = {api_call_failed, Api_error},
On_event({failed, Err@1}),
{error, Err@1};
{ok, Response} ->
On_event({assistant_response, Response}),
New_input = Total_input + erlang:element(
2,
erlang:element(8, Response)
),
New_output = Total_output + erlang:element(
3,
erlang:element(8, Response)
),
Assistant_params = claude@types@message:content_to_params(
erlang:element(4, Response)
),
Assistant_msg = {message_param,
assistant,
{block_content, Assistant_params}},
Updated_messages = [Assistant_msg | Messages],
case erlang:element(6, Response) of
{some, tool_use_stop} ->
Tool_calls = extract_tool_calls(
erlang:element(4, Response)
),
gleam@list:each(
Tool_calls,
fun(Block) -> case Block of
{tool_use, Id, Name, _} ->
On_event({tool_executing, Name, Id});
_ ->
nil
end end
),
Results = claude@agent@tool_runner:execute_concurrent(
Tool_calls,
erlang:element(6, Config),
erlang:element(10, Config)
),
gleam@list:each(
Results,
fun(Result) ->
{Id@1, Outcome} = Result,
On_event({tool_completed, Id@1, Outcome})
end
),
Tool_results_msg = build_tool_results_message(
Results
),
Next_messages = [Tool_results_msg |
Updated_messages],
event_loop(
Config,
Next_messages,
Iteration + 1,
New_input,
New_output,
On_event
);
_ ->
Result@1 = {agent_result,
lists:reverse(Updated_messages),
Response,
Iteration + 1,
New_input,
New_output},
On_event({done, Result@1}),
{ok, Result@1}
end
end
end.
-file("src/claude/agent.gleam", 84).
?DOC(
" Run the agent loop.\n"
"\n"
" Starts with the given prompt, sends messages to the API, and automatically\n"
" handles tool-use responses by executing tools and feeding results back.\n"
" Continues until the model stops with a non-tool-use reason or max_iterations\n"
" is reached.\n"
).
-spec run(claude@agent@config:agent_config(), binary()) -> {ok, agent_result()} |
{error, agent_error()}.
run(Config, Prompt) ->
Initial_messages = [claude@types@message:new_user(Prompt)],
event_loop(
Config,
lists:reverse(Initial_messages),
0,
0,
0,
fun(_) -> nil end
).
-file("src/claude/agent.gleam", 102).
?DOC(
" Run the agent loop with an existing message history.\n"
"\n"
" Like `run` but allows passing in an existing conversation history.\n"
).
-spec run_with_messages(
claude@agent@config:agent_config(),
list(claude@types@message:message_param())
) -> {ok, agent_result()} | {error, agent_error()}.
run_with_messages(Config, Messages_list) ->
event_loop(Config, lists:reverse(Messages_list), 0, 0, 0, fun(_) -> nil end).