A tool the agent can call. Implementations are plain modules, so a host
can register one at runtime by module, or serve changing sets of them
from a source (see Rho.ToolRegistry).
run/2 gets the decoded JSON arguments (string keys) and a context map
(:session_id, :call_id, :data_dir; from a loop also :loop, its
pid, and :withheld, the tool names that loop doesn't offer, so read it
with Map.get(ctx, :withheld, [])). It returns {:ok, output} or
{:error, reason}; non-binary values are JSON-encoded (or inspected) for
the model. Pictures, and data for the host's screens beside the text
({:ok, {:details, output, details}}), are Rho.ToolRunner's. A raise,
exit or timeout becomes an error result; it never takes the loop down.
Untrusted tools. The loop reads a tool's declaration (name/0,
description/0, parameter_schema/0, timeout_ms/0,
concurrency/0) in its own process. A Dyn tool's
(Rho.Untrusted.untrusted?/1) is read instead in one throwaway process
with a time and heap bound, checked to be plain data and kept for the
module's life: one whose name, description or schema raises or isn't a
string / JSON Schema map is left out of what the loop offers (logged
once), and a bad timeout_ms/0 or concurrency/0 means the default. A
read that runs over (a busy phone) leaves the tool out for now and is
tried again after 1, 5 and 30 minutes; after that it stays out.
Summary
Callbacks
How a call shares its turn with the reply's other calls (omp's tool
concurrency). :parallel (the default) runs alongside them;
:exclusive is for a tool that acts on or reads something only one
call may touch at a time (the front's one screen): a turn with any
exclusive call runs all its calls one at a time, in the model's order.
JSON Schema (string keys) of the arguments object.
Checks the tool works, without side effects outside a temp dir.
Per-call timeout; default default_timeout_ms/0.
Functions
module's concurrency/0, or :parallel. The loop asks it in its own
process, so anything but :exclusive, a raise included, is :parallel.
The per-call timeout of a tool without timeout_ms/0 (30 000 ms).
module's per-call timeout: its timeout_ms/0, else
default_timeout_ms/0. A Dyn tool's comes from its checked declaration.
The req_llm declaration sent to the model, nil for an untrusted tool whose declaration failed (it isn't offered). Execution never goes through req_llm's callback: the loop runs tools itself (in parallel, gated, with timeouts).
Is module a loaded tool (exports the required callbacks)?
Types
@type info() :: %{ name: String.t(), description: String.t(), parameter_schema: map(), timeout_ms: pos_integer(), concurrency: :parallel | :exclusive }
A tool's declaration: its callbacks' answers, checked.
Callbacks
@callback concurrency() :: :parallel | :exclusive
How a call shares its turn with the reply's other calls (omp's tool
concurrency). :parallel (the default) runs alongside them;
:exclusive is for a tool that acts on or reads something only one
call may touch at a time (the front's one screen): a turn with any
exclusive call runs all its calls one at a time, in the model's order.
@callback description() :: String.t()
@callback name() :: String.t()
@callback parameter_schema() :: map()
JSON Schema (string keys) of the arguments object.
@callback selftest() :: :ok | {:error, term()}
Checks the tool works, without side effects outside a temp dir.
@callback timeout_ms() :: pos_integer()
Per-call timeout; default default_timeout_ms/0.
Functions
@spec concurrency(module()) :: :parallel | :exclusive
module's concurrency/0, or :parallel. The loop asks it in its own
process, so anything but :exclusive, a raise included, is :parallel.
@spec default_timeout_ms() :: pos_integer()
The per-call timeout of a tool without timeout_ms/0 (30 000 ms).
@spec timeout_ms(module()) :: pos_integer()
module's per-call timeout: its timeout_ms/0, else
default_timeout_ms/0. A Dyn tool's comes from its checked declaration.
@spec to_req_llm(module()) :: ReqLLM.Tool.t() | nil
The req_llm declaration sent to the model, nil for an untrusted tool whose declaration failed (it isn't offered). Execution never goes through req_llm's callback: the loop runs tools itself (in parallel, gated, with timeouts).
Is module a loaded tool (exports the required callbacks)?