Stately
PackagesAgent

Models and providers

Reuse models and executors from other AI frameworks via AI SDK LanguageModel objects and OpenAI-compatible endpoints.

Alpha: @statelyai/agent 2.0 is in alpha. APIs can change between releases; pin an exact version. Feedback: github.com/statelyai/agent.

This page covers how to supply models to a machine's executors, and which capabilities each integration path supports.

Reusing models from other frameworks

A host's executors are the only integration point. Most frameworks expose models as an AI SDK LanguageModel object. Pass that object to createAiSdkExecutors({ models }) to get a full { generateText, streamText, decide } set.

import { createAiSdkExecutors } from "@statelyai/agent/ai-sdk";

const executors = createAiSdkExecutors({
  models: { quick: someLanguageModel, careful: anotherLanguageModel },
});

await runAgent(machine, { input, executors });

There are four integration paths. The first two both go through createAiSdkExecutors, so they appear as one row in the support table.

  • AI SDK adapter. Accepts any LanguageModel, including models from Mastra, Cloudflare Workers AI through workers-ai-provider, TanStack AI, OpenRouter's AI SDK provider, and any @ai-sdk/* package. Supports all three executors, including decide.
  • OpenAI-compatible endpoints. Point createOpenAI({ baseURL }) from @ai-sdk/openai at any OpenAI-shaped endpoint, such as Groq, Ollama, vLLM, Together, or LM Studio, then pass the result to the same adapter. Supports all three executors, including decide.
  • Hand-written executors. Write the three executors yourself against a provider's HTTP API, or against a client that is not an AI SDK LanguageModel, such as a LangChain BaseChatModel. Supports all three executors, but you map structured output and decision retries yourself. See Hosts and LangChain models.
  • Raw ai functions. Pass the ai package's generateText and streamText as your executors set. This path supports text only. decide requires the adapter, and structured output is best-effort.

Host-owned model settings

Pass provider-specific settings to createAiSdkExecutors, not the machine. An object applies to every call; a function can vary settings by request. A model entry may also pair one model with its defaults.

const executors = createAiSdkExecutors({
  models: {
    quick: openai("gpt-5.4-mini"),
    careful: { model: openai("gpt-5.4"), settings: { reasoning: "high" } },
  },
  settings: (request) => ({ temperature: request.name === "draft" ? 0.7 : 0 }),
});

Precedence is global settings, then model-entry settings, then portable generation fields declared by the request.

Mastra models

Mastra is a TypeScript agent framework that configures agents with an AI SDK LanguageModel. Pass the same models to createAiSdkExecutors, and expose the run through a Mastra tool.

import { z } from "zod";
import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { createAiSdkExecutors } from "@statelyai/agent/ai-sdk";
import { runAgent } from "@statelyai/agent";

const executors = createAiSdkExecutors({ models });

const startWorkflow = createTool({
  id: "start_workflow",
  description: "Start the drafting workflow from the user's request.",
  inputSchema: z.object({ prompt: z.string() }),
  outputSchema: z.object({ status: z.string() }),
  execute: async ({ prompt }) => {
    const result = await runAgent(machine, { input: { prompt }, executors });
    return { status: result.status };
  },
});

export const hostAgent = new Agent({
  id: "drafting-host",
  name: "Drafting Host",
  instructions: "Call start_workflow with the user's request, then report what came back.",
  model: "openai/gpt-5.4-mini",
  tools: { start_workflow: startWorkflow },
});

Any framework that exposes a LanguageModel works the same way. The machine and the Mastra agent share one model definition. See examples/mastra-host for the full bridge, including pause and resume across tool calls.

LangChain models

LangChain chat models are not AI SDK LanguageModel objects, so they take the hand-written path. Wrap any BaseChatModel in the three executors and the machine keeps LangChain's model config, callbacks, and retries.

  • generateText and streamText call the model's invoke and stream.
  • decide binds one tool per allowed event and forces a tool call.
  • LangSmith tracing is env-var driven, so a wrapped model traces without code changes. For tracing the machine's own spans instead, see Observability.

See examples/langchain-host for both directions: LangChain models as executors, and the machine handed to a LangChain createAgent loop as start_workflow and resume_workflow tools.

Cloudflare Workers AI

Workers AI runs models on Cloudflare's edge and is reached through a binding on the Worker's env. The workers-ai-provider package turns that binding into an AI SDK provider, so its models are ordinary LanguageModel objects.

import { createWorkersAI } from "workers-ai-provider";
import { createAiSdkExecutors } from "@statelyai/agent/ai-sdk";

export default {
  async fetch(request, env) {
    // Model IDs here are illustrative; substitute your provider's current models.
    const workersai = createWorkersAI({ binding: env.AI });
    const result = await runAgent(machine, {
      input: await request.json(),
      executors: createAiSdkExecutors({
        models: { quick: workersai("@cf/meta/llama-3.1-8b-instruct") },
      }),
    });
    return Response.json(result);
  },
};

Pass Cloudflare-specific per-call options through request metadata. The host reads metadata; the machine only carries it.

Ollama and OpenAI-compatible endpoints

Ollama runs models locally and serves them over an OpenAI-compatible HTTP API. Point the AI SDK's OpenAI provider at the local endpoint. Pass a placeholder apiKey: the provider throws while building request headers when neither apiKey nor OPENAI_API_KEY is set, even for a local server that ignores the value.

import { createOpenAI } from "@ai-sdk/openai";
import { createAiSdkExecutors } from "@statelyai/agent/ai-sdk";

// Model IDs here are illustrative; substitute your provider's current models.
const ollama = createOpenAI({ baseURL: "http://localhost:11434/v1", apiKey: "ollama" });

await runAgent(machine, {
  input,
  executors: createAiSdkExecutors({
    models: { quick: ollama("llama3.1") },
  }),
});

To use Groq, vLLM, Together, OpenRouter, or LM Studio, change baseURL and use the endpoint's real apiKey. Nothing else changes.

For OpenAI itself, @statelyai/agent/openai maps the three executors onto the raw openai package's Chat Completions API, with no ai dependency. See Hosts.

To avoid depending on ai or openai, write the three executors over raw fetch against the same Chat Completions endpoint. Build the request body from the plain AgentTextRequest fields. For structured output, providerOutputSchema and getJsonSchema from @statelyai/agent build the { result, reasoning? } schema to send; on the way back, parseProviderOutput validates the model's JSON against it and returns { result, reasoning? }, which is already the shape an executor returns. Use parseOutput when you validate a bare value on its own, against the schema the request declared. Use renderDecisionAttempts for decision retries. See Hosts.

Testing with a mock model

To exercise the adapter path itself without a provider, put one of the AI SDK's own mock models from ai/test in the models map.

import { MockLanguageModelV3 } from "ai/test";
import { createAiSdkExecutors, } from "@statelyai/agent/ai-sdk";

const executors = createAiSdkExecutors({
  models: {
    fast: new MockLanguageModelV3({
      doGenerate: async () => ({
        content: [{ type: "text", text: "Because transitions constrain behavior." }],
        finishReason: { unified: "stop", raw: "stop" },
        usage: {
          inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
          outputTokens: { total: 1, text: 1, reasoning: 0 },
        },
        warnings: [],
      }),
    }),
  },
});

When the adapter is not what is under test, a plain function executor that routes on request.name is lighter. See Evals.

Support by path

PathgenerateTextstreamTextdecideStructured output
createAiSdkExecutorsyesyesyesyes
createOpenAiExecutorsyesyesyesyes
Hand-written executorsyesyesyesyes (you map it)

The decide executor maps each machine event to a forced tool call, and structured output is requested as { result, reasoning? }. Both live in the adapter layer, so an executor is either an adapter's or a function of yours that returns { result }. See Text requests and Decisions.

Reference hosts by provider

Each example is a runnable host for one provider stack.

ExampleBacking
ai-sdk-hostVercel AI SDK, through the optional adapter
openai-sdk-hostcreateOpenAiExecutors from @statelyai/agent/openai, over the raw openai package (Chat Completions)
anthropic-sdk-hostraw @anthropic-ai/sdk (Messages); structured via forced tool call, decisions via tool_choice: { type: 'any' }
langchain-hostLangChain BaseChatModel (@langchain/core), wrapped into the executor contract
mastra-hostMastra Agent and createTool, bridging to runAgent
cloudflare-agent-hostDurable Object
cloudflare-workers-ai-hostWorkers AI binding

On this page