FabricFabricHarness
Building Agents

React applications

Build agent conversations and finite-run interfaces with the public Fabric client protocol.

@fabric-harness/react provides FabricProvider, useFabricClient, useFabricAgent, and useFabricJob. The package talks only to the public authenticated HTTP protocol, so the same UI can target local development, a Node artifact, Databricks Apps, or another compatible deployment.

pnpm add @fabric-harness/react @fabric-harness/client react

Configure the provider

Create one client for the application. Authentication headers stay in the application transport and are not placed in agent messages or model context.

src/main.tsx
import { FabricProvider } from '@fabric-harness/react';

export function Root({ children }: { children: React.ReactNode }) {
  return (
    <FabricProvider options={{
      baseUrl: '/api',
      headers: { authorization: `Bearer ${window.fabricToken}` },
    }}>
      {children}
    </FabricProvider>
  );
}

For server-rendered frameworks, pass a request-scoped client through client. Hooks use stable external-store server snapshots and start network observation only after the component mounts.

Agent conversations

useFabricAgent catches up from offset zero, follows new records, deduplicates reconnect replay, and applies truncation records to the canonical transcript. Optimistic messages remain separate in pending until their submission-correlated input is observed.

import { useFabricAgent } from '@fabric-harness/react';

function SupportChat({ customerId }: { customerId: string }) {
  const chat = useFabricAgent({ agent: 'support', id: customerId, session: 'default' });

  async function send(text: string) {
    const submissionId = await chat.send(text);
    const settled = await chat.wait(submissionId, { timeoutMs: 30_000 });
    console.log(settled.outcome);
  }

  return (
    <>
      {chat.transcript.map((entry) => <p key={entry.id}>{String(entry.data?.text ?? '')}</p>)}
      {chat.pending.map((message) => (
        <button key={message.id} disabled={message.status !== 'failed'} onClick={() => chat.retry(message.id)}>
          {message.status}: retry
        </button>
      ))}
      <button onClick={() => send('Check account status')}>Send</button>
      <button onClick={() => chat.abort()}>Abort</button>
    </>
  );
}

Send image attachments with the normal delivered-message shape:

await chat.send({
  kind: 'user',
  body: 'Inspect this screenshot',
  attachments: [{ type: 'image', mimeType: 'image/png', data: base64 }],
});

Finite runs

useFabricJob admits an asynchronous run, follows its offset events, publishes the terminal run envelope, and exposes retry and abort. Starting a newer invocation supersedes the previous observation, so a late terminal response from the old run cannot replace the new run's state. Use invokeSync only for short request/response work.

const report = useFabricJob<{ accountId: string }, { url: string }>('account-report');

await report.invoke(
  { accountId: 'account-42' },
  { idempotencyKey: 'account-report:account-42' },
);

console.log(report.status, report.run?.output?.url);
await report.abort();

Agent observation reconnects with bounded exponential backoff and resets the delay after receiving a record. Stores with no subscribers or in-flight work are evicted, which keeps long-lived applications with dynamic agent and job addresses bounded.

Runnable application

examples/react-chat starts fh dev --mock and Vite with one pnpm dev command. It includes multiple persistent agent instances, image attachments, optimistic retry, abort, canonical transcript streaming, and a finite job panel. Its automated browser check captures desktop/mobile layouts and fails on browser errors or mobile horizontal overflow.