Ratel Docs
TypeScript SDK

Framework integrations

Wire Ratel into the Vercel AI SDK: one adapter to the framework's tool type, one loop that assembles the capability tools plus a top-K pre-filter.

Ratel ships no framework plugins because it needs none. Everything the SDK hands you — your registered tools and the capability tools (search_capabilities / invoke_tool; get_skill_content joins the set when a SkillCatalog is registered) — is a plain executable tool: metadata plus an execute handler. Wiring a framework is two small pieces:

  1. An adapter from that shape to the framework's tool type.
  2. A turn assembler that builds each turn's tool list: the always-present capability tools plus the top-K catalog hits for the user's message (ADR 0004 replace-mode pre-filtering).

This page follows the shipped examples/ai-sdk example. Using Pydantic AI? See the Python framework integrations.

Vercel AI SDK

The example pins ai ^6.0.0 and @ai-sdk/openai ^3.0.0 alongside @ratel-ai/sdk; the loop is AI SDK v6's ToolLoopAgent.

The adapter

AI SDK tools are built with tool(), wrapping the input schema in jsonSchema(), so the schema the model sees is the same one Ratel ranks:

import { type ExecutableTool } from "@ratel-ai/sdk";
import { jsonSchema, tool } from "ai";

export function toAISDKTool(executable: ExecutableTool) {
  return tool({
    description: executable.description,
    inputSchema: jsonSchema(executable.inputSchema),
    execute: executable.execute,
  });
}

That is the entire adapter. It passes execute straight through, which is fine here because every executor in the example is async; note that direct top-K calls then bypass catalog.invoke and its invoke_* trace events.

Assemble the turn and run the loop

Each run puts the two capability tools in the list unconditionally, then adds the top-K BM25 hits for the prompt with their full schemas:

import { invokeToolTool, searchCapabilitiesTool, type ToolCatalog } from "@ratel-ai/sdk";
import { type LanguageModel, stepCountIs, type Tool, ToolLoopAgent } from "ai";
import { toAISDKTool } from "./tools.js";

export async function runAgent(args: {
  prompt: string;
  model: LanguageModel;
  catalog: ToolCatalog;
  initialTopK?: number;
  maxSteps?: number;
}): Promise<string> {
  const { prompt, model, catalog } = args;
  const initialTopK = args.initialTopK ?? 3;
  const maxSteps = args.maxSteps ?? 8;

  const tools: Record<string, Tool> = {
    search_capabilities: toAISDKTool(searchCapabilitiesTool(catalog)),
    invoke_tool: toAISDKTool(invokeToolTool(catalog)),
  };
  for (const hit of catalog.search(prompt, initialTopK)) {
    const exec = catalog.getExecutable(hit.toolId);
    if (exec) tools[exec.id] = toAISDKTool(exec);
  }

  const agent = new ToolLoopAgent({
    model,
    tools,
    toolChoice: "auto",
    stopWhen: stepCountIs(maxSteps),
  });

  const result = await agent.generate({ prompt });
  return result.text;
}

ToolLoopAgent multi-steps inside one .generate() call, chaining search_capabilities, then invoke_tool, then the final answer when the pre-filtered set falls short.

Run it

Nothing in the wiring is OpenAI-specific: runAgent accepts any LanguageModel, so swapping providers is one import edit (@ai-sdk/anthropic instead of @ai-sdk/openai).

import { openai } from "@ai-sdk/openai";

const text = await runAgent({
  prompt: "read the files and find every TODO comment under src/",
  model: openai(process.env.AI_MODEL ?? "gpt-5-mini"),
  catalog, // your populated ToolCatalog
});

To run the shipped example from a checkout of the ratel repo (it is a private workspace package, so it rebuilds the SDK first):

export OPENAI_API_KEY=sk-...
pnpm install
pnpm -F @ratel-ai/example-ai-sdk start "send an email to alice@example.com saying ship it"

Leave OPENAI_API_KEY unset and the example runs in diagnostic mode: it prints the BM25 top-3 for the prompt and exits without calling a model.

Direct top-K tools get the AI SDK's strict schema validation at the LLM boundary. Calls routed through invoke_tool do not: the model reads each tool's inputSchema from search_capabilities results and serializes the arguments itself, so validation happens only at the catalog's own execute.

The same pattern over an MCP server

examples/mcp-chat applies the identical wiring to tools it does not own: it ingests an upstream MCP server with registerMcpServer, then rebuilds the tool list every turn. With 13 upstream tools and a top-K of 3, the model sees five tool definitions per turn; the rest stay reachable through search_capabilities / invoke_tool without occupying context.

Any other framework

LangChain, OpenAI Agents, a hand-rolled loop: the recipe does not change, because ToolCatalog entries and the capability-tool factories all return the same plain executable shape. Write one adapter and one turn assembler. The Progressive disclosure page covers the contract the capability tools expose to the model.

Next steps

On this page