Tools and approvals

Add typed tools and control when the browser asks before a call.

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:

pnpm add typebox
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():

PolicyBehavior
"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().
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:

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:

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 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:

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.