# Tool Calling With Actions

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.CallWithTools`
- `Jido.AI.Actions.ToolCalling.ExecuteTool`
- `Jido.AI.Actions.ToolCalling.ListTools`

## Define A Tool Action

```elixir
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}}
end
```

## Tool I/O Boundary

Tool actions may be pure or effectful. In ReAct-style loops, the model calls a
tool because it needs the result before it can continue reasoning, so HTTP, LLM,
database, file, or retrieval I/O inside `run/2` is normal when that result is the
tool output.

This direct I/O is separate from tool-emitted effects returned in the optional
third tuple. `effect_policy` filters returned effects such as `StateOp.SetState`
or `Directive.Emit`; it does not require every `run/2` implementation to be pure.

Use directives, signals, or runtime integrations when the workflow has already
decided on an outbound effect and wants the runtime to own delivery, retries, or
observability.

| Workflow step | Use |
|---|---|
| Fetch an account record, search an index, call a weather API, or ask another model so the current turn can continue | Effectful tool action that returns the data from `run/2` |
| Send a final email, publish a webhook, emit an audit signal, or dispatch a notification after the decision is made | Runtime-owned directive, signal, or integration |

## One-Shot Tool Calling (`CallWithTools`)

One-shot mode returns a terminal turn map without executing tools automatically.

```elixir
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_answer
```

## Auto-Execute Tool Loop (`CallWithTools`)

Auto-execute mode runs tools and continues until a final answer or max-turn limit.

```elixir
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 messages
```

Deterministic 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}`

## Direct Tool Execution (`ExecuteTool`)

Use direct execution when your app already chose the tool and args.

```elixir
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}}
```

## Tool Discovery And Security Filtering (`ListTools`)

`ListTools` defaults to excluding sensitive tool names (`include_sensitive: false`) and returns only public metadata (`name`, optional serialized `schema`).

```elixir
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: [...]`

## Tool Registry Precedence (Tool Map / Context / Plugin State)

`CallWithTools`, `ExecuteTool`, and `ListTools` resolve tool registries with this precedence:

1. `context[:tools]`
2. `context[:tool_calling][:tools]`
3. `context[:chat][:tools]`
4. `context[:state][:tool_calling][:tools]`
5. `context[:state][:chat][:tools]`
6. `context[:agent][:state][:tool_calling][:tools]`
7. `context[:agent][:state][:chat][:tools]`
8. `context[:plugin_state][:tool_calling][:tools]`
9. `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.

## Dynamic Registration On A Running Agent

```elixir
{:ok, _agent} = Jido.AI.register_tool(agent_pid, MyApp.Actions.Multiply)
{:ok, true} = Jido.AI.has_tool?(agent_pid, "multiply")
```

## Transform Agent Tool Calls And Results

`Jido.AI.Agent` provides optional callbacks for data that must have a different
model-facing representation. For example, an agent can replace a long record
key with a short alias in a tool result, then resolve that alias before the
model calls another tool.

```elixir
defmodule MyApp.SupportAgent do
  use Jido.AI.Agent,
    name: "support_agent",
    tools: [MyApp.Actions.GetTicket, MyApp.Actions.UpdateTicket]

  @impl Jido.AI.ToolInterceptor
  def before_tool_call(%{arguments: arguments} = tool_call, context) do
    resolved_arguments = MyApp.ToolAliases.resolve(arguments, context)
    {:ok, %{tool_call | arguments: resolved_arguments}}
  end

  @impl Jido.AI.ToolInterceptor
  def after_tool_call(_tool_call, {:ok, result, effects}, context) do
    projected_result = MyApp.ToolAliases.project(result, context)
    {:ok, {:ok, projected_result, effects}}
  end

  def after_tool_call(_tool_call, result, _context), do: {:ok, result}
end
```

The callback contract is:

- `before_tool_call/2` runs once before argument validation and retries.
- It can change `arguments`, but it cannot change the call ID, tool name, or action module.
- `after_tool_call/3` runs once after the final attempt.
- It receives and must return a canonical `{:ok | :error, value, effects}` result.
- The active effect policy filters effects returned by `after_tool_call/3`.
- Callback errors and invalid returns stop the current tool round.

Callbacks receive the configured tool context plus runtime data such as
`:state`, `:request_id`, and `:agent_id`. Keep callback work short. The
callbacks run in the agent strategy path, not in the action process.

These callbacks apply to agent-managed ReAct and Tree-of-Thoughts tool
execution. Direct `Jido.Exec`, `Jido.AI.Turn`, `CallWithTools`, and
`ExecuteTool` calls do not invoke them.

## Failure Mode: Tool Execution Returns `:not_found`

Symptom:
- `ExecuteTool` returns tool-not-found error content

Fix:
- pass a tools map in one of the registry precedence paths above
- verify `module.name/0` matches the requested `tool_name`

## Defaults You Should Know

- `CallWithTools` `auto_execute` default: `false`
- `CallWithTools` `max_turns` default: `10` (hard-capped by validation)
- `ExecuteTool` timeout default: `30_000ms`
- `ListTools` schema inclusion default: `true`
- `ListTools` sensitive filtering default: enabled (`include_sensitive: false`)

## Next

- [Actions Catalog](../developer/actions_catalog.md)
- [Plugins And Actions Composition](../developer/plugins_and_actions_composition.md)
