@usestratus/sdk
v1.9.0
Published
<p align="center"> <picture> <source media="(prefers-color-scheme: dark)" srcset=".github/logo-dark.svg"> <source media="(prefers-color-scheme: light)" srcset=".github/logo.svg"> <img src=".github/logo.svg" alt="Stratus" width="80" height="8
Maintainers
Readme
Stratus
A TypeScript agent SDK purpose-built for Azure OpenAI.
- One line to start —
createModel()reads your env vars. No config objects, no API version guessing. - One interface, two backends — Chat Completions and Responses API through the same agent, tool, and session code.
- Agents that compose — handoffs, subagents, guardrails, and hooks in a single run loop. Deny or modify tool calls at runtime.
- Workflow orchestration — fan out bounded parallel agent tasks, stream progress events, resume completed work from snapshots, and synthesize the final answer.
- Human-in-the-loop — permission callbacks, per-tool approval, glob-filtered tools, and graceful mid-run interrupts.
- State you own — save, resume, and fork conversations as JSON. No server-side threads.
- Type-safe end to end — Zod schemas drive parameters, structured output, and validation. Types flow through agents, hooks, and guardrails at compile time.
- Tiny dependency surface — Zod as a peer dep, with optional Effect integration when you want it.
agents tools streaming structured output handoffs subagents workflows guardrails hooks tracing sessions abort signals code mode todo tracking cost tracking human-in-the-loop predicted output audio data sources context compaction background tasks testing utilities debug mode
Install
bun add @usestratus/sdkStratus requires Zod as a peer dependency:
bun add zodQuick Start
import { z } from "zod";
import { Agent, createModel, run, tool } from "@usestratus/sdk";
const model = createModel(); // reads AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_OPENAI_DEPLOYMENT
const getWeather = tool({
name: "get_weather",
description: "Get the current weather for a city",
parameters: z.object({
city: z.string().describe("The city name"),
}),
execute: async (_ctx, { city }) => {
return `72°F and sunny in ${city}`;
},
});
const agent = new Agent({
name: "weather-assistant",
instructions: "You are a helpful weather assistant.",
model,
tools: [getWeather],
});
const result = await run(agent, "What's the weather in New York?");
console.log(result.output);Core Concepts
Agents
Agents are the primary building block. Each agent has a name, instructions, a model, and optional tools, handoffs, guardrails, and hooks.
const agent = new Agent({
name: "my-agent",
instructions: "You are a helpful assistant.",
model,
tools: [myTool],
});
// Dynamic instructions based on context
const agent = new Agent({
name: "my-agent",
instructions: (ctx) => `You are helping ${ctx.userName}.`,
model,
});Tools
Define tools with Zod schemas for type-safe parameter validation:
const searchTool = tool({
name: "search",
description: "Search for information",
parameters: z.object({
query: z.string().describe("Search query"),
limit: z.number().optional().describe("Max results"),
}),
execute: async (context, { query, limit }) => {
// Tool logic here
return "search results";
},
});Streaming
Stream responses token-by-token:
const { stream: s, result } = stream(agent, "Tell me a story");
for await (const event of s) {
if (event.type === "content_delta") {
process.stdout.write(event.content);
} else if (event.type === "tool_call_start") {
console.log(`Calling: ${event.toolCall.name}`);
}
}
const finalResult = await result;Vercel AI SDK Interop
Use @usestratus/sdk/ai-sdk to connect Stratus agents to AI SDK UI streams without adding ai as a required runtime dependency:
import { Agent, createModel } from "@usestratus/sdk";
import {
createStratusChatResponse,
type AISDKUIMessage,
} from "@usestratus/sdk/ai-sdk";
const agent = new Agent({
name: "assistant",
instructions: "You are helpful and concise.",
model: createModel(),
});
export async function POST(req: Request) {
const { messages }: { messages: AISDKUIMessage[] } = await req.json();
return createStratusChatResponse({ agent, messages });
}The interop package also includes helpers for UIMessage history conversion, tool approval resume flows, AI SDK-style model adapters, and OpenAI Agents-style stream event projection.
Structured Output
Use Zod schemas to get typed, validated output:
const PersonSchema = z.object({
name: z.string(),
age: z.number(),
occupation: z.string(),
});
const agent = new Agent({
name: "extractor",
instructions: "Extract person information.",
model,
outputType: PersonSchema,
});
const result = await run(agent, "Marie Curie was a 66-year-old physicist.");
console.log(result.finalOutput); // { name: "Marie Curie", age: 66, occupation: "physicist" }Sessions
Sessions maintain conversation history across multiple interactions:
import { createSession } from "@usestratus/sdk";
const session = createSession({ model, tools: [myTool] });
// Option 1: stream events
session.send("Hello!");
for await (const event of session.stream()) {
// handle events
}
// Option 2: just get the result
session.send("Follow-up question");
const result = await session.wait();
// Save and resume sessions
const snapshot = session.save();
const resumed = resumeSession(snapshot, { model });
// Fork a session (new ID, same history)
const forked = forkSession(snapshot, { model });
// Cleanup
session.close();
// Or use Symbol.asyncDispose:
await using session = createSession({ model });Handoffs
Transfer control between specialized agents:
import { handoff } from "@usestratus/sdk";
const orderAgent = new Agent({
name: "order_specialist",
instructions: "Help with order inquiries.",
model,
tools: [lookupOrder],
handoffDescription: "Transfer for order questions",
});
const triageAgent = new Agent({
name: "triage",
instructions: "Route to the right specialist.",
model,
handoffs: [
orderAgent, // shorthand
handoff({ // with options
agent: refundAgent,
onHandoff: () => console.log("Transferring..."),
}),
],
});
const result = await run(triageAgent, "Where is my order?");
console.log(result.lastAgent.name); // "order_specialist"Subagents
Delegate subtasks to child agents that run independently:
import { subagent } from "@usestratus/sdk";
const researcher = new Agent({
name: "researcher",
instructions: "Research topics thoroughly.",
model,
});
const parentAgent = new Agent({
name: "parent",
instructions: "Use the researcher for deep dives.",
model,
subagents: [
subagent({
agent: researcher,
inputSchema: z.object({ topic: z.string() }),
mapInput: ({ topic }) => `Research: ${topic}`,
}),
],
});Workflows
Use workflows when the orchestration itself should live in code: audits, migrations, research passes, and verification loops that need many agents without stuffing every intermediate result into one conversation.
import { Agent, createModel, runWorkflow, workflow, workflowTask } from "@usestratus/sdk";
const model = createModel();
const reviewer = new Agent({
name: "reviewer",
instructions: "Review the target carefully and report concrete findings only.",
model,
});
const synthesizer = new Agent({
name: "synthesizer",
instructions: "Merge independent findings into a concise final report.",
model,
});
const auditWorkflow = workflow({
name: "parallel-audit",
run: async (ctx, files: string[]) => {
const findings = await ctx.phase(
"review files",
files.map((file) =>
workflowTask({
id: file,
name: `review ${file}`,
agent: reviewer,
input: `Audit ${file} for correctness, security, and missing tests.`,
metadata: { file },
}),
),
{ concurrency: 8, failFast: false },
);
const synthesis = await ctx.synthesize(
synthesizer,
findings
.map((finding) => `## ${finding.name}\n${finding.output || finding.error}`)
.join("\n\n"),
);
return synthesis.output;
},
});
const result = await runWorkflow(auditWorkflow, [
"src/routes/users.ts",
"src/routes/billing.ts",
]);
console.log(result.output);
console.log(result.usage.totalTokens);Workflows support:
- Bounded parallel phases (
concurrency, capped bymaxConcurrency) - Progress streaming with
streamWorkflow() - Anthropic-style helpers:
ctx.fanOutAndSynthesize(),ctx.adversarialVerify(),ctx.generateAndFilter(),ctx.tournament(), andctx.loopUntilDone() - Token, cost, and duration guardrails through
budget - Managed runs with
WorkflowRunManagerfor event collection, stop, restart, and snapshot resume - Generated workflow drafts with
generateWorkflowDraft()so users can preview the plan and script before running it - Saved workflow discovery/loading from
.stratus/workflows AbortSignalcancellationfailFast: falsephases for tolerant audit-style runsresumeFromsnapshots that skip completed task IDs- Custom function tasks for non-model work alongside agent tasks
const result = await runWorkflow(auditWorkflow, files, {
concurrency: 8,
maxTasks: 1000,
budget: { maxTotalTokens: 50_000, maxDurationMs: 10 * 60_000 },
});Budget checks run after tasks or synthesis calls complete, so concurrent phases can overshoot a threshold before the workflow stops. Use conservative concurrency for strict spend control.
Guardrails
Validate inputs and outputs with guardrails:
import type { InputGuardrail, OutputGuardrail } from "@usestratus/sdk";
const profanityFilter: InputGuardrail = {
name: "profanity_filter",
execute: (input) => ({
tripwireTriggered: containsProfanity(input),
outputInfo: "Blocked by profanity filter",
}),
};
const piiFilter: OutputGuardrail = {
name: "pii_filter",
execute: (output) => ({
tripwireTriggered: /\d{3}-\d{2}-\d{4}/.test(output),
outputInfo: "Output contained PII",
}),
};
const agent = new Agent({
name: "guarded",
model,
inputGuardrails: [profanityFilter],
outputGuardrails: [piiFilter],
});Guardrails run in parallel. When a tripwire is triggered, an InputGuardrailTripwireTriggered or OutputGuardrailTripwireTriggered error is thrown.
Hooks
Lifecycle hooks for observability and control:
import type { AgentHooks } from "@usestratus/sdk";
const hooks: AgentHooks = {
beforeRun: ({ agent, input }) => { /* ... */ },
afterRun: ({ agent, result }) => { /* ... */ },
// Return a decision to allow, deny, or modify tool calls
beforeToolCall: ({ toolCall }) => {
if (toolCall.function.name === "dangerous_tool") {
return { decision: "deny", reason: "Not allowed" };
}
return { decision: "allow" };
},
afterToolCall: ({ toolCall, result }) => { /* ... */ },
// Allow or deny handoffs
beforeHandoff: ({ fromAgent, toAgent }) => {
return { decision: "allow" };
},
};Tracing
Opt-in tracing with zero overhead when inactive:
import { withTrace } from "@usestratus/sdk";
const { result, trace } = await withTrace("my-workflow", () =>
run(agent, "Hello"),
);
console.log(trace.id);
console.log(trace.duration);
for (const span of trace.spans) {
console.log(`[${span.type}] ${span.name} (${span.duration}ms)`);
// span.type: "model_call" | "tool_execution" | "handoff" | "guardrail" | "subagent" | "custom"
}Abort Signals
Cancel runs with AbortSignal:
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
try {
const result = await run(agent, "Long task...", {
signal: controller.signal,
});
} catch (error) {
if (error instanceof RunAbortedError) {
console.log("Run was cancelled");
}
}Todo Tracking
Track task progress during agent execution:
import { todoTool, TodoList } from "@usestratus/sdk";
const todos = new TodoList();
todos.onUpdate((items) => {
for (const item of items) {
const icon = item.status === "completed" ? "+" : item.status === "in_progress" ? ">" : "-";
console.log(`${icon} ${item.content}`);
}
});
const agent = new Agent({
name: "planner",
instructions: "Break tasks into steps and track progress with todo_write.",
model,
tools: [todoTool(todos)],
});
await run(agent, "Set up a new TypeScript project");Usage & Cost Tracking
Track token usage and estimate costs:
import { createCostEstimator } from "@usestratus/sdk";
const estimator = createCostEstimator({
inputTokenCostPer1k: 0.01,
outputTokenCostPer1k: 0.03,
});
const result = await run(agent, "Hello", { costEstimator: estimator });
console.log(result.usage.totalTokens); // token counts
console.log(result.totalCostUsd); // estimated cost
console.log(result.numTurns); // model call count
// Set budget limits
const result = await run(agent, "Hello", {
costEstimator: estimator,
maxBudgetUsd: 0.50, // throws MaxBudgetExceededError if exceeded
});Tool Choice & Tool Use Behavior
Control how the model uses tools:
const agent = new Agent({
name: "my-agent",
model,
tools: [myTool],
modelSettings: {
// "auto" | "none" | "required" | { type: "function", function: { name: "..." } }
toolChoice: "required",
},
// "run_llm_again" (default) | "stop_on_first_tool" | { stopAtToolNames: ["..."] }
toolUseBehavior: "stop_on_first_tool",
});Code Mode (Experimental)
Let LLMs write code that orchestrates multiple tools instead of calling them one at a time. Inspired by Cloudflare's Code Mode — LLMs are better at writing code than making individual tool calls.
import { createCodeModeTool, FunctionExecutor } from "@usestratus/sdk/core";
const executor = new FunctionExecutor({ timeout: 30_000 });
const codemode = createCodeModeTool({
tools: [getWeather, sendEmail, lookupOrder],
executor,
});
const agent = new Agent({
name: "assistant",
model,
tools: [codemode],
});
// The LLM writes code like:
// async () => {
// const weather = await codemode.get_weather({ location: "London" });
// if (weather.temp > 60) {
// await codemode.send_email({ to: "[email protected]", subject: "Nice day!", body: ... });
// }
// return { weather, notified: true };
// }createCodeModeTool generates TypeScript types from your tools, presents the LLM with a single execute_code tool, and runs the generated code in an executor. All tool calls happen within one invocation — no round-trips through the model between calls.
Two built-in executors:
FunctionExecutor— fast, same-process (NOT sandboxed)WorkerExecutor— isolated viaworker_threads(separate V8 context, no host access)
Implement the Executor interface for custom sandboxes (containers, Cloudflare Workers, etc.).
Imports
Stratus provides four export paths:
// Everything (core + Azure)
import { Agent, run, tool, createModel } from "@usestratus/sdk";
// Core only (provider-agnostic)
import { Agent, run, tool, validateAgent } from "@usestratus/sdk/core";
// Azure provider only
import { createModel, AzureResponsesModel } from "@usestratus/sdk/azure";
// Test utilities (keep out of production bundles)
import { createMockModel, textResponse, toolCallResponse } from "@usestratus/sdk/testing";Configuration
Azure OpenAI
The fastest way — createModel() reads from environment variables:
import { createModel } from "@usestratus/sdk";
const model = createModel(); // Responses API (default)
const model = createModel("chat-completions"); // Chat Completions APIOr configure explicitly:
import { AzureResponsesModel } from "@usestratus/sdk";
const model = new AzureResponsesModel({
endpoint: "https://your-resource.openai.azure.com",
apiKey: process.env.AZURE_OPENAI_API_KEY!,
deployment: "gpt-5.2",
});Both models implement the same Model interface — swap one for the other without changing any agent, tool, or session code.
Environment Variables
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_API_KEY=your-api-key
AZURE_OPENAI_DEPLOYMENT=gpt-5.2Testing
Stratus ships test utilities as a separate entrypoint:
import { createMockModel, textResponse, toolCallResponse } from "@usestratus/sdk/testing";
const model = createMockModel([
toolCallResponse([{ name: "search", args: { q: "test" } }]),
textResponse("Found 3 results"),
]);
const agent = new Agent({ name: "test", model, tools: [searchTool] });
const result = await run(agent, "Search for test");
expect(result.output).toBe("Found 3 results");
// Capture requests for assertions
const model = createMockModel([textResponse("ok")], { capture: true });
await run(agent, "Hello");
expect(model.requests[0].messages[0].content).toBe("Hello");Debug Mode
Log model calls, tool executions, and handoffs to stderr:
const result = await run(agent, "Hello", { debug: true });
// [stratus:model] 2026-04-02T... request to assistant {"messages":2,"tools":1,"turn":0}
// [stratus:model] 2026-04-02T... response from assistant {"content":"Hi!","toolCalls":[],...}Also works on sessions: createSession({ model, debug: true }).
Error Handling
All errors extend StratusError:
| Error | Description |
|---|---|
| StratusError | Base error class |
| ModelError | API call failures (includes status and code) |
| ContentFilterError | Content filtered by Azure's content management policy |
| MaxTurnsExceededError | Agent exceeded the maxTurns limit |
| OutputParseError | Structured output failed Zod validation |
| RunAbortedError | Run cancelled via AbortSignal |
| InputGuardrailTripwireTriggered | Input guardrail blocked the request |
| OutputGuardrailTripwireTriggered | Output guardrail blocked the response |
import { ModelError, MaxTurnsExceededError, RunAbortedError } from "@usestratus/sdk";
try {
await run(agent, input);
} catch (error) {
if (error instanceof MaxTurnsExceededError) {
// Agent ran too many turns
} else if (error instanceof ModelError) {
console.log(error.status, error.code);
}
}Packages
Stratus is a monorepo with two packages:
| Package | Description |
|---|---|
| @usestratus/sdk | Agent SDK for Azure OpenAI (this README) |
Development
bun install # Install all workspace dependencies
bun test # Run tests (all packages)
bun run lint # Lint with Biome
bun run typecheck # TypeScript type checking