@lumifai/harness-react
v0.1.0
Published
Headless React bindings for the Lumif harness browser client: provider, hooks, SSE client, and optional client-side session persistence. The shared browser client and state reducer live in `@lumifai/harness-client`.
Readme
@lumifai/harness-react
Headless React bindings for the Lumif harness browser client: provider, hooks, SSE
client, and optional client-side session persistence. The shared browser client and
state reducer live in @lumifai/harness-client.
Usage
import {
HarnessProvider,
createLocalStorageHarnessSessionStore,
useHarnessSessionStore,
useHarnessActions,
useHarnessState,
} from '@lumifai/harness-react';
export function App() {
const { bindings, resetSession, sessionKey } = useHarnessSessionStore({
store: createLocalStorageHarnessSessionStore({
storageKey: 'my-harness.session',
}),
});
return (
<>
<button onClick={resetSession}>New conversation</button>
<HarnessProvider
key={sessionKey}
baseUrl=""
prefix="/harness"
autoConnect
clientOptions={{
credentials: 'include',
headers: async () => ({ Authorization: 'Bearer <token>' }),
}}
{...bindings}
>
<Chat />
</HarnessProvider>
</>
);
}
function Chat() {
const { messages, displayState } = useHarnessState();
const { sendMessage } = useHarnessActions();
// render your own UI
}For a Mantine-based studio UI, use @lumifai/harness-react-mantine.
clientOptions passes auth and transport settings through to the browser client. Use it for bearer headers, cookies, custom fetch, or a custom EventSource factory.
The provider treats the server snapshot as authoritative after an SSE error. It
refreshes the snapshot when the stream fails and reports stream connectivity through
connected; the latest run failure is available as displayState.lastError.
Human-in-the-loop
displayState surfaces pending approvals, questions, plan reviews, and tool suspensions. Pending suspensions can be concurrent; use each entry's toolCallId, pendingActionId, and runId to target the correct resume action after reconnect.
Helpers from @lumifai/harness-client (re-exported here) parse suspend payloads:
import {
defineSuspensionRenderer,
HarnessProvider,
useHarnessActions,
useHarnessState,
} from '@lumifai/harness-react';
const confirmPurchaseRenderer = defineSuspensionRenderer({
toolName: 'confirm_purchase',
parse: (suspension) => {
const payload = suspension.suspendPayload;
if (!payload || typeof payload !== 'object') return null;
const { item } = payload as { item?: unknown };
return typeof item === 'string' ? { item } : null;
},
formatDecision: ({ toolCall, toolResult }) => {
const item =
toolCall.args &&
typeof toolCall.args === 'object' &&
'item' in toolCall.args &&
typeof (toolCall.args as { item?: unknown }).item === 'string'
? (toolCall.args as { item: string }).item
: 'item';
const confirmed =
toolResult.result &&
typeof toolResult.result === 'object' &&
'confirmed' in toolResult.result
? Boolean((toolResult.result as { confirmed: unknown }).confirmed)
: false;
return {
label: 'Purchase',
summary: `${item} → ${confirmed ? 'confirmed' : 'cancelled'}`,
};
},
render: ({ payload, resume, busy }) => (
<button disabled={busy} onClick={() => void resume({ confirmed: true })}>
Confirm {payload.item}
</button>
),
});
<HarnessProvider suspensionRenderers={[confirmPurchaseRenderer]} ...>
...
</HarnessProvider>resumeToolSuspension accepts the tool's resume payload — for example { answer: 'yes' } for
ask_user, { approved: true } for request_access, or { action: 'approved' } for submit_plan.
Always pass toolCallId when more than one suspension is pending.
Studio’s conversation transcript renders completed HITL tools as collapsed decision chips
(question → answer). Expand a chip to see full detail. Chips are built from message
tool_call/tool_result pairs via optional formatDecision on each renderer.
Tool progress cards
Long-running tools can stream progress via Mastra writer.custom with
type: 'data-mastracode-tool-progress' (or sandbox stdout/stderr). That updates
displayState.activeTools[toolCallId].partialResult / shellOutput.
Opt into progress cards with enableToolProgressCards and optional per-tool renderers
(default card when a tool has emitted progress but no custom renderer matches):
import {
defineToolProgressRenderer,
HarnessProvider,
} from '@lumifai/harness-react';
const longJobProgress = defineToolProgressRenderer({
toolName: 'long_job',
render: ({ tool }) => <pre>{tool.partialResult}</pre>,
});
<HarnessProvider
enableToolProgressCards
toolProgressRenderers={[longJobProgress]}
...
>
...
</HarnessProvider>HarnessStudio accepts the same props (enableToolProgressCards is OR'd with the
provider flag). Cards appear only when the tool has partialResult or shellOutput.
Parallel calls of the same tool are separate entries keyed by toolCallId in
activeTools. Progress updates only touch that call’s slot. Custom renderers receive
{ toolCallId, tool } (including tool.args) so you can label each instance; the
default card shows toolCallId plus a short args summary.
Progress is turn-scoped in live activeTools. The server also persists
progress-bearing activeTools so a reconnect / cold session load can rehydrate the
last snapshot for the UI — it does not resume the tool process itself.
Custom session store
Implement HarnessSessionStoreAdapter for Zustand, Redux, IndexedDB, etc.:
import type { HarnessSessionStoreAdapter } from '@lumifai/harness-react';
const zustandStore: HarnessSessionStoreAdapter = {
load: () => useMyStore.getState().harnessSession,
save: (session) => useMyStore.getState().setHarnessSession(session),
clear: () => useMyStore.getState().clearHarnessSession(),
};
useHarnessSessionStore({ store: zustandStore });