Skip to main content
import { tools, read, write, edit, grep, bash } from "smithers-orchestrator";
tools bundles all five tools keyed by name:
const { read, write, edit, grep, bash } = tools;

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")

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, assign the agent to a <Task>:
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { read, write, edit, grep, bash } from "smithers-orchestrator";

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
const result = await runWorkflow(workflow, {
  input: { file: "src/auth.ts" },
  rootDir: "/home/project",
  allowNetwork: false,
  maxOutputBytes: 500_000,
  toolTimeoutMs: 120_000,
});