---
title: Vercel AI SDK
description: Connect a Vercel AI SDK agent to Redline — experiments, production monitoring and runtime protection. The complete document, written to be followed by a coding agent inside the repository.
---

You are connecting a repository whose agent is built on the **Vercel AI SDK** —
`generateText`, `streamText`, or a `ToolLoopAgent` — to the Redline platform:
experiments AND production monitoring. Follow these steps exactly and do not
modify the repository's existing agent code. The whole integration is new files
under `redline/`, plus one flag on AI SDK v6.

## 0. Confirm the shape

Grep the repository for `from "ai"` / `from 'ai'` together with
`generateText`, `streamText` or `ToolLoopAgent`. If present, this is the page.
If the agent is LangChain.js or a hand-rolled loop over the OpenAI client, it
is not — see [Integrations](/integrations) for the right page.

The Vercel AI SDK is **loader-hooked** by `redline dev`: attached tools reach
your agent as it is constructed, and its own telemetry fills the waterfall.
Nothing to merge by hand.

## 1. Install the SDK and scaffold

At the repository root:

```bash
npm i -D @redlineai/sdk
npx redline init
```

This creates `redline.config.ts` (leave it as generated) and
`redline/my-agent.ts`, the doorway file. Every file under `redline/` is imported
by the CLI: it must only export agents and must not start a server on import.

## 2. Find the repository's agent entry point

Locate the function that RUNS the agent — the one that receives a prompt or a
messages array and returns (or streams) the model's answer. Search for
`generateText(`, `streamText(`, `new ToolLoopAgent(`, `.generate(`,
`.stream(`. Read its signature. **Do NOT modify it.**

## 3. Wire it into `redline/my-agent.ts`

Replace the TODO. The contract:

- Export `defineAgent({ id, name, description, run })` from `@redlineai/sdk`.
- `id` is a kebab-case slug unique to this agent.
- `run(task, ctx)` is async. `task.prompt` is the task's instructions. It
  **must return the final answer as a string**.
- If the entry point takes a messages array, pass
  `[{ role: "user", content: task.prompt }]`.
- If it STREAMS, collect the streamed text and return the whole of it at the end.
- If it needs a working directory, create a fresh one:
  `fs.mkdtempSync(path.join(os.tmpdir(), "run-"))`.

```ts title="redline/my-agent.ts"
import { defineAgent } from "@redlineai/sdk";
import { customerSupportAgent } from "../src/support-agent";   // unchanged

export const supportAgent = defineAgent({
  id: "support-agent",
  name: "Support Agent",
  description: "Order lookup, policy lookup and ticket creation for an e-commerce store.",
  async run(task, ctx) {
    const result = await customerSupportAgent.generate({ prompt: task.prompt });

    // Optional — the run's transcript, from the SDK's own step record.
    for (const step of result.steps ?? []) {
      for (const call of step.toolCalls ?? []) ctx?.tool?.(call.toolName, call.input);
      for (const out of step.toolResults ?? []) ctx?.toolResult?.(out.toolName, out.output);
    }
    if (result.usage?.totalTokens) ctx?.usage?.(result.usage.totalTokens);

    return result.text;
  },
});
```

For a plain `generateText` entry point:

```ts
run: async (task) => (await generateText({ model, prompt: task.prompt, tools })).text,
```

For `streamText`:

```ts
run: async (task) => {
  const { textStream } = streamText({ model, prompt: task.prompt, tools });
  let out = "";
  for await (const chunk of textStream) out += chunk;
  return out;
},
```

## 4. Attached tools — do NOTHING

An experiment can attach MCP servers and, on every run, the project's Linux
machine as a `machine_run` shell tool. `redline dev` hooks the AI SDK's module
loader **before** it imports your files, so those tools are merged into the
`ToolLoopAgent` / `generateText` call as it is built.

**Do not call `redlineTools`** — that would connect everything a second time.
Verify by counting: three tools declared, four in the agent, the injection
landed. Attached SKILLS need nothing at all — they arrive inside `task.prompt`.

## 5. Three failures that are nearly guaranteed if you skip them

**5a. The agent must know its tools exist.** Registering a tool is not the same
as the agent reaching for it. An agent whose `instructions` describe a narrow
job answers "I have no way to do that" while `machine_run` sits unused. Because
the AI SDK is patched, the tool is in the list — but the instructions may still
need one line saying attached tools may be used when the task calls for them.

**5b. A tool that throws must not kill the run.** The AI SDK surfaces a tool
error as a `tool-error` part and continues by default; do not wrap the call in
something that re-throws it to the top.

**5c. Do not drop the reasoning.** Map the result's steps onto `ctx` as shown
in §3 — `ctx.thinking(text)`, `ctx.tool(name, input)`,
`ctx.toolResult(name, out)`, `ctx.usage(tokens)`. A run whose transcript shows
only an answer is a run nobody can judge. Not every model emits reasoning; if
the transcript has none, check the model before changing code.

## 6. Connect and verify

The agent's own environment (model API keys etc.) must load the way the repo
normally loads it — `.env`, exported vars. Then:

```bash
export REDLINE_URL=https://tryredlineai.co
export REDLINE_API_KEY=rl_…          # Agents page → Runner key
npx redline dev
```

`redline dev` **must be running on this machine at all times** for anything to
work. It is the long-lived worker that connects this repository's agent to
Redline: experiments execute inside it, the agent shows ONLINE on the Agents
page only while it runs, asset snapshots for Protect upload through it,
policies and honeypots arrive and re-arm through it every 20 seconds, and dev
sessions stream to Monitor through it. If it stops, the agent goes offline and
none of that happens.

What the two variables are:

- `REDLINE_URL` is the web address of the Redline platform. It is NOT an
  address of anything in this repository; nothing in the repository hosts it.
- `REDLINE_API_KEY` is the runner key minted on the Agents page (**Runner key**
  button). It is the only credential needed. **Do not commit it.**

In a terminal you are watching: `npx redline dev`. On a server or VM, after
you log out:

```bash
nohup npx redline dev > ~/redline-dev.log 2>&1 &
disown
```

It opens NO inbound port — outbound only, NAT-friendly. Success is the line:

```
connected — support-agent registered and online
```

Leave it running; runs execute in it when an experiment is launched. It runs
several runs in parallel by spawning worker processes (`REDLINE_CONCURRENCY`,
default 4) — an experiment's "at once" setting fills by itself.

## 7. When something is wrong

- **"no agents found"** → the `redline/` file exports no `defineAgent` object.
- **Registration rejected** → the id collides with another agent; pick another slug.
- **The agent says it has no tools** → §5a, or the repository imports `ai`
  before `redline dev` could hook it (a bundled/pre-compiled entry). Run
  against the source, not a build.
- **Session shows the transcript but no model calls, tokens or tool calls** →
  AI SDK ≤6 without `experimental_telemetry` (§8a).
- **Waterfall labels every step "turn N"** → see
  [known issues](/integrations/known-issues#waterfall-labels-every-step-as-its-own-turn).
- **The run dies on the first tool error** → §5b.
- **`ask_user` (if the agent has one) ENDS the run.** Redline runs are
  unattended — return the questions as the answer rather than inventing replies.

Everything else: [known issues](/integrations/known-issues).

## 8. Monitoring — the same agent, watched in production

Nothing extra to write for dev: while `redline dev` is up, monitoring is on by
default (`REDLINE_MONITOR=1`). Every conversation the agent works — user
message, model calls with token counts, both halves of every tool call,
reasoning, the final answer — ALSO streams to **Monitor → Sessions**.

**8a. REQUIRED on AI SDK v6 and below — enable telemetry.** AI SDK **v7**
reports its calls with no opt-in. AI SDK **≤6** emits spans only when the call
sets it, and without it every session shows the transcript with **no waterfall,
tokens or tool calls** — which reads as monitoring being broken when it is this
flag missing:

```ts title="src/support-agent.ts"
export const customerSupportAgent = new ToolLoopAgent({
  model,
  experimental_telemetry: { isEnabled: true },   // v6: required. v7: harmless.
  instructions: `…`,
  tools: { checkOrderStatus, lookupPolicy, createTicket },
});
// or on each generateText / streamText call
```

VERIFY after the first run: the session must show model calls and a token
count, not a single line per turn.

**For the DEPLOYED agent** — real users, no `redline dev` — wrap the message
handler with `observe()`. ONE wrapper, no other monitoring code anywhere:

```ts title="redline/monitored.ts"
import { observe } from "@redlineai/sdk";
import { customerSupportAgent } from "../src/support-agent";

export const handleMessage = observe(
  async (conversationId: string, userId: string, text: string) => {
    const result = await customerSupportAgent.generate({ prompt: text });
    return result.text;
  },
  { agent: "support-agent", sessionArg: 0, userArg: 1, inputArg: 2 },
);
```

Each call becomes a session: the wrapper records the input and the returned
answer itself, and every model and tool call the AI SDK emits during the call
lands in the SAME session on the async context. Calls sharing `sessionArg`
land in one transcript. Config is environment only — `REDLINE_API_KEY`,
`REDLINE_URL`, `REDLINE_AGENT` — so a deploy needs no code; **without the key
the wrapper is inert** and the handler runs untouched. Errors are reported and
re-thrown; nothing in it can take the handler down.

**8b. Your application has to CALL `handleMessage`.** A wrapper only records
calls to the function it wraps. If the repository already has a pipeline for
inbound messages — a route handler, a queue worker, a websocket loop — that
pipeline must call `handleMessage` for each message. That is one call-site
change in existing code and the only one this integration asks for. Without
it, experiments work and production sessions never appear.

(`createMonitor` remains as the explicit low-level client underneath, for a
handler shape the wrapper cannot wrap — see [monitoring](/integrations/monitoring).)

What the platform does with a session, with no further code:

- It appears in **Monitor → Sessions** as a readable conversation with the
  waterfall beside it.
- Rule detectors match every span as it is written; within minutes the
  monitor re-examines the session against every rule the project defines
  (**Monitor → Violations**). Rules are written in the console, in plain
  language, not in the repository.
- When the session closes, the intent miner reads it for recurring user
  behaviour (**Monitor → Intents**).

## 9. Runtime protection — what to expect once connected (nothing to write)

While `redline dev` is up, the SDK also ENFORCES the project's security
policies inside the agent's process. Nothing to add, nothing in the repository
changes; everything is configured in the console (**Protect → the agent →
Policies**) and reaches the running process within ~20 seconds of being saved.
The AI SDK's model calls go through its loader-hooked provider, so the gate
sees them.

- **Governance rules** decide what the agent may DO — a condition over the
  action about to happen (tool name and arguments) and a verdict. A denied
  call never executes: the model receives
  `[redline] The call to <tool> was denied by policy: <reason>` as the tool
  result and answers around it. `require_approval` holds the call and names
  who could approve. `warn` lets the call run and appends
  `[redline] Policy note on <tool>: <reason>` to the tool result the model
  reads; `log` lets it run and records it silently.
- **The guard classifier** decides about TEXT — a separate switch, not a rule.
  It scores every user message and tool result for a prompt injection; above
  the block threshold the content is withheld and the model never sees it.
- **Honeypots** — approved bait tools are injected alongside the agent's own; a
  call to one is recorded as a hijack signal and answered with a bland
  unavailability line.

Streaming responses pass through unjudged — text already left as it was
produced. Everything fails OPEN: if the platform or classifier is unreachable,
the agent runs ungoverned rather than breaking. Every enforcement is recorded
with its evidence in the console.

## What a healthy session looks like

Seven user turns, one model call each, the tool nested under the call that
made it: **23 spans** — 7 inputs, 7 model calls, 7 outputs, 2 tools. If the
turn count matches the user messages and tools indent under their model call,
the tree is right.
