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; bunx smthrs ps still lists it, and reopening the UI resumes the same view from the database.

Three surfaces, one state

bunx smthrs 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 - smthrs/gateway-react hooks (useGatewayRun, useGatewayRuns, useGatewayRunEvents, useGatewayNodeOutput, useGatewayNodeEvents, useGatewayApprovals, useGatewayActions, useGatewayRunDiff, useGatewayScores, useGatewayRunTokenUsage, and more, over TanStack DB collections), smthrs/gateway-ui run-aware widgets (RunList, RunTree, RunEventLog, ApprovalPanel, NodeChatStream, NodeOutputCard, WorkflowGraph, LaunchButton, StatusPill, SimpleWorkflowDashboard), and the smthrs/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, bunx smthrs 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). Gateway UI bundles rebuild automatically on browser refresh when the entry or any bundled import changes; unchanged bundles stay cached. Set SMITHERS_GATEWAY_UI_NO_CACHE=1 only when you need to force a rebuild on every request.

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. Token-protected gateways serve browser UIs through the session handoff: monitor/ui/gui exchange the bearer for an HttpOnly cookie before the browser lands, so a --mint-token gateway’s UI URLs open cleanly. 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.
  • 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