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:
- Callable tools — the model calls
approveByHumanorsendToHumanwhen it needs a person. - 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 aiai 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
| Mode | Where to use | How it waits |
|---|---|---|
polling (default) | Scripts, long-lived servers, API routes with a generous timeout | client.sendToHuman() polls the API |
Trigger.dev (mode: "trigger") | Trigger.dev workers running generateText | sendToHumanTask / approveByHumanTask wait tokens (durable) |
Vercel Workflow (mode: "workflow") | Vercel Workflow runs and DurableAgent tools | sendToHumanInWorkflow / 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:
- Model requests a tool call.
- AI SDK emits
tool-approval-requestparts (manual approval). resolveToolApprovalsViaRobotRockcreates inbox tasks and polls.- You append
tool-approval-responsemessages and call the model again. - 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 approve → approved: 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
| Environment | Mode | Callable tools | Approval bridge |
|---|---|---|---|
| Trigger.dev worker | mode: "trigger" | approveByHumanTool({ mode: "trigger" }) | context: { mode: "trigger" } |
| Vercel Workflow / DurableAgent | mode: "workflow" | approveByHumanTool({ mode: "workflow" }) | context: { mode: "workflow" } |
| Long-running Node | polling | approveByHumanTool(robotrock) | client: robotrock |
| Vercel serverless | polling (short timeout only) | Short polling.timeoutMs | Prefer 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_KEYROBOTROCK_BASE_URL(local dev)- Model credentials for your AI SDK provider (for example
AI_SDK_MODEL)
Related docs
- Send to human —
sendToHumanclient API used inside tools - Vercel Workflow — durable waits with Vercel Workflow webhooks
- Trigger.dev — durable waits with wait tokens
- Polling — blocking until handled without webhooks
- Task context — rich inbox UI for human reviewers