Documentation Index
Fetch the complete documentation index at: https://smithers.sh/llms.txt
Use this file to discover all available pages before exploring further.
import { tools, read, write, edit, grep, bash, defineTool, getDefinedToolMetadata } from "smithers-orchestrator";
tools bundles all five tools keyed by name:
const { read, write, edit, grep, bash } = tools;
The smithers-orchestrator/tools subpath also exports lower-level helpers for advanced integrations:
| Export | Purpose |
|---|
readFileTool, writeFileTool, editFileTool, grepTool, bashTool | Call the underlying implementation directly instead of the AI SDK tool wrapper. |
getDefinedToolMetadata(tool) | Read Smithers metadata (name, sideEffect, idempotent) from a defineTool() result. |
getToolContext(), runWithToolContext(ctx, fn) | Inspect or provide the task-local tool runtime context. |
getToolIdempotencyKey(ctx?), nextToolSeq(ctx) | Build stable idempotency keys and task-local tool-call sequence numbers. |
BASH_TOOL_MAX_* constants | Upper bounds for bash command length, args, cwd, output bytes, and timeout. |
Sandboxing
All tools are sandboxed to rootDir (defaults to the workflow directory). Paths are resolved relative to this root; escapes via symlinks are rejected.
| Policy | Behavior |
|---|
| Path resolution | Relative paths resolve against rootDir. Absolute paths must fall within root. |
| Symlinks | Rejected if target is outside sandbox. |
| Output size | Truncated to maxOutputBytes (default 200KB). |
| Timeouts | bash and grep default to 60s; exceeded processes killed with SIGKILL. |
| Network | bash blocks network commands by default. See bash. |
Every invocation is logged to _smithers_tool_calls:
| Field | Description |
|---|
runId | Workflow run ID |
nodeId | Task node that invoked the tool |
iteration | Loop iteration |
attempt | Retry attempt number |
seq | Sequential call counter within the task |
toolName | read, write, edit, grep, or bash |
inputJson | Serialized input arguments |
outputJson | Serialized output (truncated if over limit) |
startedAtMs | Start timestamp |
finishedAtMs | End timestamp |
status | "success" or "error" |
errorJson | Error details (if "error") |
defineTool() wraps custom AI SDK tools with Smithers runtime context, deterministic idempotency keys, and durable tool-call logging.
import { defineTool } from "smithers-orchestrator";
import { z } from "zod";
const placeOrder = defineTool({
name: "wholefoods.place_order",
description: "Place a grocery order",
schema: z.object({
sku: z.string(),
}),
sideEffect: true,
idempotent: false,
async execute(args, ctx) {
return await wholeFoods.placeOrder({
sku: args.sku,
idempotencyKey: ctx.idempotencyKey,
});
},
});
ctx.idempotencyKey is stable across retries and resumes for the same task iteration.
sideEffect: true opts the tool into Smithers side-effect tracking.
idempotent: false warns resumed/retried agent loops when the tool was called in a previous attempt.
- Every
defineTool() call is logged to _smithers_tool_calls.
Side Effects and Idempotency
Every custom tool that modifies external state must declare sideEffect: true. This is how Smithers protects your workflow during retries and resumes. Without it, Smithers treats the tool as a pure read and replays it freely — potentially sending duplicate emails, double-charging payments, or creating duplicate records.
The two flags work together:
sideEffect | idempotent | Smithers behavior |
|---|
false (default) | true (default) | Pure read. Safe to replay on retry. No warnings. |
true | true | Mutates external state, but calling twice with the same input produces the same result (e.g. an upsert, a PUT request). Safe to replay. No warnings. |
true | false | Mutates external state and is not safe to replay (e.g. sending an email, placing an order, charging a payment). On retry, Smithers injects a warning listing the tool as already called so the agent can verify external state before calling again. |
With sideEffect: true and idempotent: false, Smithers does two things on retry:
- Warns the agent. The retry prompt lists which non-idempotent tools were already called.
- Provides a stable idempotency key.
ctx.idempotencyKey is deterministic for a given task + iteration; pass it to external APIs that support idempotency (Stripe, AWS) to deduplicate.
If your execute function has sideEffect: true, idempotent: false but omits the ctx parameter, Smithers logs a startup warning. This is almost always a bug — you need ctx.idempotencyKey to handle retries safely.
// ✗ Bad: non-idempotent side effect without ctx
const sendEmail = defineTool({
name: "email.send",
schema: z.object({ to: z.string(), body: z.string() }),
sideEffect: true,
idempotent: false,
async execute(args) { // ← missing ctx parameter, Smithers warns
await mailer.send(args);
},
});
// ✓ Good: uses ctx.idempotencyKey to deduplicate
const sendEmail = defineTool({
name: "email.send",
schema: z.object({ to: z.string(), body: z.string() }),
sideEffect: true,
idempotent: false,
async execute(args, ctx) {
await mailer.send({ ...args, idempotencyKey: ctx.idempotencyKey });
},
});
What counts as a side effect
A side effect is any mutation the runtime should not blindly repeat on retry. If a custom tool talks to an external API, writes to a database, sends a message, or triggers a webhook, mark it.
File system changes from the built-in write and edit tools are sandbox-local and are not marked as side-effecting. The built-in bash tool is intentionally conservative: it is registered as sideEffect: true and idempotent: false because arbitrary commands can mutate files, launch processes, or perform non-repeatable local work.
| Tool | Side effect? | Why |
|---|
Built-in read, grep | No | Pure reads |
Built-in write, edit | No | Sandboxed file changes, tracked by git |
Built-in bash | Yes | Arbitrary shell commands may not be safe to repeat |
| Custom tool calling an external API | Yes | Mutates state outside the sandbox |
| Custom tool writing to a database | Yes | External persistent state |
| Custom tool sending a Slack message | Yes | Irreversible external communication |
| Custom tool creating a GitHub PR | Yes | External state visible to others |
The rule is simple: if you cannot undo it with git reset, mark it as a side effect.
read
Read a file from the sandbox.
{ path: string } // relative to rootDir or absolute
Returns file contents as UTF-8. Throws "File too large" if size exceeds maxOutputBytes.
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { read, grep } from "smithers-orchestrator";
const codeAgent = new Agent({
model: anthropic("claude-sonnet-4-20250514"),
tools: { read, grep },
});
{/* outputs comes from createSmithers() */}
<Task id="review" output={outputs.review} agent={codeAgent}>
Read the file src/auth.ts and identify any security vulnerabilities.
</Task>
write
Write content to a file. Creates parent directories as needed.
{
path: string // relative to rootDir or absolute
content: string
}
Returns "ok". Throws "Content too large" if content exceeds maxOutputBytes. Logs content hash (SHA-256) and byte size; full content is not stored.
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { write, read } from "smithers-orchestrator";
const writerAgent = new Agent({
model: anthropic("claude-sonnet-4-20250514"),
tools: { write, read },
});
edit
Apply a unified diff patch to an existing file.
{
path: string // file to patch
patch: string // unified diff format
}
Returns "ok". The file must exist. Reads current contents, applies the patch via applyPatch, writes back. Throws on size limits ("Patch too large", "File too large") or mismatched context ("Failed to apply patch"). Logs patch hash and byte size.
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -10,3 +10,4 @@
const token = jwt.sign(payload, secret);
+ logger.info("Token issued", { userId: payload.sub });
return token;
grep
Search for a regex pattern using ripgrep.
{
pattern: string // regex
path?: string // directory or file (default: rootDir)
}
Returns matching lines with file paths and line numbers (rg -n format). Exit code 1 (no matches) returns empty string. Exit code 2 throws stderr as error. Requires ripgrep in PATH.
src/auth.ts:15: if (token.expired()) {
src/auth.ts:42: validateToken(token);
tests/auth.test.ts:8: const token = createTestToken();
bash
Execute a shell command.
{
cmd: string // executable or command
args?: string[] // arguments
opts?: { cwd?: string } // working directory (sandboxed)
}
Returns combined stdout and stderr. Working directory defaults to rootDir. Timeout: 60s (killed with SIGKILL via process group). Non-zero exit codes throw.
Network Blocking
Controlled by allowNetwork in RunOptions, --allow-network on CLI, or server config. Default: blocked.
When blocked, the command string (executable + args) is checked against these fragments:
| Category | Blocked strings |
|---|
| HTTP clients | curl, wget |
| URL prefixes | http://, https:// |
| Package managers | npm, bun, pip |
| Git remote ops | git push, git pull, git fetch, git clone, git remote |
Local git commands (git status, git diff, git log) are allowed.
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { bash } from "smithers-orchestrator";
const devAgent = new Agent({
model: anthropic("claude-sonnet-4-20250514"),
tools: { bash },
});
{/* outputs comes from createSmithers() */}
<Task id="lint" output={outputs.lint} agent={devAgent}>
Run the linter on src/ and report any issues.
</Task>
Pass tools to an AI SDK agent and assign the agent to a <Task>:
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { createSmithers, read, write, edit, grep, bash, Task, Workflow } from "smithers-orchestrator";
import { z } from "zod";
const codeAgent = new Agent({
model: anthropic("claude-sonnet-4-20250514"),
tools: { read, write, edit, grep, bash },
instructions: "You are a senior engineer. Use the available tools to complete tasks.",
});
const { Workflow, smithers, outputs } = createSmithers({
result: z.object({ summary: z.string() }),
});
export default smithers((ctx) => (
<Workflow name="refactor">
<Task id="refactor" output={outputs.result} agent={codeAgent}>
{`Refactor the function in ${ctx.input.file} to improve readability.`}
</Task>
</Workflow>
));
The full bundle works too:
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { tools } from "smithers-orchestrator";
const fullAgent = new Agent({
model: anthropic("claude-sonnet-4-20250514"),
tools,
});
Configuration
| Option | Default | Description |
|---|
rootDir | Workflow directory | Sandbox root |
allowNetwork | false | Allow network commands in bash |
maxOutputBytes | 200000 (200KB) | Max output size per tool |
toolTimeoutMs | 60000 (60s) | Timeout for bash and grep |
import { Effect } from "effect";
import { runWorkflow } from "smithers-orchestrator";
const result = await Effect.runPromise(runWorkflow(workflow, {
input: { file: "src/auth.ts" },
rootDir: "/home/project",
allowNetwork: false,
maxOutputBytes: 500_000,
toolTimeoutMs: 120_000,
}));
See Also