stratus

Tools

Give agents the ability to call functions

Tools let agents call your TypeScript functions. Each tool has a name, description, Zod parameter schema, and an execute function. Stratus requires Zod 4 (zod@^4.0.0).

Stratus uses Zod 4's built-in toJSONSchema() to convert tool parameter schemas to JSON Schema for the Azure API. This means all Zod types are supported — including recursive objects, template literals, discriminated unions, and everything else Zod 4 can represent.

Stratus handles the full tool loop automatically - parallel execution, error recovery, and 429 retries. No JSON.parse(), no dispatch tables, no manual message management. See the Agentic Tool Use guide for the full picture.

Looking for server-side tools like web search, code interpreter, or MCP? See Built-in Tools.

Want the LLM to write code that chains multiple tools together? See Code Mode.

Defining a Tool

tools.ts
import { tool } from "@usestratus/sdk/core";
import { z } from "zod";

const getWeather = tool({
  name: "get_weather",
  description: "Get the current weather for a city",
  parameters: z.object({
    city: z.string().describe("City name"),
    unit: z.enum(["celsius", "fahrenheit"]).optional(),
  }),
  execute: async (_ctx, { city, unit }) => {
    const temp = await fetchWeather(city, unit);
    return `${temp}° in ${city}`;
  },
});

Tool Anatomy

PropertyTypeDescription
namestringUnique tool name sent to the model
descriptionstringWhat the tool does (helps the model decide when to use it)
parametersz.ZodTypeZod schema for the parameters
execute(context, params, options?) => stringFunction that runs when the model calls the tool
timeoutnumber?Timeout in milliseconds. Throws ToolTimeoutError if exceeded
isEnabledboolean | (ctx) => booleanWhen false, the tool is excluded from the model's tool list
needsApprovalboolean | (params, ctx) => booleanWhen truthy, pauses for human approval before execution. See Human-in-the-Loop
retriesobject?Retry configuration for transient failures. See Retries

The execute function receives up to three arguments:

  1. context - The context object passed via run() options or session config
  2. params - Parsed and validated parameters matching the Zod schema
  3. options - Optional ToolExecuteOptions with an AbortSignal for cancellation

Using Context

Tools can access shared context for things like database connections, API clients, or user info:

context-tool.ts
interface AppContext {
  userId: string;
  db: Database;
}

const lookupOrder = tool({
  name: "lookup_order",
  description: "Look up an order by ID",
  parameters: z.object({ orderId: z.string() }),
  execute: async (ctx: AppContext, { orderId }) => {
    const order = await ctx.db.orders.find(orderId, ctx.userId);
    return JSON.stringify(order);
  },
});

const agent = new Agent<AppContext>({
  name: "support",
  model,
  tools: [lookupOrder],
});

await run(agent, "Where is my order #123?", {
  context: { userId: "user_abc", db: myDb },
});

Passing Tools to Agents

const agent = new Agent({
  name: "assistant",
  model,
  tools: [getWeather, lookupOrder, searchDocs],
});

Passing Tools to Sessions

const session = createSession({
  model,
  tools: [getWeather, lookupOrder],
});

Tool Call Flow

Model returns a tool call

The model responds with a tool call containing a name and JSON arguments.

Arguments are parsed

Stratus parses the arguments JSON and validates them against the Zod schema.

Execute function runs

The execute function runs with the parsed parameters and context.

Result sent back to model

The result string is sent back to the model as a tool message.

Model generates final response

The model generates a final response (or calls more tools). This loop continues until the model responds without tool calls, or toolUseBehavior causes an early stop.

Abort Signal

When a run is started with an AbortSignal, it's passed to each tool's execute function via the options parameter. Use it to cancel long-running operations:

abort-aware-tool.ts
const searchTool = tool({
  name: "search",
  description: "Search documents",
  parameters: z.object({ query: z.string() }),
  execute: async (_ctx, { query }, options) => { 
    const res = await fetch(`/api/search?q=${query}`, {
      signal: options?.signal, 
    });
    return await res.text();
  },
});

See Streaming - Abort Signal for details on passing a signal.

Timeout

Set a timeout in milliseconds to limit how long a tool can run. If the tool doesn't complete in time, a ToolTimeoutError is thrown internally. The error message is sent back to the model as a tool result so it can recover.

timeout-tool.ts
const slowSearch = tool({
  name: "search",
  description: "Search with a timeout",
  parameters: z.object({ query: z.string() }),
  timeout: 5000, // 5 second limit
  execute: async (_ctx, { query }) => {
    return await slowExternalApi(query);
  },
});

ToolTimeoutError is caught by the run loop and converted to a tool error message. It does not propagate out of run() — the model sees the timeout and can respond accordingly.

Conditional Tools (isEnabled)

Use isEnabled to dynamically include or exclude a tool based on context. When false, the tool is not sent to the model at all.

conditional-tool.ts
const adminTool = tool({
  name: "delete_user",
  description: "Delete a user account",
  parameters: z.object({ userId: z.string() }),
  isEnabled: (ctx: AppContext) => ctx.isAdmin, 
  execute: async (ctx, { userId }) => {
    await ctx.db.users.delete(userId);
    return "User deleted";
  },
});

isEnabled accepts a boolean or a function that receives the context and returns boolean | Promise<boolean>. The check runs before each model call, so a tool can appear or disappear mid-run based on changing context.

Human-in-the-Loop (needsApproval)

Tools that need human approval before execution can set needsApproval. When the model calls this tool, the run pauses and returns an InterruptedRunResult instead of executing.

approval-tool.ts
const deleteFile = tool({
  name: "delete_file",
  description: "Delete a file",
  parameters: z.object({ path: z.string() }),
  needsApproval: true, 
  execute: async (_ctx, { path }) => {
    await fs.rm(path);
    return `Deleted ${path}`;
  },
});

needsApproval accepts a boolean or an async function that receives the parsed parameters and context:

conditional-approval.ts
const processPayment = tool({
  name: "process_payment",
  description: "Process a payment",
  parameters: z.object({ amount: z.number(), recipient: z.string() }),
  needsApproval: async (params, ctx) => params.amount > 100, 
  execute: async (_ctx, { amount, recipient }) => {
    return await chargeCard(amount, recipient);
  },
});

When approval is needed, run() returns an InterruptedRunResult:

handle-approval.ts
import { run, resumeRun } from "@usestratus/sdk/core";
import type { InterruptedRunResult } from "@usestratus/sdk/core";

const result = await run(agent, "Delete /tmp/test.txt");

if (result.interrupted) {
  // Show pending tool calls to the user
  for (const pending of result.pendingToolCalls) {
    console.log(`Tool: ${pending.toolName}, Args: ${pending.arguments}`);
  }

  // Get human decision, then resume
  const resumed = await resumeRun(result, [
    { toolCallId: result.pendingToolCalls[0].toolCallId, decision: "approve" },
  ]);
  console.log(resumed.output);
}

To deny a tool call:

await resumeRun(result, [
  {
    toolCallId: result.pendingToolCalls[0].toolCallId,
    decision: "deny",
    denyMessage: "User declined this action",
  },
]);

needsApproval works with both run() and stream(). In streaming mode, the stream completes and the result promise resolves to an InterruptedRunResult.

Retries

Configure automatic retries for tools that call unreliable external services:

retry-tool.ts
const flakyApi = tool({
  name: "search_api",
  description: "Search an external API",
  parameters: z.object({ query: z.string() }),
  retries: { 
    limit: 3,
    delay: 1000,
    backoff: "exponential",
    shouldRetry: (error) => !(error instanceof ClientError),
  },
  execute: async (_ctx, { query }) => {
    return await externalSearch(query);
  },
});
OptionTypeDefaultDescription
limitnumberRequired. Maximum retry attempts
delaynumber1000Base delay in ms between retries
backoff"fixed" | "exponential""exponential"Backoff strategy
shouldRetry(error) => booleanRetry allPredicate to skip retries for specific errors

ToolTimeoutError is never retried — timeouts are treated as deterministic failures.

Error Handling

If a tool's execute function throws, the error message is sent back to the model as the tool result. This lets the model recover gracefully:

execute: async (_ctx, { query }) => {
  const results = await search(query);
  if (results.length === 0) {
    throw new Error("No results found. Try a different query.");
  }
  return JSON.stringify(results);
},

Schema Conversion

Stratus converts Zod schemas to JSON Schema for the Azure API. The conversion adds additionalProperties: false to all objects for Azure strict mode compatibility.

Supported Zod types include objects, strings, numbers, booleans, arrays, enums, optionals, nullables, defaults, unions, and descriptions.

Edit on GitHub

Last updated on

On this page