Runnable browser-safe boundary

Consume a hosted run from a remote client

Install the independent remote package when run events cross an untrusted transport boundary or a client can disconnect while work continues.

Customization depth

Client protocol

Hosting depth

Browser or remote JavaScript runtime

Documentation status

Runnable reference

Assumptions

  • The server exposes an authorized run lifecycle and a documented public payload contract.
  • The client can persist the last fully handled sequence when reload recovery is required.
  • Your application still owns timers, UI state, retry presentation, and product result handling.
Heddle owns
  • Runtime validation of the run envelope and host-supplied activity and result schemas.
  • Cursor advancement, duplicate suppression, sequence-gap failure, terminal detection, and bounded retry calculation.
  • Optional fetch lifecycle, incremental SSE parsing, response validation, identity checks, and reader cleanup.
Your product owns
  • Which fields are authorized for remote users and the schemas that enforce that allowlist.
  • Authentication headers, transport timers, subscription handles, cursor persistence, online/offline policy, and retry UX.
  • Messages, tool rendering, optimistic state, notifications, errors, and application of terminal results.

Install only the browser-safe package

Shell
npm install @roackb2/heddle-remote zod

Do not bundle @roackb2/heddle into a browser. The independent remote package excludes the Node agent runtime, model providers, server, CLI, and control plane. This tutorial uses Zod; substitute another synchronous Standard Schema validator only if you install it directly and adjust the imports.

Define the public event contract

TypeScript
import { z } from 'zod'
import { ConversationRunProtocolCodec } from '@roackb2/heddle-remote'

const protocol = new ConversationRunProtocolCodec({
  activity: z.object({
    type: z.string().min(1),
    text: z.string().optional(),
  }),
  result: z.object({
    outcome: z.string().min(1),
    summary: z.string(),
  }),
})

Validators use the Standard Schema interface. Zod 3.24+, Zod 4, Valibot, ArkType, and compatible validators work without Heddle-specific adapters. Validation must be synchronous because parsing and serialization happen within the stream.

The schema output is the public projection. The codec validates your choice; it does not decide authorization or sanitize secrets for you. With Zod, ordinary object schemas strip unknown fields. Configure equivalent allowlist behavior explicitly in another validator.

The codec validates a non-empty runId, positive safe-integer sequence, ISO timestamp, one of activity, result, cancelled, or error, the host payload, and JSON-safe values. It rejects bigint, functions, symbols, non-finite numbers, and undefined that cannot safely cross JSON.

Let the consumer own cursor correctness

The following composition excerpt assumes runId came from an accepted start response and restoredSequence came from product-owned persisted UI state. The linked runnable example shows the complete lifecycle.

TypeScript
import { ConversationRunConsumerService } from '@roackb2/heddle-remote'

const consumer = new ConversationRunConsumerService<{ runId: string }>({
  retry: { maxAttempts: 6, baseDelayMs: 500, maxDelayMs: 4_000 },
})

consumer.select({ runId }, { afterSequence: restoredSequence })

Before subscribing, call consumer.subscriptionInput(). For each validated event, call consumer.accept(event) before rendering it. An accepted event advances the cursor; a duplicate is ignored; an event for another run is ignored; a sequence gap throws; a result, cancellation, or error marks terminal; a later post-terminal event is rejected.

Persist subscriptionInput().afterSequence only after your application has successfully handled the corresponding event. Otherwise a reload can skip data the user never saw.

If the stream disconnects before terminal, call consumer.nextRetry(). Heddle calculates the bounded delay and canonical next cursor. Your application owns the actual timer, abort controller, online/offline behavior, and visible retry state. Accepted progress resets the attempt budget.

Opt into the REST/SSE client

When the server uses the conventional run resource, import the preset:

This is a composition excerpt. StartRunResultSchema and CancelRunResultSchema must be the same public schemas used by the server; accessToken, sessionId, prompt, and the AbortController named subscription come from your authenticated product flow. Use the linked runnable browser client for a complete file.

TypeScript
import { ConversationRunHttpSseClient } from '@roackb2/heddle-remote/http-sse'

const client = new ConversationRunHttpSseClient({
  baseUrl: '/api/agent',
  protocol,
  accepted: StartRunResultSchema,
  cancellation: CancelRunResultSchema,
  getHeaders: () => ({ Authorization: `Bearer ${accessToken}` }),
})

const accepted = await client.start({ sessionId, prompt })
await client.subscribe({
  runId: accepted.runId,
  afterSequence: consumer.subscriptionInput()?.afterSequence,
  signal: subscription.signal,
  onEvent(event) {
    const acceptance = consumer.accept(event)
    if (acceptance.accepted) renderProductEvent(event)
  },
})

The client owns URL encoding, header composition, HTTP error decoding, response validation, incremental SSE parsing, event identity checks, reader cleanup, and abort propagation. It does not own reconnect timers or UI state.

Bearer-authenticated browser streams use fetch because native EventSource cannot attach an Authorization header. Cookie-authenticated products may use EventSource and let the browser send Last-Event-ID during reconnect.

Scope of support

The maintained package targets JavaScript and TypeScript. A non-JavaScript client may implement the documented wire envelope and lifecycle, but Heddle does not currently ship an official client for other languages. Treat those integrations as adaptations, not drop-in support.

Canonical sources