Tools & schemas
defineTool() gives you full type inference from a schema — zod, or any Standard Schema V1 validator like valibot — with no manual typing or casts.
Defining a tool
A tool is a name, a description the model reads to decide when to call it, a schema describing its arguments, and an execute() function. Arguments are validated against the schema before execute() ever runs — an invalid call comes back as an isError tool result, not a crash.
import { defineTool } from "samai-sdk";
import { z } from "zod";
const getWeather = defineTool({
name: "get_weather",
description: "Get the current weather for a city",
parameters: z.object({ city: z.string() }),
// args is inferred as { city: string } — no manual typing
execute: async ({ city }) => `18°C and cloudy in ${city}`,
});Standard Schema support (valibot, and others)
parameters also accepts any Standard Schema V1 validator — valibot 0.31+/1.x is fully supported, including JSON Schema generation for the model-facing tool definition. Behavior is identical to the zod path in every other respect:
import { defineTool } from "samai-sdk";
import * as v from "valibot";
const getWeather = defineTool({
name: "get_weather",
description: "Get the current weather for a city",
parameters: v.object({
city: v.pipe(v.string(), v.minLength(1)),
units: v.optional(v.picklist(["metric", "imperial"]), "metric"),
}),
// args is inferred from valibot's own output type, same as the zod path
execute: async ({ city, units }) =>
`18${units === "imperial" ? "F" : "C"} and cloudy in ${city}`,
});This works the same way across all 8 provider adapters — each one's tool-conversion step resolves valibot's JSON Schema via the optional @valibot/to-json-schema peer dependency (npm install @valibot/to-json-schema).
Also Standard Schema-aware
generateObject(), streamObject(), createSchemaGuardrail(), and Agent.outputSchema all accept the same zod-or-Standard-Schema input. zod behavior is completely unchanged everywhere — this is purely additive.Requiring approval before execution
For a tool with real side effects — writing outside /tmp, sending an email, an MCP tool that mutates something — gate it behind human sign-off instead of letting it fire automatically:
const writeFile = defineTool({
name: "write_file",
description: "Write content to a file outside /tmp",
parameters: z.object({ path: z.string(), content: z.string() }),
execute: async ({ path, content }) => { /* ... */ },
// require sign-off, always — or pass a predicate for conditional approval
requiresApproval: true,
});
const result = await runAgent(client, agent, input, {
onApprovalRequest: async ({ toolName, args }) => {
// show a confirm dialog, check an allowlist, whatever your app needs
return userConfirmedInUI;
},
});If a call requires approval and no onApprovalRequest handler is supplied, the call is rejected by default — fail closed, not silently executed.
MCP tools & web search
createMCPClient() connects to any MCP server and exposes its tools as ordinary ToolDefinitions — mix them into an agent's tools array alongside locally-defined ones. createWebSearchTool() gives the model a real web_search tool backed by the Tavily or Brave search API — an actual HTTP request, not a stub.