You want model-selected tool calls with Jido.Action modules, plus deterministic terminal shapes for one-shot and multi-turn runs.
After this guide, you can use:
Jido.AI.Actions.ToolCalling.CallWithToolsJido.AI.Actions.ToolCalling.ExecuteToolJido.AI.Actions.ToolCalling.ListTools
defmodule MyApp.Actions.Multiply do
use Jido.Action,
name: "multiply",
schema: Zoi.object(%{a: Zoi.integer(), b: Zoi.integer()})
@impl true
def run(%{a: a, b: b}, _context), do: {:ok, %{product: a * b}}
endOne-shot mode returns a terminal turn map without executing tools automatically.
alias Jido.AI.Actions.ToolCalling.CallWithTools
params = %{
prompt: "What is 6 * 7?",
tools: ["multiply"]
}
context = %{
tools: %{"multiply" => MyApp.Actions.Multiply}
}
{:ok, result} = Jido.Exec.run(CallWithTools, params, context)
# result.type == :tool_calls or :final_answerAuto-execute mode runs tools and continues until a final answer or max-turn limit.
alias Jido.AI.Actions.ToolCalling.CallWithTools
params = %{
prompt: "Use multiply to compute 6 * 7 and explain briefly.",
tools: ["multiply"],
auto_execute: true,
max_turns: 5
}
context = %{
tools: %{"multiply" => MyApp.Actions.Multiply}
}
{:ok, result} = Jido.Exec.run(CallWithTools, params, context)
# result.type == :final_answer when loop completes
# result.turns includes executed loop turns
# result.messages includes assistant/tool conversation messagesDeterministic terminal shapes:
- completed loop:
%{type: :final_answer, text: text, usage: usage, turns: turns, messages: messages, model: model} - max turns reached:
%{type: :tool_calls, reason: :max_turns_reached, turns: max_turns, usage: usage, model: model}
Use direct execution when your app already chose the tool and args.
alias Jido.AI.Actions.ToolCalling.ExecuteTool
params = %{
tool_name: "multiply",
params: %{a: 6, b: 7}
}
context = %{
tools: %{"multiply" => MyApp.Actions.Multiply}
}
{:ok, result} = Jido.Exec.run(ExecuteTool, params, context)
# %{tool_name: "multiply", status: :success, result: %{product: 42}}ListTools defaults to excluding sensitive tool names (include_sensitive: false) and returns only public metadata (name, optional serialized schema).
alias Jido.AI.Actions.ToolCalling.ListTools
context = %{
tools: %{
"multiply" => MyApp.Actions.Multiply,
"admin_delete_user" => MyApp.Actions.AdminDeleteUser
}
}
{:ok, public_tools} = Jido.Exec.run(ListTools, %{}, context)
{:ok, all_tools} = Jido.Exec.run(ListTools, %{include_sensitive: true}, context)
{:ok, allowlisted} = Jido.Exec.run(ListTools, %{allowed_tools: ["multiply"]}, context)Security filtering behavior:
- default denylist filtering by sensitive name fragments (
admin,delete,token,secret, etc.) - explicit override with
include_sensitive: true - optional allowlist hard filter with
allowed_tools: [...]
CallWithTools, ExecuteTool, and ListTools resolve tool registries with this precedence:
context[:tools]context[:tool_calling][:tools]context[:chat][:tools]context[:state][:tool_calling][:tools]context[:state][:chat][:tools]context[:agent][:state][:tool_calling][:tools]context[:agent][:state][:chat][:tools]context[:plugin_state][:tool_calling][:tools]context[:plugin_state][:chat][:tools]
First non-nil registry wins. This keeps behavior deterministic across direct action execution, plugin-routed calls, and fallback context paths.
{:ok, _agent} = Jido.AI.register_tool(agent_pid, MyApp.Actions.Multiply)
{:ok, true} = Jido.AI.has_tool?(agent_pid, "multiply")Symptom:
ExecuteToolreturns tool-not-found error content
Fix:
- pass a tools map in one of the registry precedence paths above
- verify
module.name/0matches the requestedtool_name
CallWithToolsauto_executedefault:falseCallWithToolsmax_turnsdefault:10(hard-capped by validation)ExecuteTooltimeout default:30_000msListToolsschema inclusion default:trueListToolssensitive filtering default: enabled (include_sensitive: false)