# Smithers — full documentation > Durable AI workflow orchestration as a JSX runtime. > Repo: github.com/evmts/smithers · Package: smithers-orchestrator (npm) This is the complete Smithers documentation in one file. It is the concatenation of every fragment listed in /llms.txt. If you only need the everyday surface (runtime, JSX, CLI, components, recipes, types, errors) read /llms-core.txt instead — it is roughly half the size and skips the opt-in fragments below. Fragments included in this file: 1. /llms-core.txt — core runtime, JSX, CLI, components, recipes, types 2. /llms-memory.txt — cross-run memory 3. /llms-openapi.txt — OpenAPI tool generation 4. /llms-observability.txt — HTTP server, gateway, MCP, OpenTelemetry 5. /llms-effect.txt — low-level Effect-ts integration 6. /llms-integrations.txt — agent runtimes, IDE, CI, ecosystem Changelogs are not included; see /docs/changelogs/ on the docs site. =============================================================================== # Smithers > Smithers — durable AI workflow orchestration as a JSX runtime. > Repo: github.com/evmts/smithers · Package: smithers-orchestrator (npm) This file contains the core Smithers documentation. Read top to bottom for a complete picture of the runtime, JSX surface, CLI, and components. Opt-in fragments cover features most users do not need: - Memory (cross-run state): /llms-memory.txt - OpenAPI tools: /llms-openapi.txt - Observability + HTTP server: /llms-observability.txt - Integrations + CLI agents: /llms-integrations.txt - Event types (full union): /llms-events.txt Changelogs are not included; see /docs/changelogs/ on the docs site. --- ## Smithers > Durable AI workflow orchestration as a JSX runtime. Smithers turns a JSX tree into a durable, resumable workflow. ```tsx /** @jsxImportSource smithers-orchestrator */ import { createSmithers, Sequence, Task } from "smithers-orchestrator"; import { z } from "zod"; const { Workflow, smithers, outputs } = createSmithers({ hello: z.object({ message: z.string() }), }); export default smithers((ctx) => ( {{ message: `Hello, ${ctx.input.name}` }} )); ``` Run it: ```bash bunx smithers-orchestrator init bunx smithers-orchestrator up workflow.tsx --input '{"name":"world"}' ``` Outputs are validated by Zod, persisted to SQLite, and survive crashes. Resume with `--resume true`. ## Read next - [Tour](/tour) — a 6-step worked example that introduces every core feature. - [How It Works](/how-it-works) — the render → execute → persist loop. - [Components](/components/workflow) — JSX surface reference. - [CLI](/cli/overview) — every command in one table. - [Recipes](/recipes) — common patterns. - [Types](/reference/types) — public TypeScript surface. --- ## Introduction > What Smithers is and when to use it. Smithers is a durable workflow runtime. You write a workflow as a JSX tree, and Smithers repeatedly renders it. Each render answers: given what has already finished, what can run now? Tasks produce outputs validated by Zod schemas; the runtime persists them to SQLite. Crashes, restarts, and approvals are first-class — the runtime resumes from the last persisted state without re-running completed work. ```tsx {`Review ${ctx.input.repo}`} {analysis ? ( {`Fix: ${analysis.issues}`} ) : null} ``` Use Smithers when: - order matters across multiple AI or compute steps - you need crash recovery - humans must approve or answer questions mid-run - different tasks need different models, tools, or policies Don't use it for a single prompt → single response. That's just an `Agent.generate(...)` call. ## Read next - [Tour](/tour) for a working code-review example. - [How It Works](/how-it-works) for the execution model. --- ## Installation > Install smithers-orchestrator with the workflow pack, or manually for standalone JSX workflow projects. Most teams should start with the workflow pack. It gives you a working `.smithers/` directory with seeded workflows, prompts, and agent configuration instead of assembling the project structure by hand. ## Always Run with `bunx` Every CLI invocation in these docs is `bunx smithers-orchestrator `. Do **not** install Smithers globally and do **not** use the bare `smithers` or `bunx smithers` shorthand. - `bunx` resolves the package locally if your project depends on it, otherwise it pulls and runs the latest published version. - The published npm package is [`smithers-orchestrator`](https://www.npmjs.com/package/smithers-orchestrator). The bare name `smithers` is a different package, so `bunx smithers` runs something else entirely. - A global install creates version-drift problems across machines, CI, and contributors. With `bunx`, every project pins Smithers through its own `package.json`. If you previously ran `npm i -g smithers-orchestrator`, uninstall it (`npm rm -g smithers-orchestrator`) and switch to `bunx`. ## Recommended: Install the Workflow Pack ```bash bunx smithers-orchestrator init ``` That scaffolds `.smithers/` with files such as: | Directory / File | Contents | |---|---| | `.smithers/workflows/` | Pre-built workflows (`implement`, `review`, `plan`, `ralph`, `debug`, ...) | | `.smithers/prompts/` | Shared MDX prompt templates | | `.smithers/components/` | Reusable TSX components (`Review`, `ValidationLoop`, ...) | | `.smithers/package.json` | Local workflow project manifest with `smithers-orchestrator` dependency | | `.smithers/tsconfig.json` | TypeScript config for JSX workflow authoring | | `.smithers/bunfig.toml` | Bun preload config for MDX workflow prompts | | `.smithers/preload.ts` | Registers the MDX preload plugin | | `.smithers/agents.ts` | Auto-detected agent configuration | | `.smithers/smithers.config.ts` | Repo-level config (lint, test, coverage commands) | | `.smithers/tickets/` | Ticket workspace used by ticket-oriented workflows | | `.smithers/executions/` | Execution artifacts directory preserved across re-inits | | `.smithers/.gitignore` | Ignore rules for generated workflow state | To overwrite an existing scaffold: ```bash bunx smithers-orchestrator init --force ``` ## When to Use Manual Installation Use manual installation when embedding Smithers into an existing TypeScript codebase to author a standalone [workflow project](/guides/project-structure) from scratch. See [JSX Installation](/jsx/installation) for the package list, TypeScript configuration, and optional MDX prompt setup. ## Requirements - [Bun](https://bun.sh) >= 1.3 - TypeScript >= 5 - Model or provider credentials (e.g. [Anthropic](https://docs.anthropic.com) `ANTHROPIC_API_KEY`) ## Desktop App (macOS) The Smithers desktop GUI is a separate download. It's a native macOS app (Apple Silicon) that includes the orchestrator, an embedded terminal, and workflow tooling. Download SmithersGUI.dmg Or from the terminal: ```bash curl -LO https://get.smithers.sh/SmithersGUI.dmg open SmithersGUI.dmg ``` The current build is unsigned. The first time you open it, right-click the app and choose **Open**, then confirm the Gatekeeper dialog. This only needs to be done once. ## After Installation - [Quickstart](/quickstart) — run a seeded workflow immediately. - [CLI Quickstart](/cli/quickstart) — operational workflow. - [JSX Installation](/jsx/installation) — manual TSX authoring setup. - [Project Structure](/guides/project-structure) — how a standalone workflow project fits together. - [Tools Integration](/integrations/tools) — the built-in tool sandbox. --- ## Quickstart > Scaffold and run a Smithers workflow in two commands. ```bash bunx smithers-orchestrator init bunx smithers-orchestrator workflow run implement --prompt "Add rate limiting" ``` `init` scaffolds `.smithers/` (workflows, prompts, components, agent config). The `workflow run` command launches one of the seeded workflows. Inspect the run: ```bash bunx smithers-orchestrator ps bunx smithers-orchestrator inspect bunx smithers-orchestrator logs --tail 20 ``` Resume after a crash: ```bash bunx smithers-orchestrator up workflow.tsx --run-id --resume true ``` For a full worked example see the [Tour](/tour). For every CLI command see the [CLI catalog](/cli/overview). --- ## Tour > Build a code-review workflow in six steps. Every core feature shows up. A code-review workflow built one capability at a time. Each step is a diff against the previous. Reading time: 15 minutes. ## 1. Install and scaffold ```bash bunx smithers-orchestrator init bun add smithers-orchestrator ai @ai-sdk/anthropic zod bun add -d typescript @types/bun export ANTHROPIC_API_KEY="sk-ant-..." ``` `init` creates `.smithers/` with seeded workflows, prompts, and components. The bun deps add the AI SDK, Anthropic provider, and Zod (schemas). A minimal `tsconfig.json`: ```json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "jsxImportSource": "smithers-orchestrator", "strict": true, "noEmit": true, "skipLibCheck": true } } ``` `jsxImportSource` is the only line specific to Smithers — it routes JSX through the workflow runtime instead of React DOM. ## 2. One-task workflow ```tsx /** @jsxImportSource smithers-orchestrator */ import { createSmithers, Sequence, Task } from "smithers-orchestrator"; import { z } from "zod"; const { Workflow, smithers, outputs } = createSmithers({ greeting: z.object({ message: z.string() }), }); export default smithers((ctx) => ( {{ message: `Hello, ${ctx.input.name}` }} )); ``` `createSmithers` registers Zod schemas; each becomes a SQLite table. `outputs.greeting` is the typed reference for the `greeting` schema — using it as the `output` prop gives compile-time checks (typo `outputs.greting` is a type error). This Task has no `agent`, just a literal value. Run it. ```bash bunx smithers-orchestrator up workflow.tsx --input '{"name":"world"}' ``` Inspect: ```bash bunx smithers-orchestrator ps # find the run id bunx smithers-orchestrator inspect # structured state sqlite3 smithers.db "SELECT * FROM greeting;" # the persisted output ``` ## 3. Add an agent task Replace the literal Task with an agent Task whose output is structured. ```tsx import { ToolLoopAgent as Agent } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; const { Workflow, smithers, outputs } = createSmithers({ analysis: z.object({ summary: z.string(), issues: z.array(z.object({ file: z.string(), line: z.number(), severity: z.enum(["low", "medium", "high"]), description: z.string(), })), }), }); const analyst = new Agent({ model: anthropic("claude-sonnet-4-20250514"), instructions: "You are a senior code reviewer. Return structured JSON.", }); export default smithers((ctx) => ( {`Review the code in ${ctx.input.repo} and return analysis as JSON.`} )); ``` The runtime injects a JSON-schema description of `outputs.analysis` into the prompt, parses the agent's response, validates against Zod, and persists. Validation failure triggers a retry. ## 4. A second task that depends on the first Tasks see each other's outputs through `ctx.outputMaybe(...)`. An incomplete upstream returns `undefined`; on the next render frame the upstream output appears and the downstream Task mounts. ```tsx const { Workflow, smithers, outputs } = createSmithers({ analysis: AnalysisSchema, fix: z.object({ patch: z.string(), filesChanged: z.array(z.string()), }), }); const fixer = new Agent({ model: anthropic("claude-sonnet-4-20250514"), instructions: "Write minimal, correct fixes as a unified diff.", }); export default smithers((ctx) => { const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" }); return ( {`Review ${ctx.input.repo}`} {analysis ? ( {`Fix these issues:\n${analysis.issues.map(i => `- [${i.severity}] ${i.file}:${i.line} — ${i.description}` ).join("\n")}`} ) : null} ); }); ``` Render 1: only `analyze` is mounted. Render 2 (after `analyze` finishes): `analysis` is populated, `fix` mounts and runs. The entire reactivity story — no hooks, no subscriptions, JSX conditionals over persisted state. Same shape works for branching, parallel groups, and loops: ```tsx ... ... ... ... ... ``` ## 5. An approval gate Pause for a human. The runtime persists the pending decision and exits cleanly; an operator approves or denies through the CLI; resume picks up from the gate. ```tsx import { Approval } from "smithers-orchestrator"; {analysis ? ( {/* children rendered after approval */} ) : null} {ctx.outputMaybe(outputs.confirmFix, { nodeId: "confirm-fix" })?.approved ? ( {`Apply patches`} ) : null} ``` Operator side: ```bash bunx smithers-orchestrator ps --status waiting-approval # find paused runs bunx smithers-orchestrator inspect # see the request bunx smithers-orchestrator approve --node confirm-fix --by alice bunx smithers-orchestrator up workflow.tsx --run-id --resume true ``` `onDeny` controls behavior on rejection: `"fail"` aborts the run, `"continue"` proceeds without the approved branch, `"skip"` skips the gated tasks. ## 6. Crash, then resume Every completed task's output sits in SQLite. A crash, kill, or restart loses no work — the next run with `--resume true` skips finished tasks. ```bash bunx smithers-orchestrator up workflow.tsx --input '{"repo":"."}' --run-id review-1 # ...analyze finishes, fix is mid-flight, you Ctrl+C bunx smithers-orchestrator up workflow.tsx --run-id review-1 --resume true # analyze is skipped (already in DB), fix re-runs from scratch (was incomplete) ``` In-flight attempts are marked stale and re-tried; finished tasks are not. Resume is deterministic — same input + same code = same task IDs. For unattended recovery, run the supervisor: ```bash bunx smithers-orchestrator supervise --interval 30s --stale-threshold 1m ``` It auto-resumes runs whose owner process died. ## What you skipped (and where to find it) - **Time travel** (replay a frame, fork a run, diff two runs): `bunx smithers-orchestrator replay|fork|diff|timeline` — see [How It Works → Time travel](/how-it-works#time-travel). - **Scorers** (attach evaluators to Tasks): see [Recipes → Scoring tasks](/recipes#scoring-tasks). - **Memory** (cross-run facts, semantic recall): see [llms-memory.txt](/llms-memory.txt). - **RAG**, **voice**, **OpenAPI tools**: opt-in fragments — see the index in [llms.txt](/llms.txt). - **Tool sandbox** (read/grep/bash with path containment): see [Recipes → Tools](/recipes#tools). ## Read next - [How It Works](/how-it-works) — the render → execute → persist loop. - [Components](/components/workflow) — JSX surface reference. - [CLI](/cli/overview) — every command in one TOON catalog. - [Recipes](/recipes) — patterns from production workflows. --- ## How It Works > The render → execute → persist loop, in one page. Smithers is a React reconciler whose host elements are tasks instead of DOM nodes. Each render produces a snapshot of the workflow plan; the runtime extracts ready tasks from that plan, executes them, persists their outputs, and re-renders. The plan evolves because each render reads the persisted state. ``` +--------+ +-----------+ +---------+ +---------+ | Render | -----> | Extract | -----> | Execute | -----> | Persist | | JSX | | task list | | ready | | outputs | +--------+ +-----------+ +---------+ +---------+ ^ | |__________________________________________________________| (re-render with new state) ``` That loop is the entire model. Everything below — branching, loops, approvals, resume, time travel — is either a JSX construct that affects rendering or a CLI surface over the persisted state. ## The render loop in detail 1. **Render**. The runtime calls your `smithers((ctx) => ...)` builder. The returned JSX tree is reconciled by React; the reconciler emits a graph of host elements (`smithers:workflow`, `smithers:task`, `smithers:sequence`, `smithers:parallel`, `smithers:branch`, `smithers:loop`, `smithers:approval`, etc.). 2. **Extract**. The runtime walks the tree to produce a `GraphSnapshot` — a flat list of `TaskDescriptor`s. Each descriptor captures: node id, ordinal, dependencies, output schema, agent, retries, timeouts. 3. **Schedule**. The scheduler computes the ready set: tasks whose dependencies have completed, whose enclosing sequence has reached them, whose enclosing branch resolved them, and which fit within `maxConcurrency`. 4. **Execute**. Each ready task runs. Three modes: agent (call the LLM, validate output against the Zod schema, retry on failure), compute (run the function), static (write the literal value). 5. **Persist**. Validated outputs are written to per-schema SQLite tables. Internal `_smithers_*` tables capture node state, attempts, frame snapshots, events, and durable approval/signal state. 6. **Re-render**. The next frame begins with `ctx` reading the updated outputs. Tasks that depended on now-completed outputs mount on this frame and become eligible to run. The frame is the unit of progress. Time travel, observability, hot reload, and resume all key off the frame number. ## The `ctx` API `ctx` is the only way the workflow body talks to the runtime. | Method | Returns | Use for | |---|---|---| | `ctx.input` | `T` | The immutable input passed to `runWorkflow`. | | `ctx.outputMaybe(schema, { nodeId })` | `Row \| undefined` | Conditional rendering — returns `undefined` until the upstream task completes. | | `ctx.output(schema, { nodeId })` | `Row` | Same, but throws if missing. Use inside a Task body where the dep is guaranteed. | | `ctx.latest(schema, nodeId)` | `Row \| undefined` | Highest iteration of a node — used inside `` to read the previous iteration's output. | | `ctx.iterationCount(schema, nodeId)` | `number` | Number of completed iterations for a loop node. | | `ctx.runId` / `ctx.iteration` | `string` / `number` | Identifiers for logging. | | `ctx.auth` | `RunAuthContext \| null` | Auth context passed via `RunOptions.auth`. | Outputs are keyed by `(runId, nodeId, iteration)`. `iteration` is `0` outside loops; inside `` each pass writes a new row at the next iteration index. ## Tasks: three modes ```tsx // Agent — call an LLM. Children become the prompt; output validated against schema. {`Review ${ctx.input.repo}`} // Compute — children is a function. Runs at execution time. {() => fs.readdirSync(ctx.input.dir).length} // Static — children is a plain value. Persisted directly. {{ region: "us-east-1", retries: 3 }} ``` Agent output validation: the runtime injects a JSON-schema description of the output Zod schema into the prompt, parses the response, validates, and persists. Validation failure feeds the error back into a retry attempt — agents self-correct on schema drift. Agents can be a fallback chain: `agent={[primary, fallback]}` tries `primary` first and falls through on failure. ## Control flow Four primitives. Compose freely. ```tsx // children execute top-to-bottom; default for // children execute concurrently } else={}> ``` `` implicitly sequences its children. An explicit `` is only needed when nesting sequential groups inside `` or another control-flow primitive. Use `.map()` and ternaries when the *number* or *presence* of tasks depends on state. Use `` and `` for fixed task sets whose execution shape depends on state. ## Data flow is unidirectional Workflow state lives in SQLite. The render function is a pure function of `ctx` (which reads SQLite). Tasks emit outputs; the runtime persists them; the next render reads them. No mutation, no refs, no `useState` for durable values. This is the same shape as React rendering UI from props/state, except: - the "DOM" is the task graph - "events" are task completions - "state updates" are output writes that the runtime triggers Three consequences: - The plan is a **derived value**. Re-render after any state change automatically computes the new plan; you never manually mutate the plan. - **Time travel works** because every frame is a snapshot of (state → plan). - **Hot reload works** because reloading the workflow code with the same persisted state produces a new plan; the runtime diffs the two and continues from where you left off. ## Reactivity & React patterns Smithers JSX is real React. Components, props, children, composition, context, hooks, custom hooks — all work. ```tsx function useReviewState(ticketId: string) { const ctx = useCtx(); const claudeReview = ctx.latest("review", `${ticketId}:review-claude`); return { claudeReview, allApproved: !!claudeReview?.approved }; } ``` `useState` and `useMemo` are process-local — they reset on every render frame. Use them for ephemeral render-time state. **Anything the workflow must remember across crashes goes through `ctx` and a Task output.** Conditional mounting matters: a Task that doesn't render is not in the plan. No "skipped" placeholder unless you use `` or `skipIf`. That's what lets `{analysis ? : null}` work as a clean dependency check. ## Approvals & human-in-the-loop Two surfaces. `needsApproval` on a Task is a **gate** — pause before execution, no decision data: ```tsx Deploy to production. ``` `` is a **decision node** — produces a typed `ApprovalDecision` row that downstream rendering can branch on: ```tsx {ctx.outputMaybe(outputs.shipDecision, { nodeId: "ship-decision" })?.approved ? : } ``` Three denial policies: `"fail"` (abort the run), `"continue"` (proceed without the gated branch), `"skip"` (skip the gated tasks but continue siblings). Operator side is identical for both: ```bash bunx smithers-orchestrator ps --status waiting-approval bunx smithers-orchestrator approve --node ship-decision --by alice bunx smithers-orchestrator up workflow.tsx --run-id --resume true ``` `` is for richer interaction — a human submits arbitrary structured JSON. `` and `` are higher-level patterns built from these. ## Durability & resume The contract: **a completed task is never re-executed.** Resume loads persisted state, validates the environment (workflow source hash + VCS revision must match the original run), cleans stale in-progress attempts (>15 min without a heartbeat are abandoned), re-renders, and continues. ```bash bunx smithers-orchestrator up workflow.tsx --run-id --resume true ``` For resume to work, **task IDs must be stable across renders.** Derive them from data, not from indices or timestamps: ```tsx {tickets.map((t) => )} // NOT id={`work-${i}`} or id={`work-${Date.now()}`} ``` Same rule as React keys. A changed task ID looks like a new task to the runtime, and an old task whose ID disappeared is dropped from the plan. The supervisor auto-resumes runs whose owner process died: ```bash bunx smithers-orchestrator supervise --interval 30s --stale-threshold 1m ``` ## Caching Per-Task caching with explicit invalidation: ```tsx ({ repo: ctx.input.repo, version: "v3" }), version: "v3", }} > Analyze {ctx.input.repo} ``` Cache key = `cache.by(ctx)` + `cache.version` + the schema signature (SHA-256 of the table structure). A schema change invalidates the cache automatically. Don't cache side-effect tasks (deploys, emails, mutations). Caching is for pure work that's expensive to recompute. ## Time travel Every frame commit produces a `GraphSnapshot`. ```bash bunx smithers-orchestrator timeline # frames + forks bunx smithers-orchestrator diff # node DiffBundle bunx smithers-orchestrator fork workflow.tsx --run-id --frame 5 --reset-node analyze bunx smithers-orchestrator replay workflow.tsx --run-id --frame 5 --restore-vcs ``` Replay with `--restore-vcs` checks out the jj revision the snapshot was taken at — re-execution sees the same source code as the original run. ## Scorers (evals) Attach evaluators to a Task. They run **after** completion and never block. ```tsx import { schemaAdherenceScorer, latencyScorer } from "smithers-orchestrator/scorers"; Analyze... ``` Five built-ins: `schemaAdherenceScorer`, `latencyScorer`, `relevancyScorer`, `toxicityScorer`, `faithfulnessScorer`. Sampling: `all` / `ratio` / `none`. Custom scorers and LLM-judge scorers with `createScorer` and `llmJudge`. ```bash bunx smithers-orchestrator scores ``` ## Memory (cross-run state) Memory is **state that survives across runs** — facts, message history, semantic recall. Not the same as task outputs (which are per-run). Three layers, four namespaces (`workflow`, `agent`, `user`, `global`). Three processors (`TtlGarbageCollector`, `TokenLimiter`, `Summarizer`). See [llms-memory.txt](/llms-memory.txt) for the full surface. ## Tools & sandboxing Five built-in tools — `read`, `write`, `edit`, `grep`, `bash` — sandboxed to `rootDir`. Symlinks, network, and timeouts are denied by default; `--allow-network` opens bash to the network. Least-privilege per task: ```tsx const analyst = new Agent({ model, instructions: "..." }); // no tools const reviewer = new Agent({ model, instructions: "...", tools: { read, grep } }); const implementer = new Agent({ model, instructions: "...", tools: { read, write, edit, bash } }); ``` `defineTool` builds custom tools. Mark side-effecting ones with `sideEffect: true` and use `ctx.idempotencyKey` so retries don't double-fire. ## Common gotchas - **Stable task IDs.** `id="implement-${i}"` or `id={Math.random()}` breaks resume. Derive from data. - **`useState` is not durable.** Resets on every render. Persist via `ctx` and a Task. - **Input is immutable.** Resuming with different `--input` is an error — the input is persisted at first run. - **Code changes block resume.** A workflow source change = a different workflow. Start a new run, don't resume across edits. - **Cached output is re-validated.** Schema drift after caching is caught (the validator rejects the stale row), so the cache misses safely. - **Side-effect tasks should not be cached.** Pure work only. ## Read next - [Components](/components/workflow) — JSX surface reference. - [CLI](/cli/overview) — every command. - [Recipes](/recipes) — patterns from production workflows. - [Types](/reference/types) — public TypeScript surface. --- ## JSX API > Author workflows as JSX trees. Setup, tsconfig, and the render model on one page. Workflows are JSX trees. Smithers renders the tree, extracts ready tasks, executes them, persists their outputs, and re-renders. Branching, looping, and parallelism are normal JSX. ## Setup Most projects should use `bunx smithers-orchestrator init` — it scaffolds everything below. To embed into an existing codebase: ```bash bun add smithers-orchestrator ai @ai-sdk/anthropic zod bun add -d typescript @types/bun @types/ws ``` Minimal `tsconfig.json`: ```json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "jsxImportSource": "smithers-orchestrator", "strict": true, "noEmit": true, "skipLibCheck": true } } ``` `jsxImportSource` is the only non-standard line — it routes JSX through `smithers-orchestrator/jsx-runtime` instead of React DOM. Optional MDX prompts: add `bun add -d @types/mdx` and a `preload.ts` that calls `mdxPlugin()`, register it in `bunfig.toml` as `preload = ["./preload.ts"]`. Verify with `bunx tsc --noEmit` and `bunx smithers-orchestrator --help`. ## A minimal workflow ```tsx /** @jsxImportSource smithers-orchestrator */ import { createSmithers, Sequence, Task } from "smithers-orchestrator"; import { z } from "zod"; const { Workflow, smithers, outputs } = createSmithers({ analysis: z.object({ summary: z.string() }), }); export default smithers((ctx) => ( {{ summary: `Analyze ${ctx.input.repo}` }} )); ``` `outputs.analysis` is the typed reference for the Zod schema — typos are compile errors. ## Reactivity The tree re-renders on every frame, so branching is a normal JSX conditional: ```tsx const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" }); {analysis ? ... : null} ``` The `report` Task doesn't exist in the plan until `analysis` completes. No placeholder, no skipped node — the conditional IS the dependency. ## Read next - [Tour](/tour) — six-step worked example with agents, schemas, approvals, resume. - [How It Works](/how-it-works) — the render → execute → persist loop. - [Components](/components/workflow) — full prop surface for every JSX element. --- ## CLI > Every Smithers CLI command in one TOON catalog. Always invoke as `bunx smithers-orchestrator ` (see [Installation](/installation) for why). Use `--help` on any command for the canonical option list. ## Conventions - Persisted state lives in the nearest `smithers.db` (walk up from the working directory). Most read commands fail with a friendly message if no DB is found. - Boolean flags accept either bare form (`--watch`) or explicit `--watch true|false`. - `--format json|yaml|md|jsonl`, `--filter-output `, `--token-count`, `--token-limit N`, `--token-offset N`, `--schema`, `--llms`, `--mcp`, `--verbose`, `--help`, `--version` are global. - Workflow resolution: `up`, `graph`, `revert`, `replay`, `fork`, `retry-task`, `timetravel` take a workflow file path. `workflow run ` resolves IDs from `.smithers/workflows/.tsx`. `workflow.tsx` invoked directly behaves like `up workflow.tsx`. ## Exit codes ``` 0 success 1 execution failure 2 run cancelled / cancel succeeded 3 `up` ended in waiting-approval 4 invalid arguments / user-correctable input error 130 SIGINT 143 SIGTERM ``` ## Command catalog (TOON) Commands listed by dotted name. Subcommands of `workflow`, `cron`, `human`, `alerts`, `memory`, `openapi`, `agents`, `mcp`, `skills` appear as `parent.sub`. ```toon commands[56]: - name: init purpose: Install the local workflow pack into .smithers/ flags[3]{name,short,type,default,desc}: force,,boolean,false,Overwrite existing scaffold files agents-only,,boolean,false,Only create .smithers/agents/ install,,boolean,true,Run bun install inside .smithers/ after scaffolding - name: up purpose: Start or resume a workflow execution args[1]{name,type,required,desc}: workflow,string,true,Workflow file path flags[23]{name,short,type,default,desc}: detach,d,boolean,false,Background mode; print runId/pid/logFile and exit run-id,r,string,,Explicit run ID max-concurrency,c,number,4,Maximum parallel tasks root,,string,,Tool sandbox root (defaults to workflow's parent dir) log,,boolean,true,NDJSON event log output log-dir,,string,,NDJSON log directory allow-network,,boolean,false,Allow bash tool network access max-output-bytes,,number,,Max bytes per tool call return tool-timeout-ms,,number,,Max wall-clock per tool call hot,,boolean,false,Hot reload for .tsx workflows input,i,string,,Input JSON string resume,,boolean,false,Resume existing run force,,boolean,false,Resume even if run is marked running serve,,boolean,false,Start an HTTP server alongside the workflow port,,number,7331,HTTP server port when --serve host,,string,127.0.0.1,HTTP bind address when --serve auth-token,,string,,Bearer token for HTTP auth (or SMITHERS_API_KEY env) metrics,,boolean,true,Expose /metrics Prometheus endpoint when --serve supervise,,boolean,false,Run stale-run supervisor with --serve supervise-dry-run,,boolean,false,With --supervise; detect without resuming supervise-interval,,string,10s,Supervisor poll interval supervise-stale-threshold,,string,30s,Heartbeat staleness threshold supervise-max-concurrent,,number,3,Max runs resumed per poll - name: tui purpose: Interactive observability dashboard - name: ps purpose: List active, paused, and recent runs flags[5]{name,short,type,default,desc}: status,s,string,,"Filter: running|waiting-approval|waiting-event|waiting-timer|continued|finished|failed|cancelled" limit,l,number,20,Max rows all,a,boolean,false,Include all statuses watch,w,boolean,false,Refresh continuously interval,i,number,2,Watch refresh seconds - name: logs purpose: Tail lifecycle events for a run args[1]{name,type,required,desc}: runId,string,true,Run ID flags[4]{name,short,type,default,desc}: follow,f,boolean,true,Poll for new events while run is active follow-ancestry,,boolean,false,Include events from parent runs (root-to-current) since,,number,,Start from event sequence number tail,n,number,50,Last N events - name: events purpose: Query run event history with filters and grouping args[1]{name,type,required,desc}: runId,string,true,Run ID flags[8]{name,short,type,default,desc}: node,n,string,,Filter by node ID type,t,string,,"Category: agent|approval|frame|memory|node|openapi|output|revert|run|sandbox|scorer|snapshot|supervisor|timer|token|tool-call|workflow" since,s,string,,Window (5m, 2h) limit,l,number,1000,Max events (cap 100000) json,j,boolean,false,NDJSON output group-by,,string,,"node | attempt" watch,w,boolean,false,Append new events as they arrive interval,i,number,2,Watch poll seconds - name: chat purpose: Show agent transcripts (prompts, replies, stderr) args[1]{name,type,required,desc}: runId,string,false,Run ID (latest if omitted) flags[4]{name,short,type,default,desc}: all,a,boolean,false,Show every agent attempt follow,f,boolean,false,Watch for new output tail,n,number,,Last N chat blocks stderr,,boolean,true,Include agent stderr - name: chat.create purpose: Create and start a one-task auto-hijacked chat run flags[2]{name,short,type,default,desc}: agent,,string,,"claude-code|codex|gemini" cwd,,string,.,Working directory for the chat session - name: inspect purpose: Print structured run state, steps, approvals, loop info args[1]{name,type,required,desc}: runId,string,true,Run ID flags[2]{name,short,type,default,desc}: watch,w,boolean,false,Refresh continuously interval,i,number,2,Watch refresh seconds - name: node purpose: Enriched node details for debugging retries, tools, output args[1]{name,type,required,desc}: nodeId,string,true,Node ID flags[6]{name,short,type,default,desc}: run-id,r,string,,Run ID iteration,i,number,,Loop iteration (default latest) attempts,,boolean,false,Expand all attempts tools,,boolean,false,Expand tool input/output payloads watch,w,boolean,false,Refresh continuously interval,,number,2,Watch refresh seconds - name: why purpose: Explain why a run is currently blocked or paused args[1]{name,type,required,desc}: runId,string,true,Run ID flags[1]{name,short,type,default,desc}: json,,boolean,false,Structured JSON diagnosis - name: scores purpose: View scorer results for a run args[1]{name,type,required,desc}: runId,string,true,Run ID flags[1]{name,short,type,default,desc}: node,,string,,Filter to specific node ID - name: approve purpose: Record approval for a pending gate args[1]{name,type,required,desc}: runId,string,true,Run ID flags[4]{name,short,type,default,desc}: node,n,string,,Node ID (required if multiple gates pending) iteration,,number,0,Loop iteration note,,string,,Approval note by,,string,,Approver identifier - name: deny purpose: Deny a pending approval gate args[1]{name,type,required,desc}: runId,string,true,Run ID flags[4]{name,short,type,default,desc}: node,n,string,,Node ID iteration,,number,0,Loop iteration note,,string,,Denial note by,,string,,Denier identifier - name: signal purpose: Deliver a durable signal to a run waiting on WaitForEvent args[2]{name,type,required,desc}: runId,string,true,Run ID signalName,string,true,Signal name flags[3]{name,short,type,default,desc}: data,,string,{},Signal payload as JSON correlation,,string,,Correlation ID to match a specific waiter by,,string,,Sender identifier - name: supervise purpose: Watch for stale runs and auto-resume them flags[4]{name,short,type,default,desc}: dry-run,n,boolean,false,Detect without resuming interval,i,string,10s,Poll interval stale-threshold,t,string,30s,Heartbeat staleness threshold max-concurrent,c,number,3,Max runs resumed per poll - name: cancel purpose: Halt one active run; mark in-progress attempts as cancelled args[1]{name,type,required,desc}: runId,string,true,Run ID - name: down purpose: Cancel all active runs in nearest smithers.db flags[1]{name,short,type,default,desc}: force,,boolean,false,Cancel stale runs too - name: hijack purpose: Hand off the latest resumable agent session args[1]{name,type,required,desc}: runId,string,true,Run ID flags[3]{name,short,type,default,desc}: target,,string,,"Expected engine: claude-code|codex" timeout-ms,,number,30000,Wait time for live handoff launch,,boolean,true,Open session immediately - name: graph purpose: Render a workflow tree without executing args[1]{name,type,required,desc}: workflow,string,true,Workflow file path flags[2]{name,short,type,default,desc}: run-id,r,string,graph,Run ID for persisted input/outputs input,,string,,Input JSON (overrides persisted) - name: revert purpose: Restore workspace to a previous task attempt snapshot args[1]{name,type,required,desc}: workflow,string,true,Workflow file path flags[4]{name,short,type,default,desc}: run-id,r,string,,Run ID node-id,n,string,,Node ID attempt,,number,1,Attempt number iteration,,number,0,Loop iteration - name: retry-task purpose: Retry a specific task within a run args[1]{name,type,required,desc}: workflow,string,true,Workflow file path flags[5]{name,short,type,default,desc}: run-id,r,string,,Run ID node-id,n,string,,Task/node ID to retry iteration,,number,0,Loop iteration no-deps,,boolean,false,Only reset this node (skip dependents) force,,boolean,false,Allow retry even if run is still running - name: timetravel purpose: Atomic revert + node reset (filesystem via jj + DB) args[1]{name,type,required,desc}: workflow,string,true,Workflow file path flags[7]{name,short,type,default,desc}: run-id,r,string,,Run ID node-id,n,string,,Task/node ID iteration,,number,0,Loop iteration attempt,a,number,,Attempt number (default latest) no-vcs,,boolean,false,Skip filesystem revert (DB only) no-deps,,boolean,false,Only reset this node resume,,boolean,false,Resume after time travel - name: replay purpose: Fork from a checkpoint and resume execution args[1]{name,type,required,desc}: workflow,string,true,Workflow file path flags[6]{name,short,type,default,desc}: run-id,r,string,,Source run ID frame,f,number,,Frame number to fork from node,n,string,,Node ID to reset to pending input,i,string,,Input overrides as JSON label,l,string,,Branch label restore-vcs,,boolean,false,Restore jj revision to source frame - name: diff purpose: Compare two time-travel snapshots args[2]{name,type,required,desc}: a,string,true,Snapshot ref (run_id or run_id:frame_no) b,string,true,Snapshot ref flags[1]{name,short,type,default,desc}: json,,boolean,false,JSON output - name: fork purpose: Create a branched run from a snapshot args[1]{name,type,required,desc}: workflow,string,true,Workflow file path flags[6]{name,short,type,default,desc}: run-id,r,string,,Source run ID frame,f,number,,Frame number reset-node,n,string,,Node ID to reset input,i,string,,Input overrides label,l,string,,Branch label run,,boolean,false,Immediately start the forked run - name: timeline purpose: View execution timeline + forks args[1]{name,type,required,desc}: runId,string,true,Run ID flags[2]{name,short,type,default,desc}: tree,,boolean,false,Include all child forks recursively json,,boolean,false,JSON output - name: observability purpose: Start or stop the local Docker Compose observability stack flags[2]{name,short,type,default,desc}: detach,d,boolean,false,Background containers down,,boolean,false,Stop and remove stack - name: ask purpose: Query via the best available installed agent CLI args[1]{name,type,required,desc}: query,string,true,Question or prompt flags[6]{name,short,type,default,desc}: agent,,string,,"claude|codex|gemini|kimi|pi" tool-surface,,string,semantic,"MCP tool surface: semantic|raw" no-mcp,,boolean,false,Disable MCP bootstrap print-bootstrap,,boolean,false,Print bootstrap config instead of running dump-prompt,,boolean,false,Print full system prompt list-agents,,boolean,false,List available agents with status - name: agents.capabilities purpose: Print capability registry for all built-in CLI agents - name: agents.doctor purpose: Validate capability metadata for drift or contradictions flags[1]{name,short,type,default,desc}: json,,boolean,false,JSON report - name: human.inbox purpose: List all pending human requests across runs flags[1]{name,short,type,default,desc}: format,,string,,"json for structured output" - name: human.answer purpose: Submit a JSON response to a pending HumanTask request args[1]{name,type,required,desc}: requestId,string,true,Request ID flags[2]{name,short,type,default,desc}: value,,string,,Response as JSON (required) by,,string,,Operator identifier - name: human.cancel purpose: Cancel a pending human request args[1]{name,type,required,desc}: requestId,string,true,Request ID flags[1]{name,short,type,default,desc}: by,,string,,Operator identifier - name: alerts.list purpose: List active alerts from nearest smithers.db - name: alerts.ack purpose: Mark an alert as acknowledged args[1]{name,type,required,desc}: alertId,string,true,Alert ID - name: alerts.resolve purpose: Mark an alert as resolved args[1]{name,type,required,desc}: alertId,string,true,Alert ID - name: alerts.silence purpose: Silence an alert without resolving args[1]{name,type,required,desc}: alertId,string,true,Alert ID - name: memory.list purpose: List all stored facts in a namespace args[1]{name,type,required,desc}: namespace,string,true,"Namespace (e.g. workflow:my-flow, global:default)" flags[1]{name,short,type,default,desc}: workflow,w,string,,Path to .tsx workflow file (locates DB) - name: memory.recall purpose: Search stored facts by semantic similarity args[1]{name,type,required,desc}: query,string,true,Natural language query flags[3]{name,short,type,default,desc}: workflow,w,string,,Path to .tsx workflow file namespace,n,string,global:default,Namespace to search top-k,k,number,5,Number of results - name: openapi.list purpose: Parse an OpenAPI spec and print tool names + summaries args[1]{name,type,required,desc}: specPath,string,true,File path or URL to spec (JSON/YAML) - name: workflow.list purpose: List discovered workflows in .smithers/workflows/ - name: workflow.run purpose: Run a discovered workflow by ID args[1]{name,type,required,desc}: name,string,true,Workflow ID flags[8]{name,short,type,default,desc}: prompt,p,string,,Shorthand for input.prompt input,,string,,Input JSON detach,d,boolean,,Background mode run-id,r,string,,Explicit run ID resume,,boolean,,Resume existing run serve,,boolean,,Start HTTP server alongside port,,number,,HTTP server port hot,,boolean,,Hot reload - name: workflow.path purpose: Resolve workflow ID to entry file path args[1]{name,type,required,desc}: name,string,true,Workflow ID - name: workflow.create purpose: Create a new workflow scaffold file args[1]{name,type,required,desc}: name,string,true,New ID (lowercase-kebab) - name: workflow.doctor purpose: Inspect workflow discovery health args[1]{name,type,required,desc}: name,string,false,Workflow ID (omit for all) - name: cron.start purpose: Start the background scheduler loop (polls every 15s) - name: cron.add purpose: Register a new workflow cron schedule args[2]{name,type,required,desc}: pattern,string,true,"Cron expression (e.g. 0 * * * *)" workflowPath,string,true,Path or ID of workflow - name: cron.list purpose: List all registered cron schedules - name: cron.rm purpose: Delete a cron schedule by ID args[1]{name,type,required,desc}: cronId,string,true,UUID of cron entry - name: completions purpose: Generate shell completion scripts args[1]{name,type,required,desc}: shell,string,true,"bash|zsh|fish|nushell" - name: mcp.add purpose: Register Smithers as an MCP server for an agent integration flags[3]{name,short,type,default,desc}: command,c,string,,Override the agent command no-global,,boolean,false,Project-local install agent,,string,,"Target: claude-code|cursor|..." - name: skills.add purpose: Sync skill files to agent integrations flags[2]{name,short,type,default,desc}: depth,,number,1,Grouping depth no-global,,boolean,false,Project-local install - name: skills.list purpose: List available skills ``` ## Operational notes - **Detached mode** (`up --detach`): redirects stdout/stderr to a log file; prints `runId`/`pid`/`logFile` and exits. - **Serve mode** (`up --serve`): starts the HTTP app and keeps the process alive until interrupted. - **Progress reporting**: writes events to stderr in the form `[+MM:SS.mmm] `. - **Signal handling**: SIGINT/SIGTERM trigger a clean cancellation and SQLite close. - **Supervisor guards**: skips runs whose workflow file is missing, whose owner PID is alive, or which another supervisor already claimed. - **Exit code 4** for `human.answer`: returned when the request is not found, not pending, or expired. --- ## CLI Quickstart > Short operational cheatsheet. The full catalog lives in CLI. ```bash bunx smithers-orchestrator init # scaffold .smithers/ bunx smithers-orchestrator workflow run implement --prompt "…" # launch a seeded workflow bunx smithers-orchestrator ps # list runs bunx smithers-orchestrator inspect # structured run state bunx smithers-orchestrator logs --tail 20 --follow # stream events bunx smithers-orchestrator why # why is it paused? bunx smithers-orchestrator approve --node --by bunx smithers-orchestrator up workflow.tsx --run-id --resume true # resume bunx smithers-orchestrator cancel ``` See [Tour](/tour) for the full worked example. See [CLI](/cli/overview) for every command with flags and examples. --- ## > Root container; sequences direct children, optionally caches for resume. ```ts import { Workflow } from "smithers-orchestrator"; type WorkflowProps = { name: string; cache?: boolean; // skip already-completed nodes on resume children?: ReactNode; }; ``` ```tsx import { createSmithers } from "smithers-orchestrator"; import { ToolLoopAgent as Agent } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; import { z } from "zod"; const { Workflow, Task, smithers, outputs } = createSmithers({ research: z.object({ findings: z.string() }), }); const researcher = new Agent({ model: anthropic("claude-sonnet-4-20250514"), instructions: "You are a research assistant.", }); export default smithers((ctx) => ( {`Research: ${ctx.input.topic}`} )); ``` ## Notes - Direct children run sequentially; `` is only needed inside other control-flow components. - Custom Drizzle tables require `runId` and `nodeId` columns; tasks inside `` also need `iteration`. --- ## > Executable node; runs an agent, compute callback, or emits a static value. ```ts import { Task } from "smithers-orchestrator"; type TaskProps = { id: string; output: z.ZodObject | Table | string; outputSchema?: z.ZodObject; // inferred when output is a Zod schema agent?: AgentLike | AgentLike[]; // array = [primary, ...fallbacks] fallbackAgent?: AgentLike; dependsOn?: string[]; needs?: Record; deps?: Record; // typed render-time upstream outputs allowTools?: string[]; // CLI-agent tool allowlist key?: string; skipIf?: boolean; needsApproval?: boolean; // pause for human before executing async?: boolean; // with needsApproval: let unrelated flow continue timeoutMs?: number; retries?: number; // default Infinity with exponential backoff noRetry?: boolean; retryPolicy?: { backoff?: "fixed" | "linear" | "exponential"; initialDelayMs?: number }; continueOnFail?: boolean; cache?: { by?: (ctx) => unknown; version?: string }; label?: string; meta?: Record; scorers?: ScorersMap; memory?: { recall?: { namespace?: string; query?: string; topK?: number }; remember?: { namespace?: string; key?: string }; threadId?: string; }; heartbeatTimeoutMs?: number; // fail if no heartbeat in window children: | string | Row | (() => Row | Promise) | ReactNode | ((deps) => Row | ReactNode); }; ``` ```tsx import { ToolLoopAgent as Agent } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; const codeAgent = new Agent({ model: anthropic("claude-sonnet-4-20250514"), instructions: "You are a senior software engineer.", }); {`Analyze: ${ctx.input.repoPath}`} {(deps) => `Review: ${deps.analyze.summary}`} ``` ## Notes - Three modes by `children` shape: agent (with `agent`), compute (function, no agent), static (value, no agent). - When `outputSchema` is set, JSON is extracted from agent text; schema-validation retries don't consume `retries`. - Auth errors short-circuit retries; non-idempotent tool reuse warns on the next attempt. --- ## > Run children in source order. ```ts import { Sequence } from "smithers-orchestrator"; type SequenceProps = { skipIf?: boolean; children?: ReactNode; }; ``` ```tsx {{ status: "built" }} {{ passed: true }} {{ status: "built" }} {{ passed: true }} ``` ## Notes - `` sequences direct children implicitly; `` is only needed inside other control-flow components. - Empty `` is valid and produces no tasks. --- ## > Run children concurrently with an optional concurrency cap. ```ts import { Parallel } from "smithers-orchestrator"; type ParallelProps = { id?: string; maxConcurrency?: number; // default Infinity skipIf?: boolean; children?: ReactNode; }; ``` ```tsx {{ errors: 0 }} {{ passed: true }} {{ passed: true }} ``` ## Notes - Group completes when all children finish (or fail, unless `continueOnFail`). - Children receive `parallelGroupId` and `parallelMaxConcurrency` in their descriptor. --- ## > Conditional fork; mounts `then` when `if` is true, otherwise `else`. ```ts import { Branch } from "smithers-orchestrator"; type BranchProps = { if: boolean; then: ReactElement; else?: ReactElement; skipIf?: boolean; }; ``` ```tsx {{ passed: true }} {{ url: "https://prod.example.com" }} } else={ {{ message: "Tests failed, skipping deploy." }} } /> ``` ## Notes - `if` re-evaluates every render frame; read completed-task outputs via `ctx.outputMaybe()`. - Each branch takes one element; wrap multiples in `` or ``. - Unselected branch tasks are absent from the task graph. --- ## > Re-run children until `until` is true or `maxIterations` is hit. ```ts import { Loop } from "smithers-orchestrator"; type LoopProps = { id?: string; // auto-generated from tree position until: boolean; maxIterations?: number; // default 5 onMaxReached?: "fail" | "return-last"; // default "return-last" continueAsNewEvery?: number; // checkpoint every N iters to bound history skipIf?: boolean; children?: ReactNode; }; ``` ```tsx export default smithers((ctx) => { const latestReview = ctx.latest("review", "review"); return ( {latestReview ? `Improve the draft. Feedback: ${latestReview.feedback}` : `Write a draft about: ${ctx.input.topic}`} {`Review the latest draft.`} ); }); ``` ## Notes - `ctx.latest(table, nodeId)` reads the highest-iteration output; `until` must use `ctx.outputMaybe()` since output is absent on iter 0. - Direct nesting of `` in `` throws; wrap the inner loop in ``. - Custom Drizzle tables for loop tasks require `iteration` in the primary key. --- ## > Durable human approval; persists ApprovalDecision, selection, or ranking. ```ts import { Approval, approvalDecisionSchema } from "smithers-orchestrator"; type ApprovalProps = { id: string; mode?: "approve" | "select" | "rank"; // default "approve" options?: ApprovalOption[]; // required for select/rank output: z.ZodObject | Table | string; outputSchema?: z.ZodObject; // default approvalDecisionSchema request: { title: string; summary?: string; metadata?: Record }; onDeny?: "fail" | "continue" | "skip"; // default "fail" allowedScopes?: string[]; allowedUsers?: string[]; autoApprove?: { after?: number; // auto-approve after N consecutive manual approvals condition?: (ctx: WorkflowContext) => boolean; audit?: boolean; revertOn?: (ctx: WorkflowContext) => boolean; }; async?: boolean; // unrelated downstream may continue while pending dependsOn?: string[]; needs?: Record; skipIf?: boolean; timeoutMs?: number; retries?: number; retryPolicy?: { backoff?: "fixed" | "linear" | "exponential"; initialDelayMs?: number }; continueOnFail?: boolean; cache?: { by?: (ctx) => unknown; version?: string }; label?: string; meta?: Record; }; ``` ```tsx import { Approval, Sequence, Task, Workflow, approvalDecisionSchema, createSmithers } from "smithers-orchestrator"; import { z } from "zod"; const { smithers, outputs } = createSmithers({ publishApproval: approvalDecisionSchema, publishResult: z.object({ status: z.enum(["published", "rejected"]) }), }); export default smithers((ctx) => { const decision = ctx.outputMaybe(outputs.publishApproval, { nodeId: "approve-publish" }); return ( {decision ? ( {{ status: decision.approved ? "published" : "rejected" }} ) : null} ); }); ``` ## Notes - `mode="select"` returns `{ selected, notes }`; `mode="rank"` returns `{ ranked, notes }`. - Durable deferred keyed on (run, node, iteration) survives restarts; `smithers approve`/`deny` resolves it. - For a pre-task pause without persisted decision, use ``. --- ## > Conditional approval; pauses for human when `when` is true, else auto-approves. ```ts import { ApprovalGate } from "smithers-orchestrator"; type ApprovalGateProps = { id: string; output: z.ZodObject | Table | string; request: { title: string; summary?: string; metadata?: Record }; when: boolean; // true => require human; false => auto-approve immediately onDeny?: "fail" | "continue" | "skip"; // default "fail" skipIf?: boolean; timeoutMs?: number; retries?: number; retryPolicy?: { backoff?: "fixed" | "linear" | "exponential"; initialDelayMs?: number }; continueOnFail?: boolean; }; ``` ```tsx const risk = ctx.output(outputs.riskScore, { nodeId: "risk" }); Assess deploy risk. {{ deployed: true }} ``` ## Notes - Auto-approve emits a valid `ApprovalDecision` (`{ approved: true, note: "auto-approved", ... }`); downstream branching stays uniform. - `onDeny` applies only to the human path; auto-approve always succeeds. --- ## > Sequential agent escalation with optional human fallback. ```ts // Props import { EscalationChain } from "smithers-orchestrator"; type EscalationChainProps = { id?: string; // default "escalation" levels: EscalationLevel[]; humanFallback?: boolean; // default false humanRequest?: ApprovalRequest; escalationOutput: z.ZodObject | Table | string; skipIf?: boolean; children?: ReactNode; // prompt forwarded to every level }; type EscalationLevel = { agent: AgentLike; output: z.ZodObject | Table | string; label?: string; escalateIf?: (result: any) => boolean; // true -> next level }; ``` ```tsx r.confidence < 0.7 }, { agent: powerAgent, output: outputs.tier2, label: "Tier 2", escalateIf: (r) => r.confidence < 0.9 }, ]} > Resolve this ticket: {ctx.input.ticketBody} ``` ## Notes - Each level uses `continueOnFail`; failures propagate to the next level. - `escalateIf` runs at task-execution time, not render time. --- ## > Flat rule table that replaces nested Branch trees with declarative routing. ```ts // Props import { DecisionTable } from "smithers-orchestrator"; type DecisionTableProps = { id?: string; rules: DecisionRule[]; default?: ReactElement; // rendered when no rule matches strategy?: "first-match" | "all-match"; // default "first-match" skipIf?: boolean; }; type DecisionRule = { when: boolean; // evaluated at render time then: ReactElement; label?: string; }; ``` ```tsx Page the on-call engineer immediately. ), }, { when: triage.severity === "high", then: {{ assignee: "senior-pool" }}, }, ]} default={{{ assignee: "general-pool" }}} /> ``` ## Notes - `first-match` builds nested Branches; order matters. - `all-match` wraps every matching rule in a Parallel; no ordering guarantee. --- ## > Suspend until a human submits JSON matching the output schema. ```ts import { HumanTask } from "smithers-orchestrator"; type HumanTaskProps = { id: string; output: z.ZodObject | Table | string; outputSchema?: z.ZodObject; // inferred when output is a Zod schema prompt: string | ReactNode; maxAttempts?: number; // default 10 async?: boolean; // unrelated downstream may continue while pending skipIf?: boolean; timeoutMs?: number; continueOnFail?: boolean; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; }; ``` ```tsx import { Workflow, Sequence, Task, HumanTask, createSmithers } from "smithers-orchestrator"; import { z } from "zod"; const { smithers, outputs } = createSmithers({ review: z.object({ approved: z.boolean(), comments: z.string(), severity: z.enum(["low", "medium", "high"]), }), summary: z.object({ status: z.string() }), }); export default smithers((ctx) => { const review = ctx.outputMaybe(outputs.review, { nodeId: "human-review" }); return ( {review ? ( {{ status: review.approved ? "approved" : "changes-requested" }} ) : null} ); }); ``` ## Notes - Submit via `bunx smithers-orchestrator approve --note ''`. - Failed JSON re-prompts up to `maxAttempts` with zero backoff. - Same durable deferred mechanism as ``; survives restarts. --- ## > Typed wrapper around <WaitForEvent>; signal name equals node id. ```ts import { Signal } from "smithers-orchestrator"; type SignalProps = { id: string; // signal name and node id schema: z.ZodObject; // typed payload + output target correlationId?: string; timeoutMs?: number; onTimeout?: "fail" | "skip" | "continue"; // default "fail" async?: boolean; skipIf?: boolean; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; children?: (data) => ReactNode; // mounts only after payload arrives }; ``` ```tsx import { Signal, Task, Workflow, createSmithers } from "smithers-orchestrator"; import { z } from "zod"; const { smithers, outputs } = createSmithers({ feedback: z.object({ rating: z.number(), comment: z.string() }), summary: z.object({ upper: z.string() }), }); export default smithers(() => ( {(feedback) => ( {{ upper: feedback.comment.toUpperCase() }} )} )); ``` ## Notes - Renders `` internally. - Async waits show in `smithers_external_wait_async_pending{kind="event"}`. --- ## > Durably suspend until a correlated external event arrives, or time out. ```ts import { WaitForEvent } from "smithers-orchestrator"; type WaitForEventProps = { id: string; event: string; output: z.ZodObject | Table | string; correlationId?: string; outputSchema?: z.ZodObject; timeoutMs?: number; onTimeout?: "fail" | "skip" | "continue"; // default "fail" async?: boolean; // unrelated downstream may proceed while pending skipIf?: boolean; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; }; ``` ```tsx The deploy finished. Summarize the result. ``` ## Notes - Push-based; for poll-based checks use `` with a compute function. - With `async`, dependents via `dependsOn`/`needs` still block until payload arrives. - Async waits increment `smithers_external_wait_async_pending{kind="event"}` while pending. --- ## > Durably suspend for a relative duration or until an absolute time. ```ts import { Timer } from "smithers-orchestrator"; type TimerProps = { id: string; duration?: string; // "500ms" | "30s" | "2h" | "7d"; exactly one of duration/until required until?: string | Date; // ISO 8601 string or Date skipIf?: boolean; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; }; ``` ```tsx Generate the daily summary report. ``` ## Notes - Exactly one of `duration` or `until` is required; both or neither throws at render time. - Produces no output. Past `until` timestamps fire immediately. - Worker restarts during the wait don't reset the timer. --- ## > Forward steps with compensations run in reverse on failure. ```ts import { Saga } from "smithers-orchestrator"; type SagaStepDef = { id: string; action: ReactElement; compensation: ReactElement; label?: string; }; type SagaProps = { id?: string; steps?: SagaStepDef[]; // takes priority over children onFailure?: "compensate" | "compensate-and-fail" | "fail"; // default "compensate" skipIf?: boolean; children?: React.ReactNode; // alternative: children }; ``` ```tsx Create a PR., compensation: Close the PR., }, { id: "deploy-staging", action: Deploy staging., compensation: Rollback staging., }, ]} /> ``` ## Notes - Steps run sequentially; compensations run in reverse from the failed step. - Compensations should be idempotent. - `steps` and `` children are mutually exclusive; `steps` wins if both are given. --- ## > Workflow-scoped error boundary with catch handlers and guaranteed cleanup. ```ts import { TryCatchFinally } from "smithers-orchestrator"; type TryCatchFinallyProps = { id?: string; try: ReactElement; catch?: ReactElement | ((error: SmithersError) => ReactElement); catchErrors?: SmithersErrorCode[]; // restrict which codes trigger catch finally?: ReactElement; // always runs after try or catch skipIf?: boolean; }; ``` ```tsx Build the project. Deploy to production. } catch={(error) => ( {`Recover from ${error.code}: ${error.summary}`} )} finally={ {{ cleanedUp: true }} } /> ``` ## Notes - `try` takes one `ReactElement`; wrap multiples in `` or ``. - `finally` runs even if `catch` fails. - Unmatched `catchErrors` propagate to outer boundaries. --- ## > Run a child workflow in an isolated runtime; collect its result bundle. ```ts import { Sandbox } from "smithers-orchestrator"; type SandboxVolumeMount = { host: string; container: string; readonly?: boolean }; type SandboxWorkspaceSpec = { name: string; snapshotId?: string; idleTimeoutSecs?: number; persistence?: "ephemeral" | "sticky"; }; type SandboxProps = { id: string; output: ZodObject | DrizzleTable | string; workflow?: (...args: any[]) => any; input?: unknown; runtime?: "bubblewrap" | "docker" | "codeplane"; // default "bubblewrap"; docker auto-falls back allowNetwork?: boolean; // default false reviewDiffs?: boolean; // default true autoAcceptDiffs?: boolean; // default false image?: string; env?: Record; ports?: Array<{ host: number; container: number }>; volumes?: SandboxVolumeMount[]; memoryLimit?: string; // e.g. "512m", "2g" cpuLimit?: string; // e.g. "0.5", "2" command?: string; // override default `smithers up bundle.tsx` workspace?: SandboxWorkspaceSpec; // codeplane only skipIf?: boolean; timeoutMs?: number; heartbeatTimeoutMs?: number; retries?: number; retryPolicy?: RetryPolicy; continueOnFail?: boolean; cache?: CachePolicy; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; children?: ReactNode; }; ``` ```tsx ``` ## Notes - Bundle limits: 100 MB total, 5 MB manifest, 1,000 patches max; path traversal rejected. - Concurrency cap via `SMITHERS_MAX_CONCURRENT_SANDBOXES` (default 10). - `cleanup` always runs in `finally`; `codeplane` requires `CODEPLANE_API_URL` and `CODEPLANE_API_KEY`. --- ## > Invoke a child workflow with its own retry, cache, resume boundary. ```ts import { Subflow } from "smithers-orchestrator"; type SubflowProps = { id: string; workflow: Function; output: z.ZodObject | Table | string; input?: unknown; mode?: "childRun" | "inline"; // default "childRun" skipIf?: boolean; timeoutMs?: number; retries?: number; retryPolicy?: RetryPolicy; continueOnFail?: boolean; cache?: CachePolicy; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; }; ``` ```tsx Summarize the child workflow result. ``` ## Notes - `childRun` (default) gives the child its own DB row; retry/cache/resume scope it as a unit. - `inline` renders the child tree as siblings in the parent plan, sharing its scope. - Subflows compose; children may contain `` themselves. --- ## > Close the current run; start a fresh one with optional carried state. ```ts import { ContinueAsNew, continueAsNew } from "smithers-orchestrator"; type ContinueAsNewProps = { state?: unknown; // JSON-serializable; arrives as ctx.input.__smithersContinuation.payload }; // continueAsNew(state?) is a helper equivalent to ``` ```tsx export default smithers((ctx) => { const continuation = (ctx.input as any)?.__smithersContinuation as | { payload?: { cursor?: string; count?: number } } | undefined; const cursor = continuation?.payload?.cursor ?? null; const count = continuation?.payload?.count ?? 0; return ( {`Process next page. Cursor: ${cursor ?? "start"}. Total so far: ${count}.`} ); }); ``` ## Notes - `state` must be JSON-serializable; total continuation envelope < 10 MB. - Workflow id is preserved across continuations; only run id increments. - Nodes rendered after `` in the same sequence don't execute. --- ## > Reads and modifies source code at runtime via a markdown strategy doc. ```ts import { SuperSmithers } from "smithers-orchestrator"; type SuperSmithersProps = { strategy: string | ReactElement; // markdown or MDX strategy document agent: AgentLike; id?: string; // default "super-smithers"; prefixes internal task ids targetFiles?: string[]; // glob patterns scoped to the agent prompt reportOutput?: OutputTarget; dryRun?: boolean; // default false; skips the apply step skipIf?: boolean; }; ``` ```tsx ``` ## Notes - Expands to read → propose → apply → report (`dryRun` skips apply). - Only meaningful in hot-reload mode; otherwise changes apply on the next run. - All internal tasks share `agent`; for per-stage agents compose `` manually. --- ## > Propagate token, latency, and cost budgets to descendant tasks. ```ts import { Aspects } from "smithers-orchestrator"; type TokenBudgetConfig = { max: number; perTask?: number; onExceeded?: "fail" | "warn" | "skip-remaining"; // default "fail" }; type LatencySloConfig = { maxMs: number; perTask?: number; onExceeded?: "fail" | "warn"; // default "fail" }; type CostBudgetConfig = { maxUsd: number; onExceeded?: "fail" | "warn" | "skip-remaining"; // default "fail" }; type TrackingConfig = { tokens?: boolean; latency?: boolean; cost?: boolean }; type AspectsProps = { tokenBudget?: TokenBudgetConfig; latencySlo?: LatencySloConfig; costBudget?: CostBudgetConfig; tracking?: TrackingConfig; // default all true children: ReactNode; }; ``` ```tsx Analyze the repository. Review the analysis. ``` ## Notes - Nested `` inherit outer values; inner fields override per-config. - Budgets enforce regardless of `tracking`; `tracking` controls metric emission only. - Accumulator is per-run and resets on resume. --- ## > Run a subtree in a separate JJ worktree rooted at `path`. ```ts import { Worktree } from "smithers-orchestrator"; type WorktreeProps = { path: string; // required, non-empty; resolved against baseRootDir or cwd if relative id?: string; branch?: string; // omit to use current branch baseBranch?: string; // default "main" skipIf?: boolean; children: ReactNode; }; ``` ```tsx {{ value: 1 }} {{ value: 2 }} {{ value: 3 }} {{ value: 4 }} ``` ## Notes - Descendants inherit `worktreeId` and absolute `worktreePath` as `cwd`. - Innermost `` wins when nested; duplicate ids are rejected. - Empty/whitespace `path` is rejected at render time. --- ## > Produce, review, and fix in a loop until the reviewer approves. ```ts // Props import { ReviewLoop } from "smithers-orchestrator"; type ReviewLoopProps = { id?: string; // default "review-loop"; task ids derived as {id}-produce, {id}-review producer: AgentLike; reviewer: AgentLike | AgentLike[]; produceOutput: OutputTarget; reviewOutput: OutputTarget; // must include `approved: boolean` maxIterations?: number; // default 5 onMaxReached?: "return-last" | "fail"; // default "return-last" skipIf?: boolean; children: string | ReactNode; // initial producer prompt }; ``` ```tsx export default smithers(() => ( Implement a REST API for user authentication with JWT tokens. )); ``` ## Notes - The runtime reads `approved` each frame to decide whether to loop. - On subsequent iterations, the producer receives reviewer feedback via re-render. --- ## > Generate, evaluate, and improve in a loop until a target score is reached. ```ts // Props import { Optimizer } from "smithers-orchestrator"; type OptimizerProps = { id?: string; // default "optimizer"; task ids {id}-generate, {id}-evaluate generator: AgentLike; evaluator: AgentLike | ((candidate: unknown) => unknown); // function = compute task generateOutput: OutputTarget; evaluateOutput: OutputTarget; // must include `score: number` targetScore?: number; // omit to run all iterations maxIterations?: number; // default 10 onMaxReached?: "return-last" | "fail"; // default "return-last" skipIf?: boolean; children: string | ReactNode; // initial generation prompt }; ``` ```tsx export default smithers(() => ( Generate a prompt for summarizing legal documents. )); ``` ## Notes - `score` drives convergence against `targetScore`. - A function `evaluator` renders as a compute task rather than an agent task. --- ## > Typed waterfall of refinement stages, each depending on the previous. ```ts // Props import { ContentPipeline } from "smithers-orchestrator"; type ContentPipelineProps = { id?: string; stages: ContentPipelineStage[]; skipIf?: boolean; children: string | ReactNode; // initial prompt for stage[0] }; type ContentPipelineStage = { id: string; agent: AgentLike; output: OutputTarget; label?: string; }; ``` ```tsx export default smithers(() => ( Write a blog post about building AI workflows with React components. )); ``` ## Notes - Each stage after the first depends on the previous via `needs`. - Stage `id` values must be unique within the workflow. --- ## > Capture state, compare against a baseline, and alert on meaningful drift. ```ts // Props import { DriftDetector } from "smithers-orchestrator"; type DriftDetectorProps = { id?: string; // default "drift"; ids {id}-capture, {id}-compare captureAgent: AgentLike; compareAgent: AgentLike; captureOutput: OutputTarget; compareOutput: OutputTarget; // include `drifted: boolean` baseline: unknown; alertIf?: (comparison: any) => boolean; // override the default `drifted` check alert?: ReactElement; poll?: { intervalMs: number; maxPolls?: number }; // default maxPolls = 100 skipIf?: boolean; }; ``` ```tsx API drift detected — notify the team. } /> ``` ## Notes - Without `poll`, the component runs once; with `poll`, it wraps in a Loop. - Without `alert`, the component compares but takes no action on drift. --- ## > Scan for problems, fix in parallel, verify, then report in a retry loop. ```ts // Props import { ScanFixVerify } from "smithers-orchestrator"; type ScanFixVerifyProps = { id?: string; // default "sfv" scanner: AgentLike; fixer: AgentLike | AgentLike[]; // array cycles across issues verifier: AgentLike; scanOutput: OutputTarget; // include `issues: Array` fixOutput: OutputTarget; verifyOutput: OutputTarget; reportOutput: OutputTarget; maxConcurrency?: number; // default Infinity maxRetries?: number; // default 3 skipIf?: boolean; children?: ReactNode; // scan prompt }; ``` ```tsx Scan the codebase for linting errors and type issues. ``` ## Notes - The loop exits when the verifier reports all clear or `maxRetries` is hit. - The report task always runs, even when retries are exhausted. --- ## > Poll an external condition with configurable backoff until satisfied or timed out. ```ts // Props import { Poller } from "smithers-orchestrator"; type PollerProps = { id?: string; // default "poll" check: AgentLike | (() => Promise | unknown); checkOutput: OutputTarget; // must include `satisfied: boolean` maxAttempts?: number; // default 30 backoff?: "fixed" | "linear" | "exponential"; // default "fixed" intervalMs?: number; // default 5000 onTimeout?: "fail" | "return-last"; // default "fail" skipIf?: boolean; children?: ReactNode; // condition description }; ``` ```tsx Check whether the deployment to production has completed successfully. ``` ## Notes - `satisfied` drives the loop's `until`. - Backoff: fixed = `intervalMs`; linear = `intervalMs * (N+1)`; exponential = `intervalMs * 2^N`. --- ## > Sequential steps with risk classification; safe auto-runs, risky/critical gate on approval. ```ts // Props import { Runbook } from "smithers-orchestrator"; type RunbookProps = { id?: string; // default "runbook" steps: RunbookStep[]; defaultAgent?: AgentLike; stepOutput: OutputTarget; approvalRequest?: Partial; onDeny?: "fail" | "skip"; // default "fail" skipIf?: boolean; }; type RunbookStep = { id: string; agent?: AgentLike; command?: string; risk: "safe" | "risky" | "critical"; // critical adds `elevated: true` to approval meta label?: string; output?: OutputTarget; }; ``` ```tsx export default smithers(() => ( backup.sql", risk: "risky" }, { id: "run-migration", command: "npx prisma migrate deploy", risk: "critical" }, { id: "smoke-test", command: "npm run test:smoke", risk: "safe" }, ]} /> )); ``` ## Notes - Each step depends on the previous via `needs`; execution order is guaranteed. - Critical steps set `elevated: true` in approval metadata for stronger auth UIs. - Approval output is stored at `{prefix}-{step.id}-approval-decision`. --- ## > Boss plans, workers run in parallel, boss reviews and re-delegates failures. ```ts // Props import { Supervisor } from "smithers-orchestrator"; type SupervisorProps = { id?: string; // default: "supervisor" boss: AgentLike; workers: Record; // { coder, tester, ... } planOutput: OutputTarget; // { tasks: [{ id, workerType, instructions }] } workerOutput: OutputTarget; reviewOutput: OutputTarget; // { allDone: boolean, retriable: string[] } finalOutput: OutputTarget; maxIterations?: number; // default: 3 maxConcurrency?: number; // default: 5 useWorktrees?: boolean; // default: false skipIf?: boolean; children: string | ReactNode; // goal/prompt for the boss }; ``` ```tsx export default smithers(() => ( Build the user authentication module with tests. )); ``` ## Notes - Generated node ids: `{id}-plan`, `{id}-loop`, `{id}-worker-{type}`, `{id}-review`, `{id}-final`. - Workers run with `continueOnFail`; a single failure does not abort the cycle. - With `useWorktrees`, each worker runs in `.worktrees/{prefix}-worker-{type}` on branch `worker/{prefix}-worker-{type}`. --- ## > Queue child tasks so at most maxConcurrency run; defaults to single-lane. ```ts // Props import { MergeQueue } from "smithers-orchestrator"; type MergeQueueProps = { id?: string; maxConcurrency?: number; // default: 1 skipIf?: boolean; children: ReactNode; }; ``` ```tsx {items.map((it, i) => ( {{ value: i }} ))} ``` ## Notes - Innermost group determines the effective cap for its descendants. - Tasks outside the queue are unaffected by its limit. --- ## > Parallel checks with auto-aggregated pass/fail verdict. ```ts // Props import { CheckSuite } from "smithers-orchestrator"; type CheckConfig = { id?: string; agent?: AgentLike; command?: string; label?: string }; type CheckSuiteProps = { id?: string; // default: "checksuite" checks: CheckConfig[] | Record; verdictOutput: OutputTarget; strategy?: "all-pass" | "majority" | "any-pass"; // default: "all-pass" maxConcurrency?: number; // default: Infinity continueOnFail?: boolean; // default: true skipIf?: boolean; }; ``` ```tsx ``` ## Notes - Check task ids are `{prefix}-{checkId}`; verdict is `{prefix}-verdict`. - `strategy` is passed as prompt context to the verdict agent, which interprets it. - Use `command` instead of `agent` for shell-based checks. --- ## > Classify items into categories, then route each to a category-specific agent in parallel. ```ts // Props import { ClassifyAndRoute } from "smithers-orchestrator"; type CategoryConfig = { agent: AgentLike; output?: OutputTarget; prompt?: (item: unknown) => string; }; type ClassifyAndRouteProps = { id?: string; // default: "classify-and-route" items: unknown | unknown[]; categories: Record; classifierAgent: AgentLike; classifierOutput: OutputTarget; routeOutput: OutputTarget; classificationResult?: { classifications: Array<{ category: string; itemId?: string }> } | null; maxConcurrency?: number; // default: Infinity skipIf?: boolean; children?: ReactNode; // custom classifier prompt }; ``` ```tsx const classification = ctx.outputMaybe(outputs.classification, { nodeId: "classify-and-route-classify", }); ; ``` ## Notes - Two-phase: first render classifies; pass the result back via `classificationResult` to mount route handlers. - Each entry's `category` must match a key in `categories`; unknown categories are silently skipped. - Route tasks default to `continueOnFail`. --- ## > Parallel gather from multiple sources, then synthesize into a unified result. ```ts // Props import { GatherAndSynthesize } from "smithers-orchestrator"; type SourceDef = { agent: AgentLike; prompt: string; output?: OutputTarget; children?: ReactNode; // overrides prompt }; type GatherAndSynthesizeProps = { id?: string; // default: "gather-and-synthesize" sources: Record; synthesizer: AgentLike; gatherOutput: OutputTarget; synthesisOutput: OutputTarget; gatheredResults?: Record | null; // typically from ctx.outputMaybe() maxConcurrency?: number; // default: Infinity synthesisPrompt?: string; skipIf?: boolean; children?: ReactNode; // overrides synthesisPrompt }; ``` ```tsx ``` ## Notes - Synthesis task auto-receives `needs` for every source, gating it on all gathers. - Source `children` takes priority over `prompt`. - When `gatheredResults` is provided, it is folded into the default synthesis prompt. --- ## > Parallel specialist agents review the same input; a moderator synthesizes results. ```ts // Props import { Panel } from "smithers-orchestrator"; type PanelistConfig = { agent: AgentLike; role?: string; label?: string }; type PanelProps = { id?: string; // default: "panel" panelists: PanelistConfig[] | AgentLike[]; moderator: AgentLike; panelistOutput: OutputTarget; moderatorOutput: OutputTarget; strategy?: "synthesize" | "vote" | "consensus"; // default: "synthesize" minAgree?: number; // for "vote" / "consensus" maxConcurrency?: number; // default: Infinity skipIf?: boolean; children: string | ReactNode; // prompt sent to every panelist }; ``` ```tsx Review the changes in src/auth/ for security, quality, and architecture concerns. ``` ## Notes - Panelist task ids: `{prefix}-{label|role|panelist-N}`; moderator is `{prefix}-moderator`. - `strategy` and `minAgree` are passed as prompt context to the moderator, which interprets them. - All panelists write to the same `panelistOutput` schema, differentiated by task id. --- ## > Adversarial multi-round debate between proposer and opponent, then judge verdict. ```ts // Props import { Debate } from "smithers-orchestrator"; type DebateProps = { id?: string; // default: "debate" proposer: AgentLike; // arguing FOR opponent: AgentLike; // arguing AGAINST judge: AgentLike; // renders final verdict rounds?: number; // default: 2 argumentOutput: OutputTarget; verdictOutput: OutputTarget; topic: string | ReactNode; skipIf?: boolean; }; ``` ```tsx ``` ## Notes - Task ids: `{prefix}-proposer`, `{prefix}-opponent`, `{prefix}-judge`, loop `{prefix}-loop`. - Loop runs exactly `rounds` iterations with `onMaxReached="return-last"`. - Proposer and opponent share `argumentOutput`, differentiated by task id. --- ## > Process items through ordered columns with a pluggable ticket source. ```ts // Props import { Kanban } from "smithers-orchestrator"; type ColumnDef = { name: string; agent: AgentLike; output: OutputTarget; prompt?: (ctx: { item: unknown; column: string }) => string; task?: Partial; // retries, timeoutMs, etc. }; type KanbanProps = { id?: string; // default: "kanban" columns: ColumnDef[]; useTickets: () => Array<{ id: string }>; agents?: Record; // overrides column-level agents maxConcurrency?: number; // default: Infinity, per column onComplete?: OutputTarget; until?: boolean; // default: false maxIterations?: number; // default: 5 skipIf?: boolean; children?: ReactNode; // content for onComplete task }; ``` ```tsx const columns = [ { name: "triage", agent: triageAgent, output: outputs.triage }, { name: "work", agent: workerAgent, output: outputs.work }, { name: "review", agent: reviewAgent, output: outputs.review }, ]; tickets} until={allDone} maxIterations={3} /> ; ``` ## Notes - Item tasks default to `continueOnFail={true}`; use `column.task` to add retries or override. - `useTickets` is called at render time; return different items per iteration for dynamic sources. - Use `until` with `ctx.outputMaybe()` to exit when all items reach the final column. --- ## Recipes > Tight, reusable patterns. Copy, paste, adapt. Each recipe is a working snippet plus one line of context. They compose freely. ## Implement → review loop Iterate until a reviewer signs off, with a hard cap. ```tsx {`${ctx.input.task}\nPrior review: ${ctx.latest(outputs.review, "review")?.feedback ?? "none"}`} {`Review the latest implementation. Return { approved, feedback }.`} ``` Stop conditions must be measurable (boolean, count, array length). Avoid "looks good" prompts — agents are literal. ## Parallel multi-agent review Two models catch different bugs. Cost = the slower model's latency. ```tsx ``` `continueOnFail` keeps one model's timeout from blocking the other. ## Approval gate with branching Decision data drives the next branch. ```tsx {ctx.outputMaybe(outputs.shipDecision, { nodeId: "ship-decision" })?.approved ? : } ``` `onDeny`: `"fail"` aborts, `"continue"` proceeds without the gated branch, `"skip"` skips the gated tasks. ## Retry policy & timeouts ```tsx Call external API. ``` Defaults to fit the work: simple tasks 30–60s + 1–2 retries, tool-heavy 2–5m + 1–2, large generations 5–10m + 0–1. Exponential backoff for rate-limited APIs. ## Optional, non-blocking step ```tsx Run lint checks. Pipeline continues if this fails. ``` Use for nice-to-have telemetry, lint, optional analysis. ## Conditional branch on output ```tsx const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" }); {analysis?.risk === "high" ? ( {`Critical: ${analysis.summary}`} ) : null} ``` `ctx.outputMaybe` for control flow; `deps={{...}}` for typed data flow into prompts. ## Dynamic ticket discovery Discover work, run each ticket, re-render to catch the next batch. Scales to large projects. ```tsx export default smithers((ctx) => { const discover = ctx.latest(outputs.discover, "discover"); const unfinished = (discover?.tickets ?? []).filter( (t) => !ctx.latest(outputs.report, `${t.id}:report`) ); return ( } /> {unfinished.map((t) => ( ))} ); }); ``` Use stable IDs (`t.id`, not array index) so resume matches. ## Coherent task with tools One context boundary per logical operation, not per step. Splitting too finely loses cross-step reasoning. ```tsx {`Analyze config files in ${ctx.input.dir}, find bugs, fix them, write results. Use read, edit, bash. Return { summary, filesChanged }.`} ``` ## Per-agent least-privilege tools ```tsx const analyst = new Agent({ model, instructions: "Return JSON" }); // no tools const reviewer = new Agent({ model, instructions: "...", tools: { read, grep } }); // read-only const implementer = new Agent({ model, instructions: "...", tools: { read, write, edit, bash } }); ``` Match the tool surface to the role. ## Side-effect tools with idempotency External mutations must mark themselves and use the runtime idempotency key. ```tsx import { defineTool } from "smithers-orchestrator/tools"; const createTicket = defineTool({ name: "jira.create", schema: z.object({ title: z.string() }), sideEffect: true, idempotent: false, async execute(args, ctx) { return jira.createIssue({ ...args, idempotencyKey: ctx.idempotencyKey }); }, }); ``` Retries reuse the same idempotency key, so a successful side effect from attempt 1 isn't doubled by attempt 2. ## Caching for iterative authoring ```tsx ({ repo: ctx.input.repo }), version: "v2" }} > {`Analyze ${ctx.input.repo}`} {(deps) => `Report on ${deps.analyze.summary}`} ``` Tweak the downstream Task without re-running the expensive upstream one. Don't cache side effects. ## Schemas in their own file ```ts // schemas.ts export const schemas = { analysis: z.object({ summary: z.string(), issues: z.array(z.string()) }), review: z.object({ approved: z.boolean(), feedback: z.string() }), report: z.object({ title: z.string(), body: z.string() }), }; // workflow.tsx import { schemas } from "./schemas"; const { Workflow, smithers, outputs } = createSmithers(schemas); ``` All data shapes in one place; new contributors read schemas.ts first. ## MDX prompt with auto-injected schema ```mdx {/* Review.mdx */} Review this code: **Files**: {props.files.join(", ")} **Tests**: {props.testsPassed}/{props.testsRun} passing Return JSON matching schema: {props.schema} ``` `props.schema` is the JSON-schema description of the Task's `outputSchema`, auto-injected. Keeps the prompt and the validator in sync. ## Custom hooks over `ctx` ```tsx function useReviewState(ticketId: string) { const ctx = useCtx(); const claude = ctx.latest("review", `${ticketId}:review-claude`); const codex = ctx.latest("review", `${ticketId}:review-codex`); return { claude, codex, allApproved: !!(claude?.approved && codex?.approved) }; } ``` Workflow logic factors out into hooks the same way React UI logic does. ## VCS revert & per-attempt snapshots Smithers records a jj change ID (or git SHA) per attempt. Revert any attempt to its exact workspace state: ```bash bunx smithers-orchestrator revert workflow.tsx --run-id --node-id implement --attempt 1 ``` Useful when an experiment leaves the worktree in a bad state. ## Time travel: fork, replay, diff ```bash bunx smithers-orchestrator timeline --tree bunx smithers-orchestrator diff bunx smithers-orchestrator fork workflow.tsx --run-id --frame 5 --reset-node analyze --label exp1 bunx smithers-orchestrator replay workflow.tsx --run-id --frame 5 --restore-vcs ``` Fork makes a child run; replay re-executes from the snapshot in the same run. `--restore-vcs` checks out the original revision so re-execution sees the same source. ## Scoring tasks ```tsx import { schemaAdherenceScorer, latencyScorer, llmJudge } from "smithers-orchestrator/scorers"; Analyze... ``` Scorers run after the task and never block. Sample expensive scorers with `ratio`. ## Continue-as-new for very long runs A run with too much accumulated state hands off to a fresh run with carried state. ```tsx 100} carry={{ summary: rolledUpState }} /> ``` Avoids unbounded SQLite growth in long-lived loops. ## Hot reload while authoring ```bash bunx smithers-orchestrator up workflow.tsx --hot true ``` Edits to the workflow source apply on the next render frame without losing in-flight task state. Schema changes still require a fresh run. ## Read next - [How It Works](/how-it-works) — the model these recipes plug into. - [Components](/components/workflow) — full prop surface. - [CLI](/cli/overview) — every command. --- ## Types > Public TypeScript surface for smithers-orchestrator. One source of truth: `tsc --emitDeclarationOnly` would produce something close to this. Import these types from `smithers-orchestrator` unless noted otherwise. ```ts // ============================================================================= // Workflow // ============================================================================= interface SmithersWorkflow { readonly readableName?: string; readonly description?: string; readonly db?: unknown; readonly build: (ctx: SmithersCtx) => JSX.Element; readonly opts: SmithersWorkflowOptions; readonly schemaRegistry?: Map; } type SmithersWorkflowOptions = { alertPolicy?: SmithersAlertPolicy; cache?: boolean; workflowHash?: string; }; type SchemaRegistryEntry = { table: any; zodSchema: import("zod").ZodObject; }; type SmithersAlertPolicy = { defaults?: SmithersAlertPolicyDefaults; rules?: Record; reactions?: Record; }; type SmithersAlertSeverity = "info" | "warning" | "critical"; type SmithersAlertLabels = Record; type SmithersAlertPolicyDefaults = { owner?: string; severity?: SmithersAlertSeverity; runbook?: string; labels?: SmithersAlertLabels; }; type SmithersAlertPolicyRule = SmithersAlertPolicyDefaults & { afterMs?: number; reaction?: string | SmithersAlertReaction; }; type SmithersAlertReaction = | { kind: "emit-only" } | { kind: "pause" } | { kind: "cancel" } | { kind: "open-approval" } | { kind: "deliver"; destination: string }; // ============================================================================= // Context // ============================================================================= declare class SmithersCtx { readonly runId: string; readonly iteration: number; readonly iterations?: Record; readonly input: Schema extends { input: infer T } ? T : any; readonly auth: RunAuthContext | null; readonly outputs: OutputAccessor; output(table: any, key: OutputKey): any; outputMaybe(table: any, key: OutputKey): any | undefined; latest(table: any, nodeId: string): any | undefined; latestArray(value: unknown, schema: any): unknown[]; iterationCount(table: any, nodeId: string): number; } type OutputKey = { nodeId: string; iteration?: number }; type OutputAccessor = ((table: any) => any[]) & Record; type InferRow = TTable extends { $inferSelect: infer R } ? R : never; type InferOutputEntry = T extends import("zod").ZodTypeAny ? import("zod").infer : T extends { $inferSelect: any } ? InferRow : never; type RunAuthContext = { triggeredBy: string; scopes: string[]; role: string; createdAt: string; }; // ============================================================================= // Run // ============================================================================= type RunOptions = { runId?: string; parentRunId?: string | null; input: Record; maxConcurrency?: number; // default 4 onProgress?: (e: SmithersEvent) => void; signal?: AbortSignal; resume?: boolean; force?: boolean; // resume even if marked running workflowPath?: string; rootDir?: string; logDir?: string | null; allowNetwork?: boolean; // default false; bash tool network access maxOutputBytes?: number; // default 200000 toolTimeoutMs?: number; // default 60000 hot?: boolean | HotReloadOptions; auth?: RunAuthContext | null; config?: Record; cliAgentToolsDefault?: "all" | "explicit-only"; // default "all" resumeClaim?: { // internal supervisor coordination claimOwnerId: string; claimHeartbeatAtMs: number; restoreRuntimeOwnerId?: string | null; restoreHeartbeatAtMs?: number | null; }; }; type HotReloadOptions = { rootDir?: string; outDir?: string; // default .smithers/hmr/ maxGenerations?: number; // default 3 cancelUnmounted?: boolean; // default false debounceMs?: number; // default 100 }; type RunResult = { readonly runId: string; readonly status: RunStatus; readonly output?: unknown; readonly error?: unknown; readonly nextRunId?: string; // set when the run continued-as-new }; type RunStatus = | "running" | "waiting-approval" | "waiting-event" | "waiting-timer" | "finished" | "continued" | "failed" | "cancelled"; type RetryTaskOptions = { runId: string; nodeId: string; iteration?: number; resetDependents?: boolean; // default true force?: boolean; // default false onProgress?: (e: SmithersEvent) => void; }; type RetryTaskResult = { success: boolean; resetNodes: string[]; error?: string; }; // ============================================================================= // Task // ============================================================================= type TaskDescriptor = { nodeId: string; ordinal: number; iteration: number; ralphId?: string; dependsOn?: string[]; needs?: Record; worktreeId?: string; worktreePath?: string; worktreeBranch?: string; worktreeBaseBranch?: string; outputTable: unknown | null; outputTableName: string; outputRef?: import("zod").ZodObject; outputSchema?: import("zod").ZodObject; parallelGroupId?: string; parallelMaxConcurrency?: number; needsApproval: boolean; waitAsync?: boolean; approvalMode?: "gate" | "decision" | "select" | "rank"; approvalOnDeny?: "fail" | "continue" | "skip"; approvalOptions?: ApprovalOption[]; approvalAllowedScopes?: string[]; approvalAllowedUsers?: string[]; approvalAutoApprove?: { after?: number; audit?: boolean; conditionMet?: boolean; revertOnMet?: boolean; }; skipIf: boolean; retries: number; retryPolicy?: RetryPolicy; timeoutMs: number | null; heartbeatTimeoutMs: number | null; continueOnFail: boolean; cachePolicy?: CachePolicy; agent?: AgentLike | AgentLike[]; prompt?: string; staticPayload?: unknown; computeFn?: () => unknown | Promise; label?: string; meta?: Record; scorers?: ScorersMap; memoryConfig?: TaskMemoryConfig; }; type RetryPolicy = { backoff?: "fixed" | "linear" | "exponential"; // default "fixed" initialDelayMs?: number; // default 0 maxDelayMs?: number; multiplier?: number; jitter?: boolean; }; type CachePolicy = { by?: (ctx: Ctx) => unknown; version?: string; key?: string; ttlMs?: number; scope?: "run" | "workflow" | "global"; }; type AgentLike = { id?: string; tools?: Record; capabilities?: any; generate: (args: any) => Promise; }; type TaskMemoryConfig = { recall?: { namespace?: MemoryNamespace; query?: string; topK?: number }; remember?: { namespace?: MemoryNamespace; key?: string }; threadId?: string; }; type MemoryNamespace = { kind: MemoryNamespaceKind; id: string }; type MemoryNamespaceKind = "workflow" | "agent" | "user" | "global"; // ============================================================================= // Graph // ============================================================================= type GraphSnapshot = { readonly runId: string; readonly frameNo: number; readonly xml: XmlNode | null; readonly tasks: readonly TaskDescriptor[]; }; type XmlNode = XmlElement | XmlText; type XmlElement = { readonly kind: "element"; readonly tag: string; // "Workflow" | "Task" | "Sequence" | ... readonly props: Record; readonly children: readonly XmlNode[]; }; type XmlText = { readonly kind: "text"; readonly text: string }; // ============================================================================= // Events // ============================================================================= // // `SmithersEvent` is a discriminated union of every lifecycle event the runtime // emits. The full union (~50 variants) is documented separately to keep this // file usable as the everyday type reference. // // See: docs/reference/event-types.mdx (rendered) or /llms-events.txt (LLM fragment). type SmithersEvent = { type: string; runId: string; timestampMs: number } & Record; // (Each variant has additional fields per its `type`. See event-types.) // ============================================================================= // Component props // ============================================================================= type WorkflowProps = { name: string; cache?: boolean; children?: React.ReactNode; }; type OutputTarget = import("zod").ZodObject | { $inferSelect: any } | string; type DepsSpec = Record; type InferDeps = { [K in keyof D]: D[K] extends string ? unknown : InferOutputEntry; }; type TaskProps = { key?: string; id: string; output: Output; outputSchema?: import("zod").ZodObject; agent?: AgentLike | AgentLike[]; fallbackAgent?: AgentLike; dependsOn?: string[]; needs?: Record; deps?: D; skipIf?: boolean; needsApproval?: boolean; async?: boolean; // only with needsApproval timeoutMs?: number; heartbeatTimeoutMs?: number; noRetry?: boolean; retries?: number; // default Infinity (set 0 to disable) retryPolicy?: RetryPolicy; // default exponential, 1000ms, capped 5min continueOnFail?: boolean; cache?: CachePolicy; scorers?: ScorersMap; memory?: TaskMemoryConfig; allowTools?: string[]; // CLI-agent tool allowlist label?: string; meta?: Record; children: string | Row | (() => Row | Promise) | React.ReactNode | ((deps: InferDeps) => Row | React.ReactNode); }; type SequenceProps = { skipIf?: boolean; children?: React.ReactNode }; type ParallelProps = { id?: string; maxConcurrency?: number; skipIf?: boolean; children?: React.ReactNode }; type BranchProps = { if: boolean; then: React.ReactElement; else?: React.ReactElement | null; skipIf?: boolean }; type LoopProps = { id?: string; until?: boolean; maxIterations?: number; onMaxReached?: "fail" | "return-last"; // default "return-last" continueAsNewEvery?: number; skipIf?: boolean; children?: React.ReactNode; }; type RalphProps = LoopProps; // deprecated alias type ApprovalDecision = { approved: boolean; note: string | null; decidedBy: string | null; decidedAt: string | null }; type ApprovalSelection = { selected: string; notes: string | null }; type ApprovalRanking = { ranked: string[]; notes: string | null }; type ApprovalRequest = { title: string; summary?: string; metadata?: Record }; type ApprovalMode = "approve" | "select" | "rank"; type ApprovalOption = { key: string; label: string; summary?: string; metadata?: Record }; type ApprovalAutoApprove = { after?: number; condition?: ((ctx: any) => boolean) | (() => boolean); audit?: boolean; revertOn?: ((ctx: any) => boolean) | (() => boolean); }; type ApprovalProps = { id: string; mode?: ApprovalMode; options?: ApprovalOption[]; output: Output; outputSchema?: import("zod").ZodObject; request: ApprovalRequest; onDeny?: "fail" | "continue" | "skip"; allowedScopes?: string[]; allowedUsers?: string[]; autoApprove?: ApprovalAutoApprove; async?: boolean; dependsOn?: string[]; needs?: Record; skipIf?: boolean; timeoutMs?: number; heartbeatTimeoutMs?: number; retries?: number; retryPolicy?: RetryPolicy; continueOnFail?: boolean; cache?: CachePolicy; label?: string; meta?: Record; key?: string; children?: React.ReactNode; }; type SignalProps = import("zod").ZodObject> = { id: string; schema: S; correlationId?: string; timeoutMs?: number; onTimeout?: "fail" | "skip" | "continue"; async?: boolean; skipIf?: boolean; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; key?: string; children?: (data: import("zod").infer) => React.ReactNode; }; type WaitForEventProps = { id: string; event: string; correlationId?: string; output: OutputTarget; outputSchema?: import("zod").ZodObject; timeoutMs?: number; onTimeout?: "fail" | "skip" | "continue"; async?: boolean; skipIf?: boolean; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; key?: string; }; type TimerProps = { id: string; duration?: string; // e.g. "30s", "5m" until?: string | Date; // absolute timestamp every?: string; // periodic skipIf?: boolean; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; key?: string; }; type SagaStepDef = { id: string; action: React.ReactElement; compensation: React.ReactElement; label?: string }; type SagaProps = { id?: string; steps?: SagaStepDef[]; onFailure?: "compensate" | "compensate-and-fail" | "fail"; skipIf?: boolean; children?: React.ReactNode }; type SagaStepProps = { id: string; compensation: React.ReactElement; children: React.ReactElement }; type TryCatchFinallyProps = { id?: string; try: React.ReactElement; catch?: React.ReactElement | ((error: SmithersError) => React.ReactElement); catchErrors?: SmithersErrorCode[]; finally?: React.ReactElement; skipIf?: boolean; }; // Higher-level composites type PollerProps = { id?: string; check: AgentLike | ((...args: any[]) => any); checkOutput: OutputTarget; maxAttempts?: number; backoff?: "fixed" | "linear" | "exponential"; intervalMs?: number; onTimeout?: "fail" | "return-last"; skipIf?: boolean; children?: React.ReactNode; }; type ColumnDef = { name: string; agent: AgentLike; output: OutputTarget; prompt?: (ctx: { item: unknown; column: string }) => string; task?: Partial>; }; type KanbanProps = { id?: string; columns: ColumnDef[]; useTickets: () => Array<{ id: string; [key: string]: unknown }>; agents?: Record; maxConcurrency?: number; onComplete?: OutputTarget; until?: boolean; maxIterations?: number; skipIf?: boolean; children?: React.ReactNode | Record; }; // Sandbox type SandboxRuntime = "bubblewrap" | "docker" | "codeplane"; type SandboxVolumeMount = { host: string; container: string; readonly?: boolean }; type SandboxWorkspaceSpec = { name: string; snapshotId?: string; idleTimeoutSecs?: number; persistence?: "ephemeral" | "sticky"; }; type SandboxProps = { id: string; workflow?: (...args: any[]) => any; input?: unknown; output: OutputTarget; runtime?: SandboxRuntime; allowNetwork?: boolean; reviewDiffs?: boolean; autoAcceptDiffs?: boolean; image?: string; env?: Record; ports?: Array<{ host: number; container: number }>; volumes?: SandboxVolumeMount[]; memoryLimit?: string; cpuLimit?: string; command?: string; workspace?: SandboxWorkspaceSpec; skipIf?: boolean; timeoutMs?: number; heartbeatTimeoutMs?: number; retries?: number; retryPolicy?: RetryPolicy; continueOnFail?: boolean; cache?: CachePolicy; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; key?: string; children?: React.ReactNode; }; // ============================================================================= // Errors // ============================================================================= // // Every Smithers error is a SmithersError with a typed code. See the Errors page // for the full list of built-in codes. declare class SmithersError extends Error { readonly code: SmithersErrorCode; readonly summary: string; readonly docsUrl: string; readonly details?: Record; readonly cause?: unknown; } type SmithersErrorCode = KnownSmithersErrorCode | (string & {}); type KnownSmithersErrorCode = | "INVALID_INPUT" | "MISSING_INPUT" | "MISSING_INPUT_TABLE" | "RESUME_METADATA_MISMATCH" | "UNKNOWN_OUTPUT_SCHEMA" | "INVALID_OUTPUT" | "WORKTREE_CREATE_FAILED" | "VCS_NOT_FOUND" | "SNAPSHOT_NOT_FOUND" | "VCS_WORKSPACE_CREATE_FAILED" | "TASK_TIMEOUT" | "TASK_ABORTED" | "RUN_NOT_FOUND" | "NODE_NOT_FOUND" | "UI_COMMAND_FAILED" | "INVALID_EVENTS_OPTIONS" | "SANDBOX_BUNDLE_INVALID" | "SANDBOX_BUNDLE_TOO_LARGE" | "WORKFLOW_EXECUTION_FAILED" | "SANDBOX_EXECUTION_FAILED" | "TASK_HEARTBEAT_TIMEOUT" | "HEARTBEAT_PAYLOAD_TOO_LARGE" | "HEARTBEAT_PAYLOAD_NOT_JSON_SERIALIZABLE" | "RUN_CANCELLED" | "RUN_NOT_RESUMABLE" | "RUN_OWNER_ALIVE" | "RUN_STILL_RUNNING" | "RUN_RESUME_CLAIM_LOST" | "RUN_RESUME_CLAIM_FAILED" | "RUN_RESUME_ACTIVATION_FAILED" | "RUN_HIJACKED" | "CONTINUATION_STATE_TOO_LARGE" | "INVALID_CONTINUATION_STATE" | "RALPH_MAX_REACHED" | "SCHEDULER_ERROR" | "SESSION_ERROR" | "TASK_ID_REQUIRED" | "TASK_MISSING_OUTPUT" | "DUPLICATE_ID" | "NESTED_LOOP" | "WORKTREE_EMPTY_PATH" | "MDX_PRELOAD_INACTIVE" | "CONTEXT_OUTSIDE_WORKFLOW" | "MISSING_OUTPUT" | "DEP_NOT_SATISFIED" | "ASPECT_BUDGET_EXCEEDED" | "APPROVAL_OUTSIDE_TASK" | "APPROVAL_OPTIONS_REQUIRED" | "WORKFLOW_MISSING_DEFAULT" | "TOOL_PATH_INVALID" | "TOOL_PATH_ESCAPE" | "TOOL_FILE_TOO_LARGE" | "TOOL_CONTENT_TOO_LARGE" | "TOOL_PATCH_TOO_LARGE" | "TOOL_PATCH_FAILED" | "TOOL_NETWORK_DISABLED" | "TOOL_GIT_REMOTE_DISABLED" | "TOOL_COMMAND_FAILED" | "TOOL_GREP_FAILED" | "AGENT_CLI_ERROR" | "AGENT_RPC_FILE_ARGS" | "AGENT_BUILD_COMMAND" | "AGENT_DIAGNOSTIC_TIMEOUT" | "DB_MISSING_COLUMNS" | "DB_REQUIRES_BUN_SQLITE" | "DB_QUERY_FAILED" | "DB_WRITE_FAILED" | "STORAGE_ERROR" | "INTERNAL_ERROR" | "PROCESS_ABORTED" | "PROCESS_TIMEOUT" | "PROCESS_IDLE_TIMEOUT" | "PROCESS_SPAWN_FAILED" | "TASK_RUNTIME_UNAVAILABLE" | "SCHEMA_CHANGE_HOT" | "HOT_OVERLAY_FAILED" | "HOT_RELOAD_INVALID_MODULE" | "SCORER_FAILED" | "WORKFLOW_EXISTS" | "PROMPT_EXISTS" | "PROMPT_MDX_INVALID" | "TICKET_EXISTS" | "TICKET_NOT_FOUND" | "CLI_DB_NOT_FOUND" | "CLI_AGENT_UNSUPPORTED" | "PI_HTTP_ERROR" | "EXTERNAL_BUILD_FAILED" | "SCHEMA_DISCOVERY_FAILED" | "OPENAPI_SPEC_LOAD_FAILED" | "OPENAPI_OPERATION_NOT_FOUND" | "OPENAPI_TOOL_EXECUTION_FAILED"; // ============================================================================= // Server // ============================================================================= type ServerOptions = { port?: number; db?: unknown; authToken?: string; maxBodyBytes?: number; rootDir?: string; allowNetwork?: boolean; headersTimeout?: number; requestTimeout?: number; }; type ServeOptions = { workflow: SmithersWorkflow; adapter: any; runId: string; abort: AbortController; authToken?: string; metrics?: boolean; }; type GatewayTokenGrant = { role: string; scopes: string[]; userId?: string }; type GatewayAuthConfig = | { mode: "token"; tokens: Record } | { mode: "jwt"; issuer: string; audience: string | string[]; secret: string; scopesClaim?: string; roleClaim?: string; userClaim?: string; defaultRole?: string; defaultScopes?: string[]; clockSkewSeconds?: number } | { mode: "trusted-proxy"; trustedHeaders?: string[]; allowedOrigins?: string[]; defaultRole?: string; defaultScopes?: string[] }; type GatewayOptions = { protocol?: number; features?: string[]; heartbeatMs?: number; auth?: GatewayAuthConfig; defaults?: { cliAgentTools?: "all" | "explicit-only" }; maxBodyBytes?: number; maxPayload?: number; maxConnections?: number; eventWindowSize?: number; headersTimeout?: number; requestTimeout?: number; }; // ============================================================================= // Scorers (smithers-orchestrator/scorers) // ============================================================================= type ScoreResult = { score: number; reason?: string; meta?: Record }; type ScorerInput = { input: unknown; output: unknown; groundTruth?: unknown; context?: unknown; latencyMs?: number; outputSchema?: import("zod").ZodObject }; type ScorerFn = (input: ScorerInput) => Promise; type Scorer = { id: string; name: string; description: string; score: ScorerFn }; type SamplingConfig = | { type: "all" } | { type: "ratio"; rate: number } | { type: "none" }; type ScorerBinding = { scorer: Scorer; sampling?: SamplingConfig }; type ScorersMap = Record; type LlmJudgeConfig = { model: string; systemPrompt?: string; temperature?: number; maxTokens?: number }; type CreateScorerConfig = { id: string; name: string; description: string; model: string; criteria: string; examples?: Array<{ input: unknown; output: unknown; score: number; explanation: string }>; }; // ============================================================================= // Memory (smithers-orchestrator/memory) // ============================================================================= type MemoryFact = { key: string; value: unknown; metadata?: Record; createdAt?: number; expiresAt?: number }; type MemoryMessage = { role: "user" | "assistant" | "system"; content: string; createdAt?: number }; type MemoryThread = { id: string; messages: MemoryMessage[] }; type MemoryStore = { set(namespace: MemoryNamespace, key: string, value: unknown): Promise; get(namespace: MemoryNamespace, key: string): Promise; recall(namespace: MemoryNamespace, query: string, topK?: number): Promise; saveMessage(threadId: string, message: MemoryMessage): Promise; getMessages(threadId: string): Promise; }; // ============================================================================= // OpenAPI tools (smithers-orchestrator/openapi) // ============================================================================= type OpenApiAuth = | { type: "none" } | { type: "apiKey"; name: string; in: "header" | "query" | "cookie"; value: string } | { type: "bearer"; token: string } | { type: "basicAuth"; username: string; password: string }; type OpenApiToolsOptions = { spec: unknown | string; // OpenAPI spec object or path/URL auth?: OpenApiAuth; baseUrl?: string; includeOperations?: string[]; excludeOperations?: string[]; }; // ============================================================================= // CreateSmithers (createSmithers(...) return) // ============================================================================= type CreateSmithersApi = { Workflow: (props: WorkflowProps) => React.ReactElement; Approval: (props: ApprovalProps) => React.ReactElement; Task: (props: TaskProps) => React.ReactElement; Sequence: (props: SequenceProps) => React.ReactElement; Parallel: (props: ParallelProps) => React.ReactElement; Branch: (props: BranchProps) => React.ReactElement; Loop: (props: LoopProps) => React.ReactElement; Ralph: (props: LoopProps) => React.ReactElement; Worktree: any; Sandbox: (props: SandboxProps) => React.ReactElement; Signal: >(props: SignalProps) => React.ReactElement; Timer: (props: TimerProps) => React.ReactElement; ContinueAsNew: any; continueAsNew: any; useCtx: () => SmithersCtx; smithers: (build: (ctx: SmithersCtx) => React.ReactElement, opts?: SmithersWorkflowOptions) => SmithersWorkflow; db: any; tables: Record; outputs: Record; }; // ============================================================================= // Observability (smithers-orchestrator/observability) // ============================================================================= type SmithersLogFormat = "json" | "pretty"; type SmithersObservabilityService = { emit(event: SmithersEvent): void | Promise }; type SmithersObservabilityOptions = { service?: SmithersObservabilityService; logFormat?: SmithersLogFormat }; type ResolvedSmithersObservabilityOptions = SmithersObservabilityOptions & { metricsPort?: number; metricsPath?: string }; ``` For canonical, machine-checked types, install `smithers-orchestrator` and use editor go-to-definition. For runtime errors, see [Errors](/reference/errors). --- ## Error Reference > Exhaustive Smithers error codes, typed error helpers, and HTTP API error responses. ```ts import { ERROR_REFERENCE_URL, SmithersErrorInstance, errorToJson, getSmithersErrorDefinition, getSmithersErrorDocsUrl, isKnownSmithersErrorCode, isSmithersError, knownSmithersErrorCodes, } from "smithers-orchestrator"; import type { KnownSmithersErrorCode, SmithersError, SmithersErrorCode, } from "smithers-orchestrator"; ``` Every built-in `SmithersErrorInstance` carries three pieces of documentation metadata: | Field | Meaning | |---|---| | `message` | Human-readable message with a docs link appended. | | `summary` | Raw message without the docs suffix. | | `docsUrl` | Reference URL for Smithers errors. | Use `KnownSmithersErrorCode` for an exhaustive switch over built-in Smithers codes. `SmithersErrorCode` includes the `(string & {})` escape hatch for user-defined custom codes. | Export | Kind | Description | |---|---|---| | `SmithersErrorInstance` | class | Runtime error class used throughout Smithers internals. | | `isSmithersError(err)` | function | Type guard for values carrying a Smithers-style `code`. | | `isKnownSmithersErrorCode(code)` | function | Narrows a string to the built-in exhaustive error-code union. | | `knownSmithersErrorCodes` | value | Array of every built-in Smithers error code documented on this page. | | `getSmithersErrorDocsUrl(code)` | function | Returns the docs URL appended to built-in error messages. | | `getSmithersErrorDefinition(code)` | function | Returns category, description, and details metadata for known codes. | | `errorToJson(err)` | function | Serializes `message`, `summary`, `docsUrl`, `code`, `details`, `cause`, and `stack`. | | `ERROR_REFERENCE_URL` | value | Base docs URL for Smithers runtime errors. | | `KnownSmithersErrorCode` | type | Exact built-in Smithers code union. | | `SmithersErrorCode` | type | Built-in codes plus the custom string escape hatch. | | `SmithersError` | type | Public typed shape for serialized Smithers errors. | ```ts try { await runWorkflow(workflow, { input: {} }); } catch (err) { if (isSmithersError(err) && isKnownSmithersErrorCode(err.code)) { switch (err.code) { case "INVALID_INPUT": console.error("Bad input:", err.summary); break; case "AGENT_CLI_ERROR": console.error("Agent failed:", err.summary); break; default: console.error(`[${err.code}] ${err.summary}`); } console.error("Docs:", err.docsUrl); } } ``` ## Engine | Code | When | Details | |---|---|---| | `INVALID_INPUT` | Workflow input fails validation or the runtime receives a non-object input payload. | -- | | `MISSING_INPUT` | A resume run references an input row that is missing from the database. | -- | | `MISSING_INPUT_TABLE` | The workflow schema does not expose the expected input table during resume or hydration. | -- | | `RESUME_METADATA_MISMATCH` | Stored run metadata no longer matches the workflow being resumed. | -- | | `UNKNOWN_OUTPUT_SCHEMA` | A task references an output table that is not present in the schema registry. | -- | | `INVALID_OUTPUT` | Agent output cannot be parsed or validated against the declared output schema. | -- | | `WORKTREE_CREATE_FAILED` | Smithers fails to create or hydrate a git or jj worktree for a task. | `{ worktreePath, vcsType, branch? }` | | `VCS_NOT_FOUND` | No supported git or jj repository root can be found for the workflow. | `{ rootDir }` | | `SNAPSHOT_NOT_FOUND` | A requested time-travel snapshot or frame does not exist. | `{ runId, frameNo }` | | `VCS_WORKSPACE_CREATE_FAILED` | Smithers fails to materialize a jj workspace for time-travel or replay. | `{ runId, frameNo, vcsPointer, workspacePath }` | | `TASK_TIMEOUT` | A task compute callback exceeds its configured timeout. | `{ nodeId, attempt, timeoutMs }` | | `TASK_ABORTED` | A running task is aborted through an AbortSignal or shutdown path. | -- | | `RUN_NOT_FOUND` | A CLI or engine command references a run ID that does not exist in the database. | `{ runId }` | | `NODE_NOT_FOUND` | A CLI command references a node ID that does not exist for the given run. | `{ runId, nodeId }` | | `UI_COMMAND_FAILED` | The smithers ui command fails to open the browser or probe the server. | `{ url }` | | `INVALID_EVENTS_OPTIONS` | The smithers events command receives invalid filter options. | -- | | `SANDBOX_BUNDLE_INVALID` | A sandbox bundle fails validation (missing README, invalid manifest, etc.). | `{ bundlePath }` | | `SANDBOX_BUNDLE_TOO_LARGE` | A sandbox bundle exceeds the maximum allowed size. | `{ bundlePath, maxBytes }` | | `WORKFLOW_EXECUTION_FAILED` | A child or builder workflow exits unsuccessfully without surfacing a typed error payload. | `{ status }` | | `SANDBOX_EXECUTION_FAILED` | Sandbox setup or execution fails before a more specific sandbox error can be emitted. | `{ sandboxId, runId?, maxConcurrent?, activeSandboxCount? }` | | `TASK_HEARTBEAT_TIMEOUT` | A task has not heartbeated within its configured timeout. | `{ nodeId, iteration, attempt, timeoutMs, staleForMs, lastHeartbeatAtMs }` | | `HEARTBEAT_PAYLOAD_TOO_LARGE` | A task heartbeat payload exceeds the maximum allowed size. | `{ nodeId, sizeBytes, maxBytes }` | | `HEARTBEAT_PAYLOAD_NOT_JSON_SERIALIZABLE` | A task heartbeat payload cannot be serialized to JSON. | `{ nodeId }` | | `RUN_CANCELLED` | A run is cancelled while runtime work is still active. | `{ runId }` | | `RUN_NOT_RESUMABLE` | A resume request targets a run state that cannot be resumed. | `{ runId, status }` | | `RUN_OWNER_ALIVE` | Resume recovery is skipped because the previous runtime owner is still heartbeating. | `{ runId, runtimeOwnerId }` | | `RUN_STILL_RUNNING` | A recovery or resume operation finds a run that is still active. | `{ runId }` | | `RUN_RESUME_CLAIM_LOST` | A runtime loses the resume claim before it can update the run. | `{ runId, runtimeOwnerId }` | | `RUN_RESUME_CLAIM_FAILED` | A runtime cannot claim a stale run for resume. | `{ runId, runtimeOwnerId }` | | `RUN_RESUME_ACTIVATION_FAILED` | A claimed run cannot be moved back into active execution. | `{ runId, runtimeOwnerId }` | | `RUN_HIJACKED` | A run is interrupted because another runtime hijacked execution. | `{ runId, hijackTarget }` | | `CONTINUATION_STATE_TOO_LARGE` | Continue-as-new state exceeds the configured serialized size limit. | `{ runId, sizeBytes, maxBytes }` | | `INVALID_CONTINUATION_STATE` | Continue-as-new state cannot be parsed or applied. | -- | | `RALPH_MAX_REACHED` | A Ralph loop reaches maxIterations with fail-on-max behavior. | `{ ralphId, maxIterations }` | | `SCHEDULER_ERROR` | The scheduler cannot produce a valid execution decision. | -- | | `SESSION_ERROR` | The workflow session state machine reaches an invalid or failed state. | -- | ## Components | Code | When | Details | |---|---|---| | `TASK_ID_REQUIRED` | `` is missing a valid string id. | -- | | `TASK_MISSING_OUTPUT` | `` is missing its output prop. | `{ nodeId }` | | `DUPLICATE_ID` | Two nodes with the same runtime id are mounted in one workflow graph. | `{ kind, id }` | | `NESTED_LOOP` | `` or `` is nested inside another loop construct that Smithers does not support. | -- | | `WORKTREE_EMPTY_PATH` | `` is mounted with an empty path. | -- | | `MDX_PRELOAD_INACTIVE` | A prompt object is rendered without the MDX preload layer being active. | -- | | `CONTEXT_OUTSIDE_WORKFLOW` | Workflow context access happens outside an active Smithers workflow render. | -- | | `MISSING_OUTPUT` | Code calls `ctx.output()` for a node result that does not exist. | `{ nodeId, iteration }` | | `DEP_NOT_SATISFIED` | A typed dep on `` references an upstream output that has not been produced yet. | `{ taskId, depKey, resolvedNodeId }` | | `ASPECT_BUDGET_EXCEEDED` | An Aspects budget (tokens, latency, or cost) has been exceeded. | `{ kind, limit, current }` | | `APPROVAL_OUTSIDE_TASK` | `` is resolved outside the active task runtime. | -- | | `APPROVAL_OPTIONS_REQUIRED` | An approval mode that requires explicit options is missing them. | -- | | `WORKFLOW_MISSING_DEFAULT` | A workflow module does not export a default Smithers workflow. | -- | ## Tools | Code | When | Details | |---|---|---| | `TOOL_PATH_INVALID` | A filesystem tool receives a non-string path. | -- | | `TOOL_PATH_ESCAPE` | A filesystem tool resolves a path outside the sandbox root, including through symlinks. | -- | | `TOOL_FILE_TOO_LARGE` | A read or edit operation exceeds the configured file size limit. | -- | | `TOOL_CONTENT_TOO_LARGE` | A write operation exceeds the configured content size limit. | -- | | `TOOL_PATCH_TOO_LARGE` | An edit patch exceeds the configured patch size limit. | -- | | `TOOL_PATCH_FAILED` | A unified diff patch cannot be applied to the target file. | -- | | `TOOL_NETWORK_DISABLED` | The bash tool tries to access the network while network access is disabled. | -- | | `TOOL_GIT_REMOTE_DISABLED` | The bash tool attempts a remote git operation while network access is disabled. | -- | | `TOOL_COMMAND_FAILED` | A bash tool command exits with a non-zero status. | -- | | `TOOL_GREP_FAILED` | The grep tool fails with an rg execution error. | -- | ## Agents | Code | When | Details | |---|---|---| | `AGENT_CLI_ERROR` | A CLI-backed agent exits unsuccessfully, streams an explicit error, or its RPC transport fails. | -- | | `AGENT_RPC_FILE_ARGS` | Pi RPC mode is used with file arguments that the transport does not support. | -- | | `AGENT_BUILD_COMMAND` | An agent implementation forbids `buildCommand()` because it uses a custom `generate()` transport. | -- | | `AGENT_DIAGNOSTIC_TIMEOUT` | An internal agent diagnostic check exceeds the per-check timeout budget. | -- | ## Database | Code | When | Details | |---|---|---| | `DB_MISSING_COLUMNS` | A table used by Smithers does not expose required columns such as `runId` or `nodeId`. | -- | | `DB_REQUIRES_BUN_SQLITE` | The database adapter is not backed by a Bun SQLite client with `exec()`. | -- | | `DB_QUERY_FAILED` | A database read query throws or rejects while running inside an Effect. | -- | | `DB_WRITE_FAILED` | A database write or migration fails, including after SQLite retry exhaustion. | -- | | `STORAGE_ERROR` | A storage service operation fails before surfacing a more specific database code. | -- | ## Effect / Runtime | Code | When | Details | |---|---|---| | `INTERNAL_ERROR` | An unexpected internal exception crossed a runtime boundary without a more specific Smithers code. | -- | | `PROCESS_ABORTED` | A spawned child process is aborted by signal or shutdown. | `{ command, args, cwd }` | | `PROCESS_TIMEOUT` | A spawned child process exceeds its total timeout. | `{ command, args, cwd, timeoutMs }` | | `PROCESS_IDLE_TIMEOUT` | A spawned child process stops producing output longer than its idle timeout. | `{ command, args, cwd, idleTimeoutMs }` | | `PROCESS_SPAWN_FAILED` | The runtime cannot spawn the requested child process. | `{ command, args, cwd }` | | `TASK_RUNTIME_UNAVAILABLE` | Builder task runtime APIs are accessed outside an executing step. | -- | ## Hot Reload | Code | When | Details | |---|---|---| | `SCHEMA_CHANGE_HOT` | Hot reload detects a schema change that requires a full restart. | -- | | `HOT_OVERLAY_FAILED` | Building or cleaning the generated hot-reload overlay fails. | -- | | `HOT_RELOAD_INVALID_MODULE` | A hot-reloaded workflow module does not export a valid default workflow build. | -- | ## Scorers | Code | When | Details | |---|---|---| | `SCORER_FAILED` | A scorer throws or rejects while Smithers is evaluating a result. | -- | ## CLI | Code | When | Details | |---|---|---| | `WORKFLOW_EXISTS` | The workflow creation CLI refuses to overwrite an existing workflow file. | -- | | `PROMPT_EXISTS` | The prompt creation CLI refuses to overwrite an existing prompt file. | -- | | `PROMPT_MDX_INVALID` | An MDX prompt file does not export a valid default component. | -- | | `TICKET_EXISTS` | The ticket creation CLI refuses to overwrite an existing ticket file. | -- | | `TICKET_NOT_FOUND` | A CLI command references a ticket file that does not exist. | -- | | `CLI_DB_NOT_FOUND` | A CLI command cannot find a nearby `smithers.db` file. | -- | | `CLI_AGENT_UNSUPPORTED` | The ask command selects an agent integration that Smithers does not support in that mode. | -- | ## Integrations | Code | When | Details | |---|---|---| | `PI_HTTP_ERROR` | The Pi or server integration receives a non-success HTTP response from Smithers. | -- | | `EXTERNAL_BUILD_FAILED` | An external workflow host fails to build a Smithers HostNode payload. | `{ scriptPath, error?, exitCode?, stderr?, stdout? }` | | `SCHEMA_DISCOVERY_FAILED` | External workflow schema discovery fails or returns invalid output. | `{ scriptPath, error?, exitCode?, stderr? }` | | `OPENAPI_SPEC_LOAD_FAILED` | An OpenAPI spec cannot be loaded or parsed. | -- | | `OPENAPI_OPERATION_NOT_FOUND` | The requested operationId does not exist in the OpenAPI spec. | -- | | `OPENAPI_TOOL_EXECUTION_FAILED` | An OpenAPI tool call fails during HTTP execution. | -- | ## HTTP API Errors JSON response codes, not `SmithersErrorInstance` objects. | Code | Status | When | |---|---|---| | `INVALID_REQUEST` | 400 | Invalid request body or query params | | `PAYLOAD_TOO_LARGE` | 413 | Body exceeds `maxBodyBytes` | | `INVALID_JSON` | 400 | Body not valid JSON | | `SERVER_ERROR` | 500 | Unexpected server error | | `UNAUTHORIZED` | 401 | Missing or invalid auth token | | `WORKFLOW_PATH_OUTSIDE_ROOT` | 400 | Workflow path outside server root | | `RUN_ID_REQUIRED` | 400 | `runId` required when `resume: true` | | `RUN_ALREADY_EXISTS` | 409 | Run ID already exists | | `RUN_NOT_FOUND` | 404 | No run with given ID | | `RUN_NOT_ACTIVE` | 409 | Run not active (cannot cancel) | | `NOT_FOUND` | 404 | Route or resource not found | | `DB_NOT_CONFIGURED` | 400 | Server database not configured | ## Related - [Error Handling Guide](/guides/error-handling) - [Debugging Guide](/guides/debugging) - [Troubleshooting](/guides/troubleshooting) --- ## Package Configuration > Reference for the smithers-orchestrator package exports, TypeScript configuration, and Bun preload setup. Build and package configuration shipped with `smithers-orchestrator`. Use it when setting up a new project, debugging import resolution, or understanding why your `tsconfig.json` needs specific options. ## Binary Use `bunx smithers-orchestrator ` for CLI commands. The repository root keeps a private development bin that points at `apps/cli/src/index.js`; application code should import from the package exports below. ## Subpath Exports Use the subpath form to import only the surface you need. | Import path | Entry file | Purpose | |---|---|---| | `smithers-orchestrator` | `./src/index.js` | Core API: `createSmithers`, components, `runWorkflow`, `renderMdx`, errors | | `smithers-orchestrator/gateway` | `./src/gateway.js` | Gateway client for remote workflow coordination | | `smithers-orchestrator/jsx-runtime` | `./src/jsx-runtime.js` | JSX runtime (auto-resolved by `jsxImportSource`) | | `smithers-orchestrator/jsx-dev-runtime` | `./src/jsx-runtime.js` | JSX dev runtime (auto-resolved in dev mode) | | `smithers-orchestrator/tools` | `./src/tools.js` | Tool sandbox: `defineTool`, `read`, `grep`, `bash`, `edit`, `write` | | `smithers-orchestrator/server` | `./src/server.js` | HTTP server for run management and event streaming | | `smithers-orchestrator/observability` | `./src/observability.js` | OpenTelemetry traces, metrics, and Prometheus integration | | `smithers-orchestrator/pi-plugin` | `./src/pi-plugin.js` | PI CLI agent plugin | | `smithers-orchestrator/pi-extension` | `./src/pi-extension.js` | PI extension UI bridge | | `smithers-orchestrator/mdx-plugin` | `./src/mdx-plugin.js` | Bun preload plugin for `.mdx` imports | | `smithers-orchestrator/dom/renderer` | `./src/dom/renderer.js` | Internal renderer (advanced use) | | `smithers-orchestrator/serve` | `./src/serve.js` | Single-workflow HTTP server via `createServeApp` | | `smithers-orchestrator/scorers` | `./src/scorers.js` | Eval scorers: `createScorer`, `llmJudge`, `aggregate` | | `smithers-orchestrator/memory` | `./src/memory.js` | Cross-run memory storage and recall | | `smithers-orchestrator/openapi` | `./src/openapi.js` | Generate AI SDK tools from OpenAPI specs | ### Usage ```ts // Core API import { createSmithers, runWorkflow } from "smithers-orchestrator"; // Tools import { defineTool, bash, read, write } from "smithers-orchestrator/tools"; // Scorers import { createScorer, llmJudge } from "smithers-orchestrator/scorers"; // MDX plugin (in preload.ts) import { mdxPlugin } from "smithers-orchestrator/mdx-plugin"; ``` ## TypeScript Configuration ### JSX Import Source ```json { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "smithers-orchestrator" } } ``` This tells TypeScript to resolve JSX transforms from `smithers-orchestrator/jsx-runtime` instead of `react/jsx-runtime`. The Smithers JSX runtime re-exports React's runtime, so component behavior is identical -- this setting enables proper type resolution for Smithers workflow components. See [JSX Installation](/jsx/installation) for the complete TypeScript setup. ### Path Aliases When developing inside the `smithers-orchestrator` monorepo, the root `tsconfig.json` defines path aliases so source imports resolve without a build step: ```json "paths": { "smithers-orchestrator": ["./packages/smithers/src/index.js"], "smithers-orchestrator/jsx-runtime": ["./packages/smithers/src/jsx-runtime.js"], "smithers-orchestrator/jsx-dev-runtime": ["./packages/smithers/src/jsx-runtime.js"], "smithers-orchestrator/tools": ["./packages/smithers/src/tools.js"], "smithers-orchestrator/*": [ "./packages/smithers/src/*.js", "./packages/smithers/src/*/index.js" ], "smithers-orchestrator/scorers": ["./packages/scorers/src/index.js"] } ``` The root package is a private `smithers-monorepo`; `smithers-orchestrator` resolves to `packages/smithers`. **End users do not need path aliases** — only framework developers do. Installing `smithers-orchestrator` as a dependency lets Node/Bun module resolution handle import paths automatically. ### Local Type Root Shims ```json "typeRoots": ["./src/types", "./node_modules/@types"] ``` The `./src/types` directory contains ambient type declarations that fill gaps in third-party packages. One shim ships today: - `react-dom-server.d.ts` -- Declares the `react-dom/server` module so TypeScript doesn't error when server-side rendering types are referenced. End users should add `@types/react-dom` to `devDependencies` instead of relying on this shim. ## Bun Configuration ### Runtime Preload ```toml # bunfig.toml preload = ["./preload.ts"] ``` The preload script registers the MDX esbuild plugin with Bun's bundler so `.mdx` files can be imported as JSX components at runtime. See [MDX Prompts](/guides/mdx-prompts) for details. ### Test Configuration ```toml [test] root = "./tests" preload = ["./preload.ts"] ``` | Key | Value | Purpose | |---|---|---| | `root` | `./tests` | Bun discovers test files from this directory instead of scanning the entire project | | `preload` | `["./preload.ts"]` | Registers the MDX plugin for test files so `.mdx` imports work in tests | The test preload is separate from the runtime preload. Both point to the same file, but Bun's `[test]` section only applies when running `bun test`. Without it, tests that import `.mdx` files fail with a module resolution error. ## npm Scripts Defined in the root `package.json` for development: | Script | Command | Purpose | |---|---|---| | `typecheck` | `tsc --noEmit` | Type-check the `src/` and `tests/` trees against `tsconfig.json` | | `typecheck:examples` | `tsc -p examples/tsconfig.json --noEmit` | Type-check example files against a separate config that maps `smithers-orchestrator` to `examples-entry.js` | | `lint` | `oxlint ...` | Lint source, test, and CLI code with oxlint | | `test` | `bash ./scripts/run-all-tests.sh` | Run the full test suite | | `e2e` | `playwright test` | Run Playwright end-to-end tests against the docs site and integration surfaces | | `docs` | `cd docs && bunx mintlify dev` | Start the Mintlify docs dev server for local preview | ### For end-user projects When scaffolding your own project (with `smithers init` or manually), add a typecheck script: ```json { "scripts": { "typecheck": "tsc --noEmit" } } ``` See [Production Project Structure](/guides/project-structure) for a complete user-project `package.json` example. --- ## VCS Helper Reference > Public JJ helper APIs exported by smithers-orchestrator for repo detection, snapshot inspection, and workspace management. Smithers exports a small JJ helper surface for applications that inspect or manage Jujutsu state directly. Lightweight by design: - every helper accepts an optional `cwd` to target a specific repository - spawn failures are normalized instead of throwing — safe to call even when `jj` is not installed - workspace helpers try a few command shapes to tolerate JJ version drift ## Import ```ts import { runJj, getJjPointer, revertToJjPointer, isJjRepo, workspaceAdd, workspaceList, workspaceClose, } from "smithers-orchestrator"; ``` ## `runJj(args, opts?)` Run an arbitrary `jj` command and capture its output. ```ts const result = await runJj(["status"], { cwd: "/path/to/repo" }); ``` ```ts type RunJjOptions = { cwd?: string; }; type RunJjResult = { code: number; stdout: string; stderr: string; }; ``` Notes: - returns `{ code: 127, stdout: "", stderr: "..." }` when `jj` cannot be started - does not throw for ordinary process failures - a raw escape hatch beyond the higher-level helpers below ## `getJjPointer(cwd?)` Return the current workspace `change_id` for `@`, or `null` when JJ is unavailable or the current directory is not a JJ repo. ```ts const pointer = await getJjPointer("/path/to/repo"); ``` ```ts function getJjPointer(cwd?: string): Promise; ``` Smithers uses the same pointer model internally for revert support and cache invalidation. ## `revertToJjPointer(pointer, cwd?)` Restore the working copy from a previously recorded JJ pointer. ```ts const result = await revertToJjPointer("zqkopwvn", "/path/to/repo"); ``` ```ts type JjRevertResult = | { success: true } | { success: false; error?: string }; ``` This helper wraps `jj restore --from `. ## `isJjRepo(cwd?)` Detect whether a directory is a readable JJ repository. ```ts const enabled = await isJjRepo("/path/to/repo"); ``` ```ts function isJjRepo(cwd?: string): Promise; ``` Use this before showing JJ-specific UI or attempting a revert flow. ## `workspaceAdd(name, path, opts?)` Create a JJ workspace with a friendly name at a target filesystem path. ```ts const result = await workspaceAdd("feature-auth", "/tmp/wt-feature-auth", { cwd: "/path/to/repo", atRev: "@", }); ``` ```ts type WorkspaceAddOptions = { cwd?: string; atRev?: string; }; type WorkspaceResult = | { success: true } | { success: false; error?: string }; ``` Behavior notes: - removes an existing workspace with the same name before retrying - recreates the target directory if needed - tries multiple `jj workspace add` syntaxes to work across JJ versions ## `workspaceList(cwd?)` List known workspaces for the current JJ repo. ```ts const workspaces = await workspaceList("/path/to/repo"); ``` ```ts type WorkspaceInfo = { name: string; path: string | null; selected: boolean; }; ``` Prefers template output when supported, falls back to parsing the human-readable `jj workspace list` output. ## `workspaceClose(name, opts?)` Forget a JJ workspace by name. ```ts const result = await workspaceClose("feature-auth", { cwd: "/path/to/repo", }); ``` ```ts function workspaceClose( name: string, opts?: { cwd?: string }, ): Promise; ``` This wraps `jj workspace forget `. ## When To Use These Helpers Use these helpers when your application needs to: - show whether JJ-backed revert is available - record or inspect a pointer outside the Smithers engine - manage JJ workspaces directly from an app or integration layer For workflow-level revert behavior, prefer the runtime and CLI docs: - [VCS Integration](/guides/vcs) - [CLI Reference](/cli/overview) - [Revert](/runtime/revert) --- ## runWorkflow > Programmatic entry point. Equivalent to `bunx smithers-orchestrator up`. ```ts import { runWorkflow } from "smithers-orchestrator"; const result = await runWorkflow(workflow, { input: { task: "fix bug" }, }); result.runId; // string result.status; // "finished" | "failed" | "cancelled" | "continued" | "waiting-approval" | "waiting-event" | "waiting-timer" result.output; // populated only if your schema has a key literally named `output` result.error; // serialized SmithersError on failure ``` Signature: ```ts function runWorkflow( workflow: SmithersWorkflow, opts: RunOptions, // see Types ): Promise; ``` Both `RunOptions` and `RunResult` are defined in [Types](/reference/types). ## Resume Pass the original `runId` plus `resume: true`. State loads from SQLite, completed tasks are skipped, in-progress attempts older than 15 minutes are abandoned and retried. ```ts await runWorkflow(workflow, { input: {}, runId: "my-run-123", resume: true }); ``` The original input row is loaded from the DB; pass `{}` for `input`. The workflow file hash and VCS revision must match the original run. ## Cancel via AbortSignal ```ts const controller = new AbortController(); setTimeout(() => controller.abort(), 5 * 60 * 1000); const result = await runWorkflow(workflow, { input: {...}, signal: controller.signal }); // result.status === "cancelled" ``` All in-flight attempts are marked cancelled and `NodeCancelled` events are emitted. ## Hijack handoff If a CLI hijack happens mid-run (`bunx smithers-orchestrator hijack `), the run ends `"cancelled"` and the latest attempt metadata stores `hijackHandoff`. On `resume: true`, Smithers waits for a safe handoff point and continues with the persisted CLI session id (Claude/Codex/Gemini/Pi/Kimi/Forge/Amp) or the persisted message history (SDK agents). ## `result.output` Populated only when the schema passed to `createSmithers()` has a key literally named `output`. Other schema rows live in their own SQLite tables — query them directly: ```ts import { Database } from "bun:sqlite"; const db = new Database("smithers.db", { readonly: true }); const rows = db.query("SELECT * FROM page WHERE run_id = ? ORDER BY iteration DESC").all(result.runId); ``` ## Notes - On macOS, `runWorkflow` acquires a `caffeinate` lock to prevent idle sleep; released on completion. No-op elsewhere. - Set `SMITHERS_DEBUG=1` to print engine errors to stderr. - For lifecycle events, pass `onProgress` (see [Events](/runtime/events)). --- ## renderFrame > Render a workflow tree to a GraphSnapshot without executing. ```ts import { renderFrame } from "smithers-orchestrator"; const snap = await renderFrame(workflow, { runId: "preview", iteration: 0, input: { task: "preview" }, outputs: {}, }); snap.frameNo; // 0 snap.tasks; // TaskDescriptor[] snap.xml; // XmlNode tree (see Types) ``` `TaskDescriptor` and `GraphSnapshot` are defined in [Types](/reference/types). Same shape the runtime extracts on every render frame; `renderFrame` doesn't execute or persist. `outputs` lets you simulate completed upstream tasks: ```ts const snap = await renderFrame(workflow, { runId: "sim", iteration: 0, input: { x: 1 }, outputs: { analyze: [{ runId: "sim", nodeId: "analyze", iteration: 0, summary: "..." }], }, }); ``` CLI equivalent: ```bash bunx smithers-orchestrator graph workflow.tsx --input '{"task":"preview"}' ``` --- ## Revert to Attempt > Restore the workspace to a previous task attempt's filesystem state via JJ. Each successful task attempt captures the current [JJ](https://jj-vcs.github.io/jj/) change ID into `_smithers_attempts.jj_pointer`. `revert` restores the workspace to that state and trims any DB frames recorded after the attempt started. ```bash bunx smithers-orchestrator revert workflow.tsx \ --run-id --node-id [--attempt N=1] [--iteration N=0] ``` Revert restores files only — it doesn't alter JJ history; the restoration creates a new change on top of the current working copy. ## Requirements - JJ in `PATH` (`brew install jj` or `cargo install jj-cli`) - Workspace is a JJ repository (`jj git init` or `jj init`) - The target attempt was completed when JJ was available (otherwise no pointer was captured) ## Programmatic ```ts import { revertToAttempt } from "smithers-orchestrator"; await revertToAttempt({ runId: "abc123", nodeId: "implement", attempt: 2, iteration: 0, }); ``` `revertToAttempt` throws if the attempt has no recorded JJ pointer. --- ## Terminal UI (TUI) > Chat-first terminal control plane for orchestrating, monitoring, and steering Smithers workflows. ```bash bunx smithers-orchestrator tui ``` A chat-first orchestration console. The default surface is a unified activity feed and composer, not a dashboard. Monitoring, approvals, and telemetry are visible without leaving the main workspace. ## Product stance Smithers TUI complements the CLI. Every meaningful UI action maps to a Smithers API or CLI operation, preserving the same durable, scriptable mental model. The TUI is **not** a replacement for Claude Code, Codex CLI, Gemini CLI, or Amp. It is not a direct-edit harness by default. It is the orchestration layer above harnesses and API providers. ## Shell layout The shell has four regions: workspace rail, activity feed, inspector, and composer. ```text Smithers repo: api workspace: auth-fix profile: Claude+SDK mode: operator 2 runs 1 approval Ctrl+O actions | auth refactor [CC] .1 12:41 You Build a reusable Smithers workflow for auth fixes. docs sync [AI] v 12:41 Smithers Plan: ! pr review [SM] A1 - inspect existing .smithers/workflows incident triage [GM] x - factor shared retry and review steps - launch #auth-fix over current diff 12:42 Run auth-fix a93f running validate -> patch 3/7 12:43 Tool smithers.workflows.read .smithers/workflows/review-pr.tsx 18ms 12:44 Approval Push generated patch to workspace branch? [Enter] open [a] approve [d] deny 12:45 Artifact .smithers/workflows/auth-fix.tsx [#auth-fix] [@src/auth.ts] [@README.md] Build it to be reusable, not one-off. budget 18k ctx Enter send Alt+Enter queue Ctrl+G editor ``` ### Adaptive layouts | Width | Behavior | | --- | --- | | >= 140 cols | Full three-column layout. Left rail 24 cols, right inspector 36--42 cols. | | 100--139 cols | Inspector narrows to 28--32 cols. Less metadata in workspace rows. | | 80--99 cols | Inspector becomes a toggleable overlay. Feed is the dominant pane. | | < 80 cols | Single-pane mode. Workspace switcher and inspector are modal overlays. | ## Workspaces A workspace is the top-level unit of activity. It holds a title, repo/cwd, current provider profile, mode, feed history, queued messages, linked runs, pinned context, and approval state. The left rail shows all open workspaces. Each row displays: - State marker (`|` active, `!` attention, `.` running) - Title - Provider tag (`[CC]`, `[SM]`, `[AI]`, `[GM]`, `[CX]`) — progressively disclosed (hidden until hover/focus in standard layouts) - Compound Status badge (combines unread/approval into clear priority icons) ```text | auth refactor [CC] .1 docs sync [AI] v ! pr review [SM] A1 incident triage [GM] x ``` Workspace actions: create, switch, close, archive, rename, pin, duplicate, fork from current. Switching workspaces takes under 100ms. Active run and approval badges update without manual refresh. ## Activity feed The center pane is a unified activity feed that mixes all orchestration activity: | Item type | Display | | --- | --- | | User | Compact text with optional attachment pills | | Assistant | Markdown with code blocks, collapsible long sections | | Tool | Collapsed by default, one-line summary with name/target/status/duration | | Run | Compact badge with workflow name, run ID, step, elapsed, progress | | Approval | Detaches from feed into an Action Bar above the composer to prevent scrolling off-screen | | Artifact | File name, type, source workflow/run, open/diff/copy affordances | | Diff | Structured diff card | | Error | Red label, compact summary first, stack collapsed underneath | | Summary | Assistant-generated summaries for long activity blocks | The feed streams incrementally and auto-scrolls unless you scroll away. A `LIVE` / `PAUSED` indicator appears in the feed header. Long-running workflows pin a sticky status header tracking progress, and related tool events group with vertical ASCII spines. ### Empty states New workspaces avoid the "blank terminal" syndrome by rendering a non-persistent Welcome Bento Board in the feed area. It displays current repo git status and proposes 3 suggested actions based on repository heuristics (e.g. spotting a `package.json` and suggesting a test workflow). The board scrolls away once feed items appear. ## Inspector The right rail shows details for the currently selected feed item. A dynamic, context-sensitive precision surface. Instead of persistent empty tabs, the pane's title and contents morph with the selection. A breadcrumb (e.g., `Inspector • Run a93f` or `Inspector • src/auth.ts`) grounds the user. Depending on the selection, the inspector renders: - **Run** -- run graph, status, steps, cost - **Context** -- pinned context items, token budget - **Workflow** -- schema, last runs, provider hints - **Diff** -- file diffs - **Logs** -- timestamped lifecycle events - **Details** -- raw output, structured output, scorer results The inspector reacts to the current feed selection immediately. Selecting a run item shows the run graph. Selecting a workflow mention shows schema and last runs. Selecting an attachment shows preview and token estimate. ## Composer A small command desk at the bottom of the shell. ```text [#review-pr] [@src/auth.ts] [@README.md] [+2] Build a reusable auth-fix workflow and run it against current diff. budget 18k ctx Enter send Alt+Enter queue Ctrl+G editor ``` Features: - Multiline input (auto-grows up to 6 rows, then scrolls) - `@` unified mentions for attaching files, directories, images, workspaces, sessions, and runs - `#` invokes workflows (opens a fuzzy workflow picker) - Slash commands (`/run`, `/workflows`, `/approvals`, etc.) - Queued follow-up messages with `Alt+Enter` - Large paste guard -- detects large paste and offers attach-as-file, inline, summarize, or cancel - Draft preserved while switching workspaces ## Keyboard model ### Global | Key | Action | | --- | --- | | `Ctrl+O` | Open global command palette | | `Tab` / `Shift+Tab` | Cycle focus: workspace rail, feed, inspector, composer | | `Esc` | Dismiss overlay, abort transient action, return focus toward composer | | `?` | Show shortcuts/help for current context | | `.` | Open contextual action menu for selected item | | `/` | Search current pane (when composer is not focused) | | `Ctrl+L` | Provider / model / profile picker | | `Ctrl+R` | Prompt history search | | `Ctrl+G` | Open composer in external editor | ### Composer | Key | Action | | --- | --- | | `Enter` | Send | | `Alt+Enter` | Queue follow-up | | `Shift+Enter` / `Ctrl+J` | Newline | | `@` | Unified context attach (file/image/directory/run/session) | | `#` | Invoke workflow | | `Ctrl+A` / `Ctrl+E` | Start / end of line | | `Alt+B` / `Alt+F` | Word backward / forward | | `Ctrl+W` / `Ctrl+U` / `Ctrl+K` | Kill word / line before / line after | ### Feed and lists | Key | Action | | --- | --- | | `Up` / `Down` or `j` / `k` | Move selection | | `g` / `G` | Jump to top / bottom | | `PageUp` / `PageDown` | Page | | `Space` | Expand / collapse selected item | | `Enter` | Default action (open detail view) | | `v` | Toggle verbose view | | `o` | Open artifact/diff/log in pager or external viewer | | `/` | Filter/search within the current pane | ### Destructive actions No global single-key kill/approve while unfocused. Approval actions only appear inside the approval context. Confirmation dialogs always show the exact target. A per-workspace "always allow" path exists for repetitive safe actions. ## Modes Three operating modes, switchable with `/mode` or `Ctrl+L`: ### Operator (default) The assistant prefers creating, modifying, and reusing Smithers workflows over direct file edits: 1. Inspect existing `.smithers/workflows/` first 2. Reuse or refactor shared Smithers components 3. Scaffold or edit workflows/scripts in `.smithers/` 4. Launch durable runs for non-trivial work 5. Monitor and report results 6. Use cheaper API providers for broad analysis 7. Escalate to harness-backed workers only when needed 8. Ask before direct edits outside `.smithers/` ### Plan Read-only. No file writes, no destructive shell, no workflow execution without confirmation. ### Direct Direct repo edits allowed. Still encourages Smithers scripts where useful but does not block one-off edits. ## Provider routing A provider profile routes work by task class. Example: - Repo scan -> AI SDK / cheap model - Workflow generation -> API strong model - Repo implementation -> Claude Code or Codex harness - Final summary -> cheap model The current provider and routing policy are visible in the top line and editable with `/provider` and `/profiles`. ## Workflow catalog Type `#` in the composer to open the workflow picker: ```text +-- Workflows ----------------------------------------------------------------+ | > review-pr PR review against current diff last v 4m | | auth-fix Reusable auth remediation flow last x 1h | | docs-refresh Refresh docs and changelog last v 1d | | | | review-pr | | input: { target?: string, diff?: boolean, push?: boolean } | | providers: SDK analyze -> Claude Code patch -> SDK summary | | tags: review, reusable, repo | +------------------------------------------------------------------------------+ ``` The catalog auto-discovers workflows from `.smithers/workflows/`. Features: - Favorites and recents - Fuzzy search by ID, tags, description, provider hints - Input schema summary - Last-run status, duration, success rate - Launch form generated from schema when possible ## Run monitoring Run cards in the feed show: - Workflow name, run ID, provider - Elapsed time, step count, progress bar - Latest node, approval state - Retries, failures, token/cost summary The inspector supports deep run inspection: - Overview and DAG/step graph - Node attempts - Logs and chat transcript - Artifacts and scorer results - Raw/structured output - Retry, resume, and cancel actions Navigate from a feed item to the deep run inspector in one action. Attach to any active run. Run state persists across TUI exits. ## Notifications Events that trigger notifications: - Approval needed - Run failed or completed - Provider disconnected - Queued message delivered Notifications appear as in-app badges on the workspace rail. Terminal bell, OSC notifications, and desktop notifications are available. Notifications are suppressed when the relevant workspace is focused. ## Slash commands ### Core | Command | Purpose | | --- | --- | | `/help` | Help | | `/new` | New workspace | | `/resume` | Resume workspace | | `/tree` | Session tree | | `/compact` | Compact feed | | `/clear` | Clear feed | | `/export` | Export feed to markdown/JSON | | `/theme` | Switch theme | | `/settings` | Settings | ### Smithers | Command | Purpose | | --- | --- | | `/workflows` | Open workflow catalog | | `/run` | Launch a workflow | | `/runs` | Show live runs | | `/approvals` | Show pending approvals | | `/inbox` | Unified attention queue (alerts + approvals + human requests) | | `/telemetry` | Telemetry board | | `/triggers` | Trigger manager | | `/datagrid` | SQL query browser | | `/docs` | Search Smithers docs | | `/attach-run` | Attach to a run | | `/resume-run` | Resume a run | | `/cancel-run` | Cancel a run | ### Provider | Command | Purpose | | --- | --- | | `/provider` | Switch provider profile | | `/mode` | Switch mode (operator/plan/direct) | | `/budget` | Token budget | | `/profiles` | Manage provider profiles | ### Context | Command | Purpose | | --- | --- | | `/attach` | Attach file/context | | `/detach` | Remove context | | `/history` | Prompt history | | `/editor` | Open external editor | ## Command palette Press `Ctrl+O` to open the global command palette. All slash commands and contextual actions are searchable here. Natural language works too — type what you want and the assistant interprets it. ## Persistence and recovery The TUI survives: - Accidental exits (workspace restore on relaunch) - TTY resize - Provider disconnects (reconnect automatically) - Broker crashes (workflow runs continue independently) - Large paste mistakes (paste guard dialog) Persisted state includes: last active workspace, composer draft, attachment chips, inspector tab, follow mode, selected feed entry, pending queued messages, and broker reconnect cursor. ## Monitoring with Claude Code Smithers persists all state to SQLite and exposes it through the CLI, so Claude Code queries status and reports progress without interrupting execution. Set up a recurring health check with `/cron`: ``` /cron 10m Check the smithers workflow running in this directory. Run `smithers ps` to see active runs, then `smithers inspect ` for the latest run. Summarize what tasks have completed, what's currently running, any failures, and overall progress. Keep it brief. ``` With `--hot`, Smithers watches for file changes and hot-reloads the workflow definition. Claude Code can edit prompts, swap agents, or restructure the JSX tree mid-run. In-flight tasks keep their original code; new tasks use the updated definition. Other patterns: - **Ad hoc inspection** -- Read the Smithers database and explain a failed run - **Approval handling** -- Run `smithers approve` or `smithers deny` based on criteria - **Live tuning** -- With `--hot`, tweak prompts or switch models mid-run - **Post-run analysis** -- Summarize outputs and suggest next steps ## Beyond the terminal ### Burns [Burns](https://github.com/l3wi/burns) is a workspace-first local control plane for Smithers. React web app, ElectroBun desktop shell, and headless CLI for authoring, running, and supervising workflows. See [Ecosystem](/integrations/ecosystem). ### JJHub Cloud [jjhub.tech](https://jjhub.tech) will have first-class Smithers support for hosted workflows with scheduling, observability, and team collaboration. ## Next steps - [CLI Reference](/cli/overview) -- all CLI commands including `smithers tui`. - [Monitoring & Logs](/guides/monitoring-logs) -- observability with Grafana and Prometheus. - [Debugging](/guides/debugging) -- diagnosing workflow issues. =============================================================================== # Smithers Memory > Smithers cross-run memory: working memory, message history, semantic recall, processors. --- ## Memory > Cross-run state — facts, message history, semantic recall. Memory persists state **across runs**. Task outputs are per-run; memory is per-namespace and survives every workflow execution. ## Three layers ```ts import { createMemoryStore } from "smithers-orchestrator/memory"; import { Database } from "bun:sqlite"; const store = createMemoryStore(new Database("smithers.db")); const ns = { kind: "workflow" as const, id: "code-review" }; ``` | Layer | API | Use for | |---|---|---| | Working memory | `store.set(ns, key, value)` / `store.get(ns, key)` | Facts. Optional TTL. Last-write-wins. | | Message history | `store.saveMessage(threadId, msg)` / `store.listMessages(threadId, limit)` | Ordered chat threads per agent or user. | | Semantic recall | `store.recall(ns, query, topK)` | Vector search over stored facts. Requires embedding setup. | ## Namespaces ```ts type MemoryNamespace = { kind: "workflow" | "agent" | "user" | "global"; id: string }; ``` Pick the kind to match the lifetime: `workflow` is scoped to a workflow definition; `agent` to an agent identity; `user` to an end user; `global` is shared across everything. ## Wire memory into a Task ```tsx Review the latest PR. ``` `recall` adds matching facts to the prompt before the agent runs. `remember` writes the validated output into memory after success. `threadId` ties the agent's prompt/response to a durable conversation. ## Processors Maintenance jobs you run periodically: ```ts import { TtlGarbageCollector, TokenLimiter, Summarizer } from "smithers-orchestrator/memory"; const gc = TtlGarbageCollector(); // expire facts past their TTL const limiter = TokenLimiter(4000); // keep history under token budget const summarizer = Summarizer(myAgent); // compress old messages with an LLM await gc.process(store); await limiter.process(store); await summarizer.process(store); ``` ## Inspect from the CLI ```bash bunx smithers-orchestrator memory list workflow:code-review -w workflow.tsx bunx smithers-orchestrator memory recall "auth bugs" -w workflow.tsx --top-k 10 ``` ## Notes - Memory and task outputs are **distinct stores**. Don't use memory for run-scoped state — it's not transactional with the workflow's frame commits. - Semantic recall requires an embedding provider; configure via `SemanticRecallConfig.embedding`. - Working-memory writes are unordered. Use message history when sequence matters. --- ## Memory Quickstart > Folded into the opt-in memory fragment. This material is now in the opt-in [`/llms-memory.txt`](/llms-memory.txt) fragment. =============================================================================== # Smithers OpenAPI Tools > Smithers OpenAPI tools: turn an OpenAPI spec into AI SDK tools, with auth, filters, and observability. --- ## OpenAPI Tools > Generate AI SDK tools from an OpenAPI spec. Auth, filtering, observability built in. `createOpenApiTools` parses an OpenAPI 3.x spec and returns AI SDK tools — one per operation, with Zod schemas converted from the spec's JSON schemas. ```ts import { createOpenApiTools } from "smithers-orchestrator/openapi"; const tools = await createOpenApiTools("./petstore.json", { baseUrl: "https://api.petstore.example.com", auth: { type: "bearer", token: process.env.PETSTORE_TOKEN! }, }); const agent = new ToolLoopAgent({ model: anthropic("claude-sonnet-4-20250514"), tools, }); ``` ## Options ```ts type OpenApiToolsOptions = { spec: object | string; // parsed spec, file path, or URL baseUrl?: string; // overrides spec.servers[0].url auth?: | { type: "none" } | { type: "apiKey"; name: string; in: "header" | "query" | "cookie"; value: string } | { type: "bearer"; token: string } | { type: "basicAuth"; username: string; password: string }; includeOperations?: string[]; // operationId allowlist excludeOperations?: string[]; // operationId blocklist }; ``` ## CLI preview ```bash bunx smithers-orchestrator openapi list ./api/openapi.yaml ``` Lists every operationId, method, path, and summary — useful for auditing what an agent will be able to call before wiring it up. ## What gets generated For each operation: - A Zod schema for the request body + path/query/header parameters. - An `execute(args)` function that performs the HTTP call. - The operation's `summary` / `description` becomes the tool description. ## Response handling - JSON responses are parsed and returned as objects. - Non-JSON responses are returned as strings. - HTTP errors are wrapped as `{ error: true, message, status }` and **returned**, not thrown — the agent can react in its loop. ## Filtering `includeOperations` / `excludeOperations` accept arrays of `operationId`. If both are set, exclude wins. Useful for limiting an agent's surface area to a specific feature ("just the inventory endpoints"). ## Observability Each tool call emits an `OpenApiToolCalled` event with `operationId`, `method`, `path`, `durationMs`, `status`. Visible via `bunx smithers-orchestrator events --type openapi`. ## Notes - Schema composition (`allOf`, `anyOf`, `oneOf`) is supported — converts to Zod unions/intersections. - Nullable fields and defaults from the spec are preserved. --- ## OpenAPI Tools Quickstart > Folded into the opt-in OpenAPI fragment. This material is now in the opt-in [`/llms-openapi.txt`](/llms-openapi.txt) fragment. =============================================================================== # Smithers Observability > Smithers observability surface: HTTP server, gateway, MCP, OpenTelemetry, metrics. --- ## HTTP Server > Run Smithers as an HTTP server: REST routes for runs, approvals, tools. `startServer` boots a multi-workflow HTTP server with REST endpoints for run lifecycle, SSE event streams, and [human-in-the-loop approvals](/concepts/human-in-the-loop). For a single-workflow variant alongside `smithers up`, see [Serve Mode](/integrations/serve). ## Quick start ```ts import { startServer } from "smithers-orchestrator"; import { drizzle } from "drizzle-orm/bun-sqlite"; const server = startServer({ port: 7331, db: drizzle("./smithers.db"), authToken: process.env.SMITHERS_API_KEY, rootDir: process.cwd(), }); ``` ```bash curl -X POST http://localhost:7331/v1/runs \ -H "Authorization: Bearer $SMITHERS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"workflowPath": "./bugfix.tsx", "input": {"description": "fix auth"}}' ``` ## ServerOptions ```ts type ServerOptions = { port?: number; // default 7331 db?: BunSQLiteDatabase;// enables GET /v1/runs and approvals listing authToken?: string; // falls back to process.env.SMITHERS_API_KEY maxBodyBytes?: number; // default 1_048_576 rootDir?: string; // sandbox root for workflow paths and tools allowNetwork?: boolean; // default false; allows network in `bash` headersTimeout?: number; // default 30_000 requestTimeout?: number; // default 60_000 }; ``` `startServer` returns a listening `http.Server`. `headersTimeout` and `requestTimeout` are applied to that server to bound slow header/body uploads. ## API Routes (TOON) ```toon routes[15]{method,path,purpose,auth}: GET,/health,Liveness probe,none GET,/metrics,Prometheus exposition,bearer POST,/v1/runs,Start or resume a run,bearer GET,/v1/runs,List runs (requires db),bearer GET,/v1/runs/:runId,Run status and node summary,bearer POST,/v1/runs/:runId/resume,Resume paused or failed run,bearer POST,/v1/runs/:runId/cancel,Abort an active run,bearer GET,/v1/runs/:runId/events,SSE event stream (?afterSeq=N),bearer GET,/v1/runs/:runId/frames,List render frames,bearer POST,/v1/runs/:runId/nodes/:nodeId/approve,Approve a paused node,bearer POST,/v1/runs/:runId/nodes/:nodeId/deny,Deny a paused node,bearer POST,/v1/runs/:runId/signals/:signalName,Deliver a named signal,bearer GET,/v1/approvals,List pending approvals (requires db),bearer GET,/v1/approval/list,Legacy alias for /v1/approvals,bearer POST,/signal/:runId/:signalName,Legacy alias for signals,bearer ``` JSON requests/responses use `Content-Type: application/json`, `Cache-Control: no-store`, and `X-Content-Type-Options: nosniff`. SSE events are named `smithers` and carry [`SmithersEvent`](/runtime/events) JSON; the stream sends a keep-alive comment every 10 s and closes on terminal state. Errors use the envelope `{ "error": { "code", "message", "details" } }`. Common codes: `INVALID_REQUEST`, `RUN_NOT_FOUND`, `RUN_IN_PROGRESS`, `RUN_ALREADY_EXISTS`, `WORKFLOW_PATH_OUTSIDE_ROOT`, `DB_NOT_CONFIGURED`, `RUN_NOT_ACTIVE`, `SERVER_ERROR`. ## Tool surface Tools resolve relative to `rootDir`. The example below exposes a workflow that uses the built-in `bash` tool through the server; clients call it via `POST /v1/runs`. ```tsx /** @jsxImportSource smithers-orchestrator */ import { Task, Workflow, createSmithers, bash } from "smithers-orchestrator"; import { z } from "zod"; const { smithers, outputs } = createSmithers({ result: z.object({ stdout: z.string() }), }); export default smithers((ctx) => ( {async () => ({ stdout: await bash(`echo ${ctx.input.msg}`) })} )); ``` `allowNetwork: false` (the default) keeps `bash` offline. Set `rootDir` to constrain the filesystem the workflow can touch. ## Authentication When `authToken` is configured (directly or via `SMITHERS_API_KEY`), every request except `GET /health` must include either: - `Authorization: Bearer `, or - `x-smithers-key: `. Missing or invalid tokens return `401`. No scopes — for finer access control use the [Gateway](/integrations/gateway). ## Notes - Each `POST /v1/runs` and `/resume` reloads the workflow source via a content-addressed shadow file (`.${name}.smithers-${sha1}.tsx`), so edits take effect on the next call without a restart. - Active runs heartbeat to `_smithers_runs.heartbeat_at_ms` every 5 s; stale rows are treated as crashed and may be resumed. - When a server-level `db` differs from a workflow's database, runs and events are mirrored asynchronously to the server db so they show up in `GET /v1/runs`. - Metrics are exported via `/metrics`; set `SMITHERS_OTEL_ENABLED=1` plus `OTEL_EXPORTER_OTLP_ENDPOINT` for OTLP. See [Observability](/guides/monitoring-logs). --- ## Serve Mode > Run a single workflow as an HTTP server with Hono — interact with it over REST, stream events via SSE, and manage approvals remotely. Serve mode starts a Hono-based HTTP server alongside a running workflow. Every route operates on the single active run — no workflow path or run ID in requests. ## CLI ```bash bunx smithers-orchestrator up workflow.tsx --serve --port 3000 --host 0.0.0.0 ``` | Flag | Default | Description | |---|---|---| | `--serve` | `false` | Enable HTTP server mode | | `--port` | `7331` | TCP port | | `--host` | `127.0.0.1` | Bind address | | `--auth-token` | `SMITHERS_API_KEY` env | Bearer token for auth | | `--metrics` | `true` | Expose `/metrics` Prometheus endpoint | The process stays alive after the workflow completes so final state remains queryable. Ctrl+C stops both the server and the workflow. Detached mode: ```bash bunx smithers-orchestrator up workflow.tsx --serve --port 8080 -d ``` ## Programmatic ```ts import { createServeApp } from "smithers-orchestrator/serve"; const app = createServeApp({ workflow, adapter, runId, abort: new AbortController(), authToken: "sk-secret", }); Bun.serve({ port: 3000, fetch: app.fetch }); ``` `createServeApp` returns a standard Hono app. Mount it with `Bun.serve`, pass it to another Hono app via `app.route()`, or use `app.fetch` in tests. ## ServeOptions ```ts type ServeOptions = { workflow: SmithersWorkflow; // loaded workflow instance adapter: SmithersDb; // database adapter for the workflow runId: string; // active run ID abort: AbortController; // shared cancellation controller authToken?: string; // bearer token; falls back to SMITHERS_API_KEY; disabled if unset metrics?: boolean; // expose /metrics; default true }; ``` --- ## Authentication When `authToken` is configured, every route except `/health` requires: - `Authorization: Bearer `, or - `x-smithers-key: ` Missing or invalid tokens receive `401`. --- ## Routes ### GET /health Returns `200` regardless of auth. ```json { "ok": true } ``` ### GET / Run status and node summary. ```json { "runId": "run-1234", "workflowName": "bugfix", "status": "running", "startedAtMs": 1707500000000, "finishedAtMs": null, "summary": { "finished": 3, "in-progress": 1, "pending": 2 } } ``` ### GET /events SSE stream of lifecycle events. Same format as the [multi-workflow server](/integrations/server#get-v1runsrunidevents). | Parameter | Type | Default | Description | |---|---|---|---| | `afterSeq` | `number` | `-1` | Only events after this sequence | ``` event: smithers data: {"type":"NodeStarted","runId":"run-1234","nodeId":"analyze","iteration":0,"attempt":0} id: 1 event: smithers data: {"type":"NodeFinished","runId":"run-1234","nodeId":"analyze","iteration":0,"attempt":0} id: 2 ``` - Polls every 500ms. - Auto-closes when the run reaches a terminal state. - Reconnect with `?afterSeq=N` to resume from a known position. ### GET /frames Rendered workflow frames. | Parameter | Type | Default | Description | |---|---|---|---| | `limit` | `number` | `50` | Max frames | | `afterFrameNo` | `number` | — | Frames after this number | ### POST /approve/:nodeId Approve a pending approval gate. All fields optional. Returns `{ "runId": "run-1234" }`. ```json { "iteration": 0, "note": "Looks good", "decidedBy": "alice" } ``` ### POST /deny/:nodeId Deny a pending approval gate. Same body as `/approve/:nodeId`. ### POST /cancel Cancel the running workflow. | Status | Code | Condition | |---|---|---| | 200 | — | Cancelled successfully | | 409 | `RUN_NOT_ACTIVE` | Run already finished/failed/cancelled | ### GET /metrics Prometheus text exposition. Same metrics as the [multi-workflow server](/integrations/server#get-metrics). --- ## Error Format ```json { "error": { "code": "ERROR_CODE", "message": "Human-readable description" } } ``` Unknown routes return `404` with code `NOT_FOUND`. --- ## Serve Mode vs Multi-Workflow Server | | Serve mode | [Multi-workflow server](/integrations/server) | |---|---|---| | Scope | Single workflow, single run | Any workflow, multiple concurrent runs | | Start | `smithers up --serve` or `createServeApp()` | `startServer()` | | Routes | `/`, `/events`, `/approve/:nodeId`, ... | `/v1/runs`, `/v1/runs/:runId`, ... | | Framework | Hono | Node.js `http` | | Use case | Development, single-purpose services | Production API gateway | --- ## Example ```bash # Start a workflow with serve mode bunx smithers-orchestrator up workflow.tsx --serve --port 8080 --auth-token sk-secret # Status curl http://localhost:8080/ -H "Authorization: Bearer sk-secret" # Stream events curl -N http://localhost:8080/events -H "Authorization: Bearer sk-secret" # Approve curl -X POST http://localhost:8080/approve/deploy \ -H "Authorization: Bearer sk-secret" \ -H "Content-Type: application/json" \ -d '{"note": "Ship it", "decidedBy": "alice"}' # Health (no auth) curl http://localhost:8080/health ``` --- ## Gateway > WebSocket / RPC gateway for connecting clients to Smithers runs. `Gateway` is Smithers' headless control plane. Reach for it (instead of [`startServer()`](/integrations/server)) when long-lived clients — bots, dashboards, schedulers — need to authenticate once, stream events over WebSocket, decide [approvals](/concepts/approvals), inject [signals](/runtime/events), and manage cron schedules across many registered workflows. This page covers the multi-workflow Gateway control plane. The Hono-based surface is [Serve Mode](/integrations/serve), exposed through `createServeApp()` and `smithers up --serve`; it is the single-workflow HTTP app and is separate from the Gateway WebSocket/RPC runtime. ## Quick start ```tsx /** @jsxImportSource smithers-orchestrator */ import { Gateway, Task, Workflow, createSmithers } from "smithers-orchestrator"; import { z } from "zod"; const { smithers, outputs } = createSmithers({ result: z.object({ ok: z.boolean() }), }); const deploy = smithers((ctx) => ( {{ ok: true }} )); const gateway = new Gateway({ heartbeatMs: 15_000, auth: { mode: "token", tokens: { "operator-token": { role: "operator", scopes: ["*"] } }, }, }); gateway.register("deploy", deploy, { schedule: "0 8 * * 1-5" }); await gateway.listen({ port: 7331 }); ``` ```ts const ws = new WebSocket("ws://localhost:7331"); ws.onmessage = (m) => console.log(JSON.parse(m.data)); ws.onopen = () => ws.send(JSON.stringify({ type: "req", id: "c1", method: "connect", params: { minProtocol: 1, maxProtocol: 1, client: { name: "docs-example", version: "1.0.0" }, auth: { token: "operator-token" }, }, })); ``` ## RPC methods (TOON) ```toon rpc[17]{method,params,returns,scope,transport}: launchRun,workflow/input?/options.runId?/options.idempotencyKey?,{runId/workflow},run:write,http+websocket resumeRun,runId/options.force?,{runId/status},run:write,http+websocket cancelRun,runId,{runId/status:cancelling},run:write,http+websocket hijackRun,runId/options?,{runId/status:hijack-ready/sessionId},run:admin,http+websocket rewindRun,runId/frameNo/confirm:true,JumpResult,run:admin,http+websocket submitApproval,runId/nodeId/iteration?/decision,{runId/nodeId/iteration/approved},approval:submit,http+websocket submitSignal,runId/correlationKey/payload?/signalName?,Delivery metadata,signal:submit,http+websocket getRun,runId,RunStateView,run:read,http+websocket listRuns,filter.status?/filter.limit?,Run summaries,run:read,http+websocket streamRunEvents,runId/afterSeq?,{streamId/runId/afterSeq/currentSeq},run:read,websocket streamDevTools,runId/afterSeq?,{streamId/runId/afterSeq} + devtools.event frames,observability:read,websocket getNodeOutput,runId/nodeId/iteration?,NodeOutputResponse,run:read,http+websocket getNodeDiff,runId/nodeId/iteration?,Node diff response,run:read,http+websocket cronList,filter.workflow?,Cron rows,cron:read,http+websocket cronCreate,workflow/pattern/cronId?/enabled?,Created cron row,cron:write,http+websocket cronDelete,cronId,{cronId/removed},cron:write,http+websocket cronRun,cronId? or workflow/input?,{runId/workflow},cron:write,http+websocket ``` `health` remains available as a utility RPC and `GET /health` is available without auth. The legacy method names are still accepted for compatibility (`runs.create`, `runs.get`, `runs.list`, `runs.cancel`, `runs.rerun`, `runs.diff`, `frames.list`, `frames.get`, `attempts.list`, `attempts.get`, `approvals.list`, `approvals.decide`, `signals.send`, `cron.list`, `cron.add`, `cron.remove`, `cron.trigger`, `getDevToolsSnapshot`, `jumpToFrame`, `devtools.jumpToFrame`, `devtools.getNodeOutput`, `devtools.getNodeDiff`), but new clients should use the v1 names above. ### Scopes ```toon scopes[8]{scope,allows}: run:read,Read run state/lists/event streams/node output/node diffs run:write,Launch/resume/cancel runs; implies run:read run:admin,Hijack or rewind runs; implies run:write and run:read approval:submit,Submit approval decisions signal:submit,Submit workflow signals cron:read,List cron schedules cron:write,Create/delete/trigger cron schedules; implies cron:read observability:read,Read DevTools and other observability streams ``` `*` grants every scope. Exact method grants such as `launchRun` also work. Legacy wildcard method grants such as `cron.*` continue to match legacy method names; typed scopes are the contract to use for new integrations. Legacy ranked grants (`read`, `execute`, `approve`, `admin`) are accepted so older tokens keep working. ### `rewindRun` (destructive rewind) Rewinds a run to a prior frame and makes it resumable from that point. This is destructive: it truncates frames, attempts, output rows, and diff-cache entries beyond the target; reverts JJ sandboxes; marks the run `running` again; and emits a `TimeTravelJumped` event so `streamDevTools` subscribers rebaseline. Caller identity is authorized per-request: the connection must have `run:admin` scope and must also be the run owner (`userId` matches `ownerId`) or have `role: "admin"`. Scope alone never grants access. The legacy aliases `jumpToFrame` and `devtools.jumpToFrame` route to `rewindRun`. Request: ```ts type RewindRunRequest = { runId: string; // /^[a-z0-9_-]{1,64}$/ frameNo: number; // 0 <= frameNo <= latestFrameNo confirm: true; // must be literal true }; ``` Response (`JumpResult`): ```ts type JumpResult = { ok: true; newFrameNo: number; revertedSandboxes: number; deletedFrames: number; deletedAttempts: number; invalidatedDiffs: number; durationMs: number; }; ``` Also broadcast after the DB commit as `run.time_travel_jumped` with `{ runId, fromFrameNo, toFrameNo, timestampMs, caller }`. Quota: 10 rewinds per run per caller per hour (default window). Exceeded → `RateLimited`. Failure modes and HTTP status: | Code | Meaning | HTTP | | ---------------------- | ------------------------------------------------------------------------- | ----- | | `InvalidRunId` | `runId` fails `/^[a-z0-9_-]{1,64}$/`. | `400` | | `InvalidFrameNo` | `frameNo` is not a non-negative i32 integer. | `400` | | `ConfirmationRequired` | Caller omitted `confirm: true`. | `400` | | `FrameOutOfRange` | `frameNo` > latest frame, or run has no frames. | `400` | | `Unauthorized` | Caller is neither the run owner nor an admin (audit row still written). | `401` | | `RunNotFound` | `runId` does not exist. | `404` | | `Busy` | Another rewind is in flight for this run. | `409` | | `RateLimited` | Caller exceeded rewind quota (default 10/hour). | `429` | | `UnsupportedSandbox` | A sandbox cannot be reverted (missing / untrackable `jjPointer`). | `501` | | `VcsError` | A JJ revert call failed; DB/reconciler rolled back. | `500` | | `RewindFailed` | Rewind failed and rollback was partial; run marked `needs_attention`. | `500` | Every call — success, failure, unauthorized — writes one row to `_smithers_time_travel_audit` with `result ∈ { success, failed, partial, in_progress }`. An in-progress row is inserted before any mutation and updated in place on completion; startup recovery flips any leftover `in_progress` rows to `partial`. ### Node output `getNodeOutput` returns the DevTools Output-tab payload for a single task iteration: ```ts type NodeOutputResponse = { status: "produced" | "pending" | "failed"; row: Record | null; schema: OutputSchemaDescriptor | null; partial?: Record | null; // only when status === "failed" }; type OutputSchemaDescriptor = { fields: Array<{ name: string; type: "string" | "number" | "boolean" | "object" | "array" | "null" | "unknown"; optional: boolean; nullable: boolean; description?: string; enum?: readonly unknown[]; }>; }; ``` ### Error codes Gateway v1 RPC errors use stable code strings and HTTP status mappings: ```toon errors[18]{code,http}: InvalidRequest,400 InvalidInput,400 Unauthorized,401 Forbidden,403 RunNotFound,404 NodeNotFound,404 IterationNotFound,404 NodeHasNoOutput,404 FrameOutOfRange,400 SeqOutOfRange,400 Busy,409 RateLimited,429 PayloadTooLarge,413 BackpressureDisconnect,429 UnsupportedSandbox,501 VcsError,500 RewindFailed,500 Internal,500 ``` Some legacy DevTools aliases still surface older validation names such as `InvalidRunId`, `InvalidFrameNo`, or `ConfirmationRequired`. Treat those as legacy aliases for the matching v1 validation failure. ### Versioned wire shapes All DevTools wire types carry `version: 1`. `DevToolsSnapshot` (v1): ```ts type DevToolsSnapshot = { version: 1; runId: string; frameNo: number; // latest frame reflected in this tree seq: number; // monotonic sequence id (equals frameNo today) root: DevToolsNode; }; type DevToolsNode = { id: number; // stable across frames for the same logical node type: "workflow" | "task" | "sequence" | "parallel" | /* …see protocol */; name: string; props: Record; task?: { nodeId: string; kind: "agent" | "compute" | "static"; /* … */ }; children: DevToolsNode[]; depth: number; }; ``` `DevToolsDelta` (v1): ```ts type DevToolsDelta = { version: 1; baseSeq: number; // must match the subscriber's current seq seq: number; // new seq after applying ops, in order ops: Array< | { op: "addNode"; parentId: number; index: number; node: DevToolsNode } | { op: "removeNode"; id: number } | { op: "updateProps"; id: number; props: Record } | { op: "updateTask"; id: number; task: DevToolsNode["task"] } | { op: "replaceRoot"; node: DevToolsNode } // emitted when the root's // identity or shape changes; // `removeNode` of the root is // never emitted. >; }; ``` `DevToolsEvent` (v1) — frames pushed over `devtools.event`: ```ts type DevToolsEvent = | { version: 1; kind: "snapshot"; snapshot: DevToolsSnapshot } | { version: 1; kind: "delta"; delta: DevToolsDelta }; ``` A subscription always starts with a `snapshot` event, then emits `delta` events per frame. The server re-baselines (emits a full `snapshot` instead of a `delta`) after 50 delta events, when a delta is larger than a fresh snapshot, or when the gateway observes `TimeTravelJumped` for the run. ## WebSocket protocol Three frame types share the same socket: - `req` — `{ type: "req", id, method, params? }` from client. - `res` — `{ type: "res", id, ok, payload?, error? }` from server, correlated by `id`. - `event`— `{ type: "event", event, payload?, seq, stateVersion }` server-pushed; `seq` is per connection, `stateVersion` is global. Handshake: on connect the server immediately pushes `connect.challenge` (`{ nonce, ts }`). The client replies with a `connect` request carrying `minProtocol`, `maxProtocol`, `client` metadata, `auth`, and an optional `subscribe: string[]` to filter events by `runId`. The server returns a `hello` payload (`protocol`, `features`, `policy.heartbeatMs`, `auth` with `sessionToken`/`role`/`scopes`/`userId`, `snapshot`). After `connect`, the gateway emits `tick` events every `heartbeatMs`. `launchRun`, `submitApproval`, `submitSignal`, and `cronRun` automatically subscribe the connection to the affected `runId`. Streamed event names: `connect.challenge`, `tick`, `run.event`, `run.heartbeat`, `run.gap_resync`, `run.error`, `node.started`, `node.finished`, `node.failed`, `task.output`, `task.heartbeat`, `approval.requested`, `approval.decided`, `approval.auto_approved`, `run.time_travel_jumped`, `run.completed`, `cron.triggered`, `devtools.event`. For stateless callers, `POST /rpc` accepts the same body shape (`{ id, method, params }`) and returns the same `ResponseFrame`. Auth headers: `Authorization: Bearer ` or `x-smithers-key: ` (or trusted-proxy headers in trusted-proxy mode). ## GatewayOptions ```ts type GatewayOptions = { protocol?: number; // default 1 features?: string[]; // default ["streaming", "runs"] heartbeatMs?: number; // default 15_000 auth?: GatewayAuthConfig; defaults?: { cliAgentTools?: "all" | "explicit-only" }; maxBodyBytes?: number; // default 1_048_576 for POST /rpc maxPayload?: number; // default 1_048_576 for WebSocket frames maxConnections?: number; // default 1_000 eventWindowSize?: number; // default 10_000 per-run replay frames headersTimeout?: number; // default 30_000 requestTimeout?: number; // default 60_000 }; type GatewayAuthConfig = | { mode: "token"; tokens: Record; } | { mode: "jwt"; issuer: string; audience: string; secret: string; // HS256 scopesClaim?: string; roleClaim?: string; userClaim?: string; defaultRole?: string; defaultScopes?: string[]; clockSkewSeconds?: number; } | { mode: "trusted-proxy"; allowedOrigins?: string[]; trustedHeaders?: string[]; // default ["x-user-id","x-user-scopes","x-user-role"] defaultRole?: string; defaultScopes?: string[]; }; ``` Runs started through the gateway expose `ctx.auth = { triggeredBy, role, scopes, createdAt }`. `` may further restrict decisions with `allowedScopes` and `allowedUsers`, which the gateway enforces before accepting `submitApproval`. `headersTimeout` and `requestTimeout` are applied to the underlying Node HTTP server when `gateway.listen()` starts. Keep both below the corresponding reverse-proxy idle/read timeouts so slow clients are closed by Smithers first. ## Notes - Cron: `gateway.register(name, wf, { schedule })` writes a cron row keyed `gateway:`; the gateway polls between 1 s and 15 s (clamped from `heartbeatMs`). Cron-fired runs get `ctx.auth.role = "system"`, `triggeredBy = "cron:gateway"`, `scopes = ["*"]`. - JWT mode currently validates `alg=HS256`, HMAC, `iss`, `aud`, `exp`, `nbf`. Scope claims may be arrays or space/comma-separated strings. - Trusted-proxy mode is only safe behind something you control (Cloudflare Access, internal API gateway) that strips and rewrites identity headers. - DevTools streams re-baseline every 50 events or when a delta exceeds a fresh snapshot; over-capacity subscribers receive `BackpressureDisconnect`. --- ## MCP Server > Expose Smithers as a Model Context Protocol stdio server so any MCP client — Claude Code, Cursor, Codex, or your own agent — can list, run, inspect, and control workflows without shell scripting. Smithers ships a built-in MCP stdio server. Passing `--mcp` to the CLI speaks the Model Context Protocol over stdin/stdout instead of acting as an interactive CLI. Any MCP-aware client can connect, discover workflows, start runs, watch progress, resolve approvals, and revert bad attempts through structured tool calls. Use the MCP server when an AI agent should drive Smithers autonomously. Use the [HTTP Server](/integrations/server) for REST endpoints for human-written code or webhooks. --- ## Setup ### Start the server ```bash smithers --mcp ``` This starts the semantic surface — a stable, structured tool set for AI agent consumption, documented on this page. Two additional surfaces are available via `--surface`: ```bash # Semantic tools only (default) smithers --mcp --surface semantic # Raw CLI-mirroring tools only smithers --mcp --surface raw # Both surfaces registered on the same server smithers --mcp --surface both ``` Use `--surface raw` only for direct CLI parity. Prefer the semantic surface for new integrations: every tool returns a `{ ok, data, error }` envelope with Zod-validated input and output schemas. ### Register with Claude Code ```bash smithers mcp add ``` `smithers mcp add` writes the server entry to the MCP config file for the detected agent. Pass `--agent` to target a specific client, `--no-global` to install project-locally, or `--command` to override the launch command: ```bash smithers mcp add --agent claude-code smithers mcp add --no-global smithers mcp add --command "pnpm smithers --mcp" ``` ### Register manually For clients that read JSON config directly: ```json { "mcpServers": { "smithers": { "command": "smithers", "args": ["--mcp"] } } } ``` Project-scoped install (e.g. a monorepo where Smithers is a dev dependency): ```json { "mcpServers": { "smithers": { "command": "pnpm", "args": ["smithers", "--mcp"] } } } ``` --- ## Tool Registration On start, the server calls `registerSemanticTools`, which loops over `createSemanticToolDefinitions` and registers each via `server.registerTool`. Every tool carries: - **`inputSchema`** — Zod object describing accepted parameters. - **`outputSchema`** — Zod schema for the structured response envelope. - **`annotations`** — MCP annotation metadata (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). ### Structured tool envelope Every tool returns the same shape: ```ts { ok: boolean; data?: { ... }; // present on success error?: { // present on failure code: string; message: string; details?: Record | null; docsUrl?: string | null; }; } ``` The response is also echoed as a `text` content block, so clients that do not parse `structuredContent` still receive the JSON payload. ### Tool annotations | Annotation | Tools | Meaning | |---|---|---| | `readOnlyHint: true` | Most query tools | Tool does not modify state | | `readOnlyHint: false, openWorldHint: true` | `run_workflow` | Launches external processes | | `readOnlyHint: false, destructiveHint: true, idempotentHint: false` | `resolve_approval`, `revert_attempt` | Mutates persisted state irreversibly | --- ## Tool Reference ### list_workflows List all Smithers workflows discovered in the working directory. **Input:** none **Output:** ```ts { workflows: Array<{ id: string; displayName: string; entryFile: string; sourceType: "seeded" | "user" | "generated"; }>; } ``` Use the returned `id` values as the `workflowId` parameter for `run_workflow`. --- ### run_workflow Start or resume a discovered workflow. **Input:** | Parameter | Type | Default | Description | |---|---|---|---| | `workflowId` | `string` | required | Workflow ID from `list_workflows` | | `input` | `Record` | `{}` | Workflow input object | | `prompt` | `string` | — | Shorthand: sets `input.prompt` when `input` is not provided | | `runId` | `string` | auto | Custom run ID | | `resume` | `boolean` | `false` | Resume an existing run; requires `runId` | | `force` | `boolean` | `false` | Force-start even if a run with this ID already exists | | `waitForTerminal` | `boolean` | `false` | Block until the run reaches a terminal state | | `waitForStartMs` | `number` | `1000` | For background launches, how long to wait for the run row to appear in the database | | `maxConcurrency` | `number` | — | Max concurrent nodes | | `rootDir` | `string` | — | Root directory for tool sandboxing and path resolution | | `logDir` | `string` | — | Directory for log files | | `allowNetwork` | `boolean` | `false` | Allow network access in `bash` tool | | `maxOutputBytes` | `number` | — | Cap on node output size | | `toolTimeoutMs` | `number` | — | Per-tool call timeout | | `hot` | `boolean` | `false` | Enable hot-reloading of the workflow file | **Output:** ```ts { workflow: { id, displayName, entryFile, sourceType }; runId: string; launchMode: "background" | "waited"; requestedResume: boolean; status: string; observedRun: RunSummary | null; result: { runId, status, output?, error? } | null; } ``` **Background vs. waited launch** By default (`waitForTerminal: false`) the tool fires the workflow and returns immediately with `launchMode: "background"`. `observedRun` reflects the run state polled during `waitForStartMs`. Use `watch_run` to track progress. Set `waitForTerminal: true` to block until the workflow finishes. `result` is populated and `launchMode` is `"waited"`. **Run option forwarding** `rootDir`, `logDir`, `allowNetwork`, `maxOutputBytes`, `toolTimeoutMs`, and `hot` are forwarded verbatim to `runWorkflow`. They override values baked into the workflow file. --- ### list_runs List recent runs with summary data. **Input:** | Parameter | Type | Default | Description | |---|---|---|---| | `limit` | `number` (1–200) | `20` | Max runs to return | | `status` | `string` | — | Filter by status (`running`, `finished`, `failed`, etc.) | **Output:** ```ts { runs: RunSummary[]; } ``` `RunSummary` fields: `runId`, `workflowName`, `workflowPath`, `parentRunId`, `status`, `createdAtMs`, `startedAtMs`, `finishedAtMs`, `heartbeatAtMs`, `activeNodeId`, `activeNodeLabel`, `pendingApprovalCount`, `waitingTimers`, `countsByState`. --- ### get_run Get the full detail record for a specific run, including steps, approvals, timers, loop state, lineage, config, and error. **Input:** | Parameter | Type | Description | |---|---|---| | `runId` | `string` | Run ID | **Output:** ```ts { run: RunSummary & { steps: Array<{ nodeId, iteration, state, lastAttempt, updatedAtMs, outputTable, label }>; approvals: PendingApproval[]; loops: Array<{ loopId, iteration, maxIterations }>; continuedFromRunIds: string[]; activeDescendantRunId: string | null; config: unknown | null; error: unknown | null; }; } ``` --- ### watch_run Poll a run at a fixed interval until it reaches a terminal state or a timeout expires. **Input:** | Parameter | Type | Default | Description | |---|---|---|---| | `runId` | `string` | required | Run to watch | | `intervalMs` | `number` | `1000` | Poll interval (minimum enforced by runtime) | | `timeoutMs` | `number` | `30000` | Wall-clock budget before giving up | **Output:** ```ts { runId: string; intervalMs: number; pollCount: number; reachedTerminal: boolean; timedOut: boolean; finalRun: RunSummary; snapshots: Array<{ observedAtMs: number; run: RunSummary }>; } ``` When `timedOut` is `true` the run is still active — call `watch_run` again or raise `timeoutMs`. Terminal statuses: `finished`, `failed`, `cancelled`. --- ### explain_run Return a structured diagnosis explaining why a run is blocked, waiting, or stale. **Input:** | Parameter | Type | Description | |---|---|---| | `runId` | `string` | Run ID | **Output:** ```ts { diagnosis: { runId: string; status: string; summary: string; generatedAtMs: number; blockers: Array<{ kind: string; nodeId: string; iteration: number | null; reason: string; waitingSince: number; unblocker: string; context?: string; signalName?: string | null; dependencyNodeId?: string | null; firesAtMs?: number | null; remainingMs?: number | null; attempt?: number | null; maxAttempts?: number | null; }>; currentNodeId: string | null; }; } ``` `summary` is a human-readable sentence. `blockers` lists every node preventing progress; `unblocker` describes what action or event would unblock it. --- ### list_pending_approvals List approvals that are waiting for a human decision, optionally filtered by run, workflow, or node. **Input:** | Parameter | Type | Description | |---|---|---| | `runId` | `string` | Filter by run ID | | `workflowName` | `string` | Filter by workflow name | | `nodeId` | `string` | Filter by node ID | All parameters optional. Omit all to list every pending approval across all runs. **Output:** ```ts { approvals: Array<{ runId: string; nodeId: string; iteration: number; status: string; requestedAtMs: number | null; decidedAtMs: number | null; note: string | null; decidedBy: string | null; request: unknown; decision: unknown; autoApproved?: boolean; workflowName: string | null; runStatus: string | null; nodeLabel: string | null; }>; } ``` --- ### resolve_approval Approve or deny a pending approval. This tool is destructive and non-idempotent. **Input:** | Parameter | Type | Description | |---|---|---| | `action` | `"approve" \| "deny"` | required — decision to record | | `runId` | `string` | Filter to a specific run | | `workflowName` | `string` | Filter by workflow name | | `nodeId` | `string` | Filter by node ID | | `iteration` | `number` | Filter by loop iteration | | `note` | `string` | Optional note to record with the decision | | `decidedBy` | `string` | Identity of the decision-maker | | `decision` | `unknown` | Structured decision payload passed back to the workflow | **Ambiguity guard** Zero matches errors with `INVALID_INPUT`. More than one match errors with `INVALID_INPUT` and returns matches in `details.matches` — add `runId`, `nodeId`, or `iteration` to narrow the selection. The tool never guesses when multiple approvals match. **Output:** ```ts { action: "approve" | "deny"; approval: PendingApproval; // with updated status, decidedAtMs, note, decidedBy run: RunSummary | null; } ``` --- ### get_node_detail Get enriched detail for a single node, including all attempts, tool calls, token usage, scorer results, and validated output. **Input:** | Parameter | Type | Description | |---|---|---| | `runId` | `string` | required | | `nodeId` | `string` | required | | `iteration` | `number` | Loop iteration (default: latest) | **Output:** ```ts { detail: { node: { runId, nodeId, iteration, state, lastAttempt, updatedAtMs, outputTable, label }; status: string; durationMs: number | null; attemptsSummary: { total, failed, cancelled, succeeded, waiting }; attempts: unknown[]; toolCalls: unknown[]; tokenUsage: unknown; scorers: unknown[]; output: { validated: unknown | null; raw: unknown | null; source: "cache" | "output-table" | "none"; cacheKey: string | null; }; limits: { toolPayloadBytesHuman: number; validatedOutputBytesHuman: number; }; }; } ``` --- ### revert_attempt Revert the workspace and frame history back to the state captured at a specific attempt. This is destructive and non-idempotent. **Input:** | Parameter | Type | Default | Description | |---|---|---|---| | `runId` | `string` | required | Run containing the node | | `nodeId` | `string` | required | Node to revert | | `iteration` | `number` | `0` | Loop iteration | | `attempt` | `number` | required | Attempt number to revert to (must be ≥ 1) | **Output:** ```ts { runId: string; nodeId: string; iteration: number; attempt: number; success: boolean; error?: string; jjPointer?: string; run: RunSummary | null; } ``` --- ### list_artifacts List structured output artifacts produced by nodes in a run. **Input:** | Parameter | Type | Default | Description | |---|---|---|---| | `runId` | `string` | required | Run ID | | `nodeId` | `string` | — | Limit to a specific node | | `includeRaw` | `boolean` | `false` | Include raw (pre-validation) output values | **Output:** ```ts { artifacts: Array<{ artifactId: string; // "::" kind: "node-output"; runId: string; nodeId: string; iteration: number; label: string | null; state: string; outputTable: string | null; source: "cache" | "output-table" | "none"; cacheKey: string | null; value: unknown | null; rawValue?: unknown | null; // only when includeRaw=true }>; } ``` Only nodes with an `outputTable` and a non-`none` output source are included. --- ### get_chat_transcript Return the structured agent chat transcript for a run, grouped by attempts. **Input:** | Parameter | Type | Default | Description | |---|---|---|---| | `runId` | `string` | required | Run ID | | `all` | `boolean` | `false` | Include all attempts, not just those with known output events | | `includeStderr` | `boolean` | `true` | Include stderr messages | | `tail` | `number` | — | Return only the last N messages | **Output:** ```ts { runId: string; attempts: Array<{ attemptKey: string; nodeId: string; iteration: number; attempt: number; state: string; startedAtMs: number; finishedAtMs: number | null; cached: boolean; meta: unknown | null; }>; messages: Array<{ id: string; attemptKey: string; nodeId: string; iteration: number; attempt: number; role: "user" | "assistant" | "stderr"; stream: "stdout" | "stderr" | null; timestampMs: number; text: string; source: "prompt" | "event" | "responseText"; }>; } ``` Messages are sorted by `timestampMs`. Use `tail` to limit context window usage on long transcripts. --- ### get_run_events Return the raw structured event history for a run with optional filtering. **Input:** | Parameter | Type | Default | Description | |---|---|---|---| | `runId` | `string` | required | Run ID | | `afterSeq` | `number` | — | Only events with `seq` greater than this value | | `limit` | `number` (1–10000) | `200` | Max events to return | | `nodeId` | `string` | — | Filter to events for a specific node | | `types` | `string[]` | — | Filter to specific event types (e.g. `["NodeFinished", "NodeFailed"]`) | | `sinceTimestampMs` | `number` | — | Only events at or after this timestamp | **Output:** ```ts { runId: string; events: Array<{ runId: string; seq: number; timestampMs: number; type: string; payload: unknown | null; }>; } ``` Paginate via `afterSeq`: pass the `seq` of the last received event to fetch the next page. --- ## Usage Examples ### List workflows and start a run ``` > list_workflows {} { "ok": true, "data": { "workflows": [ { "id": "bugfix", "displayName": "bugfix", "entryFile": "./workflows/bugfix.tsx", "sourceType": "user" } ] } } > run_workflow { "workflowId": "bugfix", "prompt": "Fix the auth token expiry bug" } { "ok": true, "data": { "runId": "smi_abc123", "launchMode": "background", "status": "running", ... } } ``` ### Watch until complete ``` > watch_run { "runId": "smi_abc123", "timeoutMs": 120000 } { "ok": true, "data": { "reachedTerminal": true, "timedOut": false, "finalRun": { "status": "waiting-approval", ... } } } ``` ### Resolve a pending approval ``` > list_pending_approvals { "runId": "smi_abc123" } { "ok": true, "data": { "approvals": [ { "nodeId": "deploy", "iteration": 0, "nodeLabel": "Deploy to production", ... } ] } } > resolve_approval { "action": "approve", "runId": "smi_abc123", "nodeId": "deploy", "decidedBy": "alice", "note": "Looks good" } { "ok": true, "data": { "action": "approve", "approval": { "status": "approved", "decidedAtMs": 1707500100000, ... }, "run": { "status": "running", ... } } } ``` ### Debug a blocked run ``` > explain_run { "runId": "smi_abc123" } { "ok": true, "data": { "diagnosis": { "summary": "Run is waiting for a human approval on node 'deploy'.", "blockers": [ { "kind": "approval", "nodeId": "deploy", "reason": "Node requires human approval before proceeding.", "unblocker": "Call resolve_approval with action=approve or action=deny." } ] } } } ``` ### Revert a failed attempt ``` > get_node_detail { "runId": "smi_abc123", "nodeId": "analyze" } { "ok": true, "data": { "detail": { "attemptsSummary": { "total": 3, "failed": 2, "succeeded": 1 }, ... } } } > revert_attempt { "runId": "smi_abc123", "nodeId": "analyze", "attempt": 1 } { "ok": true, "data": { "success": true, "run": { "status": "running", ... } } } ``` --- ## Error Codes Errors follow the structured envelope. Common codes: | Code | Meaning | |---|---| | `RUN_NOT_FOUND` | No run exists with the given ID | | `INVALID_INPUT` | Missing required field, failed validation, or ambiguous approval filter | | `WORKFLOW_MISSING_DEFAULT` | Workflow file has no default export | | `WORKFLOW_NOT_FOUND` | No workflow matches the given ID | =============================================================================== # Smithers Integrations > Smithers integrations: agent runtimes (Claude Code, Codex, Gemini, Pi), tool surfaces, ecosystem partners. --- ## Integrations > Three patterns for connecting Smithers to external services. Smithers ships no first-party clients for Linear, Notion, Slack, or similar services. Treat them as external integrations your app owns and pick one of three wirings: tools on an SDK agent, skills/plugins/MCP on a CLI agent, or an external CLI run inside a task. ## Pattern 1: Pass tools to an SDK agent Use this when the agent needs judgment but the external calls should stay explicit and reviewable. ```ts import { ToolLoopAgent as Agent, tool, zodSchema } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; import { z } from "zod"; const linearGetIssue = tool({ description: "Fetch a Linear issue", inputSchema: zodSchema(z.object({ id: z.string() })), execute: async ({ id }) => linearClient.getIssue(id), }); const opsAgent = new Agent({ model: anthropic("claude-sonnet-4-20250514"), tools: { linearGetIssue }, }); ``` ## Pattern 2: Pass a skill / plugin / MCP config to a CLI agent Use this when your CLI agent already supports external integrations and Smithers should only orchestrate the task. ```ts import { PiAgent } from "smithers-orchestrator"; const pi = new PiAgent({ provider: "openai", model: "gpt-5.2-codex", skill: ["./skills/linear", "./skills/notion"], }); ``` ```tsx {`Use the Linear skill to inspect ${ctx.input.issueId}, then summarize next actions.`} ``` ## Pattern 3: Run an external CLI in a task Use this when the step is deterministic and you do not need the model involved. ```tsx {async () => { const proc = Bun.spawn(["linear", "issue", "view", ctx.input.issueId, "--json"], { stdout: "pipe", stderr: "pipe", }); const stdout = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); if (await proc.exited !== 0) throw new Error(stderr || stdout); return JSON.parse(stdout); }} ``` ## Choosing between them | If you need | Prefer | |---|---| | AI judgment over a small integration surface | Pattern 1 (SDK agent with narrow tools) | | Existing CLI ecosystem support (skills, plugins, MCP) | Pattern 2 (CLI agent) | | Deterministic sync or publish steps | Pattern 3 (compute task) | ## See also For React hook patterns see [/recipes#custom-hooks-over-ctx](/recipes#custom-hooks-over-ctx); for agent-class details see [/llms-integrations.txt](/llms-integrations.txt). --- ## CLI Agents > Spawn external CLI tools (Claude Code, Codex, Pi, …) and pipe them through the workflow runtime. CLI-backed agent classes wrap external AI command-line tools and implement the [AI SDK](https://ai-sdk.dev) `Agent` interface. Use them anywhere Smithers accepts an agent, including [``](/components/task). Reach for these for a vendor's full CLI surface (sessions, sandboxes, slash commands, MCP). For API-billed provider wrappers, see [SDK Agents](/integrations/sdk-agents). ## Quick Start ```tsx import { ClaudeCodeAgent } from "smithers-orchestrator"; const claude = new ClaudeCodeAgent({ model: "claude-sonnet-4-20250514", systemPrompt: "You are a careful code reviewer.", timeoutMs: 30 * 60 * 1000, }); export default smithers((ctx) => ( {`Analyze the codebase and identify potential improvements.`} )); ``` ## Available agents ```toon agents[7]{class,cli,defaultModel,hijack,notes}: ClaudeCodeAgent,claude,claude-sonnet-4-20250514,native session id,Anthropic Claude Code CLI CodexAgent,codex,gpt-4.1,native session id,OpenAI Codex CLI (codex exec via stdin) GeminiAgent,gemini,gemini-2.5-pro,native session id,Google Gemini CLI (falls back to gcloud auth) PiAgent,pi,gpt-5.2-codex,native session id,Pi CLI (text/json/rpc modes + extension UI hooks) KimiAgent,kimi,kimi-latest,native session id,Moonshot Kimi CLI (auto-isolates KIMI_SHARE_DIR) ForgeAgent,forge,anthropic/claude-sonnet-4-20250514,conversation id,Forge CLI (300+ models) AmpAgent,amp,claude-sonnet-4-20250514,thread id,Amp CLI (--execute headless mode) ``` CLI binaries must be on `PATH`: `claude`, `codex`, `gemini`, `pi`, `kimi`, `forge`, `amp`. ## Common options All CLI agents accept the same base option surface: ```ts type BaseCliAgentOptions = { id?: string; // Agent instance id (default: random UUID) model?: string; // Model name passed to --model systemPrompt?: string; // Prepended to the user prompt instructions?: string; // Alias for systemPrompt cwd?: string; // Working directory (default: tool ctx rootDir or process.cwd()) env?: Record; // Extra env vars merged with process.env yolo?: boolean; // Skip permission prompts (default: true) timeoutMs?: number; // Hard wall-clock cap idleTimeoutMs?: number; // Inactivity cap; resets on any stdout/stderr maxOutputBytes?: number; // Truncate captured output extraArgs?: string[]; // Additional CLI flags appended to the command }; ``` Per-call timeout override: ```ts await agent.generate({ prompt: "do the thing", timeout: { totalMs: 15 * 60 * 1000, idleMs: 2 * 60 * 1000 }, }); ``` ## Per-agent extras ```ts import { ClaudeCodeAgent } from "smithers-orchestrator"; new ClaudeCodeAgent({ permissionMode?: "acceptEdits" | "bypassPermissions" | "default" | "delegate" | "dontAsk" | "plan"; allowedTools?: string[]; disallowedTools?: string[]; disableSlashCommands?: boolean; addDir?: string[]; file?: string[]; fromPr?: string; fallbackModel?: string; appendSystemPrompt?: string; agents?: Record | string; betas?: string[]; pluginDir?: string[]; resume?: string; sessionId?: string; mcpConfig?: string[]; mcpDebug?: boolean; maxBudgetUsd?: number; jsonSchema?: string; outputFormat?: "text" | "json" | "stream-json"; // default stream-json }); ``` ```ts import { CodexAgent } from "smithers-orchestrator"; new CodexAgent({ sandbox?: "read-only" | "workspace-write" | "danger-full-access"; fullAuto?: boolean; dangerouslyBypassApprovalsAndSandbox?: boolean; config?: Record | string[]; oss?: boolean; localProvider?: string; image?: string[]; outputSchema?: string; outputLastMessage?: string; profile?: string; }); ``` ```ts import { GeminiAgent } from "smithers-orchestrator"; new GeminiAgent({ sandbox?: boolean; approvalMode?: "default" | "auto_edit" | "yolo" | "plan"; allowedTools?: string[]; allowedMcpServerNames?: string[]; extensions?: string[]; resume?: string; includeDirectories?: string[]; outputFormat?: "text" | "json" | "stream-json"; // default json }); ``` ```ts import { PiAgent, type PiExtensionUiRequest, type PiExtensionUiResponse } from "smithers-orchestrator"; new PiAgent({ provider?: string; mode?: "text" | "json" | "rpc"; print?: boolean; continue?: boolean; resume?: boolean; session?: string; sessionDir?: string; noSession?: boolean; extension?: string[]; skill?: string[]; promptTemplate?: string[]; theme?: string[]; tools?: string[]; noTools?: boolean; files?: string[]; thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; onExtensionUiRequest?: (req: PiExtensionUiRequest) => Promise; }); ``` ```ts import { KimiAgent } from "smithers-orchestrator"; new KimiAgent({ thinking?: boolean; outputFormat?: "text" | "stream-json"; finalMessageOnly?: boolean; quiet?: boolean; agent?: "default" | "okabe"; agentFile?: string; workDir?: string; session?: string; continue?: boolean; skillsDir?: string; mcpConfigFile?: string[]; mcpConfig?: string[]; maxStepsPerTurn?: number; maxRetriesPerStep?: number; maxRalphIterations?: number; }); ``` ```ts import { ForgeAgent } from "smithers-orchestrator"; new ForgeAgent({ directory?: string; provider?: string; agent?: string; conversationId?: string; sandbox?: string; restricted?: boolean; workflow?: string; event?: string; conversation?: string; }); ``` ```ts import { AmpAgent } from "smithers-orchestrator"; new AmpAgent({ visibility?: "private" | "public" | "workspace" | "group"; mcpConfig?: string; settingsFile?: string; logLevel?: "error" | "warn" | "info" | "debug" | "audit"; logFile?: string; dangerouslyAllowAll?: boolean; }); ``` ## Hijack handoff Every built-in CLI agent supports `bunx smithers-orchestrator hijack `, which relaunches the agent in its native CLI session for interactive takeover. Smithers persists the native session id (or conversation id for Forge / thread id for Amp) on each task event; on hijack it waits for a safe boundary between blocking tool calls, then reopens the session via the vendor's resume flag (`claude --resume`, `codex resume`, `gemini --resume`, `pi --session`, `kimi --session`, `forge --conversation-id`, `amp threads continue`). On clean exit the workflow resumes in detached mode. See [How it works → Durability and resume](/how-it-works#durability--resume). ## Notes - **Yolo defaults.** `yolo: true` (default) maps to each CLI's "skip approvals" flag (`--dangerously-skip-permissions`, `--dangerously-bypass-approvals-and-sandbox`, `--yolo`, `--dangerously-allow-all`). Set `yolo: false` or use the agent-specific approval option for tighter control. - **PI rpc mode** sends prompts as JSON over stdin and is required for `onExtensionUiRequest` callbacks; text/json modes pass the prompt as a positional arg with `files` emitted as `@path`. - **Kimi share dir.** `KimiAgent` auto-creates an isolated `KIMI_SHARE_DIR` per invocation to prevent `kimi.json` corruption under concurrent runs. Set `env.KIMI_SHARE_DIR` to opt out. - **Gemini auth** falls back to `gcloud auth application-default login` when neither `GOOGLE_API_KEY` nor `GEMINI_API_KEY` is set. - **Non-idempotent retries.** When a `` retries, Smithers prepends a warning listing previously-called side-effect tools so the agent can verify external state before re-invoking them. --- ## SDK Agents > Provider-backed AI SDK agent wrappers for Anthropic and OpenAI that work like first-class Smithers agents. `AnthropicAgent` and `OpenAIAgent` are thin wrappers around the [AI SDK](https://ai-sdk.dev) `ToolLoopAgent` with class-style ergonomics matching the [CLI agents](/integrations/cli-agents). ## Import ```ts import { AnthropicAgent, OpenAIAgent, tools, } from "smithers-orchestrator"; import { stepCountIs } from "ai"; ``` ## Quick Start ```ts const claude = new AnthropicAgent({ model: "claude-opus-4-6", tools, instructions: "You are a careful planner.", stopWhen: stepCountIs(40), }); const codex = new OpenAIAgent({ model: "gpt-5.3-codex", tools, instructions: "You are a precise implementation agent.", stopWhen: stepCountIs(40), }); ``` ```tsx {/* outputs comes from createSmithers() */} Analyze the repository and propose a migration plan. ``` ## Model Input Both classes accept a model ID string (`"claude-opus-4-6"`, `"gpt-5.3-codex"`) or a prebuilt AI SDK language model instance. ## Options Constructors forward standard AI SDK `ToolLoopAgent` settings: `instructions`, `tools`, `stopWhen`, `maxOutputTokens`, `temperature`, `providerOptions`, `prepareCall`. The wrapper adds `model`, which resolves model-ID strings automatically. ## Hijack Support SDK agents do not reopen a provider-native CLI. Smithers persists the agent conversation and reopens it through a Smithers-managed REPL via `smithers hijack `. Live-run behavior: - Smithers captures response history after each step via `onStepFinish`. - `smithers hijack` waits until history is durable, cancels the live run, and opens the REPL. - On clean REPL exit, Smithers writes updated message history back and resumes the workflow automatically. Limits: - Conversation hijack stays on the same agent implementation. Cross-engine hijack is not supported. - Smithers reconstructs the original task agent from the workflow source. ## CLI vs SDK | | CLI Agents | SDK Agents | |---|---|---| | Billing | Provider subscription / local CLI | API billing | | Tools | Provider CLI tool ecosystem | Smithers [tools](/integrations/tools) [sandbox](/components/sandbox) | | Flexibility | Native CLI flags | AI SDK `providerOptions` | Pass a raw `ToolLoopAgent` directly if you prefer — the wrappers are convenience, not a separate runtime. ## Example: Dual Setup ```ts const useCli = process.env.USE_CLI_AGENTS === "1"; export const claude = useCli ? new ClaudeCodeAgent({ model: "claude-opus-4-6", dangerouslySkipPermissions: true, }) : new AnthropicAgent({ model: "claude-opus-4-6", tools, instructions: "You are a careful planner.", stopWhen: stepCountIs(40), }); ``` ## Next Steps - [CLI Agents](/integrations/cli-agents) - [Built-in Tools](/integrations/tools) - [Agents and Tools](/concepts/agents-and-tools) --- ## Built-in Tools > Sandboxed file and shell tools for AI agent tasks, with exact input schemas, security policies, and usage examples. ```ts import { tools, read, write, edit, grep, bash, defineTool } from "smithers-orchestrator"; ``` `tools` bundles all five tools keyed by name: ```ts 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. | 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](#bash). | ## Tool Call Logging 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 `defineTool()` wraps custom [AI SDK](https://ai-sdk.dev) tools with Smithers runtime context, deterministic idempotency keys, and durable tool-call logging. ```ts 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](/concepts/workflows-overview) 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: 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. ```ts // ✗ 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 of state **outside the [sandbox](/components/sandbox)**. If the tool talks to an external API, writes to a database, sends a message, or triggers a webhook, mark it. File system changes inside the sandbox — writing files, editing code, running `git commit` — are **not** side effects. The built-in `write`, `edit`, and `bash` tools modify the working directory, but those changes are local, sandboxed, and tracked by git. They are reversible (`git checkout`, `git reset`) and inspectable (`git diff`, `git log`). Smithers does not need retry warnings or idempotency keys for them. | 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` (local commands) | No | Local execution within sandbox | | 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. ```ts { path: string } // relative to rootDir or absolute ``` Returns file contents as UTF-8. Throws `"File too large"` if size exceeds `maxOutputBytes`. ```ts 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 }, }); ``` ```tsx {/* outputs comes from createSmithers() */} Read the file src/auth.ts and identify any security vulnerabilities. ``` --- ## write Write content to a file. Creates parent directories as needed. ```ts { 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. ```ts 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. ```ts { 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`. ```ts { 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. ```ts { 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. ```ts 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 }, }); ``` ```tsx {/* outputs comes from createSmithers() */} Run the linter on src/ and report any issues. ``` --- ## Using Tools with Agents Pass tools to an [AI SDK](https://ai-sdk.dev) agent and assign the agent to a [``](/components/task): ```tsx 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) => ( {`Refactor the function in ${ctx.input.file} to improve readability.`} )); ``` The full bundle works too: ```ts 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` | ```ts const result = await runWorkflow(workflow, { input: { file: "src/auth.ts" }, rootDir: "/home/project", allowNetwork: false, maxOutputBytes: 500_000, toolTimeoutMs: 120_000, }); ``` ## See Also - [Agents and Tools](/concepts/agents-and-tools) - [Sandbox](/components/sandbox) - [Common External Tools](/integrations/common-tools) - [Tools Agent Example](/examples/tools-agent) --- ## Common External Tools > Patterns for hitting GitHub, Linear, Notion, Slack, Obsidian as tools. For each external service, you have three choices: 1. **OpenAPI tools** — point `createOpenApiTools()` at the service's OpenAPI spec. See `/llms-openapi.txt`. 2. **CLI in a task** — if the service has a CLI (`gh`, `linear`, `notion`, `slack`), run it inside a `` via the `bash` tool. See `/recipes#side-effect-tools-with-idempotency`. 3. **Custom `defineTool`** — wrap the service's REST API in a Zod-validated tool. See `/llms-integrations.txt#definetool`. ## Quick decision | Service | Recommended approach | |---|---| | GitHub | `gh` CLI in a task (auth via `gh auth login`) | | Linear | `linear` CLI in a task, or OpenAPI tools | | Notion | OpenAPI tools (Notion publishes a spec) | | Slack | OpenAPI tools or `slack` CLI | | Obsidian | `bash` tool with vault path; no API needed | Always mark side-effecting tools with `sideEffect: true` and use `ctx.idempotencyKey` so retries don't double-fire. --- ## Ecosystem > Community projects built on Smithers. ## Burns Workspace-first local control plane for Smithers. Single UI for authoring, running, and supervising workflows across repositories. Register repos, launch runs, stream events, inspect frames, handle approvals. - React web app, ElectroBun desktop shell, or headless CLI - AI-assisted workflow authoring via local agent CLIs - SQLite-backed workspace registry github.com/l3wi/burns ## Ralphinho Multi-agent development workflows. Two independent workflows: - **Ralphinho** (scheduled-work) -- decomposes RFC into work units, runs tier-based quality pipelines (implement, test, review), lands via merge queue with CI verification. - **Improvinho** (review-discovery) -- three parallel discovery lenses (refactoring, type safety, architecture), deduplicates findings. Optionally pushes to Linear. Requires Bun and Jujutsu (`jj`). Supports Claude and Codex agents. github.com/enitrat/ralphinho ## Cairo Coder AI-powered Cairo smart contract generator. RAG pipeline (DSPy) converting natural language to Cairo contracts for Starknet. Uses Smithers with Claude and Codex agents. github.com/KasarLabs/cairo-coder ## Agentix Opinionated RFC-to-production orchestrator. Multi-phase pipelines: research, plan, implement, test, review. Role-based agents, conflict-aware merge queues, security/performance gates. DDD + BDD + TDD by default. github.com/AbdelStark/agentix ## Era Generic multi-phase development workflow engine. Research, Plan, Implementation, Testing, Review, Fix, Final Review pipeline with outer Loop. Role-based agents, intelligent caching, dual-layer prompts. github.com/ClementWalter/era ## Local Isolated Ralph Kubernetes-native Smithers workflow runner. Runs workflows as isolated K8s Jobs and CronJobs via k3s/k3d. Sandboxed container execution. github.com/SamuelLHuber/local-isolated-ralph --- ## PI Integration > Use PI as a Smithers workflow CLI backend and understand how PI extensibility composes with Smithers declarative orchestration. Smithers provides deterministic orchestration (workflow graph, approvals, retries, durable state). PI provides adaptive agent capabilities (providers, models, extensions, skills, prompt templates). Use both when you need deterministic execution with flexible agent behavior. ## Integration Modes ### 1) PI as Workflow Agent ```tsx import { PiAgent } from "smithers-orchestrator"; const pi = new PiAgent({ provider: "openai", model: "gpt-5.2-codex", mode: "text", }); {/* outputs comes from createSmithers() */} {`Implement feature X and explain tradeoffs.`} ``` `PiAgent` supports all PI CLI flags: provider/model, tools, extensions, skills, prompt templates, themes, sessions. Text mode uses `--print` by default; JSON/RPC modes set `--mode` and omit `--print`. PI sessions are first-class hijack targets. `smithers hijack --target pi` reopens the PI session for local steering. ### 2) PI Server Client Drive Smithers server APIs from a PI extension or any Node process via `pi-plugin`: ```ts import { runWorkflow, approve, streamEvents } from "smithers-orchestrator/pi-plugin"; ``` ### 3) Hybrid: PI Extensibility + Smithers Orchestration - Keep orchestration in Smithers (``, ``, ``, ``). - Run adaptive logic in PI tasks (extensions/skills/provider overrides). Patterns: 1. PI skill-driven coding task inside a Smithers ``. 2. PI extension command that starts/resumes Smithers workflows via server API or pi-plugin. 3. Smithers workflow output persisted to SQLite and consumed by later PI-assisted tasks. ## Hijacking PI Sessions PI is a native-session hijack backend. - Live run: Smithers watches PI's event stream, waits between blocking tool calls, then hands off the session. - Finished/cancelled run: Smithers reopens the latest persisted PI session. - Relaunch uses the stored session ID: `pi --session `. - Clean exit resumes the workflow automatically. Session persistence: - `PiAgent` defaults `noSession` to `true` for one-shot calls. - For workflow hijack/resume/streaming, Smithers keeps session persistence enabled automatically. - `mode: "json"` is not required for hijack support. ## Setup 1. Install PI CLI and add to `PATH`. 2. Configure PI credentials via env/config (prefer over CLI args for API keys). 3. Instantiate `PiAgent` with explicit options in workflows. 4. For server-driven workflows, use `pi-plugin`. ```bash pi --version bun run test ``` ## Design Guidance | Use `PiAgent` tasks when | Use Smithers-native tasks when | |---|---| | You need PI capabilities inside deterministic workflows | You need strict reproducibility and narrow tool contracts | | You want PI calls as auditable workflow steps | | ## Limitations Chat-provider integration lives in host applications, not this repo. =============================================================================== # Smithers Events > Smithers event surface: how to subscribe, the event categories, and the full SmithersEvent discriminated union (~50 variants). --- ## Events > Subscribe to lifecycle events. Full event union lives in Types. `SmithersEvent` is a discriminated union of every lifecycle event the runtime emits. The full type definition lives in [Types](/reference/types) — that's the source of truth for field shapes. ## Subscribe via `onProgress` ```ts import { runWorkflow } from "smithers-orchestrator"; import workflow from "./workflow"; await runWorkflow(workflow, { input: { task: "fix bug" }, onProgress: (event) => { if (event.type === "NodeStarted") console.log(`▶ ${event.nodeId} (attempt ${event.attempt})`); if (event.type === "NodeFinished") console.log(`✓ ${event.nodeId}`); if (event.type === "NodeFailed") console.error(`✗ ${event.nodeId}`, event.error); }, }); ``` ## Read from the NDJSON log Events append to `.smithers/executions//logs/stream.ndjson` (configure with `logDir` / `--log-dir`; disable with `--no-log`). ```bash tail -f .smithers/executions/abc123/logs/stream.ndjson | jq . cat .smithers/executions/abc123/logs/stream.ndjson | jq 'select(.type == "NodeFailed")' cat .smithers/executions/abc123/logs/stream.ndjson | jq -r .type | sort | uniq -c | sort -rn ``` Or with the CLI: ```bash bunx smithers-orchestrator events --json bunx smithers-orchestrator events --type tool-call --node analyze ``` ## Common fields ```ts type CommonFields = { type: string; runId: string; timestampMs: number }; type NodeScoped = CommonFields & { nodeId: string; iteration: number }; type AttemptScoped = NodeScoped & { attempt: number }; ``` Every event includes `type`, `runId`, `timestampMs`. Node-scoped events add `nodeId` and `iteration`. Attempt-scoped add `attempt`. ## Event categories Used by `bunx smithers-orchestrator events --type ` and the metrics layer. | Category | Events | |---|---| | `run` | RunStarted, RunStatusChanged, RunFinished, RunFailed, RunCancelled, RunContinuedAsNew, RunHijackRequested, RunHijacked | | `frame` | FrameCommitted | | `node` | NodePending, NodeStarted, NodeFinished, NodeFailed, NodeCancelled, NodeSkipped, NodeRetrying, NodeOutput | | `approval` | NodeWaitingApproval, ApprovalRequested, ApprovalGranted, ApprovalDenied | | `tool-call` | ToolCallStarted, ToolCallFinished | | `agent` | AgentEvent | | `revert` | RevertStarted, RevertFinished | | `workflow` | WorkflowReloadDetected, WorkflowReloaded, WorkflowReloadFailed, WorkflowReloadUnsafe | | `scorer` | ScorerStarted, ScorerFinished, ScorerFailed | | `token` | TokenUsageReported | | `timer` | TimerCreated, TimerFired, TimerCancelled | | `memory` | MemoryFactSet, MemoryRecalled, MemoryMessageSaved | | `openapi` | OpenApiToolCalled | | `sandbox` | SandboxCreated, SandboxShipped, SandboxCompleted | | `supervisor` | TaskHeartbeat | ## Built-in metrics | Event | Metric | |---|---| | `RunStarted` | `smithers.runs.total` | | `NodeStarted` | `smithers.nodes.started` | | `NodeFinished` | `smithers.nodes.finished` | | `NodeFailed` | `smithers.nodes.failed` | | `ApprovalGranted` / `ApprovalDenied` | Approval counters | | `TokenUsageReported` | Token usage counters per model/agent | `trackSmithersEvent` from `smithers-orchestrator/observability` exposes this mapping for custom integrations. See the [observability fragment](/llms-observability.txt) for the full OTLP/Prometheus setup. --- ## Event Types > The full SmithersEvent discriminated union. `SmithersEvent` is the discriminated union of every lifecycle event the runtime emits. Every variant has `runId` and `timestampMs`. Most have `nodeId` and `iteration`. Attempt-scoped variants add `attempt`. For subscribing (`onProgress` callback, NDJSON log file, CLI), category filters (`agent`, `node`, `tool-call`, …), and built-in metrics, see [Events](/runtime/events). ```ts type SmithersEvent = | { type: "RunStarted"; runId: string; timestampMs: number } | { type: "RunStatusChanged"; runId: string; status: RunStatus; timestampMs: number } | { type: "RunFinished"; runId: string; timestampMs: number } | { type: "RunFailed"; runId: string; error: unknown; timestampMs: number } | { type: "RunCancelled"; runId: string; timestampMs: number } | { type: "RunContinuedAsNew"; runId: string; newRunId: string; iteration: number; carriedStateSize: number; ancestryDepth?: number; timestampMs: number } | { type: "RunHijackRequested"; runId: string; target?: string; timestampMs: number } | { type: "RunHijacked"; runId: string; nodeId: string; iteration: number; attempt: number; engine: string; mode: "native-cli" | "conversation"; resume?: string | null; cwd: string; timestampMs: number } | { type: "FrameCommitted"; runId: string; frameNo: number; xmlHash: string; timestampMs: number } | { type: "NodePending"; runId: string; nodeId: string; iteration: number; timestampMs: number } | { type: "NodeStarted"; runId: string; nodeId: string; iteration: number; attempt: number; timestampMs: number } | { type: "NodeFinished"; runId: string; nodeId: string; iteration: number; attempt: number; timestampMs: number } | { type: "NodeFailed"; runId: string; nodeId: string; iteration: number; attempt: number; error: unknown; timestampMs: number } | { type: "NodeCancelled"; runId: string; nodeId: string; iteration: number; attempt?: number; reason?: string; timestampMs: number } | { type: "NodeSkipped"; runId: string; nodeId: string; iteration: number; timestampMs: number } | { type: "NodeRetrying"; runId: string; nodeId: string; iteration: number; attempt: number; timestampMs: number } | { type: "NodeOutput"; runId: string; nodeId: string; iteration: number; attempt: number; text: string; stream: "stdout" | "stderr"; timestampMs: number } | { type: "NodeWaitingApproval"; runId: string; nodeId: string; iteration: number; timestampMs: number } | { type: "ApprovalRequested"; runId: string; nodeId: string; iteration: number; timestampMs: number } | { type: "ApprovalGranted"; runId: string; nodeId: string; iteration: number; timestampMs: number } | { type: "ApprovalDenied"; runId: string; nodeId: string; iteration: number; timestampMs: number } | { type: "ToolCallStarted"; runId: string; nodeId: string; iteration: number; attempt: number; toolName: string; seq: number; timestampMs: number } | { type: "ToolCallFinished"; runId: string; nodeId: string; iteration: number; attempt: number; toolName: string; seq: number; status: "success" | "error"; timestampMs: number } | { type: "AgentEvent"; runId: string; nodeId: string; iteration: number; attempt: number; engine: string; event: any; timestampMs: number } | { type: "RevertStarted"; runId: string; nodeId: string; iteration: number; attempt: number; jjPointer: string; timestampMs: number } | { type: "RevertFinished"; runId: string; nodeId: string; iteration: number; attempt: number; jjPointer: string; success: boolean; error?: string; timestampMs: number } | { type: "WorkflowReloadDetected"; runId: string; changedFiles: string[]; timestampMs: number } | { type: "WorkflowReloaded"; runId: string; generation: number; changedFiles: string[]; timestampMs: number } | { type: "WorkflowReloadFailed"; runId: string; error: unknown; changedFiles: string[]; timestampMs: number } | { type: "WorkflowReloadUnsafe"; runId: string; reason: string; changedFiles: string[]; timestampMs: number } | { type: "ScorerStarted"; runId: string; nodeId: string; scorerId: string; scorerName: string; timestampMs: number } | { type: "ScorerFinished"; runId: string; nodeId: string; scorerId: string; scorerName: string; score: number; timestampMs: number } | { type: "ScorerFailed"; runId: string; nodeId: string; scorerId: string; scorerName: string; error: unknown; timestampMs: number } | { type: "TokenUsageReported"; runId: string; nodeId: string; iteration: number; attempt: number; model: string; agent: string; inputTokens: number; outputTokens: number; cacheReadTokens?: number; cacheWriteTokens?: number; reasoningTokens?: number; timestampMs: number } | { type: "TimerCreated"; runId: string; timerId: string; firesAtMs: number; timerType: "duration" | "absolute"; timestampMs: number } | { type: "TimerFired"; runId: string; timerId: string; firesAtMs: number; firedAtMs: number; delayMs: number; timestampMs: number } | { type: "TimerCancelled"; runId: string; timerId: string; timestampMs: number } | { type: "MemoryFactSet"; runId: string; namespace: string; key: string; timestampMs: number } | { type: "MemoryRecalled"; runId: string; namespace: string; query: string; resultCount: number; timestampMs: number } | { type: "TaskHeartbeat"; runId: string; nodeId: string; iteration: number; attempt: number; hasData: boolean; dataSizeBytes: number; intervalMs?: number; timestampMs: number } | { type: "SandboxCreated"; runId: string; sandboxId: string; runtime: SandboxRuntime; configJson: string; timestampMs: number } | { type: "SandboxShipped"; runId: string; sandboxId: string; runtime: SandboxRuntime; bundleSizeBytes: number; timestampMs: number } | { type: "SandboxCompleted"; runId: string; sandboxId: string; remoteRunId?: string; runtime: SandboxRuntime; status: "finished" | "failed" | "cancelled"; durationMs: number; timestampMs: number }; ```