Skip to main content
This example is a support agent written with the Vercel AI SDK. It has two capabilities:
  • chat streams its answer as an AI SDK UI message stream. Your app’s backend starts the run with ?passthrough=true and hands the response to the browser unchanged, so useChat reads it with no mapping.
  • pay_supplier gives the model a pay tool that needs approval. The run is held until a person approves, edits or rejects the payment, then generateText continues where it stopped.
Every model call goes through the AgentOS gateway with the run token, so the environment’s models, guardrails, token counts and costs apply.

The bundle

The folder also holds vendor/agentos-sdk-0.1.0.tgz, the TypeScript SDK (see SDKs: it isn’t on npm yet and ships with AgentOS). Node 24 runs agent.ts directly. Set config.models.main to a model from your environment’s Models page, zip the folder with vendor/ in it and deploy it.

How the model is wired

agentosModel(inv) is an AI SDK model for the role main in config.models, served by the gateway. Its requests go through inv.fetch, which adds the run token only for the gateway and the platform and only while the invocation runs. Build it inside the handler, once per invocation: the token ends when the invocation answers. Pass another role for another model, for example agentosModel(inv, "judge").

Stream chat to a useChat frontend

The browser never talks to AgentOS and never sees your API key. Your backend starts a run of chat with ?passthrough=true and returns the response as it arrives: AgentOS relays the agent’s status, Content-Type, Cache-Control, X-Accel-Buffering and X-Vercel-AI-* headers and its body byte for byte, plus X-AgentOS-Run-Id (see Passthrough).
useChat sends the whole conversation each turn, so the agent needs no history of its own. To tie a conversation to one of your customers, create a session with {"user": "<their id>"} and pass its session_id in the run body; the agent then sees inv.user and can keep memory per user. From a terminal, the same call shows the stream:
Before the stream ends the agent reports the run’s outcome, so the run is completed with {"text": ...} as its output (see Streaming answers).

Approve a tool call

runWithApprovals is generateText with the SDK’s approval handling. When the model calls pay, the conversation so far is kept in run state and one hold asks a person about every pending call (approve, edit its input, or reject). On the resume the decisions become tool-approval responses: an approved call runs once, a rejected one tells the model why, and generateText continues.
1

Start the run

2

Decide

A reviewer decides in the panel’s Review queue, or through the API. Each item’s id is the tool call’s id; read it from the hold:
3

Read the result

The run continues with the decision and completes:

Good to know

  • Errors in a stream. streamText only logs errors by default, including a guardrail’s block. The example passes onError to both streamText and toUIMessageStreamResponse: the browser gets the guardrail’s message as an error part, and the run fails with blocked_by_guardrail instead of completing with a cut answer. See Guardrails.
  • Report before the stream ends. Call inv.setOutput(...) before the last chunk. A stream that ends without it fails the run with missing_output.
  • A held run’s resume isn’t streamed. A streaming capability that asks a person ends its stream; read the resumed run’s output with GET …/runs/{run_id}.
  • Run state is at most 1 MiB. runWithApprovals keeps the conversation in run state while the run is held, and it grows with the transcript.
  • Code before the ask runs again on the resume. runWithApprovals handles its own calls; keep anything else you do before it idempotent (see Human approvals).
Tested with ai 7.0.138, @ai-sdk/openai-compatible 3.0.68 and Node 24, with the conformance kit. More frameworks: Framework examples.