
# Tools and approvals

The Pi loop includes no host tools by default. Add only the capabilities the agent needs.
A tool can read application state, call an API, or act on the page, and its result becomes
part of the transcript the model can continue from.

## Add a tool

Pi tools use a TypeBox parameter schema. Install TypeBox when your application imports it
directly:

```sh
pnpm add typebox
```

```ts
import { Type } from "typebox";
import { createPiSession } from "agentak/pi";

const session = createPiSession({
  tools: [
    {
      name: "read_selection",
      label: "Read selected text",
      description: "Read the text selected in the current document.",
      parameters: Type.Object({}, { additionalProperties: false }),
      execute: async () => ({
        content: [
          {
            type: "text",
            text: String(document.getSelection() ?? ""),
          },
        ],
        details: undefined,
      }),
    },
  ],
});
```

The schema is sent to the model and validates the arguments before `execute()` runs. A
result can contain text or image content. Agentak renders the call, its arguments, status,
result, and any returned image together in the assistant turn.

If `execute()` throws, the call is shown as an error result and the loop can continue from
that failure.

## Confirmation policies

Set `approvals` on `createPiSession()`:

| Policy     | Behavior                                                                                                |
| ---------- | ------------------------------------------------------------------------------------------------------- |
| `"always"` | Ask before every call.                                                                                  |
| `"once"`   | Ask for the first call of each tool, then remember approval until the gate or the conversation changes. |
| `"never"`  | Run every host and page tool without asking. This is the default for `createPiSession()`.               |

```ts
const session = createPiSession({
  approvals: "always",
  tools,
});
```

A pending call pauses on a promise while the chat shows **Allow** and **Deny**. A denial can
include a reason. Pi receives that reason instead of tool output, so the model can adjust
its next step. Stopping or resetting the conversation denies any pending calls.

The chat carries the switch for this in the bar under the composer: **Ask** puts the gate
up, **Bypass** takes it away. It reads `"never"` as bypass and every other policy as ask,
and the policy you set here is the one **Ask** restores — without one, that is `"once"`.
Turning the gate off allows any call already waiting at it; turning it back on forgets what
`"once"` had remembered, so every tool is asked about again. Nothing is stored, so a new
session opens on the policy you set.

`createApprovalGate()` still defaults to `"once"`: a custom runtime carries no such switch,
so it opens on the gate.

## Set a policy per tool

`approvalFor` can override the session policy for selected host tools:

```ts
const session = createPiSession({
  approvals: "once",
  approvalFor(name) {
    if (name === "search_docs") return "never";
    if (name === "delete_record") return "always";
    return undefined;
  },
  tools,
});
```

Returning `undefined` keeps the session-wide rule. A session-wide `approvals: "never"`
disables the gate completely and therefore outranks per-tool policies — including the
`"never"` a session takes by default, and the one the chat's **Bypass** switch sets.

Agentak also uses this hook internally for page tools. A WebMCP tool marked read-only runs
without asking; a page tool that may change state asks every time, once the gate is up.

## Host tools and page tools together

`tools` contains capabilities supplied by your application. `page: true` or a custom
`PageTools` source adds browser page tools beside them:

```ts
const session = createPiSession({
  tools: [myApplicationTool],
  page: true,
});
```

Host tool names are reserved. If a page publishes the same name, Agentak adds a numeric
suffix instead of replacing the host tool.

Read [WebMCP page tools](/agents/pi/webmcp) for discovery, safety annotations, and custom
page bridges.

## Run a tool from the composer

The composer lists every tool the agent has under its `/model` and `/new` commands. Type a
slash, pick a tool, and press Enter: Agentak runs that tool, adds the call and its result
to the transcript, and continues the conversation, so the model reads the result and
answers from it.

The call is made with no arguments, because the chat has no way to type any. A tool that
needs arguments fails, the failure becomes the tool result, and the model then calls the
tool again with the arguments it works out. The confirmation policy is not applied to
these calls: the person chose the tool, which is the confirmation.

While the tool runs, the chat behaves as it does during a turn. A message typed meanwhile
is queued, and **Stop** ends the run.

## Calls during streaming

A message sent while the agent is already working becomes a queued follow-up. The composer
changes its placeholder to **Queue a message…**, the chat shows pending messages above it,
and the user can remove one before Pi consumes it. Pi drains the queue after the current
turn.

The same store handles stopping, retrying, and tool approval events, so the widget remains
responsive while a tool or provider is active.

## Use the gate without a full session

The lower-level `createApprovalGate()` export provides the same behavior for a custom Pi
runtime:

```ts
import { createApprovalGate } from "agentak/pi";

const gate = createApprovalGate("once", (name) => (name === "search" ? "never" : undefined));
```

Its `beforeToolCall` function plugs into Pi, while `pending()`, `answers()`, `respond()`, and
`subscribe()` let a custom surface expose the confirmation state. `policy()` and
`setPolicy()` are the pair behind the chat's own switch, for a surface that wants one.
