Skip to main content

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:
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 sizeTruncated to maxOutputBytes (default 200KB).
Timeoutsbash and grep default to 60s; exceeded processes killed with SIGKILL.
Networkbash blocks network commands by default. See bash.

Tool Call Logging

Every invocation is logged to _smithers_tool_calls:
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, 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:
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

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.
ToolSide effect?Why
Built-in read, grepNoPure reads
Built-in write, editNoSandboxed file changes, tracked by git
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
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:
CategoryBlocked strings
HTTP clientscurl, wget
URL prefixeshttp://, https://
Package managersnpm, bun, pip
Git remote opsgit 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>

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, 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

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