Read this reference when a program needs less-common spec fields, precise schema overloads, custom tools, or invocation overrides.
Use agent({ name, ... }) as the only declaration form. name is optional and normalizes to "agent".
| Field | Purpose |
|---|---|
name |
Name included in the agent prompt |
instructions |
Plain string or p`...` prompt builder |
input |
Input schema; defaults to s.string |
output |
Output schema; defaults to s.string |
model |
Default model for calls |
maxTurns |
Total turn budget, including the initial attempt |
addons |
Per-turn steering, validation, and retry behavior |
agents |
Named subagents exposed to the harness |
tools |
Function-calling tools created with defineTool or compatible plain objects |
| Setting | Spec | Call-time | .use(addon) |
|---|---|---|---|
name, instructions, input, output, agents, tools |
yes | — | — |
model, maxTurns |
default | override | — |
timeout, signal |
— | yes | — |
addons |
stable addons | — | additional addons |
agent.use() accepts only AgentAddon | AgentAddon[]; passing spec fields or invocation options is a type error.
| Setting | Default |
|---|---|
| Name | "agent" |
| Input/output | s.string |
| Model | small |
| Max turns | 4 |
| Addons | none |
Use declarative s.* values for every schema node. They remain JSON Schema-compatible and are rendered directly into prompt schema blocks.
s.string
s.string("description")
s.nonEmptyString
s.nonEmptyString("description")
s.url
s.url("description")
s.path
s.path("description")
s.number
s.integer
s.int
s.boolean
s.unknown
s.array(item)
s.array(item, "description")
s.nonEmptyArray(item)
s.nonEmptyArray(item, "description")
s.object(fields)
s.object(fields, "description")
s.record(value)
s.record(value, "description")
s.nonEmptyObject(value)
s.nonEmptyObject(value, "description")
s.enum(...values)
s.enum(values, "description")
s.optional(shape)
s.optional(shape, "description")
s.nullable(shape)
s.nullable(shape, "description")
s.literal(value)
s.literal(value, "description")Helper constraints:
s.nonEmptyStringsetsminLength: 1.s.urluses format"uri";s.pathuses format"path".s.intaliasess.integer.- Prefer
s.intfor counts/line numbers ands.numberfor measurements, scores, and ratios. s.nonEmptyArray(item)setsminItems: 1.s.nonEmptyObject(value)describesRecord<string, V>withminProperties: 1.s.optional(shape)allows omission;s.nullable(shape)allowsnull.
Use s.optional when absence is valid and s.nullable when the field should still be present but may be null:
const output = s.object({
lastFile: s.optional(s.path),
archivedAt: s.nullable(s.string),
});Use s.record(value) for string-keyed maps when keys are not fixed ahead of time. Record keys are always string; there is no key-type constraint:
s.record(s.number) // e.g. churn score keyed by filename
s.record(s.int) // e.g. occurrence counts keyed by identifier — use instead of s.array(s.object({key, count}))s.url uses format "uri" and is a valid schema helper for URL-valued fields (same usage as s.path for file-system locations).
Description placement:
| Helper family | Description argument |
|---|---|
Scalars (s.string, s.int, s.path, s.url, s.boolean, s.unknown) |
First |
Containers (s.array, s.object, s.record, s.optional, s.nullable, s.nonEmptyArray, s.nonEmptyObject) |
Second, after the shape/value |
s.enum, s.literal |
Last |
Common contracts:
s.enum("bug", "feature", "question")
s.optional(s.int)
s.record(s.string)
s.record(s.int) // aggregate counts keyed by string
s.record(s.array(s.string))
s.record(s.object({ name: s.string, age: s.number }))
s.record(s.object({ path: s.path, homepage: s.url }))
s.nullable(s.string)
s.literal("done")
s.nonEmptyArray(s.path)
s.nonEmptyObject(s.boolean)Prefer s.path for file-system locations, s.url for URIs, and s.int for counts or line numbers. Omit schemas entirely when free-form strings are sufficient.
Register SDK-neutral tools with defineTool. Rig converts s.* parameters to JSON Schema and infers the handler argument type.
import { agent, defineTool, s } from "rig";
const lookupIssue = defineTool("lookup_issue", {
description: "Look up an issue by id.",
parameters: s.object({
issue: s.string,
}),
handler: async ({ issue }) => ({ issue, status: "open" }),
});
// Agent role: triage an issue using the lookup tool.
const triage = agent({
model: "mini",
instructions: "Use lookup_issue before answering.",
tools: [lookupIssue],
});
export default triage;For plain JSON Schema parameters, provide a generic such as defineTool<{ issue: string }>(...). A handler may return a string or any JSON-serializable value; Rig serializes non-string values, so do not call JSON.stringify in the handler. Tools default to skipPermission: true.
s.unknown is valid in tool parameter schemas for caller-provided values whose concrete type is intentionally open:
const scoreField = defineTool("score_field", {
description: "Score one package.json field.",
parameters: s.object({
field: s.string,
value: s.unknown,
}),
handler: async ({ field, value }) => ({ field, present: value !== undefined }),
});Async handlers can import Node built-ins directly:
import { defineTool, s } from "rig";
// Using node:child_process
const countLines = defineTool("count_lines", {
description: "Count file lines with wc.",
parameters: s.object({ path: s.path }),
handler: async ({ path }) => {
const { execSync } = await import("node:child_process");
const output = execSync(`wc -l ${JSON.stringify(path)}`, { encoding: "utf8" });
return { lineCount: Number(output.trim().split(/\s+/)[0] ?? "0") };
},
});
// Using node:fs/promises
const readFileSize = defineTool("read_file_size", {
description: "Return the byte size of a file.",
parameters: s.object({ path: s.path }),
handler: async ({ path }) => {
const { stat } = await import("node:fs/promises");
const info = await stat(path);
return { bytes: info.size };
},
});For recursive directory walks with node:fs/promises readdir, set encoding: "utf-8" when you need string paths:
const { readdir } = await import("node:fs/promises");
const paths = await readdir("src", { recursive: true, encoding: "utf-8" });Without encoding, the recursive overload can resolve to Dirent[], which will not type-check as string[].
Strict TypeScript compilation reports unused handler bindings. Destructure only the keys the handler uses, or rename an unavoidable binding with a leading underscore, such as { filename: _filename, content }.
When returning discriminated unions from a handler, keep discriminator literals narrow with as const (or an explicit return type) so TypeScript does not widen "missing" to string.
Arrow function callbacks inside handler bodies must have explicit type annotations on their parameters under noImplicitAny. TypeScript can infer the element type when the array is typed, but the inference chain breaks when a handler arg is any — for example, when parameters is a plain object ({ content: s.string }) rather than s.object({ content: s.string }). Add annotations to be safe:
// May fail under noImplicitAny if content is any
content.split("\n").map(line => line.trim())
// Always safe — explicit type prevents the cascade
content.split("\n").map((line: string) => line.trim())Use : string for string array callbacks, : RegExpMatchArray for matchAll results.
Use defineTool for I/O operations and external calls the LLM should be able to invoke — fetching a URL, running a query, reading a dynamic path. Do not use it to replace LLM inference: if the handler implements the classification or reasoning logic entirely in TypeScript, the model is never exercised for that step. Express classification rules as prompt instructions instead, and use a tool only when a discrete external operation is needed to support the model's reasoning.
| Situation | Use |
|---|---|
| Static shell command known at definition time | p.bash(command) |
| Command contains literal backslashes or regex | p.bashRaw`command` |
| External I/O the model should invoke conditionally (fetch URL, read dynamic path, query DB) | defineTool |
| Deterministic transform that gives the model intermediate data to reason about | defineTool |
| Full classification or judgment logic implemented in TypeScript | Neither — use prompt instructions |
| Command depends on a caller-supplied value | Prose description in instructions; reference field with p.inputField(field) |
Use call-time options only for per-run changes:
const controller = new AbortController();
const result = await myAgent(input, {
model: "mini",
timeout: 30_000,
maxTurns: 2,
signal: controller.signal,
});Put durable model and maxTurns defaults in the spec. Put timeout and signal only on the invocation.
Use only the current API:
agent({ name, ... })p.*andp`...`fromrigs.*for explicit schemasoncePerAgent,repair(),steering, andtimeoutfromrig
Do not add deprecated hooks, alternate schema syntaxes, or compatibility bridges.