Skip to main content
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 with needsApproval is called, the run stops with approval.requested. status becomes 'awaiting-approval' and pendingApproval holds the call. Then:
  • In process, approve(note?) and reject(note?) call agent.approvals.resolve({ id, approved, note }) (see Approvals). The continued run’s text is appended to the assistant message and status returns 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 as approval.requested). A server that answers with an ApprovalOutcome JSON instead (the one below) still works.
  • Remote without approvalsUrl, approve() and reject() do nothing. Show pendingApproval and resolve it through your own API, then send() the next turn.
An agent created with 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:
The client then uses 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 is reduceAgentEvents(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.