-module(claude@agent@tool_runner). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/claude/agent/tool_runner.gleam"). -export([execute_concurrent/3]). -export_type([tool_message/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 tool_message() :: {tool_success, binary(), {ok, binary()} | {error, binary()}} | {tool_crashed, gleam@erlang@process:exit_reason()}. -file("src/claude/agent/tool_runner.gleam", 96). ?DOC(" Format an exit reason into a human-readable string.\n"). -spec format_exit_reason(gleam@erlang@process:exit_reason()) -> binary(). format_exit_reason(Reason) -> case Reason of normal -> <<"normal exit"/utf8>>; killed -> <<"killed"/utf8>>; {abnormal, Reason@1} -> <<"abnormal: "/utf8, (gleam@string:inspect(Reason@1))/binary>> end. -file("src/claude/agent/tool_runner.gleam", 29). ?DOC( " Execute tool calls concurrently using BEAM processes.\n" "\n" " Spawns one unlinked, monitored process per tool call, then collects\n" " results with a timeout. Results are returned in the same order as the\n" " input tool_calls. Non-ToolUse blocks in the input list are silently\n" " ignored.\n" "\n" " If a tool handler exceeds the timeout, an Error(\"Tool execution timed out\")\n" " is returned for that tool. If a tool handler crashes, the monitor detects\n" " the exit and an Error describing the crash reason is returned immediately\n" " rather than waiting for the full timeout.\n" ). -spec execute_concurrent( list(claude@types@content:content_block()), claude@types@tool:registry(), integer() ) -> list({binary(), {ok, binary()} | {error, binary()}}). execute_concurrent(Tool_calls, Tools, Timeout) -> Calls = gleam@list:filter_map(Tool_calls, fun(Block) -> case Block of {tool_use, Id, Name, Input} -> {ok, {Id, Name, Input}}; _ -> {error, nil} end end), Tasks = gleam@list:map( Calls, fun(Call) -> {Id@1, Name@1, Input@1} = Call, Subject = gleam@erlang@process:new_subject(), Pid = proc_lib:spawn( fun() -> Result = claude@types@tool:dispatch(Tools, Name@1, Input@1), gleam@erlang@process:send(Subject, {Id@1, Result}) end ), Mon = gleam@erlang@process:monitor(Pid), {Id@1, Subject, Mon} end ), gleam@list:map( Tasks, fun(Task) -> {Id@2, Subject@1, Mon@1} = Task, Selector = begin _pipe = gleam_erlang_ffi:new_selector(), _pipe@1 = gleam@erlang@process:select_map( _pipe, Subject@1, fun(Msg) -> {Tool_id, Tool_result} = Msg, {tool_success, Tool_id, Tool_result} end ), gleam@erlang@process:select_specific_monitor( _pipe@1, Mon@1, fun(Down) -> {tool_crashed, erlang:element(4, Down)} end ) end, Result@1 = case gleam_erlang_ffi:select(Selector, Timeout) of {ok, {tool_success, _, Tool_result@1}} -> {Id@2, Tool_result@1}; {ok, {tool_crashed, Reason}} -> {Id@2, {error, <<"Tool execution crashed: "/utf8, (format_exit_reason(Reason))/binary>>}}; {error, nil} -> {Id@2, {error, <<"Tool execution timed out"/utf8>>}} end, gleam@erlang@process:demonitor_process(Mon@1), Result@1 end ).