Tool-handler invocation contract.
Minimum impl skeleton
defmodule MyExecutor do
@behaviour ALLM.ToolExecutor
@impl true
def execute(%ALLM.Tool{handler: handler}, arguments, opts) do
# arity-1: handler.(arguments)
# arity-2: handler.(arguments, opts)
# Convert raises / exits / bad returns to
# {:error, %ALLM.Error.ToolError{}}.
handler.(arguments)
end
endexecute/3 takes a %ALLM.Tool{}, the parsed arguments map, and a
keyword opts list carrying call context (:context, :session_id,
:request_id, :tool_call, :engine). It invokes the tool's handler
and returns the handler's return value unchanged — with two exceptions
that belong to the executor, not the handler:
A handler raise / exit / throw / bad return is converted to
{:error, %ALLM.Error.ToolError{}}with a:reasonatom drawn from the closed set:handler_raised | :handler_exit | :timeout | :invalid_return | :encoding_failed | :not_found.A
nilhandler (the%Tool{}was declared for manual-mode use) is converted to{:error, %ALLM.Error.ToolError{reason: :not_found}}the executor cannot invoke a tool with no handler.
Handler-returned {:error, _} values are NOT converted; they pass
through unchanged so the orchestrator can pattern-match on them
via the on_tool_error policy. The distinction is "did the handler
crash, or did it report a failure?" — both are failures, but the
orchestrator handles them differently.
Invariants
execute/3receives a%Tool{}whosenamewas looked up by the caller; executors do not consult a registry.execute/3returns one of the fiveALLM.Tool.handler_result/0variants unchanged for handler-returned values; executor-originated failures are{:error, %ToolError{}}structs.optsis populated by the caller; executors do not synthesize:session_id/:request_id/ etc. Missing keys read asnilwhen the executor forwards them to an arity-2 handler.- Handler arity dispatch:
:erlang.fun_info(handler, :arity)—1callshandler.(arguments);2callshandler.(arguments, opts). Any other arity is an invalid handler and raisesArgumentErrorfrom the executor.
Handler result shapes
The five legal handler-returned shapes:
{:ok, value}— success;valueis handed to theALLM.ToolResultEncoder.{:error, reason}— handler-originated failure; passes through unchanged foron_tool_errordispatch.{:ask_user, question}— suspend the loop and surface a question to the user.{:ask_user, question, opts}— same, with caller-supplied options.{:halt, reason, result}— halt the loop with a handler-declared terminal result.
Summary
Callbacks
Invoke a tool's handler and return its result.
Callbacks
@callback execute(ALLM.Tool.t(), map(), keyword()) :: ALLM.Tool.handler_result()
Invoke a tool's handler and return its result.
Executor-originated error reasons
The following %ToolError{reason: ...} atoms are produced by the
executor itself (not by handlers). Handler-returned {:error, reason}
tuples pass through unchanged and do NOT carry these atoms.
| Reason | Fires when |
|---|---|
:handler_raised | Handler raised an exception; :cause carries the exception struct. Throws are also normalized here with cause: {:throw, value}. |
:handler_exit | Handler called exit/1 or the handling process died; :cause carries the exit reason term. |
:timeout | Emitted by ALLM.ToolRunner when a handler exceeds tool_timeout. The default in-process executor does not produce this directly — tests that need it use a handler that returns the struct. |
:invalid_return | Handler returned a value that is not one of the five handler_result variants; :cause carries the offending term. |
:not_found | The %Tool{} has handler: nil — the tool is not executable by this executor (typically declared for manual-mode use). |
:encoding_failed | Reserved for encoders; executors do not produce this reason. |