> ## Documentation Index
> Fetch the complete documentation index at: https://smithers.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Gateway Client API

> The low-level typed RPC + WebSocket SDK for a Smithers Gateway.

The transport layer beneath the UI: a typed client for the Gateway's HTTP RPC
and WebSocket protocol, plus the TanStack DB collection factory that turns
those calls into live, reconnecting data. Framework-free on purpose.

<Note>
  Most UIs shouldn't touch this directly. Use the React bindings in
  [`/reference/gateway-react`](/reference/gateway-react), which hold one client
  for you and expose live hooks. Drop to this client only for non-React hosts,
  custom transports, or scripts.
</Note>

Everything on this page lives at the subpath
`smithers-orchestrator/gateway-client`, **not** the bare
`smithers-orchestrator` facade; import from the subpath or the build fails.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import {
  SmithersGatewayClient,
  SmithersGatewayConnection,
  GatewayRpcError,
  gatewayBackoffDelay,
  createSmithersCollections,
  createSmithersDataClient,
  smithersCollectionKeys,
  type WorkspaceMode,
  type SmithersCollections,
} from "smithers-orchestrator/gateway-client";
```

## SmithersGatewayClient

Construct it with a base URL (and optional auth token); call a named RPC
method, or open a WebSocket and subscribe to a run's event stream.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const client = new SmithersGatewayClient({
  baseUrl: "http://127.0.0.1:7331",
  token: process.env.SMITHERS_TOKEN,
});
```

<ParamField path="options" type="SmithersGatewayClientOptions">
  <Expandable title="SmithersGatewayClientOptions">
    <ParamField path="baseUrl" type="string">
      Gateway origin. Defaults to `globalThis.location.origin` in a browser, else
      `http://127.0.0.1:7331`. A trailing slash is stripped; `ws+unix:` URLs are
      supported for local sockets.
    </ParamField>

    <ParamField path="token" type="string">
      Bearer token sent as the `authorization` header on RPC and in the WebSocket
      `connect` handshake.
    </ParamField>

    <ParamField path="headers" type="HeadersInit">
      Extra headers merged into every HTTP RPC request.
    </ParamField>

    <ParamField path="fetch" type="typeof fetch">
      `fetch` override. Defaults to `globalThis.fetch`; calls reject if neither is
      available.
    </ParamField>

    <ParamField path="WebSocket" type="typeof WebSocket">
      `WebSocket` override. Defaults to `globalThis.WebSocket`; `connect` throws if
      neither is available.
    </ParamField>

    <ParamField path="client" type="{ id?, version?, platform? }">
      Client identity sent in the `connect` handshake.
    </ParamField>
  </Expandable>
</ParamField>

### RPC methods

Each method is a thin typed wrapper over `rpc(method, params)`, which POSTs to
`/v1/rpc/<method>` and unwraps the response frame. Params and return shapes come
from the method catalog; see [`/rpc/launch-run`](/rpc/launch-run) for one in
full.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { runId } = await client.launchRun({
  workflow: "review",
  input: {},
  options: { startedBy: { harness: "codex", sessionId: "thread_123" } },
});
await client.submitApproval({ runId, nodeId: NODE_ID, decision: "approve" });
const runs = await client.listRuns();
await client.cancelRun({ runId });
```

<ResponseField name="run lifecycle" type="methods">
  `launchRun`, `resumeRun`, `pauseRun`, `cancelRun`, `hijackRun`, `rewindRun`.
</ResponseField>

`LaunchRunRequest.options.startedBy` accepts `{ harness?, sessionId?, prompt?,
detected?: true }` as durable caller-provided provenance. It is not an auth
credential; use the configured bearer/token identity for access control.

<ResponseField name="human-in-the-loop" type="methods">
  `submitApproval`, `submitSignal`, `listApprovals`.
</ResponseField>

<ResponseField name="reads" type="methods">
  `getRun`, `listRuns`, `listWorkflows`, `getNodeOutput`, `getNodeDiff`, `getRunDiff`,
  `whatHappened`.
</ResponseField>

<ResponseField name="crons" type="methods">
  `cronList`, `cronCreate`, `cronDelete`, `cronRun`.
</ResponseField>

<ResponseField name="escape hatches" type="methods">
  `rpc(method, params, { signal? })` for any typed method, `rpcRaw(method,
      params?, { signal? })` for an untyped one, and `extensionRpc(namespace, key,
      params?)` / `streamExtension(namespace, key, params?)` for gateway extensions.
</ResponseField>

### Subscribing to events

`connect()` opens a `SmithersGatewayConnection`. The higher-level generators
below filter and yield frames for you; most callers iterate one of those
instead of driving the connection directly.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const ac = new AbortController();
for await (const frame of client.streamRunEvents(
  { runId: RUN_ID },
  { signal: ac.signal },
)) {
  console.log(frame.event, frame.payload);
}
```

<ResponseField name="streamRunEvents(params, { signal? })" type="AsyncGenerator<GatewayEventFrame>">
  Subscribes to one run and yields its `run.*` frames (`run.event`,
  `run.gap_resync`, `run.heartbeat`). Pass `afterSeq` to resume. A `run.error`
  frame (the gateway dropping the subscription, e.g. a backpressure disconnect)
  throws a `GatewayRpcError` carrying the gateway's code. Closes the socket when
  iteration ends.
</ResponseField>

<ResponseField name="streamRunEventsResilient(params, options?)" type="AsyncGenerator<GatewayEventFrame>">
  Same frames, but reconnects with backoff + jitter on a silent drop and resumes
  from the last observed `seq`. Stops on terminal completion or abort. Options:
  `signal`, `backoff` ([`GatewayBackoffOptions`](#gatewaybackoffdelay)),
  `healthyAfterMs`, and `onReconnect(event)`.
</ResponseField>

<ResponseField name="streamDevTools(params, { signal? })" type="AsyncGenerator<GatewayEventFrame>">
  Subscribes to a run's DevTools snapshot stream.
</ResponseField>

<ResponseField name="connect({ subscribe?, signal? })" type="Promise<SmithersGatewayConnection>">
  Opens the WebSocket, performs the `connect` handshake (protocol + auth), and
  returns the live connection. Use only for request/response over the socket
  or raw event frames.
</ResponseField>

### Disposal

<ResponseField name="close()" type="void">
  Releases everything the client owns: aborts its in-flight RPCs and streams,
  closes the WebSockets `connect()` opened, and marks the client closed so a
  later call throws instead of opening a new socket. Idempotent, and it never
  aborts a caller's own `AbortSignal`.
</ResponseField>

<ResponseField name="closed" type="boolean">
  Whether `close()` has run.
</ResponseField>

Only the owner closes. `SmithersGatewayProvider` closes the client it built from
`options` when the options rotate one in and on unmount, and never closes a
client you passed as the `client` prop, so a client shared across trees stays
yours to dispose.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const client = new SmithersGatewayClient({ baseUrl: "/" });
try {
  await client.listWorkflows();
} finally {
  client.close();
}
```

### SmithersGatewayConnection

A single open WebSocket: RPC via `request` / `requestRaw`, plus a single
in-order async stream of server event frames via `events(signal?)`. One
consumer per connection; the generators above each own one.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const conn = await client.connect({ subscribe: [RUN_ID] });
const sub = await conn.request("streamRunEvents", { runId: RUN_ID });
for await (const frame of conn.events()) {
  if (frame.event === "run.event") console.log(frame.payload);
}
conn.close();
```

<ResponseField name="request(method, params)" type="Promise<payload>">
  Typed request/response over the socket.
</ResponseField>

<ResponseField name="requestRaw(method, params?)" type="Promise<unknown>">
  Untyped request/response over the socket.
</ResponseField>

<ResponseField name="events(signal?)" type="AsyncGenerator<GatewayEventFrame>">
  In-order server event frames. Ends on close; throws on an error or invalid
  frame.
</ResponseField>

<ResponseField name="close()" type="void">
  Closes the socket, rejects pending requests, and ends `events()`.
</ResponseField>

**Source** [`SmithersGatewayClient.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/SmithersGatewayClient.ts) · [`SmithersGatewayConnection.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/SmithersGatewayConnection.ts) · **Tests** [`SmithersGatewayClient.test.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/tests/SmithersGatewayClient.test.ts) · **See also** [`/rpc/launch-run`](/rpc/launch-run), [Gateway integration](/integrations/gateway)

## GatewayRpcError

The error every RPC rejects with on a failed frame or HTTP error. Inspect `code`
to branch (for example, `requiredScope` is set on an authorization failure).

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
try {
  await client.launchRun({ workflowId: "review", input: {} });
} catch (error) {
  if (error instanceof GatewayRpcError) {
    console.error(error.method, error.code, error.requiredScope);
  }
}
```

<ResponseField name="method" type="string">
  The RPC method that failed (or `"websocket"` for a malformed socket frame).
</ResponseField>

<ResponseField name="code" type="string">
  Machine-readable error code, e.g. `HTTP_ERROR`, `INVALID_GATEWAY_RESPONSE`, or
  the gateway's own code from the response frame.
</ResponseField>

<ResponseField name="status" type="number">
  HTTP status when the failure came over HTTP RPC.
</ResponseField>

<ResponseField name="requiredScope" type="string">
  The scope the call needed, on an authorization failure.
</ResponseField>

<ResponseField name="refresh" type="string">
  Set when the gateway asks the client to refresh credentials.
</ResponseField>

<ResponseField name="details" type="unknown">
  Extra context attached by the gateway.
</ResponseField>

**Source** [`GatewayRpcError.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/GatewayRpcError.ts) · **Tests** [`gateway-client.test.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/tests/gateway-client.test.ts)

## isGatewayUnavailableError

True when nothing that speaks the gateway protocol answered the URL: a 2xx
response whose body isn't an `{ ok, ... }` envelope, or a fetch-level
rejection (connection refused, DNS failure) because nothing is listening. The
usual cause is a host app's SPA fallback serving `index.html` for `/v1/api/*`
because no gateway is wired. The data client throws these as code
`GATEWAY_UNAVAILABLE`, and the QueryCollection layer already degrades them
quietly: collections settle on the last rows a real gateway served (empty
before the first success), one session-level `console.info` notice replaces
the per-collection `[QueryCollection]` error spam, and the SSE change stream
refuses non-`text/event-stream` answers so the connection status parks
`offline` instead of flapping online. Recovery is automatic: the stream's
reconnect backoff keeps probing, and the reset emitted on a real reconnect
refetches every collection. Branch on it when calling `api.*` directly for the
same behavior.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
try {
  await client.api.listRuns();
} catch (error) {
  if (isGatewayUnavailableError(error)) return []; // no gateway here; show empty state
  throw error;
}
```

Real gateway errors and non-2xx HTTP failures never classify as unavailable.

**Source** [`isGatewayUnavailableError.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/isGatewayUnavailableError.ts) · **Tests** [`gatewayUnavailableQuietDegrade.test.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/tests/data/gatewayUnavailableQuietDegrade.test.ts)

## gatewayBackoffDelay

Exponential backoff with full jitter for one 0-based attempt. The resilient
stream uses it internally; call it directly for your own reconnect loop.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const delayMs = gatewayBackoffDelay(attempt, { baseMs: 250, maxMs: 10_000 });
```

<ParamField path="attempt" type="number" required>
  0-based attempt index. The base delay grows by `factor ** attempt`, capped at
  `maxMs`.
</ParamField>

<ParamField path="options" type="GatewayBackoffOptions">
  <Expandable title="GatewayBackoffOptions">
    <ParamField path="baseMs" type="number" default="250">
      Base delay before growth and jitter.
    </ParamField>

    <ParamField path="maxMs" type="number" default="10000">
      Ceiling on the pre-jitter delay.
    </ParamField>

    <ParamField path="factor" type="number" default="2">
      Per-attempt growth multiplier.
    </ParamField>

    <ParamField path="jitter" type="number" default="0.5">
      Symmetric jitter fraction applied to the delay.
    </ParamField>

    <ParamField path="random" type="() => number" default="Math.random">
      RNG override for deterministic tests.
    </ParamField>
  </Expandable>
</ParamField>

<ResponseField name="number" type="number">
  Milliseconds to wait, never negative.
</ResponseField>

**Source** [`gatewayBackoffDelay.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/gatewayBackoffDelay.ts) · **Tests** [`gatewayBackoffDelay.test.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/tests/gatewayBackoffDelay.test.ts)

## createSmithersCollections

Builds the TanStack DB collection registry. Local mode uses the Gateway REST
domain API plus `/v1/api/stream` SSE invalidation. Multiplayer mode uses
Electric shapes from `electricBaseUrl` for reads and keeps the same domain API
write path.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { QueryClient } from "@tanstack/react-query";

const queryClient = new QueryClient();
const mode: WorkspaceMode = {
  kind: "local",
  apiBaseUrl: "http://127.0.0.1:7331",
};
const collections = createSmithersCollections(mode, queryClient);
```

<ParamField path="mode" type="WorkspaceMode" required>
  `{ kind: "local", apiBaseUrl, token? }` or `{ kind: "multiplayer",
      apiBaseUrl, electricBaseUrl, workspaceId, token? }`.
</ParamField>

<ParamField path="queryClient" type="QueryClient" required>
  The TanStack Query client used by QueryCollection and invalidation.
</ParamField>

<ResponseField name="SmithersCollections" type="object">
  Registry methods include `runs`, `run`, `runTree`, `runEvents`, `nodes`,
  `approvals`, `workflows`, `docs`, `prompts`, `scores`, `tickets`,
  `memoryFacts`, and `crons`, plus `connect`, `invalidate`, and `close`.
</ResponseField>

**Source** [`createSmithersCollections.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/data/createSmithersCollections.ts) · **Tests** [`smithers-collections`](https://github.com/smithersai/smithers/tree/main/packages/gateway-client/tests)

## createSmithersDataClient

Creates the domain API client that collection mutation handlers call. Reads and
writes target `/v1/api/*`; `stream.subscribe` opens `/v1/api/stream` and emits
`change`, `reset`, and `heartbeat` invalidation events.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const dataClient = createSmithersDataClient({
  mode: { kind: "local", apiBaseUrl: "http://127.0.0.1:7331" },
});

const runs = await dataClient.api.listRuns({ filter: { limit: 20 } });
const ack = await dataClient.api.cancelRun({ runId: runs[0]?.runId ?? "" });
```

<ParamField path="options.mode" type="WorkspaceMode" required>
  Workspace mode and auth token.
</ParamField>

<ParamField path="options.fetch" type="typeof fetch">
  Fetch override for tests or custom runtimes.
</ParamField>

<ParamField path="options.EventSource" type="typeof EventSource">
  SSE implementation override. When omitted, the client falls back to streaming
  `fetch`.
</ParamField>

<ParamField path="options.headers" type="HeadersInit">
  Extra headers merged into every `/v1/api/*` request and the change stream, so
  an API-key or proxy header authorizes the collection API and SSE exactly as it
  does RPC. The content type and `authorization: Bearer <mode.token>` win over
  conflicting entries. `SmithersGatewayProvider` forwards the gateway client's
  `headers` here automatically.
</ParamField>

<ResponseField name="SmithersDataClient" type="object">
  `{ mode, api, stream, close }`. The `api` object covers run lifecycle,
  approvals, signals, crons, docs, prompts, memory facts, scores, tickets, node
  output, node diffs, and schema signatures.
</ResponseField>

## Collection keys and row types

`smithersCollectionKeys` holds the TanStack Query keys used by the registry.
The same package exports the row types: `GatewayRunRow`,
`GatewayRunSummaryRow`, `GatewayRunEventRow`, `GatewayRunNode`,
`GatewayApprovalRow`, `GatewayWorkflowRow`, `GatewayCronRow`,
`GatewayMemoryFactRow`, `GatewayScoreRow`, `GatewayTicketRow`, and
`GatewayPromptRow`.

Run-tree helpers stay public for custom inspectors: `flattenGatewayRunNode`,
`snapshotToGatewayRunNode`, and `reconcileSnapshotNodes`.

***

**Source** [`index.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/index.ts) · **Tests** [`packages/gateway-client/tests`](https://github.com/smithersai/smithers/tree/main/packages/gateway-client/tests) · **See also** [Gateway React API](/reference/gateway-react), [Gateway integration](/integrations/gateway), [`/rpc/launch-run`](/rpc/launch-run)
