Agents
Build a ToolLoopAgent from a stored agent with its tools, approvals and knowledge search, and check input and output with a moderation middleware.
better-supabase/ai-sdk/agents runs an agent with
the AI SDK. It picks the tools the agent may use, asks for approval where
the tool policies say so, and adds a knowledge search tool for the agent's
scopes.
pnpm add aiRuntime
import { gateway } from "ai";
import { createAgentRuntime } from "better-supabase/ai-sdk/agents";
const runtime = createAgentRuntime({
agent,
model: (modelId) => gateway(modelId ?? "openai/gpt-5-mini"),
tools: { ...appTools, ...connectorTools },
policies: await chats.tools.policies(organizationId).orThrow(),
knowledge: { knowledge, organizationId },
instructions: "Answer in the user's language.",
});
const result = await runtime.stream({ messages });createAgentRuntime returns a ToolLoopAgent. Its instructions are the
app's instructions followed by the agent's own, and it stops after 20
steps unless you pass stopWhen. With knowledge, the model gets a
search_knowledge tool limited to the agent's knowledge scopes.
| Helper | Returns |
|---|---|
agentTools | The tools the agent lists (all of them when it lists none), minus deny |
agentToolApproval | user-approval for every tool whose policy is ask |
agentScopes | The agent's knowledge scopes, with agent resolved to its id |
An ask tool doesn't run until the user approves it: the stream carries a
tool-approval-request part, and the chat route stores
the approval.
Your own approval check
toolApproval takes the AI SDK's tool approval function (typed as
AgentToolApprovalFunction), for checks such as an authorization call or a
spending limit. It runs first for every call, and the tenant's policies then
apply on top: a tool whose policy is ask still needs the user's approval
when your check approves it, and a deny from your check always wins. A check
that throws denies the call.
const runtime = createAgentRuntime({
agent,
model,
tools,
policies,
toolApproval: async ({ toolCall }) =>
(await mayRun(user, toolCall.toolName))
? { type: "approved" }
: { type: "denied", reason: "Not allowed for this user" },
});Moderation
import { wrapLanguageModel } from "ai";
import { moderationMiddleware } from "better-supabase/ai-sdk/agents";
const model = wrapLanguageModel({
model: gateway("openai/gpt-5-mini"),
middleware: moderationMiddleware({
chats,
organizationId,
chatId,
check: async (text, stage) => classify(text),
}),
});check gets the last user message (input) and the answer (output) and
returns a verdict with an action and a category, or nothing. Every
verdict is recorded in the AI chat moderation log.
A block verdict throws ModerationBlockedError before the model runs, or
after it answers; in a stream it ends the stream with an error part.
Last updated on
Memory
Give the model the memory tool, Anthropic's built-in memory tool, a recall tool and its core memory, and extract facts from a conversation in a job.
MCP connectors
Authorize a user with an MCP server through OAuth with dynamic client registration in Vault, connect with the stored session and give the model the server's tools.