Skip to main content
A background agent run that you cannot see is a run you cannot trust. The failure mode is concrete: a five-hour workflow stalls at hour two on an approval gate, and nobody notices until the morning. Or a lane goes sideways — the agent misread the spec — and the only options are “wait for it to finish wrong” or “kill everything and start over.” Long-running agent work needs the same thing long-running infrastructure needed: a live control surface where a human can observe state, decide at gates, and correct mid-run without destroying the work already done. There is a second, subtler requirement. In most agent tools the UI is the process: close the ChatGPT tab or kill the terminal and the work dies with it. That coupling is fine for a thirty-second chat. It is fatal for a run that spans a night. The UI has to be a view over durable state — something you can close, reopen from another machine, or hand to a teammate, while the run itself keeps executing against the database. Smithers splits the two cleanly. Runs execute against durable storage (see Durability); the Gateway is a workspace-level RPC/WebSocket control plane over that state; and every UI — the built-in Monitor, the full-screen TUI, or a custom per-workflow browser app — is a disposable client of the Gateway. Kill any of them and nothing happens to the run.

How the industry solves it

Every mature orchestrator grew a live UI, and two adjacent traditions feed into agent steering: “UI from your code” frameworks and agent-UI protocols. Two lessons recur. First, the steerable systems (Temporal, LangGraph, Argo) all put a durable state layer under the UI — the UI reads and writes checkpoints, it doesn’t host the computation. Second, the systems people actually enjoy building UIs for (Streamlit, Gradio, AI SDK generative UI) made the UI belong to the code, declared next to the logic instead of built as a separate frontend project. Smithers takes both: durable state under every surface, and a <UI> element declared inside the workflow itself.

How Smithers does it

The Gateway is the control plane

smithers gateway serves one process per workspace: a domain REST API, an RPC surface, and WebSocket event streams over every run in the workspace database. Clients authenticate with bearer tokens carrying typed scopes (Authorization: Bearer <token> or x-smithers-key; JWT and trusted-proxy modes exist for shared deployments). On connect, the WebSocket handshake returns a hello payload with protocol, features, and the session’s role/scopes, then pushes live event frames filtered by runId subscription. Runs launched through the Gateway expose ctx.auth = { triggeredBy, role, scopes, createdAt }, and <Approval> can restrict who may decide with allowedScopes / allowedUsers — enforced server-side before submitApproval is accepted. Because all UI surfaces are Gateway clients over that durable state, the run/UI decoupling is structural, not a feature flag. Close the Monitor tab; the run continues. Reboot your laptop mid-run; smithers ps still lists it, and reopening the UI resumes the same view from the database.

Three surfaces, one state

smithers gui opens a directory’s workspace at the most recent run’s workflow UI; smithers monitor RUN_ID deep-links one run. All three autostart a Gateway if none is running (--no-autostart to opt out, --gateway <url> to point at a remote one).

Workflow-owned UIs

A custom UI is declared in the workflow with <UI> — it renders nothing in the graph; the Gateway discovers it at gateway.register(), bundles the entry with Bun.build into a single browser script, and serves it with a boot config (__SMITHERS_GATEWAY_UI__) injected before your code runs:
.smithers/workflows/triage.tsx
The browser side composes verified, shipped pieces — smithers-orchestrator/gateway-react hooks (useGatewayRun, useGatewayRuns, useGatewayRunEvents, useGatewayNodeOutput, useGatewayNodeEvents, useGatewayApprovals, useGatewayActions, useGatewayRunDiff, useGatewayScores, useGatewayRunTokenUsage, and more, over TanStack DB collections), smithers-orchestrator/gateway-ui run-aware widgets (RunList, RunTree, RunEventLog, ApprovalPanel, NodeChatStream, NodeOutputCard, WorkflowGraph, LaunchButton, StatusPill, SimpleWorkflowDashboard), and the smithers-orchestrator/ui shadcn-anatomy base library, all themed by the workflow UI design tokens so light/dark is automatic:
.smithers/ui/triage.tsx
Steering is first-class, not read-only: approvals decide in place, useGatewayActions() launches runs and submits decisions, smithers signal delivers durable signals to <Signal>/<WaitForEvent> nodes, and pause/resume/cancel/retry-task are one-shot RPCs. Mid-run correction — the property interruptible-agent arguments center on — comes from the durable substrate: rewind, fork-from-checkpoint, and revert-attempt are all exposed through the same Gateway (see Time Travel).

Hot reload

Workflow code hot-reloads under RunOptions.hot (schema-shape changes are blocked; in-flight tasks finish on the code they launched with — see Recipes). For UI iteration, set SMITHERS_GATEWAY_UI_NO_CACHE=1 on the Gateway to rebuild the bundle on every request, so edits to the entry or any import show up on a plain page reload.

Guarantees and limits

Guaranteed: every surface is a stateless client over durable state — closing it never affects the run; reconnecting WebSocket streams resume via afterSeq; approvals and signals are durable RPCs that survive the deciding tab. Scoped tokens gate every method. Honest limits, verified in source and the tracker:
  • Pack-extension UIs have no domain-data path. The client side of the Gateway extension surface ships (SmithersGatewayClient.extensionRpc, useGatewayExtensionResource / useGatewayExtensionAction / useGatewayExtensionStream), but smithers gateway cannot load pack-declared extension servers, so a custom UI cannot yet read pack-specific domain data through the Gateway (smithers#1438, open). Workaround: ship the data as node outputs and read them with useGatewayNodeOutput.
  • UI bundle caching is build-once by default. Without SMITHERS_GATEWAY_UI_NO_CACHE=1 a long-lived Gateway serves the cached bundle, so UI edits do not appear on refresh (smithers#1440, open).
  • --mint-token gateways can’t serve browser UIs yet. The CLI reads the minted bearer from the runtime state file, but plain browser navigation cannot send it; run the local Gateway without --mint-token when you need browser-served workflow UIs.
  • The Monitor is workspace-local by design; multi-workspace fleet views are not a shipped surface.

Operating it

Agents steer through the same state via the MCP tools: watch_run, get_run_events, list_pending_approvals, resolve_approval, ask_human.

See also