Default tool definitions and execution for Arcana.Loop.
Tools are exposed to the controller LLM as ReqLLM.Tool structs. The
callbacks on those structs are placeholders: the loop runner executes
tools itself via execute/4 so it can mutate the loop state (accumulate
chunks, record history, terminate the loop).
Default toolset
| Tool | What it does |
|---|---|
search | Search the knowledge base. Uses graph data when available. |
answer | Provide the final answer (terminates the loop). |
give_up | Admit defeat and stop (terminates the loop). |
Terminating tools (answer and give_up) end the loop. search is the
only tool that touches your Repo and the only one that accumulates state.
Query rewriting and decomposition aren't separate tools — they happen
inside the search tool's query parameter, guided by the system prompt.
Custom tools
Pass tools: [...] to Loop.run/2 with your own ReqLLM.Tool structs
appended to (or replacing) the defaults. Custom tools are invoked via
their :callback function when the controller calls them.
The callback is a 1-arity function (args) -> {:ok, text} | {:error, text}
where text is the string returned to the controller as the tool result.
Custom tools always continue the loop (they never terminate it). The
controller can still call answer or give_up to end the loop.
web_search = ReqLLM.Tool.new!(
name: "web_search",
description: "Search the web for current information.",
parameter_schema: [query: [type: :string, required: true]],
callback: fn %{query: q} ->
case MyApp.WebSearch.run(q) do
{:ok, results} -> {:ok, format_results(results)}
{:error, reason} -> {:error, inspect(reason)}
end
end
)
Loop.run(ctx, tools: Tools.default() ++ [web_search], controller_llm: llm)
Summary
Functions
Returns the default list of tool definitions as ReqLLM.Tool structs.
Executes a tool call against the loop context.
Functions
@spec default(collections :: [String.t() | nil]) :: [ReqLLM.Tool.t()]
Returns the default list of tool definitions as ReqLLM.Tool structs.
These are the tools shipped with Arcana.Loop. Pass tools: default/0
(or omit :tools) to use them. Replace with your own list to customize.
Context-aware search tool
When called with a list of collection names longer than one, the
search tool gains an optional collection parameter that the
controller can use to narrow the search per call. When called with
no collections, [nil], or a single-element list, the parameter is
omitted and the caller-supplied collection (if any) is always used.
That way, Arcana.Loop.new(collection: "docs") locks the controller
to a single collection programmatically (the tool can't even express
a different one), while Arcana.Loop.new(collections: ["a", "b"])
lets the controller pick per call.
@spec execute(Arcana.Loop.Context.t(), String.t(), map(), keyword()) :: {:continue, Arcana.Loop.Context.t(), String.t(), map()} | {:terminate, Arcana.Loop.Context.t(), atom(), String.t()}
Executes a tool call against the loop context.
Returns one of:
{:continue, updated_ctx, summary_text, meta}- the loop should continue;summary_textis what gets sent back to the controller LLM as the tool result, andmetais a map of extra metadata to record in the tool history (e.g.%{returned_chunk_ids: [...]}for search).{:terminate, updated_ctx, reason, answer_text}- the loop should stop;reasonis one of:answeredor:gave_up;answer_textbecomesctx.answer.