Skip to main content
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;
API reference: Tools lists every built-in tool and helper, its options, and links to source and tests.
The smithers-orchestrator/tools subpath also exports lower-level helpers for advanced integrations:
ExportPurpose
readFileTool, writeFileTool, editFileTool, grepTool, bashToolCall 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_* constantsUpper 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.
PolicyBehavior
Path resolutionRelative paths resolve against rootDir. Absolute paths must fall within root.
SymlinksRejected if target is outside sandbox.
Output sizeProcess output is truncated to maxOutputBytes (default 200KB); read, write, and edit reject files, content, or patches that exceed it.
Timeoutsbash and grep default to 60s; exceeded processes killed with SIGKILL.
Networkbash blocks network commands by default. See bash.

Tool call state

Smithers creates the _smithers_tool_calls table and exposes adapter methods to insert and list rows. The current engine reads that table on retry to build warnings for previously recorded non-idempotent side-effect tool calls. The defineTool() wrapper itself does not insert a durable row for every call; it attaches metadata, provides ctx.idempotencyKey, and runs the side-effect snapshot hook when a task supplies one.
FieldDescription
runIdWorkflow run ID
nodeIdTask node that invoked the tool
iterationLoop iteration
attemptRetry attempt number
seqSequential call counter within the task
toolNameread, write, edit, grep, or bash
inputJsonSerialized input arguments
outputJsonSerialized output (truncated if over limit)
startedAtMsStart timestamp
finishedAtMsEnd timestamp
status"success" or "error"
errorJsonError details (if "error")

defineTool

defineTool() wraps custom AI SDK tools with Smithers runtime context, deterministic idempotency keys, side-effect metadata, and the side-effect snapshot hook.
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 marks the tool for retry warnings when a previous attempt has a recorded _smithers_tool_calls row.
  • defineTool() does not persist _smithers_tool_calls rows directly; durable rows come from runtime paths that call the Smithers DB adapter.

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:
sideEffectidempotentSmithers behavior
false (default)true (default)Pure read. Safe to replay on retry. No warnings.
truetrueMutates 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.
truefalseMutates 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:
  1. Warns the agent. The retry prompt lists which non-idempotent tools were already called.
  2. 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

The rule is simple: if you cannot undo it with git reset, mark it 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. The built-in write and edit tools are registered as sideEffect: true and idempotent: false because their file mutations are not safe to blindly replay on retry; like bash, they are treated conservatively. All three built-in mutating tools (write, edit, bash) are side-effecting.
ToolSide effect?Why
Built-in read, grepNoPure reads
Built-in write, editYesSandboxed file writes are tracked by git, but replaying on retry could overwrite content that diverged since the first call
Built-in bashYesArbitrary shell commands may not be safe to repeat
Custom tool calling an external APIYesMutates state outside the sandbox
Custom tool writing to a databaseYesExternal persistent state
Custom tool sending a Slack messageYesIrreversible external communication
Custom tool creating a GitHub PRYesExternal state visible to others

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-fable-5"),
  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-fable-5"),
  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

Run an executable directly with arguments.
{
  cmd: string                     // executable path/name; no shell parsing
  args?: string[]                 // arguments
  opts?: { cwd?: string }        // working directory (sandboxed)
}
Use args for arguments. If you need shell syntax such as pipes or redirects, invoke a shell explicitly, for example cmd: "sh", args: ["-lc", "..."]. 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, Smithers tokenizes cmd plus args. Executable basenames are matched for known network tools, URL tokens are blocked by prefix, and git plus a remote-operation token is blocked.
CategoryMatch
HTTP clientsexecutable basename curl or wget
URL tokensany token starting with http:// or https://
Package managersexecutable basename npm, bun, or pip
Git remote opsgit plus a push, pull, fetch, clone, or remote token
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-fable-5"),
  tools: { bash },
});
{/* outputs comes from createSmithers() */}
<Task id="lint" output={outputs.lint} agent={devAgent}>
  Run the linter on src/ and report any issues.
</Task>

Using Tools with Agents

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 } from "smithers-orchestrator";
import { z } from "zod";

const codeAgent = new Agent({
  model: anthropic("claude-fable-5"),
  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-fable-5"),
  tools,
});

Configuration

OptionDefaultDescription
rootDirWorkflow directorySandbox root
allowNetworkfalseAllow network commands in bash
maxOutputBytes200000 (200KB)Max output size per tool
toolTimeoutMs60000 (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