useLoushoAgent() puts an agent behind a chat UI. It runs a turn, reads the
typed event stream as it arrives and turns it into
render-ready state: messages with their text and tool calls, a status for
the composer, and the tool call waiting for approval, if any.
It lives in the @lousho/build-ai-agent/react subpath. react (18 or 19) is
an optional peer dependency: install it in the app that uses the hook.
Sources
The first argument says where the agent runs.
In-process mode is for React code that runs where the agent can (Electron,
React Native, tests). In a browser, use the remote mode: model API keys belong
on the server.
Returned state
Unmounting the component aborts the turn in flight. Events of a
sub-agent’s run (they carry
subagent) do not change
messages; read them from lastEvent if you want to show them.
Approvals
When a tool withneedsApproval is called, the run stops with
approval.requested. status becomes 'awaiting-approval' and
pendingApproval holds the call. Then:
- In process,
approve(note?)andreject(note?)callagent.approvals.resolve({ id, approved, note })(see Approvals). The continued run’s text is appended to the assistant message andstatusreturns to'idle', or to'awaiting-approval'if the run pauses again. - Remote, pass
{ approvalsUrl }as the second argument. The hook POSTs{ "approved": true, "note": "..." }to${approvalsUrl}/${approvalId}and shows the continuation live from the SSE stream the session API answers with (the same events as a chat turn; a second pause arrives asapproval.requested). A server that answers with anApprovalOutcomeJSON instead (the one below) still works. - Remote without
approvalsUrl,approve()andreject()do nothing. ShowpendingApprovaland resolve it through your own API, thensend()the next turn.
askQuestion: true can ask the user something (see
Asking the user a question). The
pause arrives the same way, with pendingApproval.kind === 'question': show
pendingApproval.question.text and its options, and call answer(text).
The server side
A Node endpoint for the remote mode, with the approvals route:useLoushoAgent({ url: '/api/agent' }, { approvalsUrl: '/api/approvals' }).
For a multi-turn chat, keep an agent.session({ id }) per conversation on the
server and call session.stream() instead of agent.stream().
In process
The hook takes the agent itself, too. Called from a custom hook:Without React
The hook is a thin wrapper. The state logic isreduceAgentEvents(state, event), a pure reducer over AgentEvents and a few local actions
(ui.send, ui.decide, ui.resumed, ui.stopped, ui.error), and
parseEventStream(response) reads events back from a fetch response. Both are
exported from the same subpath, for a custom binding or a non-React client:
parseEventStream() skips blank lines, SSE comments and event:/id:
fields, and any line that is not a known event, so it reads both framings.
Breaking out of its loop cancels the response body.