RobotRock

Vercel AI

Use robotrock/ai when you build agents with the Vercel AI SDK (generateText, streamText, or ToolLoopAgent) and need humans in the loop.

The integration supports two patterns:

  1. Callable tools — the model calls approveByHuman or sendToHuman when it needs a person.
  2. Tool approval bridge — dangerous tools (for example deleteFile) pause until someone approves them in the RobotRock inbox.

Installation

npm install robotrock ai
# or
bun add robotrock ai

ai is a peer dependency of robotrock/ai. Install both in the app that runs your agent.

Client setup

Tools block inside execute until a human handles the task. Configure polling on the client (or a webhook for fire-and-forget creation only — not for blocking tools).

// lib/robotrock.ts
import { createClient } from "robotrock";

export const robotrock = createClient({
  app: "my-agent",
  polling: {
    intervalMs: 2_000,
    timeoutMs: 30 * 60_000,
  },
});

Set ROBOTROCK_API_KEY in your environment. See Send to human and Polling.

Execution modes

ModeWhere to useHow it waits
polling (default)Scripts, long-lived servers, API routes with a generous timeoutclient.sendToHuman() polls the API
Trigger.dev (mode: "trigger")Trigger.dev workers running generateTextsendToHumanTask / approveByHumanTask wait tokens (durable)
Vercel Workflow (mode: "workflow")Vercel Workflow runs and DurableAgent toolssendToHumanInWorkflow / createWebhook() (durable)

Use Trigger.dev when the Vercel AI SDK runs inside a Trigger.dev task, or Vercel Workflow when it runs inside a Vercel Workflow run (including @workflow/ai DurableAgent). Polling inside a worker still works but does not checkpoint the wait; prefer durable modes for production.

Register SDK tasks once (same as Trigger.dev):

// trigger/robotrock.ts
export { sendToHumanTask, approveByHumanTask } from "robotrock/trigger";

Polling mode

import { approveByHumanTool } from "robotrock/ai";

const approveByHuman = approveByHumanTool(robotrock);
// or explicitly:
const approveByHuman = approveByHumanTool(robotrock, { mode: "polling" });

Trigger.dev

import { approveByHumanTool, createRobotRockAiTools } from "robotrock/ai/trigger";

// Option 1: pass mode on the tool factory
const approveByHuman = approveByHumanTool({
  mode: "trigger",
  app: "my-agent",
  defaultType: "ai-approval",
});

// Option 2: helper
const { approveByHuman, sendToHuman } = createRobotRockAiTools({
  mode: "trigger",
  app: "my-agent",
});

robotrock/ai/trigger is the same API with Trigger.dev-oriented defaults documented in one place. Import paths from robotrock/ai also support mode: "trigger".

Vercel Workflow

import { approveByHumanTool, createRobotRockAiTools } from "robotrock/ai/workflow";

const approveByHuman = approveByHumanTool({
  mode: "workflow",
  app: "my-agent",
});

const { approveByHuman: approve, sendToHuman } = createRobotRockAiTools({
  mode: "workflow",
  app: "my-agent",
});

robotrock/ai/workflow is the same API with Vercel Workflow-oriented defaults documented in one place. Import paths from robotrock/ai also support mode: "workflow".

See Vercel Workflow for sendToHumanInWorkflow and webhook behavior.

For the tool approval bridge in Trigger.dev workers, pass context instead of a client:

import { runWithRobotRockApprovals } from "robotrock/ai";

await runWithRobotRockApprovals({
  context: { mode: "trigger", app: "my-agent" },
  generate: (messages) => generateText({ model, tools, toolApproval, messages, prompt }),
});

Callable tools

Register RobotRock tools on your generateText / streamText call. When the model invokes them, execute waits for a human (polling, Trigger.dev, or Vercel Workflow, depending on mode) and returns the decision to the model.

approveByHumanTool

Fixed approve / decline actions. The model supplies name, description, and an optional contextSummary.

import { generateText, stepCountIs } from "ai";
import { robotrock } from "@/lib/robotrock";
import { approveByHumanTool } from "robotrock/ai";

const result = await generateText({
  model: "anthropic/claude-sonnet-4",
  tools: {
    approveByHuman: approveByHumanTool(robotrock, { defaultType: "release-gate" }),
  },
  stopWhen: stepCountIs(10),
  system:
    "Before finalizing sensitive plans, call approveByHuman with a clear summary.",
  prompt: "Draft a production rollout plan.",
});

Tool result shape (returned to the model):

{
  taskId: string;
  actionId: string;       // "approve" | "decline" | ...
  data: unknown;
  handledBy?: string;
  handledAt: string;      // ISO 8601
  approved?: boolean;     // set for approve/decline-style actions
}

createSendToHumanTool

You define actions and JSON schemas at factory time. The model only fills presentation fields (type, name, description, optional context) so it cannot invent invalid action ids.

import { createSendToHumanTool } from "robotrock/ai";

const askHuman = createSendToHumanTool(robotrock, {
  defaultType: "ai-agent-input",
  actions: [
    {
      id: "answer-questions",
      title: "Answer questions",
      schema: {
        type: "object",
        required: ["priority"],
        properties: {
          priority: { type: "string", enum: ["low", "high"] },
        },
      },
    },
  ] as const,
});

const result = await generateText({
  model: "anthropic/claude-sonnet-4",
  tools: { askHuman },
  prompt: "Clarify requirements with a human if anything is ambiguous.",
});

Tool approval bridge

Use this when the model calls your tools (for example filesystem or payment tools) and you want approval in the RobotRock inbox instead of only an in-app button.

Flow:

  1. Model requests a tool call.
  2. AI SDK emits tool-approval-request parts (manual approval).
  3. resolveToolApprovalsViaRobotRock creates inbox tasks and polls.
  4. You append tool-approval-response messages and call the model again.
  5. Approved tools execute; denied tools surface a denial to the model.

AI SDK 7+

Pass createRobotRockToolApproval to toolApproval on the generation call or agent:

import { generateText } from "ai";
import {
  createRobotRockToolApproval,
  runWithRobotRockApprovals,
} from "robotrock/ai";

const deleteFile = tool({
  description: "Delete a file path",
  inputSchema: z.object({ path: z.string() }),
  execute: async ({ path }) => {
    await removeFile(path);
    return { ok: true };
  },
});

const toolApproval = createRobotRockToolApproval({
  tools: ["deleteFile"],
});

const result = await runWithRobotRockApprovals({
  client: robotrock,
  maxRounds: 20,
  generate: (messages) =>
    generateText({
      model: "anthropic/claude-sonnet-4",
      tools: { deleteFile },
      toolApproval,
      messages,
      prompt: "Remove old logs in /tmp",
    }),
});

AI SDK 5–6

Set needsApproval on tools with applyRobotRockToolApprovalToTools or createRobotRockNeedsApproval, then resolve manually:

import {
  applyRobotRockToolApprovalToTools,
  resolveToolApprovalsViaRobotRock,
} from "robotrock/ai";

const tools = applyRobotRockToolApprovalToTools(
  { deleteFile, runCommand },
  { tools: ["deleteFile"] }
);

const round1 = await generateText({ model, tools, prompt: "..." });
const { messages } = await resolveToolApprovalsViaRobotRock(robotrock, round1);
const round2 = await generateText({ model, tools, messages, prompt: "..." });

Custom inbox copy

Override how approval tasks appear in the inbox:

import { defaultFormatToolApprovalTask, resolveToolApprovalsViaRobotRock } from "robotrock/ai";

const { messages } = await resolveToolApprovalsViaRobotRock(robotrock, round1, {
  formatTask: (toolCall) => ({
    ...defaultFormatToolApprovalTask(toolCall),
    name: `Policy check: ${toolCall.toolName}`,
  }),
});

Default actions are approve and deny. Map approveapproved: true, anything else → denied.

Streaming and useChat

Run resolveToolApprovalsViaRobotRock on the server (API route or background worker). The bridge is not a client-side hook.

After a stream finishes, scan result.content or accumulated messages for tool-approval-request parts, resolve via RobotRock, append tool messages with responses, then start the next generation.

Where to run

EnvironmentModeCallable toolsApproval bridge
Trigger.dev workermode: "trigger"approveByHumanTool({ mode: "trigger" })context: { mode: "trigger" }
Vercel Workflow / DurableAgentmode: "workflow"approveByHumanTool({ mode: "workflow" })context: { mode: "workflow" }
Long-running NodepollingapproveByHumanTool(robotrock)client: robotrock
Vercel serverlesspolling (short timeout only)Short polling.timeoutMsPrefer Trigger.dev or Vercel Workflow mode

For durable waits without holding an HTTP connection open, use mode: "trigger" (Trigger.dev) or mode: "workflow" (Vercel Workflow), or call sendToHumanTask / sendToHumanInWorkflow directly.

Trigger.dev example

This repo includes apps/trigger-test/trigger/ai-sdk-approval.ts: a Trigger.dev task that runs generateText with approveByHumanTool({ mode: "trigger" }) from robotrock/ai/trigger.

Required env vars:

  • ROBOTROCK_API_KEY
  • ROBOTROCK_BASE_URL (local dev)
  • Model credentials for your AI SDK provider (for example AI_SDK_MODEL)

On this page