Ssamai-sdk

SamAI SDK

samai-sdk — a unified AI agent SDK for TypeScript — one API across 8 providers (OpenAI, Anthropic, Google, Groq, Mistral, Ollama, Azure OpenAI, AWS Bedrock), with a from-scratch agent runtime: tool calling, MCP client support, sandboxed code execution, voice/realtime sessions, multi-agent handoffs, guardrails, approval gates, RAG, resumable/checkpointed runs, and fully-traced retries/fallbacks/timeouts — plus React/Vue/Svelte hooks, a CLI, and OpenTelemetry export — built in as first-class citizens.

TypeScript MIT licensed zero framework dependency provider-agnostic 8 providers MCP sandboxed execution voice/realtime React / Vue / Svelte

This page is the full reference — you shouldn't need to read the source to use the SDK. Every section below has runnable code.

Who this is for

Developers building LLM-agent features in a Node/TypeScript codebase who don't want to adopt a heavyweight framework's opinions about state, storage, or orchestration — but also don't want to hand-roll tool-calling loops, retry logic, and handoff bookkeeping themselves for the third time this year.

What it solves

Every team building agents ends up writing the same primitives — a tool-execution loop, a way to hand off between specialized agents, guardrails that don't silently swallow errors, a place to put conversation memory, and some way to see what actually happened during a run. samai-sdk implements those primitives once, directly against provider APIs, so swapping anthropic() for openai() doesn't change how your agent behaves.

How it differs

Provider-neutral by construction

The agent runtime never talks to a provider SDK directly — it only depends on the Provider interface, so the same handoff/guardrail/tracing logic runs identically on Claude, GPT, or Gemini.

Fails closed, not silent

Approval-gated tools reject by default with no handler configured. Guardrails throw typed errors with the reason attached. Nothing quietly no-ops.

Tracing isn't bolted on

Every run produces a structured RunTrace — model calls, tool calls, handoffs, retries, fallbacks, timeouts, guardrail trips, approvals, token usage, timing — for free, not as an opt-in addon.

Real timeouts

withTimeout() uses an actual AbortController deadline. It doesn't infer "this looks like a timeout" from an error string after the fact.

Most agent frameworks pick a lane: a thin provider wrapper with no orchestration, or a full framework with its own state/deployment model that really only shines on one vendor. This sits deliberately in between — real orchestration, provider-agnostic by construction, on a runtime you can still read end to end (~6,900 lines across src/) when something misbehaves, rather than spelunking through a dependency tree.

Why choose samai-sdk

1

You're not betting on one model vendor

Eight providers behind one Provider interface. Swap anthropic({...}) for openai({...}) and nothing else — tools, guardrails, tracing, sessions — changes. If a vendor changes pricing or rate-limits you, switching is a one-line change, not a rewrite.

2

Safety is the default, not a checkbox you forget

Approval-gated tools fail closed if you forget to wire up a handler — a risky tool cannot run unattended by accident. Guardrails throw typed errors with the reason attached. Every run produces a full trace whether you asked for it or not.

3

It's small enough to actually read

Under 2,500 lines of TypeScript for the core runtime. When something misbehaves at 2am, reading run.ts is a 10-minute task — not an afternoon spent in a framework's abstraction layers.

4

It has the features other "minimal" SDKs make you build yourself

MCP client, sandboxed code execution, voice/realtime sessions, resumable/checkpointed runs, multi-agent handoffs with loop prevention, four session-store backends, OpenTelemetry export, a local trace viewer — usually the reasons teams graduate to a heavyweight framework. Here they ship with the "thin" SDK.

5

No lock-in, ever, by construction

The runtime is written against generate()/stream() and never imports a vendor SDK directly. No proprietary state format, no hosted-only tracing, no platform you must deploy through. Sessions, checkpoints, and traces are data you own, in stores you control.

How it compares

A qualitative snapshot against the frameworks people usually weigh this against. All of these move fast — treat this as directional, and check each project's own docs before deciding.

samai-sdkLangChain.jsVercel AI SDKOpenAI Agents SDKMastra
Provider model8 adapters, one interfaceMany, separate packagesMany, @ai-sdk/* packagesOpenAI-firstMultiple
Agent runtime (loop, not just calls)✅ built in✅ (LangGraph)Partial — you compose it✅ built in✅ built in
Multi-agent handoffs✅ loop-safe, maxHandoffs✅ (graph-based)❌ manual
Guardrails as first-class✅ PII/injection/blocklist/schema/budgetVia custom chains❌ manualPartialPartial
Approval / human-in-the-loop✅ fails closed by defaultVia custom graph nodes❌ manualPartialPartial
Built-in tracing✅ every run, no opt-inVia LangSmith (external)Via telemetry integrationsVia platform dashboard
MCP client support✅ stdio / HTTP / SSEEmergingEmerging
Sandboxed code execution✅ process-isolated❌ bring your own❌ bring your own❌ bring your ownPartial
Resumable / checkpointed runs✅ file or in-memory store✅ (LangGraph persistence)❌ manualPartialPartial
Runtime source size< 2,500 linesLarge, many packagesLarge, many packagesModerateModerate
Vendor lock-inNone by constructionLowLow–moderateHigh (OpenAI-centric)Low

Short version: samai-sdk trades LangChain's breadth and Vercel AI SDK's frontend-streaming polish for a smaller, single-purpose surface — the agent-loop/guardrails/tracing primitives every serious agent needs, without a framework's opinions about everything else.

Architecture

What happens inside a single runAgent() call — the tool loop, guardrails, handoffs, and tracing all run inside this one call.

runAgent() / runAgentStream()entry point
Input guardrailsPII · prompt-injection · blocklist · budget
Model call via Providerwrapped transparently by withRetry / withFallback / withTimeout
↓ response
Tool call(s)→ tool guardrails → approval gate → execute()
handoff_to__<agent>→ loop-prevention check → switch agent
Final text→ output guardrails
tool result / new active agent loops back to the model call ↑
RunResult + RunTraceevery call · tool · handoff · retry · fallback · timeout logged
Fails closed, not silent. A blocked input/output guardrail throws GuardrailBlockedError. A revisited handoff throws HandoffLoopError. An approval-gated tool with no handler wired up is rejected automatically. Nothing in this diagram quietly no-ops — see Error handling for the full list.

01 · GETTING STARTEDInstallation

Install the core package, then add whichever provider SDKs you actually use — they're optional peer dependencies, loaded lazily, so you never pay for the ones you skip.

npm install samai-sdk

# plus whichever provider SDKs you actually use:
npm install @anthropic-ai/sdk      # for anthropic()
npm install openai                 # for openai()
npm install @google/generative-ai  # for google()

# other optional peer deps, same lazy-load pattern:
npm install @modelcontextprotocol/sdk  # for createMCPClient()
npm install ws                         # for createRealtimeSession()
npm install ioredis                    # for RedisSessionStore
npm install better-sqlite3             # for SqliteSessionStore
pnpm add samai-sdk

# plus whichever provider SDKs you actually use:
pnpm add @anthropic-ai/sdk      # for anthropic()
pnpm add openai                 # for openai()
pnpm add @google/generative-ai  # for google()

# other optional peer deps, same lazy-load pattern:
pnpm add @modelcontextprotocol/sdk  # for createMCPClient()
pnpm add ws                         # for createRealtimeSession()
pnpm add ioredis                    # for RedisSessionStore
pnpm add better-sqlite3             # for SqliteSessionStore
yarn add samai-sdk

# plus whichever provider SDKs you actually use:
yarn add @anthropic-ai/sdk      # for anthropic()
yarn add openai                 # for openai()
yarn add @google/generative-ai  # for google()

# other optional peer deps, same lazy-load pattern:
yarn add @modelcontextprotocol/sdk  # for createMCPClient()
yarn add ws                         # for createRealtimeSession()
yarn add ioredis                    # for RedisSessionStore
yarn add better-sqlite3             # for SqliteSessionStore

Node ≥ 18. TypeScript ≥ 5 recommended for the fully-typed exports.

02 · GETTING STARTEDQuick start

The lowest-level surface is createClient() + a provider — a single tool-calling round-trip with no agent runtime involved:

import { createClient, anthropic, defineTool } from "samai-sdk";
import { z } from "zod";

const getWeather = defineTool({
  name: "get_weather",
  description: "Get current weather for a city",
  parameters: z.object({ city: z.string() }),
  execute: async ({ city }) => ({ city, tempC: 28, condition: "sunny" }),
});

const client = createClient({
  provider: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }),
});

const result = await client.generate({
  model: "claude-sonnet-4-6",
  system: "You are a concise assistant.",
  messages: [{ role: "user", content: "What's the weather in Chennai?" }],
  tools: [getWeather],
  maxToolRoundtrips: 2,
});

console.log(result.text);

Swap providers without touching anything else:

import { openai, google } from "samai-sdk";

const client = createClient({ provider: openai({ apiKey: "..." }) });
// or
const client = createClient({ provider: google({ apiKey: "..." }) });

For anything with multiple turns, tools, and possibly other agents involved, reach for the agent runtime instead of driving client.generate() yourself.

03 · GETTING STARTEDCLI

npx samai-sdk create <directory> [--provider anthropic|openai|groq|ollama]
npx samai-sdk trace <trace-file.json> [--port 4949]

create scaffolds a runnable starter project — package.json, tsconfig.json, .env.example, and a src/index.ts with one agent and one tool, wired to whichever provider you picked. It refuses to overwrite an existing directory rather than clobbering one silently.

npx samai-sdk create my-agent --provider groq
cd my-agent
npm install
cp .env.example .env   # add your API key
npm start

trace serves a rendered, interactive timeline for a saved RunTrace JSON file over a local HTTP server — see OpenTelemetry & trace viewer below.

npx samai-sdk trace ./trace.json --port 4949
# ✅ Trace viewer running at http://localhost:4949

See examples/cli-mock-test.ts — it runs the actual built dist/cli.js binary and typechecks the scaffolded output against this repo's real, built types, not just asserting files exist.

04 · CORE CONCEPTSTools

defineTool() bundles a name, description, a zod schema for arguments, and an execute function. Arguments are validated against the schema before execute ever runs, and both sync and async execute functions are supported.

import { defineTool } from "samai-sdk";
import { z } from "zod";

const lookupOrder = defineTool({
  name: "lookup_order",
  description: "Fetch an order by ID",
  parameters: z.object({ orderId: z.string().uuid() }),
  execute: async ({ orderId }) => {
    const order = await db.orders.findById(orderId); // async is fine
    if (!order) throw new Error(`No order found for ${orderId}`);
    return order; // typed as whatever execute() returns
  },
});

Failures inside execute (including schema-validation failures, timeouts, and rejected approvals — see Guardrails & approval) never throw out of the run. They're captured as an isError tool result and fed back to the model, which can retry, ask a clarifying question, or explain the failure to the end user.

Tool anatomy

FieldTypeNotes
namestringUnique per agent; the model calls tools by this name
descriptionstringShown to the model — be specific about when to use it
parametersz.ZodTypeValidated before execute runs
execute(args) => Result | Promise<Result>Return value becomes the tool result
requiresApprovalboolean | (args) => booleanGate this tool behind human sign-off — see Guardrails
timeoutMsnumberPer-tool execution deadline (default 30s)

Built-in: web search

createWebSearchTool() gives the model a real, ready-to-use web_search tool — backed by an actual HTTP call to the Tavily or Brave search API, not a stub. Get a key at tavily.com or brave.com/search/api.

import { createWebSearchTool, defineAgent } from "samai-sdk";

const researcher = defineAgent({
  name: "researcher",
  instructions: "Use web_search for anything time-sensitive or after your training cutoff.",
  model: "claude-sonnet-4-6",
  tools: [createWebSearchTool({ apiKey: process.env.TAVILY_API_KEY })], // or { provider: "brave" }
});

If no apiKey is supplied and no TAVILY_API_KEY/BRAVE_API_KEY env var is set, the tool throws a clear configuration error as an isError tool result — it never fails silently.

05 · CORE CONCEPTSMCP (Model Context Protocol)

createMCPClient() connects to any MCP server and exposes its tools as ordinary ToolDefinitions — mix them into an agent's tools array alongside locally-defined tools, createWebSearchTool(), whatever. Needs the optional @modelcontextprotocol/sdk peer dependency (npm install @modelcontextprotocol/sdk).

import { createClient, anthropic, defineAgent, runAgent, createMCPClient } from "samai-sdk";

// Local server, spawned as a child process over stdio:
const filesystem = createMCPClient({
  transport: { transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] },
  toolPrefix: "fs", // avoids name collisions if you wire up more than one MCP server
});

const client = createClient({ provider: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }) });

const agent = defineAgent({
  name: "file_assistant",
  instructions: "Help the user inspect and organize files in /tmp using the fs__ tools.",
  model: "claude-sonnet-4-6",
  tools: await filesystem.tools(),
});

const result = await runAgent(client, agent, "What files are in /tmp?");
await filesystem.close(); // kills the spawned process

Remote servers work the same way, over the current Streamable HTTP transport (or legacy SSE, for older servers):

const acme = createMCPClient({
  transport: { transport: "http", url: "https://mcp.acme.com/mcp", headers: { Authorization: `Bearer ${token}` } },
  toolPrefix: "acme",
});

Each MCP tool's JSON Schema reaches the model exactly as the server declares it (via ToolDefinition.rawJsonSchema, an escape hatch every built-in provider adapter checks first) — nothing is lost round-tripping through zod. Argument validation before a call reaches the server is a permissive "is this an object" check, since the server itself is the source of truth for its own schema. Call results come back as structuredContent when the server provides it, otherwise as flattened text. Pass requiresApproval (boolean, or (toolName, args) => boolean | Promise<boolean>) to gate every tool from a server behind the same approval flow as any other tool — see Guardrails & approval.

06 · CORE CONCEPTSSandboxed code execution

createSandbox() gives an agent an isolated temp directory to run real JavaScript/Python/bash in and read/write files against — the primitive behind "long-horizon" coding-agent behavior (inspect files, run commands, edit code, repeat). createCodeExecutionTool() wraps it as a single execute_code tool; createSandboxTools() bundles that with write_file/read_file/list_files against the same sandbox, so a model can write a file with one tool and run it with another across turns.

import { createClient, anthropic, defineAgent, runAgent, createSandbox, createSandboxTools } from "samai-sdk";

const sandbox = createSandbox();
const client = createClient({ provider: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }) });

const agent = defineAgent({
  name: "coder",
  instructions: "Write and test code using write_file and execute_code. JavaScript runs as an ES module.",
  model: "claude-sonnet-4-6",
  tools: createSandboxTools(sandbox),
});

const result = await runAgent(client, agent, "Write fibonacci.py, run it, and tell me the output.");
await sandbox.close(); // deletes the temp directory

For a single one-shot execution tool without file persistence, use createCodeExecutionTool() directly: tools: [createCodeExecutionTool({ languages: ["javascript", "python"] })].

What "sandboxed" means here — read before using this with untrusted input. Every execution gets its own cwd (file I/O is confined to it — path traversal via ../ is rejected), a minimal environment (only PATH/HOME/TMPDIR — your process's other env vars, including API keys, are not inherited by executed code), a wall-clock timeout that actually kills the process (SIGKILL, verified against a real sleep in the test suite), and a byte-accurate output-truncation cap. This is process-level isolation, not OS-level: there's no container, VM, or network namespace. Fine for your own experimentation or a trusted model with shell access; for untrusted code or multiple tenants, run this SDK itself inside an actual container/VM and point dir at a path inside that boundary. Supported languages: "javascript" (ES module via nodeimport, not require), "python" (via python3, must be on PATH), "bash" (via /bin/bash -c).

07 · CORE CONCEPTSVoice / realtime agents

Heads up. generateSpeech()/transcribeAudio() are straightforward REST calls (same shape as createWebSearchTool()) but haven't been exercised against a live key from this SDK's dev environment. createRealtimeSession()'s wire-protocol logic has been verified against a real local mock WebSocket server — catching and fixing a real race condition and an auth bug in the process — but the exact event names/fields haven't been confirmed against OpenAI's live server, since that API moves quickly. Read the disclaimer at the top of src/voice.ts before production use.

generateSpeech() and transcribeAudio() wrap OpenAI's TTS and Whisper REST endpoints:

import { generateSpeech, transcribeAudio } from "samai-sdk";
import { writeFile, readFile } from "node:fs/promises";

const { audio } = await generateSpeech({ input: "Hello there!", voice: "nova" });
await writeFile("out.mp3", audio);

const { text } = await transcribeAudio({ audio: await readFile("recording.mp3"), filename: "recording.mp3" });

createRealtimeSession() opens a bidirectional, streamed voice session with your agent's tools wired in:

import { createRealtimeSession } from "samai-sdk";

const session = createRealtimeSession({
  instructions: "You are a helpful, concise voice assistant.",
  voice: "alloy",
  tools: [getWeatherTool], // any ToolDefinition[] — called automatically when the model invokes them
});

session.on((event) => {
  if (event.type === "audio.delta") playAudioChunk(event.audio); // your speaker output
  if (event.type === "speech_started") stopSpeakerPlayback(); // user talking over the assistant — barge-in
});

await session.connect();
session.sendText("What's the weather in Tokyo?");
session.interrupt(); // cancels the in-flight response, the instant you detect a barge-in
await session.close();

Handles the network/protocol side only — pairing it with actual mic capture and speaker playback is up to your app. On Node < 22, or for header-based auth (recommended), install the optional ws peer dependency; without it, connections fall back to OpenAI's documented subprotocol-based auth, since the standard WebSocket global can't send custom headers at all.

08 · CORE CONCEPTSAgent runtime

defineAgent() bundles instructions, a model, tools, optional handoffs, guardrails, and an output schema into one named, reusable unit. runAgent() (or runAgentStream() for live events) is the actual agent loop — it owns tool execution and multi-turn orchestration itself, so behavior is identical no matter which provider backs the agent.

import { z } from "zod";
import { createClient, anthropic, defineAgent, runAgent } from "samai-sdk";

const client = createClient({ provider: anthropic({ apiKey: "..." }) });

const getWeather = defineTool({
  name: "get_weather",
  description: "Get the current weather for a city",
  parameters: z.object({ city: z.string() }),
  execute: async ({ city }) => `18C and cloudy in ${city}`,
});

const agent = defineAgent({
  name: "trip_planner",
  instructions: "Look up weather, then give concise packing advice.",
  model: "claude-sonnet-4-6",
  tools: [getWeather],
  maxTurns: 10, // safe stopping condition — throws MaxTurnsExceededError past this
});

const result = await runAgent(client, agent, "What should I pack for Tokyo?");
console.log(result.output);       // the agent's final answer
console.log(result.finalAgent);   // "trip_planner"
console.log(result.trace);        // full RunTrace — see Tracing below

The loop: send messages + tools to the model → detect tool calls → run tool guardrails and approval gates → execute tools → feed results back → repeat until a final answer with no tool calls, a handoff, or a stopping condition (maxTurns, an absolute turn cap, or an unhandled error).

Streaming events

Use runAgentStream() directly to consume events as the run progresses — useful for driving a chat UI:

for await (const event of runAgentStream(client, agent, "What should I pack for Tokyo?")) {
  switch (event.type) {
    case "text-delta": process.stdout.write(event.textDelta); break;
    case "tool-started": console.log("calling", event.toolName, event.args); break;
    case "tool-completed": console.log("tool result", event.result); break;
    case "handoff-started": console.log(`${event.fromAgent} -> ${event.toAgent}`); break;
    case "guardrail-triggered": console.warn(`${event.stage} guardrail blocked: ${event.reason}`); break;
    case "approval-requested": console.log("needs approval:", event.toolName); break;
    case "approval-resolved": console.log("approval:", event.toolName, event.approved); break;
    case "run-completed": console.log("done", event.usage); break;
    case "run-failed": console.error(event.error); break;
  }
}

09 · CORE CONCEPTSHandoffs

Any agent listed in another agent's handoffs becomes callable as a synthetic tool the model can invoke like any other tool call. The run loop intercepts these before normal tool execution, switches the active agent, and carries the full message history forward — the new agent sees everything that happened before the handoff.

const packingAgent = defineAgent({
  name: "packing_specialist",
  instructions: "Give concise packing advice based on weather info already in the conversation.",
  model: "claude-sonnet-4-6",
});

const routerAgent = defineAgent({
  name: "trip_router",
  instructions: "Look up weather, then hand off to packing_specialist for advice.",
  model: "claude-sonnet-4-6",
  tools: [getWeather],
  handoffs: [packingAgent],
});

const result = await runAgent(client, routerAgent, "What should I pack for Tokyo?");
console.log(result.finalAgent);      // "packing_specialist" — may differ from the starting agent
console.log(result.trace.agentPath); // ["trip_router", "packing_specialist"]

Loop prevention

To prevent infinite delegation (A → B → A → B → ...), the run loop tracks every agent visited in a run: handing off to an already-visited agent throws HandoffLoopError, and a hard maxHandoffs cap (default 5, override via runAgent(client, agent, input, { maxHandoffs: 10 })) catches runaway delegation even across distinct agents.

try {
  await runAgent(client, routerAgent, input);
} catch (err) {
  if (err instanceof AgentRunError) {
    console.error(err.cause);        // HandoffLoopError, MaxTurnsExceededError, etc.
    console.error(err.trace.events); // full trace up to the point of failure
  }
}

Every handoff appears in the trace as a handoff event and in trace.agentPath, so delegation is always visible in logs — never a silent jump.

10 · CORE CONCEPTSGuardrails & approval

Four layers, each solving a different part of "validate before/after execution":

Input guardrails

Run before a call reaches the model. Reject invalid input, redact PII, or block prompt-injection attempts.

Output guardrails

Run after a response comes back. Validate structured output, strip sensitive info, cap spend.

Tool guardrails

Run before a specific tool call executes. Block by tool name or by inspecting arguments for dangerous patterns.

Approval gates

Pause a specific tool call for human sign-off rather than blocking it outright. Fails closed with no handler configured.

Input / output guardrails

import { createClient, anthropic, createPiiInputGuardrail, createPromptInjectionGuardrail, createBudgetGuardrail } from "samai-sdk";

const budget = createBudgetGuardrail({ maxCostUsd: 5.0 });

const client = createClient({
  provider: anthropic({ apiKey: "..." }),
  inputGuardrails: [
    createPiiInputGuardrail({ mode: "redact" }),
    createPromptInjectionGuardrail({ mode: "block" }),
    budget.inputGuardrail,
  ],
  outputGuardrails: [budget.outputGuardrail],
});

Or write your own — an InputGuardrail/OutputGuardrail is just a function returning { allowed, reason? }:

const client = createClient({
  provider: anthropic({ apiKey: "..." }),
  inputGuardrails: [
    async ({ messages }) => {
      const last = messages.at(-1);
      const text = typeof last?.content === "string" ? last.content : "";
      return text.includes("secret-password")
        ? { allowed: false, reason: "contains sensitive term" }
        : { allowed: true };
    },
  ],
});

Tool guardrails

Run automatically before every tool call, scoped per-agent:

import { defineAgent, createDangerousToolGuardrail } from "samai-sdk";

const opsAgent = defineAgent({
  name: "ops_agent",
  instructions: "...",
  model: "claude-sonnet-4-6",
  tools: [wipeDatabase, sendEmail],
  guardrails: {
    tool: [createDangerousToolGuardrail({ blockedTools: ["wipe_database"] })],
  },
});
Built-in patterns. createDangerousToolGuardrail() also blocks calls whose serialized arguments match common destructive patterns (rm -rf, unscoped DROP TABLE/DELETE FROM ... WHERE 1=1, sudo) by default, on top of any tool names you block explicitly.

Approval gates

Mark a tool with requiresApproval, then supply onApprovalRequest when running the agent:

const sendEmail = defineTool({
  name: "send_email",
  description: "Sends an email to the given address",
  parameters: z.object({ to: z.string(), body: z.string() }),
  execute: async ({ to, body }) => { /* ... */ },
  requiresApproval: true, // or (args) => args.to !== "internal-test@example.com"
});

await runAgent(client, opsAgent, "Email the team about the deploy", {
  onApprovalRequest: async ({ toolName, args }) => {
    // Wire this to a UI confirm dialog, a Slack Approve/Reject message,
    // a CLI prompt — whatever fits. Return true to allow the call.
    return await askHumanToApprove(toolName, args);
  },
});
Fails closed by default. If onApprovalRequest is omitted entirely, approval-gated tools are rejected automatically — a risky action never runs unattended just because nobody wired up a handler yet.

Both tool guardrails and approvals show up in the trace (guardrail-triggered with stage: "tool", plus approval-requested/approval-resolved) and as streamed runAgentStream() events.

Structured-output guardrail

import { createSchemaGuardrail } from "samai-sdk";
import { z } from "zod";

const client = createClient({
  provider: anthropic({ apiKey: "..." }),
  outputGuardrails: [createSchemaGuardrail(z.object({ summary: z.string(), score: z.number() }))],
});

11 · CORE CONCEPTSMemory & sessions

A Session persists conversation history across separate runAgent() calls — kept deliberately distinct from defineAgent() (static configuration) and the transient message list a single run builds up internally.

ConceptLifetimeHolds
Agent config (defineAgent())Defined once, reused everywhereInstructions, model, tools, guardrails, handoffs
Run stateOne runAgent() callMessages built up during that run, current turn/handoff counters
Session stateAcross many runAgent() callsPersisted conversation history, via a pluggable SessionStore
import {
  createSession,
  InMemorySessionStore,
  FileSessionStore,
  RedisSessionStore,
  SqliteSessionStore,
} from "samai-sdk";

// Lives for the process lifetime — good for scripts and tests:
const session = createSession("user-123", new InMemorySessionStore());

// Persists to disk as JSON — survives process restarts, no extra infra:
const fileSession = createSession("user-123", new FileSessionStore("./sessions"));

// Persists to Redis — shared across processes/instances, optional TTL expiry.
// Needs the optional `ioredis` peer dependency.
const redisSession = createSession(
  "user-123",
  new RedisSessionStore({ url: process.env.REDIS_URL, ttlSeconds: 60 * 60 * 24 })
);

// Persists to a local SQLite file — durable, no external server needed.
// Needs the optional `better-sqlite3` peer dependency.
const sqliteSession = createSession("user-123", new SqliteSessionStore({ path: "./data/sessions.db" }));

await runAgent(client, agent, "What should I pack for Tokyo?", { session });
await runAgent(client, agent, "What about shoes?", { session }); // sees the prior turn

All four stores implement the same three-method SessionStore interface (getMessages / appendMessages / clear) — swap between them, or write your own for Postgres, DynamoDB, etc., without touching call sites. RedisSessionStore also accepts an already-connected client via { client } if you manage your own connection pool.

12 · CORE CONCEPTSRAG / vector search

Three independently swappable pieces: an EmbeddingProvider (text → vectors), a VectorStore (stores/searches vectors), and createRetrievalTool() (wires them into something the model can call).

import {
  createClient, anthropic, defineAgent, runAgent,
  openaiEmbeddings, InMemoryVectorStore, createRetrievalTool, embedChunks,
} from "samai-sdk";

const embeddings = openaiEmbeddings({ apiKey: process.env.OPENAI_API_KEY }); // needs `openai` installed
const store = new InMemoryVectorStore(); // or `new PineconeVectorStore({ indexHost: "..." })` for production

// Ingest: embed your chunks once, upsert into the store.
const records = await embedChunks(embeddings, [
  { id: "doc-1", text: "Refunds are processed within 3-5 business days." },
  { id: "doc-2", text: "Reset your password from Settings > Security." },
]);
await store.upsert(records);

// Give the agent a tool that can search what you just ingested.
const supportAgent = defineAgent({
  name: "support_agent",
  instructions: "Use retrieve_knowledge to ground answers in the docs before replying.",
  model: "claude-sonnet-4-6",
  tools: [createRetrievalTool({ embeddings, store, options: { topK: 3 } })],
});

const client = createClient({ provider: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }) });
const result = await runAgent(client, supportAgent, "How long do refunds take?");
PieceNotes
InMemoryVectorStoreBrute-force cosine similarity, zero setup — fine for prototyping and a few thousand vectors
PineconeVectorStore({ indexHost, apiKey })Talks to Pinecone's REST API directly over fetch, no extra SDK dependency
Custom VectorStoreThree methods (upsert/query/delete) — same shape as SessionStore — for pgvector, Qdrant, Weaviate, etc.
openaiEmbeddings()Default EmbeddingProvider, via the OpenAI embeddings endpoint

createRetrievalTool() accepts topK and a metadata filter (e.g. { tenantId: "acme" }) to scope retrieval — both apply on every call the model makes to the tool.

13 · CORE CONCEPTSStructured output & streaming

generateObject()

Guarantees a typed, schema-validated object — not hopeful JSON parsing. If the model's output fails validation, it's automatically retried with a repair prompt describing exactly what was wrong, up to maxRepairAttempts times (default 2).

import { z } from "zod";
import { generateObject } from "samai-sdk";

const ReviewSchema = z.object({
  summary: z.string(),
  sentiment: z.enum(["positive", "negative", "mixed"]),
  score: z.number().min(1).max(5),
});

const result = await generateObject(client, {
  model: "claude-sonnet-4-6",
  schema: ReviewSchema,
  messages: [{ role: "user", content: "Extract structured data from this review: ..." }],
});

result.object.sentiment; // fully typed: "positive" | "negative" | "mixed"
result.attempts;         // how many tries it took

If it never succeeds, it throws GenerateObjectError with .attempts and .lastError for logging or graceful fallback.

streamObject()

Same guarantee, but streamed — partialObjectStream yields progressively-more-complete objects as the model writes them, for rendering a form, card, or dashboard field-by-field.

const { partialObjectStream, object, usage } = streamObject(client, {
  model: "claude-sonnet-4-6",
  schema: RecipeSchema,
  messages: [{ role: "user", content: "Give me a simple chana masala recipe." }],
});

for await (const partial of partialObjectStream) {
  render(partial); // DeepPartial<Recipe> — bind straight to UI state
}

const recipe = await object; // fully typed, schema-validated
Unlike generateObject(), streamObject() doesn't auto-repair — once partial output has reached the UI, silently restarting would duplicate or contradict what the user already saw. If the final accumulated output fails validation, the object promise rejects with GenerateObjectError.

Raw streaming

for await (const chunk of client.stream({ model: "gpt-4o-mini", messages: [...] })) {
  if (chunk.type === "text-delta") process.stdout.write(chunk.textDelta);
}

14 · CORE CONCEPTSBatch output & Standard Schema

generateObjectBatch()

Runs generateObject() across many inputs with bounded concurrency — the shape of a data-extraction pipeline. One bad input never aborts the rest of the batch; results come back in the same order as items regardless of completion order.

import { z } from "zod";
import { createClient, anthropic, generateObjectBatch } from "samai-sdk";

const client = createClient({ provider: anthropic({ apiKey: "..." }) });
const TicketSchema = z.object({
  category: z.enum(["billing", "bug", "feature_request", "other"]),
  urgency: z.enum(["low", "medium", "high"]),
});

const batch = await generateObjectBatch(client, {
  items: ["My card was charged twice", "App crashes on launch"],
  buildOptions: (ticketText) => ({
    model: "claude-sonnet-4-6",
    schema: TicketSchema,
    messages: [{ role: "user", content: `Classify: "${ticketText}"` }],
  }),
  concurrency: 5, // default 5
  onItemSettled: (item) => console.log(`item ${item.index}: ${item.status}`),
});

console.log(`${batch.succeeded}/${batch.results.length} succeeded`, batch.usage);
for (const r of batch.results) {
  if (r.status === "fulfilled") console.log(r.item, "->", r.result.object);
  else console.warn(r.item, "failed:", r.error.message);
}

Pass throwOnAnyFailure: true to throw a GenerateObjectBatchError (carrying the full batchResult, including successful items) once every item has settled if any failed. For a concurrency cap shared across unrelated calls too, wrap the provider in withConcurrencyLimit() instead — the two compose.

Standard Schema support (valibot, and others)

Anywhere the SDK takes a schema — generateObject(), streamObject(), createSchemaGuardrail(), Agent.outputSchema — you can pass a zod schema, or any Standard Schema V1 validator (valibot 0.31+/1.x, arktype, etc.) instead. zod behavior is unchanged; this is purely additive.

import * as v from "valibot";
import { createClient, anthropic, generateObject } from "samai-sdk";

const ReviewSchema = v.object({
  summary: v.string(),
  score: v.pipe(v.number(), v.minValue(1), v.maxValue(10)),
});

const result = await generateObject(client, {
  model: "claude-sonnet-4-6",
  schema: ReviewSchema, // a valibot schema — works exactly the same way as zod
  messages: [{ role: "user", content: "Extract structured data from this review: ..." }],
});

result.object.score; // fully typed via valibot's own inference

Validation needs no extra dependency. Generating the model-facing JSON-Schema instruction currently supports zod and valibot specifically — valibot needs the optional @valibot/to-json-schema peer dependency. Other Standard Schema vendors work for validation but need you to describe the output shape yourself via system.

Known limitation. This currently reaches generateObject()/streamObject()/createSchemaGuardrail()/Agent.outputSchema only. Tool parameters across the 8 provider adapters still require a zod schema specifically.

15 · CORE CONCEPTSTracing

Every run produces a RunTrace (also available as result.trace): a runId, the full agentPath, a timestamped event log, and totalUsage summed across every model call. If the provider passed to createClient() was wrapped with withRetry()/withFallback()/withTimeout(), every retry, fallback, and timeout that happens during the run is captured automatically — no extra wiring needed.

RunTrace · run_8f21acagentPath: [trip_router → packing_specialist]
+0msrun-startedtrip_router
+180msretryattempt 1 after 200ms — 503 Service Unavailable
+412mstool-callget_weather({ city: "Tokyo" })
+588mstool-result18C and cloudy in Tokyo
+901mshandofftrip_router → packing_specialist
+1240msguardrail-triggeredoutput · schema mismatch, retrying
+1810msrun-completedtotalUsage: 68 tokens

Illustrative shape of a trace — actual field names below.

console.log(result.trace.runId);       // e.g. "8f21ac.."
console.log(result.trace.agentPath);   // ["trip_router", "packing_specialist"]
console.log(result.trace.events);      // model-call, tool-call, tool-result, handoff,
                                        // retry, fallback, timeout,
                                        // guardrail-triggered, approval-requested/resolved,
                                        // run-completed / run-failed — each timestamped
console.log(result.trace.totalUsage);  // { inputTokens, outputTokens, totalTokens }

for (const event of result.trace.events) {
  if (event.type === "retry")    console.log(`retry #${event.attempt}: ${event.error}`);
  if (event.type === "fallback") console.log(`${event.failedProvider} -> ${event.nextProvider}`);
  if (event.type === "timeout")  console.log(`${event.model} timed out after ${event.timeoutMs}ms`);
}

The same events stream live from runAgentStream() as retry-attempted / fallback-triggered / timeout-occurred AgentEvents, so a UI can surface "retrying…" without waiting for the run to finish. See examples/resilience-tracing-mock-test.ts for this exercised end-to-end against the real resilience wrappers.

Useful for debugging, cost tracking, and building your own observability layer — pipe trace.events straight into your logger or a tracing backend.

16 · CORE CONCEPTSReliability & timeouts

Retries and fallback chains

import { withRetry, withFallback, createResilientProvider, anthropic, openai } from "samai-sdk";

// Retries only, one provider
const retrying = withRetry(anthropic({ apiKey: "..." }), { maxRetries: 3 });

// Fallback only — tries Claude, then GPT if Claude errors
const chain = withFallback([anthropic({ apiKey: "..." }), openai({ apiKey: "..." })]);

// Both combined, each provider retried before falling through to the next
const resilient = createResilientProvider(
  [anthropic({ apiKey: "..." }), openai({ apiKey: "..." })],
  { retry: { maxRetries: 2, initialDelayMs: 500 } }
);

Streaming note: retries/fallback only apply before the first chunk reaches the caller — a mid-stream failure surfaces as-is rather than duplicating or dropping output already seen.

Timeouts

withTimeout() enforces a real deadline via AbortController — not error-message sniffing:

import { withTimeout, withRetry, anthropic } from "samai-sdk";

const provider = withRetry(
  withTimeout(anthropic({ apiKey: "..." }), { timeoutMs: 15_000 }),
  { maxRetries: 2 }
);

createResilientProvider() applies a 30s default timeout automatically (override with { timeout: { timeoutMs } }, disable with { timeout: false }). TimeoutError is retryable by default.

Tool execution has its own independent deadline — every execute() call is raced against a timeout (default 30s, set per-tool via timeoutMs, or per-run via runAgent(client, agent, input, { defaultToolTimeoutMs })). A hung tool returns as an isError result instead of hanging the run.

Loop prevention & safe secrets

  • Per-agent maxTurns plus a hard absolute turn cap (50), both raising MaxTurnsExceededError
  • HandoffLoopError for revisited agents, plus a hard maxHandoffs cap
  • API keys are only ever read from constructor options / env vars, never logged, and never appear in trace events

17 · CORE CONCEPTSConcurrency & rate limiting

Two provider wrappers, same shape as withRetry/withFallback/withTimeout — compose all of them freely. Both queue calls beyond the limit rather than rejecting.

import { withConcurrencyLimit, withRateLimit, withRetry, anthropic } from "samai-sdk";

// Caps in-flight calls — a queue, not a rejection.
const capped = withConcurrencyLimit(anthropic({ apiKey: "..." }), { maxConcurrent: 5 });

// Caps requests per time window — a token-bucket limiter, refills continuously.
const throttled = withRateLimit(anthropic({ apiKey: "..." }), { maxRequests: 60, intervalMs: 60_000 });

// Compose with retries — wrapping the limit AROUND retry means retries of the same call
// count against the limit too (usually what you want).
const provider = withConcurrencyLimit(withRetry(anthropic({ apiKey: "..." }), { maxRetries: 2 }), { maxConcurrent: 5 });

Use withConcurrencyLimit() to stay under a provider's hard concurrent-request cap when running many agents (or a generateObjectBatch()) at once. Use withRateLimit() to stay under a published requests-per-minute limit before it turns into 429s that withRetry then has to spend time recovering from.

18 · CORE CONCEPTSResumable runs

resumeAgentStream()/resumeAgent() pick a run back up after a crash, an uncaught error, or a process restart — instead of starting over from the original input. A RunCheckpoint is saved after every completed turn (model call + any tool execution or handoff).

import {
  createClient, anthropic, defineAgent, runAgent, resumeAgent, FileCheckpointStore,
} from "samai-sdk";

const client = createClient({ provider: anthropic({ apiKey: "..." }) });
const agent = defineAgent({ name: "worker", instructions: "...", model: "claude-sonnet-4-6", tools: [...] });

const checkpointStore = new FileCheckpointStore("./checkpoints"); // survives a real process restart
const runId = "run-" + Date.now();

try {
  await runAgent(client, agent, "Do a multi-step task", { checkpoint: { store: checkpointStore, runId } });
} catch (err) {
  // Resume with the SAME root agent — its handoffs tree is walked by name to find
  // whichever agent was active when the checkpoint was saved.
  const result = await resumeAgent(client, agent, { checkpoint: { store: checkpointStore, runId } });
  console.log(result.output);
}

Already-executed tool calls are never re-run on resume — the checkpoint carries the full message history, so the resumed run's first action is a fresh model call continuing the conversation, not a repeat of completed work. The checkpoint is deleted automatically on successful completion; it's left in place on failure so you can inspect or resume past it.

StoreNotes
InMemoryCheckpointStoreOnly survives within the same process — good for resuming after a caught error mid-request
FileCheckpointStore(dir)One JSON file per run — survives a real process restart/crash

Agent definitions (instructions, tools, code) aren't part of a checkpoint — only the run's accumulated state is. Resuming a runId with no saved checkpoint throws CheckpointNotFoundError rather than silently starting fresh. See examples/checkpoint-resume-mock-test.ts, which simulates a genuine mid-run crash and proves no tool call gets re-executed on resume.

19 · CORE CONCEPTSError handling

ErrorThrown when
AgentRunErrorWraps any error from a run; carries .cause and .trace
MaxTurnsExceededErrorAn agent (or the absolute cap) exceeds its turn limit
HandoffLoopErrorA handoff would revisit an already-visited agent, or exceed maxHandoffs
GuardrailBlockedErrorAn input/output guardrail returns allowed: false
GenerateObjectErrorgenerateObject()/streamObject() output never passes schema validation
TimeoutErrorA provider call exceeds its withTimeout() deadline
ToolTimeoutErrorA tool's execute() exceeds its timeout (surfaced as an isError tool result, not thrown)
AllProvidersFailedErrorEvery provider in a withFallback() chain fails
CheckpointNotFoundErrorresumeAgentStream() is given a runId with no saved checkpoint, or one referencing an unreachable agent
GenerateObjectBatchErrorgenerateObjectBatch({ throwOnAnyFailure: true }) and at least one item failed; carries the full batchResult on .batchResult
try {
  await runAgent(client, agent, input);
} catch (err) {
  if (err instanceof AgentRunError) {
    console.error(err.cause);        // the underlying typed error
    console.error(err.trace.events); // trace up to the point of failure
  }
}

20 · OPS & TESTINGOpenTelemetry & trace viewer

Every run already produces a RunTrace — these two features turn that data into something you can look at or pipe into existing infra.

exportRunTraceToOtel()

Converts a RunTrace into real OpenTelemetry spans on whatever tracer your app has already configured. Model calls and tool calls become duration spans (paired from the trace's start/end events, so they carry real timing); handoffs, retries, fallbacks, timeouts, guardrail trips, and approvals become short child spans — all correctly parented under one root span per run.

import { runAgent, exportRunTraceToOtel } from "samai-sdk";

const result = await runAgent(client, agent, "hi");
await exportRunTraceToOtel(result.trace); // needs the optional @opentelemetry/api peer dependency

// Now visible wherever your traces already go — Honeycomb, Datadog, Grafana Tempo, or
// anything else that speaks OTLP, using whatever exporter/provider you've already set up.

renderTraceHTML() + samai-sdk trace

Renders a RunTrace as a self-contained, offline-viewable HTML timeline — no server, no build step, color-coded events proportionally positioned by real elapsed time, filterable by type, raw JSON available inline.

import { writeFileSync } from "node:fs";
import { runAgent, renderTraceHTML } from "samai-sdk";

const result = await runAgent(client, agent, "hi");
writeFileSync("trace.html", renderTraceHTML(result.trace)); // open directly in a browser
npx samai-sdk trace ./trace.json --port 4949
# ✅ Trace viewer running at http://localhost:4949

See examples/otel-export-mock-test.ts (verified against the real @opentelemetry/sdk-trace-base in-memory exporter — actual span names, attributes, parent/child nesting, and status codes) and examples/trace-viewer-mock-test.ts (starts the real CLI server and fetches from it).

21 · OPS & TESTINGUsage tracking

createBudgetGuardrail() (see Guardrails) answers "has this client exceeded its budget" with one running total. createUsageLedger() answers "how much has each session/user cost so far" — cumulative tokens and estimated cost, broken down per key and per model.

import { createClient, anthropic, createUsageLedger } from "samai-sdk";

const ledger = createUsageLedger();
const provider = ledger.wrapProvider(
  anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }),
  (options) => options.metadata?.sessionId as string | undefined
);
const client = createClient({ provider });

await client.generate({
  model: "claude-sonnet-4-6",
  messages: [{ role: "user", content: "hi" }],
  metadata: { sessionId: "session-123" },
});

console.log(ledger.getStats("session-123"));
// { totalTokens, totalCostUsd, callCount, byModel: { "claude-sonnet-4-6": { ... } } }
console.log(ledger.getAllStats()); // every key seen so far
console.log(ledger.toJSON());      // JSON snapshot — feed this to a dashboard or log periodically

Calls where keyFn returns undefined are recorded under "_unattributed" rather than silently dropped. Pass { onRecord } to fire on every recorded call, and { pricing } to override the built-in per-model pricing table — provider pricing changes over time, so treat the defaults as illustrative. The ledger tracks numbers; rendering a dashboard from toJSON() is on you.

22 · OPS & TESTINGTesting your agents

createMockProvider() ships in the SDK so you don't have to hand-roll a fake Provider for your own tests.

import { createClient, defineAgent, runAgent, createMockProvider } from "samai-sdk";

const mock = createMockProvider({
  responses: [
    { toolCalls: [{ toolName: "get_weather", args: { city: "Tokyo" } }] },
    { text: "It's 18°C and cloudy in Tokyo." },
  ],
});

const client = createClient({ provider: mock });
const result = await runAgent(client, myAgent, "What's the weather in Tokyo?");

// every GenerateOptions this provider was called with, in order:
console.log(mock.calls.length); // 2
console.log(result.output);     // "It's 18°C and cloudy in Tokyo."

mock.reset(); // clears the call log so the same mock instance can be reused across test cases

Each entry in responses can set text, toolCalls, finishReason, usage, delayMs (simulate latency), or error (simulate a provider failure). Pass a function instead of an array if a turn's response needs to depend on what the agent loop actually sent.

23 · OPS & TESTINGDeployment

Provider/store compatibility across Node servers, Node serverless, and edge runtimes (Vercel Edge, Cloudflare Workers), a custom-SessionStore recipe for edge-only persistence, and runnable Vercel Edge / Cloudflare Worker examples are covered in docs/deployment.md in the repository.

Native-dependency stores don't run at the edge. SqliteSessionStore (native better-sqlite3 binding) and FileCheckpointStore/FileSessionStore (filesystem access) only work in Node runtimes. RedisSessionStore works anywhere that can reach your Redis instance over the network, including most edge runtimes.

24 · REFERENCEModel providers

The agent runtime and client only depend on the Provider interface (generate() + stream()), so adding a new one is a matter of implementing that interface against a new API — no changes needed anywhere else in the SDK. Every provider below implements it, so swapping one in only ever changes one line.

import { openai, anthropic, google, groq, mistral, ollama, azureOpenAI, bedrock } from "samai-sdk";

createClient({ provider: groq({ apiKey: "..." }) });    // fast inference (LPU hardware)
createClient({ provider: ollama() });                    // local models, no key, no cost
createClient({ provider: bedrock({ region: "us-east-1" }) }); // unified Converse API
ProviderImportPeer dependencyAPI key env var
Anthropic (Claude)anthropic()@anthropic-ai/sdkpass explicitly
OpenAIopenai()openaipass explicitly
Google (Gemini)google()@google/generative-aipass explicitly
Groqgroq()openaiGROQ_API_KEY
Mistralmistral()openaiMISTRAL_API_KEY
Ollama (local)ollama()openainone — local, no auth
Azure OpenAIazureOpenAI()openaiAZURE_OPENAI_API_KEY
AWS Bedrockbedrock()@aws-sdk/client-bedrock-runtimestandard AWS credential chain

groq(), mistral(), ollama(), and azureOpenAI() all reuse the openai SDK client pointed at a different baseURL — every OpenAI-compatible provider shares one implementation (buildOpenAIStyleProvider()), so there's nothing provider-specific to go wrong per integration. azureOpenAI() routes by deployment name, so model = your deployment name, not a model name. bedrock() uses the unified Converse API, so the same model param works across Bedrock-hosted Claude, Llama, Titan, etc.

25 · REFERENCEPrompt caching

Set promptCaching: true on a call to mark the system prompt and tool definitions as a reusable, cacheable prefix — useful in an agent loop where the same system prompt and tools get re-sent on every turn.

const result = await client.generate({
  model: "claude-sonnet-4-6",
  system: longStaticSystemPrompt,
  messages,
  tools,
  promptCaching: true,
});

console.log(result.usage.cacheReadTokens);  // tokens served from cache — billed at a fraction of input price
console.log(result.usage.cacheWriteTokens); // tokens written to the cache on this call

Currently honored by anthropic() — it adds Anthropic's cache_control breakpoints to the system prompt and the last tool definition (which caches the entire tool list as one unit). It's a no-op on providers that don't need client-side cache configuration — OpenAI and Groq cache automatically server-side above a token threshold with nothing to set.

26 · REFERENCEAPI reference

ExportPurpose
createClient(opts)Wraps a provider with input/output guardrail middleware
defineTool(tool)Type-checked tool definition helper
defineAgent(config)Bundles instructions/model/tools/handoffs/guardrails/schema
runAgent(client, agent, input, opts?)Runs an agent to completion, returns RunResult
runAgentStream(client, agent, input, opts?)Same, but yields AgentEvents as it goes
resumeAgent / resumeAgentStreamResume a run from a RunCheckpoint after a crash — see Resumable runs
InMemoryCheckpointStore / FileCheckpointStoreBuilt-in RunCheckpointStore implementations
generateObject(client, opts)Typed, schema-validated output with auto-repair
streamObject(client, opts)Streamed typed output, no auto-repair
generateObjectBatch(client, opts)Bounded-concurrency generateObject() across many inputs — see Batch output
createSession(id, store)Persistent cross-run conversation memory
InMemorySessionStore / FileSessionStoreBuilt-in SessionStore implementations (no extra infra)
RedisSessionStore / SqliteSessionStoreBuilt-in SessionStore implementations backed by Redis / SQLite (optional peer deps ioredis / better-sqlite3)
createWebSearchTool(opts?)Real Tavily/Brave-backed web_search tool
createMCPClient(opts)Connects to an MCP server (stdio/HTTP/SSE), returns its tools as ToolDefinition[] via .tools() — see MCP
createSandbox(opts?)Isolated temp directory + real code execution/file I/O — see Sandboxed code execution
createCodeExecutionTool(opts?) / createSandboxTools(sandbox?)Wrap a Sandbox as agent tools — the latter bundles execute_code/write_file/read_file/list_files
generateSpeech(opts) / transcribeAudio(opts)OpenAI TTS/Whisper REST wrappers — see Voice / realtime agents
createRealtimeSession(opts?)WebSocket session against OpenAI's Realtime API — streamed audio/text, interruption, tool-call execution
InMemoryVectorStore / PineconeVectorStoreBuilt-in VectorStore implementations — see RAG
openaiEmbeddings()Default EmbeddingProvider
createRetrievalTool(opts) / embedChunks()Wires embeddings + a vector store into a RAG tool
createDangerousToolGuardrail(opts?)Built-in tool guardrail for common destructive patterns
createPiiInputGuardrail / createPiiOutputGuardrailDetect/redact PII
createPromptInjectionGuardrail(opts?)Heuristic jailbreak detection
createBlocklistInputGuardrail / createBlocklistOutputGuardrailKeyword/regex filtering
createSchemaGuardrail(schema)Validates output as JSON against a zod OR Standard Schema schema
createBudgetGuardrail(opts)Caps cumulative token/cost spend for one client
createUsageLedger(opts?)Per-key (session/user) cumulative cost & token tracking — see Usage tracking
withRetry / withFallback / withTimeout / createResilientProviderProvider-level resilience wrappers — retries/fallbacks/timeouts show up automatically in RunTrace
withConcurrencyLimit / withRateLimitQueue-based in-flight / requests-per-window caps — see Concurrency & rate limiting
createMockProvider(opts)Scripted Provider for testing agents without a real model API — see Testing
exportRunTraceToOtel(trace, opts?)Converts a RunTrace into real OpenTelemetry spans
renderTraceHTML(trace, opts?)Renders a RunTrace as a self-contained offline HTML timeline
anthropic() / openai() / google() / groq() / mistral() / ollama() / azureOpenAI() / bedrock()Provider adapters — see Model providers
useAgent(client, agent)Framework hook wrapping runAgentStream() — import from "samai-sdk/react", "samai-sdk/vue", or "samai-sdk/svelte"
AnySchema / StandardSchemaV1 (types)The types accepted anywhere a schema is — zod or Standard Schema V1 (valibot, etc.); validation itself happens automatically inside generateObject()/streamObject()/createSchemaGuardrail(), not via a separately exported function

Full type signatures ship with the package (dist/index.d.ts) — every export above is fully typed, so your editor's autocomplete and hover docs cover anything not shown here.

27 · REFERENCEReact

The samai-sdk/react subpath exports useAgent(client, agent): a thin hook wrapping runAgentStream(). It owns no agent-loop logic of its own, so behavior matches calling runAgentStream() directly from Node — it just gives you React state to render.

import { createClient, anthropic, defineAgent } from "samai-sdk";
import { useAgent } from "samai-sdk/react";

const client = createClient({ provider: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }) });
const supportAgent = defineAgent({ name: "support_agent", instructions: "...", model: "claude-sonnet-4-6" });

function SupportChat() {
  const { run, isRunning, text, events, result, error } = useAgent(client, supportAgent);

  return (
    <div>
      <button onClick={() => run("How do I add a handoff?")} disabled={isRunning}>Ask</button>
      <p>{text}</p> {/* streams in live as text-delta events arrive */}
      {error && <p>Error: {error.message}</p>}
      {result && <p>Done — final agent: {result.finalAgent}</p>}
    </div>
  );
}
FieldTypeNotes
run(input, opts?)(string, RunAgentOptions?) => Promise<RunResult>Starts a run; safe to call again once the previous one finishes
isRunningbooleanTrue while a run is in progress
textstringAccumulates live from text-delta events
eventsAgentEvent[]Full ordered event log — tool calls, handoffs, retries/fallbacks/timeouts, guardrail trips
result / errorRunResult | null / Error | nullPopulated once the run finishes
reset()() => voidResets state back to idle

react is an optional peer dependency — nothing else in the SDK requires it. See examples/react-usage.tsx for the full version.

28 · REFERENCEVue

The samai-sdk/vue subpath exports the same useAgent(client, agent) shape for the Vue 3 Composition API — Vue refs instead of React state, identical underlying behavior.

<script setup>
import { createClient, anthropic, defineAgent } from "samai-sdk";
import { useAgent } from "samai-sdk/vue";

const client = createClient({ provider: anthropic({ apiKey: import.meta.env.VITE_ANTHROPIC_API_KEY }) });
const supportAgent = defineAgent({ name: "support_agent", instructions: "...", model: "claude-sonnet-4-6" });

const { run, isRunning, text, events, result, error } = useAgent(client, supportAgent);
</script>

<template>
  <button @click="run('How do I add a handoff?')" :disabled="isRunning">Ask</button>
  <p>{{ text }}</p>
  <p v-if="error">Error: {{ error.message }}</p>
  <p v-if="result">Done — final agent: {{ result.finalAgent }}</p>
</template>

isRunning/text/events are plain Refs (reactive, template-bindable directly); result/error are ShallowRefs. vue is an optional peer dependency. See examples/vue-usage-mock-test.ts, which exercises this against real Vue watch() reactivity.

29 · REFERENCESvelte

The samai-sdk/svelte subpath exports useAgent(client, agent) as a Svelte store — subscribe with $agent in a .svelte file.

<script>
  import { createClient, anthropic, defineAgent } from "samai-sdk";
  import { useAgent } from "samai-sdk/svelte";

  const client = createClient({ provider: anthropic({ apiKey: import.meta.env.VITE_ANTHROPIC_API_KEY }) });
  const supportAgent = defineAgent({ name: "support_agent", instructions: "...", model: "claude-sonnet-4-6" });

  const agent = useAgent(client, supportAgent);
</script>

<button on:click={() => agent.run("How do I add a handoff?")} disabled={$agent.isRunning}>Ask</button>
<p>{$agent.text}</p>
{#if $agent.error}<p>Error: {$agent.error.message}</p>{/if}
{#if $agent.result}<p>Done — final agent: {$agent.result.finalAgent}</p>{/if}

agent.run()/agent.reset() are called directly on the store object; every other field comes through the $agent subscription. svelte is an optional peer dependency. See examples/svelte-usage-mock-test.ts, which exercises this against a real store subscription.

All three framework hooks (react/vue/svelte) are thin wrappers around runAgentStream() — none of them own any agent-loop logic themselves.

30 · REFERENCEExamples

examples/basic.ts

Minimal createClient() + tool call round-trip against a real provider.

examples/agent-handoff.ts

Two agents, a real provider, one handing off packing advice to the other.

examples/agent-runtime-mock-test.ts

Tool calls, handoffs, loop prevention, and session persistence — no API key needed.

examples/agent-structured-output-mock-test.ts

generateObject() validate + repair loop — no API key needed.

examples/reliability-mock-test.ts

withTimeout(), tool guardrails, and the full approval workflow — no API key needed.

examples/resilience-tracing-mock-test.ts

Retries, fallbacks, and timeouts exercised against real wrappers, asserted visible in RunTrace — no API key needed.

examples/session-stores-mock-test.ts

SqliteSessionStore against a real on-disk database, plus RedisSessionStore logic — no API key needed.

examples/provider-conversion-mock-test.ts

Bedrock/Anthropic message + prompt-caching conversion logic — no API key needed.

examples/rag-mock-test.ts

Vector store + retrieval tool, embed → search → return, end to end — no API key needed.

examples/cli-mock-test.ts

Runs the real built CLI binary, typechecks the scaffolded output.

examples/checkpoint-resume-mock-test.ts

Genuine simulated crash mid-run + resume, proves no tool call is re-executed.

examples/testing-utils-mock-test.ts

createMockProvider(), withConcurrencyLimit(), withRateLimit() — real timing assertions.

examples/otel-export-mock-test.ts

exportRunTraceToOtel() against the real @opentelemetry/sdk-trace-base in-memory exporter.

examples/trace-viewer-mock-test.ts

renderTraceHTML() content + the real CLI trace server, fetched over HTTP.

examples/generate-object.ts

generateObject() against a real provider with a zod schema.

examples/stream-object.ts

streamObject() driving a progressively-filled UI card.

examples/react-usage.tsx

useAgent() driving a chat component end to end.

examples/vue-usage-mock-test.ts

Vue useAgent() against real Vue reactivity.

examples/svelte-usage-mock-test.ts

Svelte useAgent() against a real store subscription.

npm run example:basic
npm run example:agent-handoff                    # needs ANTHROPIC_API_KEY
npm run example:agent-runtime-mock-test          # no API key needed
npm run example:reliability-mock-test            # no API key needed
npm run example:resilience-tracing-mock-test     # no API key needed
npm run example:session-stores-mock-test         # no API key needed
npm run example:checkpoint-resume-mock-test      # no API key needed
npm run example:otel-export-mock-test            # no API key needed
npm test                                         # runs all 16 mock-test suites together