Agent Frameworks
Farm works with any JavaScript agent framework without an adapter. An API route receives a Web Request and returns a Response, and every major agent framework can return its stream as a Response. Define the agent in a server module, call it from a route, and render it with a chat UI.
This page shows the AI SDK, Mastra, LangGraph, and the OpenAI Agents SDK. All four can stream the AI SDK's UI message format, so one chat component works with every one of them.
How it fits together
| Piece | Where | Runs on |
|---|---|---|
| The agent: model, instructions, tools | src/agents/*.ts | Server only |
| The route that runs it | src/app/api/agents/<name>/route.ts | Server |
| The chat UI | A "use client" component | Browser |
Keep provider keys in server environment variables, for example OPENAI_API_KEY in .env. Import agent modules only from routes and other server code, never from a client component, so keys and tool code stay out of the browser bundle.
The chat UI
Install the AI SDK's React hooks:
pnpm add ai @ai-sdk/react"use client";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState } from "react";
export function Chat({ api }: { api: string }) {
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({ api }),
});
const [input, setInput] = useState("");
return (
<div>
{messages.map((message) => (
<p key={message.id}>
{message.role}:{" "}
{message.parts.map((part, index) =>
part.type === "text" ? <span key={index}>{part.text}</span> : null,
)}
</p>
))}
<form
onSubmit={(event) => {
event.preventDefault();
sendMessage({ text: input });
setInput("");
}}
>
<input value={input} onChange={(event) => setInput(event.target.value)} />
<button type="submit" disabled={status !== "ready"}>
Send
</button>
</form>
</div>
);
}Point api at any of the routes below, for example <Chat api="/api/agents/support" />.
AI SDK
pnpm add ai @ai-sdk/openai zodimport { openai } from "@ai-sdk/openai";
import { ToolLoopAgent, stepCountIs, tool } from "ai";
import { z } from "zod";
export const supportAgent = new ToolLoopAgent({
model: openai("gpt-4.1-mini"),
instructions: "You help customers with their orders.",
tools: {
getOrder: tool({
description: "Look up an order by id",
inputSchema: z.object({ id: z.string() }),
execute: async ({ id }) => db.orders.find(id),
}),
},
stopWhen: stepCountIs(5),
});import { createAgentUIStreamResponse } from "ai";
import { supportAgent } from "../../../../agents/support";
export async function POST(request: Request) {
const { messages } = await request.json();
return createAgentUIStreamResponse({
agent: supportAgent,
uiMessages: messages,
abortSignal: request.signal,
});
}abortSignal: request.signal stops the model when the visitor closes the page. For a chat route without tools, @farm.js/ai provides aiChatRoute().
Mastra
pnpm add @mastra/core @mastra/ai-sdk aiimport { Agent } from "@mastra/core/agent";
import { Mastra } from "@mastra/core/mastra";
const supportAgent = new Agent({
id: "support",
name: "Support",
instructions: "You help customers with their orders.",
model: "openai/gpt-4.1-mini",
});
export const mastra = new Mastra({ agents: { supportAgent } });import { handleChatStream } from "@mastra/ai-sdk";
import { createUIMessageStreamResponse } from "ai";
import { mastra } from "../../../../mastra";
export async function POST(request: Request) {
const params = await request.json();
const stream = await handleChatStream({ mastra, agentId: "support", params, version: "v7" });
return createUIMessageStreamResponse({ stream });
}version: "v7" matches the message format of ai 7. Without it, handleChatStream streams the older v5 format.
Mastra keeps memory and workflow state in its own storage. Without a storage option it uses memory and logs a warning, so configure a persistent Mastra store before production.
LangGraph
pnpm add langchain @langchain/core @langchain/openai @ai-sdk/langchain ai zodimport { createAgent, tool } from "langchain";
import { z } from "zod";
const search = tool(async ({ query }) => searchTheWeb(query), {
name: "search",
description: "Search the web",
schema: z.object({ query: z.string() }),
});
export const researchAgent = createAgent({
model: "openai:gpt-4.1-mini",
systemPrompt: "You research questions step by step.",
tools: [search],
});import { toBaseMessages, toUIMessageStream } from "@ai-sdk/langchain";
import { createUIMessageStreamResponse } from "ai";
import { researchAgent } from "../../../../agents/research";
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = await researchAgent.stream(
{ messages: await toBaseMessages(messages) },
{ streamMode: ["values", "messages"] },
);
return createUIMessageStreamResponse({ stream: toUIMessageStream(stream) });
}A graph you build yourself with StateGraph works the same way: pass its compiled graph's .stream() result to toUIMessageStream.
OpenAI Agents SDK
pnpm add @openai/agents @openai/agents-extensions aiimport { Agent } from "@openai/agents";
export const triageAgent = new Agent({
name: "Triage",
instructions: "Route the customer to the right answer.",
model: "gpt-4.1-mini",
});import { run } from "@openai/agents";
import { createAiSdkUiMessageStreamResponse } from "@openai/agents-extensions/ai-sdk-ui";
import type { UIMessage } from "ai";
import { triageAgent } from "../../../../agents/triage";
export async function POST(request: Request) {
const { messages } = (await request.json()) as { messages: UIMessage[] };
const text = messages
.at(-1)!
.parts.flatMap((part) => (part.type === "text" ? [part.text] : []))
.join("");
const result = await run(triageAgent, text, { stream: true });
return createAiSdkUiMessageStreamResponse(result);
}This route sends the agent the latest message. To keep a conversation, store the run's history (result.history) and pass it as the input of the next run().
Other frameworks
Any framework that produces a Response, a ReadableStream, or an async iterable fits the same shape: run it in a route and return the stream. Frameworks that ship their own server, such as an Eve agent or a Cloudflare Agents Durable Object, run beside Farm instead; see Eve and Cloudflare Agents.
Production notes
-
Errors. The AI SDK replaces stream errors with "An error occurred." before they reach the browser, so a failing model call is easy to miss. Log the error and choose the message the visitor sees with
onError:return createAgentUIStreamResponse({ agent: supportAgent, uiMessages: messages, abortSignal: request.signal, onError: (error) => { console.error(error); return "The assistant is unavailable right now."; }, }); -
Long runs. A route lives as long as its request. For work that outlasts the request or must survive a restart, hand it to jobs or to the framework's own durable runtime.
Run AI SDK, Mastra, LangGraph, the OpenAI Agents SDK, or any other agent framework in a Farm API route, with one chat UI.