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 reactConfigure 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.
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, streams text, reasoning, and tool-argument deltas over
SSE, replaces transient parts with the canonical durable assistant message, deduplicates reconnect
replay, resets stale projections when the stream incarnation changes, and applies truncation
records. Render the stable messages projection; transcript remains available
for diagnostics and compatibility. 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.messages
.filter((message) => message.display === 'visible')
.map((message) => (
<p key={message.id}>
{message.parts
.filter((part) => part.type === 'text')
.map((part) => part.text)
.join('')}
</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.refresh()}>Refresh</button>
<button onClick={() => chat.abort()}>Abort</button>
</>
);
}While a tool call is still streaming, its message part has
{ type: 'dynamic-tool', state: 'input-streaming', inputText }. Treat inputText as untrusted,
partial JSON; only input-available contains the provider's completed parsed input.
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 from the latest durable offset with bounded exponential backoff and
resets the delay after receiving an update. Set live: 'poll' in createFabricAgentStore when SSE
is intentionally unavailable; the default is automatic SSE with polling fallback. 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 UI renders the public conversation-message projection, including streaming text and
tool states. The example consumes @fabricorg/ui for shared Fabric semantic tokens and operator
controls; this UI package remains an application dependency and is not required by the Harness SDK
or runtime. Its automated browser check captures desktop/mobile light and dark layouts and fails on
browser errors or mobile horizontal overflow.