Human in the Loop

Human in the Loop

Choose where human approval and feedback belong.

Human-in-the-loop means the agent run waits for a person before continuing. Use it for approvals, operator feedback, missing context, escalation decisions, outbound messages, refunds, account changes, deletes, and other workflows where the model should not decide alone.

Anvia keeps the core execution model small: tools run, hooks can run/skip/cancel/request approval, and awaited promises pause the run. Studio adds a zero-config UI layer for common approval and question flows.

Choose a Pattern

PatternBest forWhere it lives
Approval settings in toolsStudio approval UI with no custom hookTool metadata
Approval by hooksDynamic policy or request-specific rulesonToolCall(...)
Ask question toolsMissing user input while a run is activeNormal tools
Approval runtimesDatabases, queues, notifications, audit logs, optional timeoutsYour app

Studio Approval Metadata

Put approval policy next to the side-effect tool when you want Studio to handle the approval UI.

const refundOrder = createTool({
  name: "refund_order",
  description: "Issue a customer refund.",
  input: z.object({
    orderId: z.string(),
    amount: z.number().positive(),
  }),
  approval: {
    when: ({ args }) => args.amount > 100,
    reason: ({ args }) => `Review refund of $${args.amount} for ${args.orderId}.`,
    rejectMessage: "Refund was not approved.",
  },
  async execute({ orderId, amount }) {
    return refunds.create(orderId, amount);
  },
});

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

Core stores this metadata but does not enforce it. Studio reads it and installs a per-run request hook that waits for a decision.

Hook Approval

Use a hook when approval is not a property of the tool itself. Studio handles tool.requestApproval(...) with the same approval UI and API used for tool metadata.

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

    return tool.requestApproval({
      reason: "Refunds require staff approval.",
      rejectMessage: "Refund was not approved.",
    });
  },
});

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

The model sees skipped tools as tool results, so write rejection messages as text the model can use in its final answer.

Ask for Feedback

Use a normal tool when the model needs information from a person. Studio recognizes ask_question and renders a compact question UI.

const askQuestion = createTool({
  name: "ask_question",
  description: "Ask the human operator one or more follow-up questions.",
  input: z.object({
    questions: z.array(
      z.object({
        id: z.string(),
        question: z.string(),
        choices: z.array(z.object({
          label: z.string(),
          value: z.string(),
        })).min(1),
      }),
    ),
  }),
  async execute() {
    return { answers: [] };
  },
});

Outside Studio, implement ask_question by awaiting your app UI, queue, or websocket and returning the answer as the tool result.

What Anvia Owns

Anvia ownsYour app owns
detecting the tool callchoosing which tools need approval
awaiting hook and tool promisesshowing UI, sending notifications, or waiting on a queue
running the tool after tool.run()deciding who can approve
returning tool.skip(...) text to the modelstoring audit records

Timeouts

Timeouts are optional. If a hook, tool, or Studio question waits forever, the run waits forever. Add a timeout in your runtime when the caller needs bounded latency.