Vercel AI SDK
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 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:
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. idis a kebab-case slug unique to this agent.run(task, ctx)is async.task.promptis 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-")).
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:
run: async (task) => (await generateText({ model, prompt: task.prompt, tools })).text,
For streamText:
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:
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_URLis 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_KEYis 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:
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 nodefineAgentobject. - Registration rejected → the id collides with another agent; pick another slug.
- The agent says it has no tools → §5a, or the repository imports
aibeforeredline devcould 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.
- 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.
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:
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:
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.)
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_approvalholds the call and names who could approve.warnlets the call run and appends[redline] Policy note on <tool>: <reason>to the tool result the model reads;loglets 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.