Human-in-the-loop for the Vercel AI SDK

Let your Vercel AI SDK agent ask a human on their phone.

Add an askHuman tool to generateText or streamText. When the model calls it, your user approves on their phone.

The integration

Add it to your Vercel AI SDK agent

Install @pushary/ai-sdk and call createPusharyTools({ externalId }). It returns an askHuman tool you spread into the tools of generateText or streamText. When the model calls it, Pushary sends a push notification to that user's phone, and the tool blocks until the person approves, denies, or types an answer. The result flows back as a normal tool result, so the agent loop continues on the human's decision. It is fail-closed, so no answer means no approval.

npm i @pushary/ai-sdktwo calls
import { generateText, stepCountIs } from "ai"
import { openai } from "@ai-sdk/openai"
import { createPusharyTools, enroll } from "@pushary/ai-sdk"

// Connect the end-user's phone once. Keyless, one tap, no account of ours.
await enroll({ apiKey: process.env.PUSHARY_API_KEY! }, user.id)

const { text } = await generateText({
  model: openai("gpt-4o"),
  tools: createPusharyTools({ apiKey: process.env.PUSHARY_API_KEY!, externalId: user.id }),
  stopWhen: stepCountIs(10),
  prompt: "Issue the refund only if a human approves it.",
})

Why you need it

Vercel AI SDK pauses the agent. Pushary reaches the human.

With toolApproval and @ai-sdk/policy-opa the AI SDK now answers what should happen to a tool call. It still does not deliver the requires-approval ones to a person, hold a wait that survives a deploy, or keep a record of who approved. Pushary completes that. The externalId is your own end-user, so the agent you shipped can ask each user about their own decision, not just ask you.

What Vercel AI SDK already gives you

toolApproval, and a policy that returns a verdict

The AI SDK does not just pause any more. Approval logic moved from needsApproval on the tool, now deprecated, onto toolApproval on generateText, streamText and ToolLoopAgent, and @ai-sdk/policy-opa evaluates each call against Open Policy Agent and answers allow, deny, requires-approval or not-applicable. That is a real policy layer and it is good. It answers what should happen. What it does not answer is who hears about the ones it cannot settle.

native
import { opaPolicy } from "@ai-sdk/policy-opa"

await generateText({
  model,
  tools,
  toolApproval: opaPolicy({ client, path: "agent/call/decision" }),
})

Where it stops

  • requires-approval still surfaces to your code, not to a person. The delivery, the waiting and the resume are yours to build.
  • The policy is Rego you deploy. Moving a refund threshold from $500 to $750 is a pull request, not a row your support lead edits.
  • It decides about the run, not about a named human. There is no notion of which of your end-users owns this action or how to reach them.
  • Running it means running OPA. That is a server or sidecar you operate, on top of the app you were already shipping.

Before you ship it

Things that bite on Vercel AI SDK

stopWhen has to allow another step
The agent has to take at least one more step after the human answers, otherwise generateText stops on the tool call and the approved action never runs. If you set stopWhen to stepCountIs(1) while testing, an approval looks like it did nothing.
The model decides when to ask, so say when in the prompt
askHuman is a tool like any other. If your prompt does not tell the model that refunds need approval, it will happily call the refund tool directly and never ask. Name the guarded actions explicitly.
Streaming hides the pause
With streamText the UI keeps rendering while the tool blocks, so it can look like the agent is still thinking when it is actually waiting on a person. Surface the pending decision in your UI or users will retry the prompt.

How it works

From ask to resume

Connect the phone once, then ask a human from the tool or step that guards the action. The waiting, the delivery, and the record are handled for you.

  1. 01

    Install and connect a phone

    Run npm i @pushary/ai-sdk. Connect each end-user's phone once with a keyless one-tap enroll link keyed to your own externalId.

  2. 02

    Add the askHuman tool

    Call createPusharyTools({ externalId }) and spread the returned tools into your tools object in generateText or streamText. Keep your existing stopWhen loop.

  3. 03

    Tell the model when to ask

    In your prompt, say when the model should ask a human, for example before issuing a refund or deleting a record. The model calls askHuman at that step.

  4. 04

    Act on the decision

    The tool blocks until the person answers, then returns the outcome to the model as a tool result. A declined, expired, or unanswered confirm is reported as not approved.

A decision is a yes or no confirm, a multiple-choice select, or a free-text input. Every one is stored with its outcome and kept in the audit log.

The Partner plan

Your agent asks your users, not just you

Ship the agent to real users and let each of them approve their own decisions from their phone, under your brand.

$99/ month, every framework included

Your users never see a Pushary login or bill.

Your users answer on their own phones

The externalId is your own id for the end-user. Connect their phone once with a keyless one-tap link that opens the Pushary app, and every approval lands with the right person. No account and no key on their side.

Durable, fail-closed waits

The agent stays open on a durable wait backed by a decisions ledger. If nobody answers in the window you set, the decision comes back denied, so a timeout or a restart never turns into an unapproved action.

Phone-first delivery

Pushary reaches the person on the device they carry: the app first, because it is the only channel that puts Approve and Deny on a lock screen, then PWA push notifications, then Slack if you route it there. You do not build a delivery pipeline.

Signed webhooks and an audit trail

Every decision is written to a durable ledger you can query and export, and resolved decisions post a signed webhook to your callback URL. You have the record of who approved what and when.

FAQ

Questions, answered

Is there a Vercel AI SDK package for Pushary?
Yes. @pushary/ai-sdk is published on npm. Call createPusharyTools({ externalId }) and it returns an askHuman tool you add to your tools map, plus an enroll helper to connect a user's phone.
How does the askHuman tool pause the agent?
The tool's execute function creates a decision and waits durably for the answer. The AI SDK holds the tool call open until execute resolves, so the agent loop pauses on that step and continues once the human decides.
What happens if the person never answers?
It fails closed. If nobody answers in your window, the tool reports the decision as not approved, so the model does not proceed with the guarded action. Because the wait is backed by a durable ledger, a restart does not lose the request.
Do my end-users need a Pushary account or API key?
No. You reference each user by your own externalId. They connect a phone once with a one-tap link that opens the Pushary app, and they need no account and no key of their own. They never see a Pushary login or bill.
Where does the notification get delivered?
To the person on the device they carry: the app first, because it is the only channel that puts Approve and Deny on a lock screen, then PWA push notifications for desktop and Android, then Slack if you route it there.

Let your Vercel AI SDK agent ask a human.

Connect a user with one tap, ask a human with one call, and act on a fail-closed decision. Every decision is saved to a durable ledger you can audit.

Partner plan, $99 a month. Your users never see a Pushary login or bill.