# Tools And Operations

Tools are model-callable work. Jidoka gives the model one operation contract:
name, description, parameters, and metadata. Actions, Ash resources, browser
tools, MCP tools, workflows, and subagents all compile to that shape.

## Use This When

- authoring a new tool for an agent;
- debugging "the model called the wrong operation" or
  "the operation handler was not found".
- writing deterministic tests that need a known set of
  operations.
- skip this guide for memory writes; use [Memory](memory.md).

## Prerequisites

- A working Jidoka project (see [Getting Started](getting-started.md)).
- Familiarity with `Jido.Action` and Zoi schemas.
- A provider key in scope for live examples.

```bash
mix deps.get
mix test
```

## Define A Tool

The minimum example is one `Jidoka.Action` and one DSL agent that lists it.

```elixir
defmodule MyApp.Tools.LocalTime do
  use Jidoka.Action,
    name: "local_time",
    description: "Returns the local time for a city.",
    schema: Zoi.object(%{city: Zoi.string() |> Zoi.default("Chicago")})

  @impl true
  def run(params, _context) do
    city = Map.get(params, :city) || Map.get(params, "city") || "Chicago"
    {:ok, %{city: city, time: "09:30"}}
  end
end

defmodule MyApp.TimeAgent do
  use Jidoka.Agent

  agent :time_agent do
    model "openai:gpt-4o-mini"
    instructions "Call local_time when asked for the time."
  end

  tools do
    action MyApp.Tools.LocalTime
  end
end

{:ok, text} = MyApp.TimeAgent.chat("What time is it in Chicago?")
```

The model sees an operation named `"local_time"`. If it calls the operation,
Jidoka runs `MyApp.Tools.LocalTime`, sends the result back to the model, and
returns the final answer.

## Concepts

A Jidoka operation has two parts:

1. **Operation metadata** (`Jidoka.Agent.Spec.Operation`) is pure data the
   model sees: name, description, parameter schema (in `metadata`),
   idempotency policy, and a free-form `metadata` map that includes the
   originating `source` (action, ash_resource, browser, mcp, subagent,
   handoff, workflow, local) and a `kind` tag used for control
   matching.
2. **Runtime capability** is a 2-arity function Jidoka calls with the
   `Jidoka.Effect.Intent` and the current `Jidoka.Effect.Journal`. Its job
   is to resolve the call to `{:ok, output}` or `{:error, reason}`.

```diagram
╭───────────────╮     ╭──────────────────────╮     ╭────────────────╮
│  tools block  │────▶│ Agent.Spec.Operation │────▶│ Model decision │
│  (or source)  │     │  (name + metadata)   │     ╰────────┬───────╯
╰───────┬───────╯     ╰──────────────────────╯              │
        │                                                   │ {:operation,
        │                                                   │  name, args}
        ▼                                                   ▼
╭───────────────────────╮                       ╭────────────────────╮
│ Runtime capability fn │◀──────────────────────│ Operation request  │
│ (intent, journal)     │   Jidoka invokes      │                    │
╰───────────┬───────────╯                       ╰────────────────────╯
            │
            ▼
     {:ok, output} | {:error, reason}
```

The model asks for `{:operation, name, arguments}`. Jidoka executes the
operation and records the result in the turn journal before asking the model
for the final answer.

### Operation Kinds And Matching

`Jidoka.Agent.Spec.Operation.kind/1` returns one of `:action`, `:operation`,
`:tool`, `:ash_resource`, `:browser`, `:skill`, `:mcp`, `:workflow`,
`:subagent`, `:handoff`. Operation controls in the `controls`
block match against `kind`, `name`, `source`, `idempotency`, and arbitrary
`metadata` keys:

```elixir
operation MyApp.RequireApproval,
  when: [kind: :handoff]

operation MyApp.LogTransfers,
  when: [name: :transfer_funds, idempotency: :unsafe_once]

operation MyApp.SourceGuard,
  when: [source: "mcp"]
```

The first matching control wins per intent. See [Controls](controls.md) for
the policy decisions a control can return.

### Idempotency Policies (Overview)

Every operation declares one of:

- `:pure` - safe to call repeatedly, no side effects.
- `:idempotent` - default; safe to retry, the runtime may de-duplicate by
  payload.
- `:dedupe` - the operation expects the runtime to skip duplicate intents
  inside a turn.
- `:reconcile` - the runtime should re-derive the result from authoritative
  state on replay.
- `:unsafe_once` - the operation must run at most once; replay requires a
  recorded result. `Operation.requires_control?/1` is true for this kind, so
  declaring an explicit operation control is recommended.

This guide covers the authoring path. Full idempotency, pause/resume, and replay
behavior live in [Runtime And Harness](runtime-and-harness.md).

## How To

### Step 1: Author A Tool With Jidoka.Action

`Jidoka.Action` wraps `Jido.Action`. The `:name` and
`:schema` are what the model sees; everything else feeds into the
`Agent.Spec.Operation` metadata.

```elixir
defmodule MyApp.Tools.Echo do
  use Jidoka.Action,
    name: "echo",
    description: "Echoes a phrase back.",
    schema: Zoi.object(%{phrase: Zoi.string()})

  @impl true
  def run(%{phrase: phrase}, _context), do: {:ok, %{echoed: phrase}}
end
```

Add it to the agent:

```elixir
tools do
  action MyApp.Tools.Echo
end
```

The compiled spec now includes:

```elixir
%Jidoka.Agent.Spec.Operation{
  name: "echo",
  description: "Echoes a phrase back.",
  idempotency: :idempotent,
  metadata: %{"source" => "jido_action", "kind" => "action", ...}
}
```

### Step 2: Match Operations With Controls

Operation controls run before the runtime executes the capability. They
gate, log, or interrupt model-chosen calls.

```elixir
defmodule MyApp.NoExternalBrowser do
  use Jidoka.Control, name: "no_external_browser"

  @impl true
  def call(%Jidoka.Runtime.Controls.OperationContext{} = op) do
    if op.metadata["source"] == "browser", do: {:block, :browser_blocked}, else: :cont
  end
end

controls do
  operation MyApp.NoExternalBrowser, when: [kind: :browser]
end
```

The `when` map can mix `:kind`, `:name`, `:source`, `:idempotency`, and any
key inside `metadata`. Matching is exact string/atom comparison.

### Step 3: Expose Local Capabilities In Tests

The fastest way to provide operations without writing modules is
`Jidoka.Operation.Source.Local`. It compiles a list of `{name, handler}`
entries into both the operation metadata and a runtime capability.

```elixir
{:ok, %{operations: operations, capability: capability}} =
  Jidoka.Operation.Source.compile(
    Jidoka.Operation.Source.Local.new!(
      operations: [
        %{name: "local_time", handler: fn _args -> {:ok, %{time: "09:30"}} end},
        %{name: "echo", handler: fn %{"phrase" => phrase} -> {:ok, %{echoed: phrase}} end}
      ]
    )
  )

spec =
  Jidoka.agent!(
    id: "ops_demo",
    model: "openai:gpt-4o-mini",
    instructions: "Use the available operations.",
    operations: operations
  )

llm = fn _intent, journal ->
  case map_size(journal.results) do
    0 -> {:ok, %{type: :operation, name: "echo", arguments: %{"phrase" => "hi"}}}
    _ -> {:ok, %{type: :final, content: "done"}}
  end
end

{:ok, result} = Jidoka.turn(spec, "ping", llm: llm, operations: capability)
result.content
#=> "done"
```

Local handlers may be `(args -> term)` or `(intent, journal -> term)`. A bare
term return value is wrapped in `{:ok, value}`.

### Step 4: Use Source-Backed Tools

The DSL exposes higher-level sources that all compile to operations:

```elixir
tools do
  action MyApp.Tools.LocalTime
  ash_resource MyApp.Accounts.User, actions: [:read]
  browser :docs, allow: ["docs.example.com"]
end
```

Each entry contributes one or more `Agent.Spec.Operation` entries with
distinct names. Duplicate operation names are a compile error.

### Step 5: Inspect The Resulting Operations

Before you spend a token, check the compiled operations and metadata:

```elixir
Jidoka.inspect(MyApp.TimeAgent).spec.operations
#=> [%{name: "local_time", idempotency: :idempotent, metadata: %{"kind" => "action", ...}}]

{:ok, preflight} = Jidoka.preflight(MyApp.TimeAgent, "What time is it?")
preflight.prompt.tool_definitions
```

`preflight` shows exactly what the prompt assembler will hand the model, so
you can confirm names, descriptions, and parameter schemas line up with what
the LLM expects.

## Common Patterns

- **One operation per side effect.** Smaller operations match better and
  control rules read more clearly.
- **Use `Jidoka.Action` for production tools.** It gives schema validation,
  consistent error shapes, and Jido instrumentation for free.
- **Use `Jidoka.Operation.Source.Local` for tests and one-off demos.** It
  removes module ceremony and keeps the LLM/operation contract obvious.
- **Tag your operations.** A short `metadata: %{"kind" => :transfer}` makes
  control matching `when: [kind: :transfer]` work without surprise.
- **Default to `:idempotent`.** Reserve `:unsafe_once` for genuinely
  irreversible side effects so Jidoka can require approval before running
  them.

## Testing

A deterministic operation test pins both the LLM decision and the operation
result. The runtime never reaches a provider.

```elixir
defmodule MyApp.TimeAgentTest do
  use ExUnit.Case, async: true

  test "uses the local_time operation" do
    operations =
      Jidoka.Runtime.LocalOperations.operations(%{
        "local_time" => fn %{"city" => city} -> {:ok, %{city: city, time: "09:30"}} end
      })

    llm = fn _intent, journal ->
      case map_size(journal.results) do
        0 -> {:ok, %{type: :operation, name: "local_time", arguments: %{"city" => "Chicago"}}}
        _ -> {:ok, %{type: :final, content: "Chicago time is 09:30."}}
      end
    end

    assert {:ok, result} =
             Jidoka.turn(MyApp.TimeAgent, "What time is it?",
               llm: llm,
               operations: operations
             )

    assert result.content =~ "09:30"

    [operation_result] =
      result.agent_state.operation_results

    assert operation_result.operation == "local_time"
  end
end
```

`Jidoka.Runtime.LocalOperations.operations/1` is the test helper for
building an operation capability from a map of handlers. The same helper
backs `Jidoka.Operation.Source.Local`.

## Troubleshooting

| Symptom | Likely Cause | Fix |
| --- | --- | --- |
| `{:error, {:missing_operation_handler, name}}` | The LLM chose an operation no capability resolves. | Add the handler to the registered capability, or restrict the prompt so the model cannot pick it. |
| `{:error, {:unsupported_effect_kind, kind}}` | A capability was handed an intent it does not understand. | Make sure the capability matches the intent kind (`:operation`); chain capabilities with a router if you serve multiple kinds. |
| `tool :name is defined more than once` at compile time | Two DSL entries produced the same operation name. | Rename one entry (`as: :other_name` on subagent/handoff, or pick a different action). |
| Operation control never fires | `when:` did not match `kind/name/source/metadata`. | Inspect `Jidoka.inspect(agent).spec.operations` to see the exact metadata, then mirror the keys in `when:`. |
| Live model picks invalid `arguments` | Schema in the action does not match the LLM-facing description. | Tighten the schema or update the description; preflight shows the JSON-schema the model receives. |

## Reference

- [`Jidoka.Agent.Spec.Operation`](`Jidoka.Agent.Spec.Operation`) - operation
  data, `kind/1`, `requires_control?/1`, `replay_safe?/1`.
- [`Jidoka.Operation.Source`](`Jidoka.Operation.Source`) - behaviour for
  sources that compile to operations plus a capability.
- [`Jidoka.Operation.Source.Local`](`Jidoka.Operation.Source.Local`) - the
  function-backed source used by tests and examples.
- [`Jidoka.Runtime.LocalOperations`](`Jidoka.Runtime.LocalOperations`) -
  capability builder for raw handler maps.
- [`Jidoka.Action`](`Jidoka.Action`) - the Jido action wrapper used in
  production tools.
- Tool-source compiler - internal compiler
  from DSL entries to operations and capabilities.

## Related Guides

- [Agent DSL](agent-dsl.md) - the DSL that owns the `tools`
  block.
- [Controls](controls.md) - input/operation/output policy and approvals.
- [Handoffs](handoffs.md) - the handoff source and conversation ownership.
- [Testing And Evals](testing-and-evals.md) - golden DSL-to-spec tests and
  the `Jidoka.Eval` runner.
- [Inspection And Preflight](inspection-and-preflight.md) - debugging the
  compiled operations and prompt before running a turn.
