# Give agents tools

```elixir
Mix.install([{:wardrik, "~> 0.1.0-rc.1"}, {:kino, "~> 0.19"}])
```

## What you'll build

By the end, an agent looks up orders on its own, asks you before it refunds one, and tells you
what each paid lookup cost. The tools say what they do. The policy reads that, not the prompt.

## Write the tools

A capability is a module that returns tools. This one keeps orders in an ETS table:

```elixir
:ets.new(:orders, [:named_table, :public])
:ets.insert(:orders, [{"A-1", "shipped"}, {"A-2", "delivered"}])

defmodule MyApp.Orders do
  @behaviour WardrikCore.Capability

  alias WardrikCore.Tool

  @impl true
  def tools(_context, _opts) do
    [
      Tool.new!(
        name: "order_status",
        description: "Says where an order is.",
        schema: [order_id: [type: :string, required: true]],
        run: fn %{order_id: id}, _context -> {:ok, status(id)} end,
        annotations: [read_only: true, idempotent: true]
      ),
      Tool.new!(
        name: "refund_order",
        description: "Refunds an order in full.",
        schema: [order_id: [type: :string, required: true]],
        run: fn %{order_id: id}, _context ->
          :ets.insert(:orders, {id, "refunded"})
          {:ok, "Refunded #{id}."}
        end,
        annotations: [destructive: true]
      ),
      Tool.new!(
        name: "track_parcel",
        description: "Asks the carrier where an order's parcel is.",
        schema: [order_id: [type: :string, required: true]],
        run: fn %{order_id: id}, context ->
          # A paid carrier API: report what the call cost.
          {:ok, "#{id} is at the Leeds depot.", WardrikCore.Context.measure(context, :usd, 0.002)}
        end,
        annotations: [read_only: true, open_world: true]
      )
    ]
  end

  def status(id) do
    case :ets.lookup(:orders, id) do
      [{^id, status}] -> status
      [] -> "unknown"
    end
  end
end
```

Each tool carries annotations:

* `read_only` - it changes nothing.
* `idempotent` - a call is safe to repeat. Wardrik repeats one lost in a crash.
* `destructive` - what it changes can't be undone.
* `open_world` - it reaches outside your systems.

`track_parcel` returns a third element: the context, with what the call cost. Wardrik records it
on the call, and the agent's budget counts it.

## Defaults by annotation

A policy's `default` decides each call no rule names. It can depend on the annotations:

```elixir
policy = WardrikPolicy.new!(default: [read_only: :allow, destructive: :ask, else: :ask])

decisions =
  for tool <- MyApp.Orders.tools(nil, []) do
    %{
      tool: tool.name,
      annotations: inspect(Map.keys(tool.annotations)),
      decision: inspect(WardrikPolicy.decide(policy, tool.name, annotations: tool.annotations))
    }
  end

Kino.DataTable.new(decisions, keys: [:tool, :annotations, :decision])
```

Reads run. The refund waits for a person. A new tool you forget to annotate falls to `else`, and
asks too. A rule that names a tool wins over the default.

## Give them to an agent

Start Wardrik with the capability, under the name `orders`, and define an agent that uses it:

```elixir
{:ok, _} =
  Wardrik.start_link(
    name: MyApp.AgentRuntime,
    capabilities: [orders: MyApp.Orders],
    mcp: [
      deepwiki: [transport: {:streamable_http, url: "https://mcp.deepwiki.com/mcp"}, gated: false]
    ]
  )

scope = Wardrik.as(MyApp.AgentRuntime, "acme/@maria", roles: [:owner], resources: ["acme/@maria/*"])

{:ok, agent} =
  Wardrik.Agent.define(scope, "acme/@maria/support",
    model: "anthropic:claude-haiku-4-5",
    instructions: "You answer questions about Acme's orders. Be brief.",
    tools: ["orders"],
    policy: [
      default: %{read_only: :allow, destructive: :ask, else: :ask},
      budget: [usd: 0.50, per: :day]
    ]
  )
```

The `deepwiki` server is for the last section.

## Ask it something

From here on, the agent calls a model. Add your Anthropic key as `ANTHROPIC_API_KEY` in the
Secrets panel (the key icon on the left). The rest of the notebook costs about two cents.

```elixir
System.put_env("ANTHROPIC_API_KEY", System.fetch_env!("LB_ANTHROPIC_API_KEY"))
```

The answer streams into the frame as the model writes it:

```elixir
frame = Kino.Frame.new() |> Kino.render()

scope
|> Wardrik.Agent.stream(agent, "Where's order A-1, and where's its parcel?")
|> Enum.reduce("", fn chunk, text ->
  text = text <> chunk
  Kino.Frame.render(frame, Kino.Markdown.new(text))
  text
end)

Kino.nothing()
```

Both tools are read-only, so both ran without asking.

## Ask it to change something

A refund is destructive. The run stops and returns the call it waits on:

```elixir
{:waiting, [refund]} = Wardrik.Agent.reply(scope, agent, "Refund order A-2, please.")

Map.take(refund, [:name, :message])
```

Nothing changed yet:

```elixir
MyApp.Orders.status("A-2")
```

Approve it. The agent carries on, and the refund runs:

```elixir
{:ok, _decisions} = Wardrik.Agent.approve(scope, agent, refund.id)

{:ok, reply} = Wardrik.Agent.reply(scope, agent, "What's A-2's status now?")
reply.text
```

A message sent while the agent runs waits its turn, so this reply comes after the refund.

```elixir
MyApp.Orders.status("A-2")
```

See the [approvals notebook](approvals-demo.livemd) to decide calls with buttons.

## What it cost

Each tool call is in the agent's log. `track_parcel` reported its own cost:

```elixir
{:ok, page} = Wardrik.Agent.events(scope, agent, types: ["wardrik.tool_call.completed"])

page.events
|> Enum.map(fn event ->
  %{
    tool: event.data["tool"]["name"],
    status: event.data["status"],
    cost_usd: (event.data["cost"]["micro_usd"] || 0) / 1_000_000
  }
end)
|> Kino.DataTable.new(keys: [:tool, :status, :cost_usd], name: "Tool calls")
```

Each run, with its tokens and what it cost in all:

```elixir
{:ok, runs} = Wardrik.Agent.runs(scope, agent)

runs.runs
|> Enum.map(
  &%{
    status: &1.status,
    tool_calls: &1.tool_calls,
    input_tokens: &1.input_tokens,
    output_tokens: &1.output_tokens,
    cost_usd: &1.cost_micro_usd / 1_000_000
  }
)
|> Kino.DataTable.new(
  keys: [:status, :tool_calls, :input_tokens, :output_tokens, :cost_usd],
  name: "Runs"
)
```

## Tools from an MCP server

[DeepWiki](https://deepwiki.com) runs a public MCP server that needs no sign-in. Wardrik started
with it as `deepwiki`, so a definition names its tools as `deepwiki/*`:

```elixir
{:ok, wiki} =
  Wardrik.Agent.define(scope, "acme/@maria/researcher",
    model: "anthropic:claude-haiku-4-5",
    instructions: "You answer questions about open source projects. Be brief.",
    tools: ["deepwiki/*"],
    policy: [budget: [usd: 0.50, per: :day]]
  )

Wardrik.Agent.definition(scope, wiki)
```

The last element lists the servers it names that aren't reachable yet. `[]` means every one is.

DeepWiki doesn't mark its tools read-only, so Wardrik would treat them as able to change things,
and each call would wait for a person. Its tools only read, so Wardrik started it with
`gated: false`.

```elixir
frame = Kino.Frame.new() |> Kino.render()

scope
|> Wardrik.Agent.stream(wiki, "In one paragraph: how does elixir-lang/elixir implement Task.async_stream?")
|> Enum.reduce("", fn chunk, text ->
  text = text <> chunk
  Kino.Frame.render(frame, Kino.Markdown.new(text))
  text
end)

Kino.nothing()
```

An MCP server's answers come from outside. From then on, any call that can change something waits
for a person, whatever `allow` says. Here that would be a refund, had this agent the `orders`
tools. See [Policies](https://hexdocs.pm/wardrik/policies.html).

## Where next

* [Define agents](https://hexdocs.pm/wardrik/definitions.html): every key of a definition.
* [Policies](https://hexdocs.pm/wardrik/policies.html): rules, defaults and budgets.
* [MCP servers](https://hexdocs.pm/wardrik/servers.html): servers that need a sign-in, and those an
  org adds as it runs.
* [Approvals notebook](approvals-demo.livemd): approve and deny waiting calls with buttons.
* [Events notebook](events-demo.livemd): see everything an agent did.
