Human in the Loop

Approval Settings

Put passive approval policy on a tool for Studio to interpret.

Use tool approval settings when a side-effect tool should usually require review in Studio. The policy stays next to the tool definition, so users can move between Studio and their own frontend/backend without changing the tool contract.

Core only stores this metadata. Studio interprets it by installing a per-run request hook.

Protect a Tool

import { AgentBuilder, createTool } from "@anvia/core";
import { Studio } from "@anvia/studio";
import { z } from "zod";

const issueRefund = createTool({
  name: "issue_refund",
  description: "Issue a customer refund.",
  input: z.object({
    orderId: z.string(),
    amount: z.number().positive(),
    reason: z.string(),
  }),
  output: z.object({
    refundId: z.string(),
    status: z.enum(["issued"]),
  }),
  approval: {
    when: ({ args }) => args.amount > 100,
    reason: ({ args }) => `Review refund of $${args.amount} for ${args.orderId}.`,
    rejectMessage: "Refund was not approved.",
  },
  async execute({ orderId }) {
    return {
      refundId: `rf_${orderId.toLowerCase()}`,
      status: "issued" as const,
    };
  },
});

const agent = new AgentBuilder("support", model)
  .tool(issueRefund)
  .defaultMaxTurns(3)
  .build();

new Studio([agent]).start();

When the model calls issue_refund, Studio evaluates approval.when(...). If it returns true, Studio creates a pending approval. If the reviewer approves, Studio runs the tool. If the reviewer rejects, Studio skips the tool and returns the rejection message to the model.

Approval Context

type ToolApprovalContext<TArgs> = {
  toolName: string;
  args: TArgs;
  rawArgs: string;
  toolCallId?: string;
  internalCallId: string;
  run: {
    agentId: string;
    runId: string;
    sessionId?: string;
    metadata?: JsonObject;
  };
};

args is parsed with the tool input schema. Use it for conditional policy.

approval: {
  when: ({ args, run }) =>
    args.amount > 100 || run.metadata?.environment === "production",
}

Dynamic Reasons

Use reason to show reviewers what they are deciding.

approval: {
  when: ({ args }) => args.amount > 100,
  reason: ({ args }) =>
    `Approve ${args.reason} refund of $${args.amount} for ${args.orderId}.`,
  rejectMessage: ({ args }) =>
    `Refund ${args.orderId} was not approved.`,
}

reason and rejectMessage may be strings or async functions.

When to Use Hooks Instead

Use hook approval requests when approval depends on request-local state, app permissions, external policy services, or policy that is not tied to one tool definition.

Agent hooks run before normal tool execution. If a hook returns tool.skip(...) or tool.cancel(...), the tool does not run and no tool-level approval policy is evaluated. If it returns tool.requestApproval(...), the active approval handler decides that hook-created approval request.