Human in the Loop

Hook Approval Requests

Request human approval from a tool-call hook.

Prefer tool-level approval policies for normal side-effect tools. Use onToolCall(...) when approval is truly request-lifecycle behavior, such as a cross-tool rule, a temporary rollout policy, or a decision that cannot live on one tool definition.

A hook can request approval before it returns tool.run(), tool.skip(...), or tool.cancel(...).

Studio handles tool.requestApproval(...) automatically. Outside Studio, configure .approvals(...) on the agent or request; without an approval handler, core throws ToolApprovalRequiredError instead of executing the guarded tool.

Require Approval for One Tool

import { createHook } from "@anvia/core";

const approvalHook = createHook({
  onToolCall({ toolName, tool }) {
    if (toolName !== "refund_order") {
      return tool.run();
    }

    return tool.requestApproval({
      reason: "Review this refund request.",
      rejectMessage: "Refund was not approved.",
    });
  },
});

Use the args value when the reviewer needs to see exactly what the model is trying to do.

Attach the Hook

Attach the hook to one request when approval rules are request-specific.

const response = await agent
  .prompt("Refund order A-100.")
  .withHook(approvalHook)
  .send();

Attach the hook to the agent when the rule should apply to every request.

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

Require Approval by Tool Name

Keep the approval rule explicit.

const sensitiveTools = new Set(["refund_order", "cancel_subscription"]);

const approvalHook = createHook({
  onToolCall({ toolName, tool }) {
    if (!sensitiveTools.has(toolName)) {
      return tool.run();
    }

    return tool.requestApproval({
      reason: `${toolName} requires human review.`,
      rejectMessage: `${toolName} was not approved.`,
    });
  },
});

Use normal tool execution for safe read-only tools such as lookup or search.

Require Approval by Argument

Parse the pending tool arguments when only some calls need review.

import { createHook } from "@anvia/core";
import { parseToolArgs } from "@anvia/core/tool";

const approvalHook = createHook({
  onToolCall({ toolName, args, tool }) {
    if (toolName !== "issue_refund") {
      return tool.run();
    }

    const parsed = parseToolArgs(args);
    if (
      typeof parsed !== "object" ||
      parsed === null ||
      !("amount" in parsed) ||
      typeof parsed.amount !== "number"
    ) {
      return tool.cancel("Invalid refund request.");
    }

    if (parsed.amount <= 100) {
      return tool.run();
    }

    return tool.requestApproval({
      reason: `Refund amount is $${parsed.amount}.`,
      rejectMessage: "Refund was not approved.",
    });
  },
});

Use tool.cancel(...) for malformed or unsafe runs that should stop immediately. Use tool.skip(...) when the model can continue with a tool result message.

Request Hook vs Agent Hook

Use .withHook(...) when approval rules are specific to one prompt request.

await agent
  .prompt("Refund order A-100.")
  .withHook(approvalHook)
  .send();

Use .hook(...) when the rule is a stable part of the agent.

const agent = new AgentBuilder("support", model)
  .tool(refundOrder)
  .hook(approvalHook)
  .build();

Use tool-level approval instead when the rule belongs to a specific side-effect tool.

const refundOrder = createTool({
  name: "refund_order",
  description: "Refund an eligible order.",
  input: z.object({
    orderId: z.string(),
    amount: z.number().positive(),
  }),
  approval: {
    when: ({ args }) => args.amount > 100,
    reason: ({ args }) => `Refund amount is $${args.amount}.`,
    rejectMessage: "Refund was not approved.",
  },
  async execute({ orderId, amount }) {
    return refunds.create(orderId, amount);
  },
});

const agent = new AgentBuilder("support", model)
  .tool(refundOrder)
  .approvals({
    handler: ({ toolName, args, reason }) =>
      approvals.waitForDecision({ toolName, args, reason }),
  })
  .build();

Rejected Approval

When approval is rejected, Studio sends the configured rejection message back to the model as the tool result.

return tool.requestApproval({
  reason: "Review this action.",
  rejectMessage: "The reviewer rejected this action.",
});

The model receives this text as the tool result, then it can produce a final answer.

Timeout Policy

Anvia does not require a timeout. Studio approval waits until a reviewer approves or rejects. If your application owns a custom approval system, add a timeout in that app code when the caller needs bounded latency.

const approved = await Promise.race([
  approvalRuntime.waitForDecision({ toolName, args }),
  sleep(60_000).then(() => false),
]);

return approved
  ? tool.run()
  : tool.skip("No reviewer approved this action in time.");