React Guide
flue-eve/react exports useEveAgent, the React wrapper around
ClientSession. It keeps the Eve frontend pattern: send a turn, stream events,
reduce them into UI state, and hold a serializable session cursor.
Hook options
import { useEveAgent } from 'flue-eve/react'
const agent = useEveAgent({
host: 'https://agent.example.com',
auth: {
bearer: async () => await getAccessToken(),
},
headers: async () => ({
'x-request-id': crypto.randomUUID(),
}),
maxReconnectAttempts: 3,
onError: (error) => console.error(error),
onEvent: (event) => console.debug(event.type),
})Same-origin apps usually omit host.
Return value
| Property | Type | Description |
|---|---|---|
data | Reducer output | { messages } by default |
status | "ready" | "submitted" | "streaming" | "error" | Current turn state |
error | Error | undefined | Last fatal error |
events | EveEvent[] | Raw authoritative stream events |
session | SessionState | Cursor for persistence |
send | (input) => Promise<void> | Send message, context, output schema, or HITL responses |
stop | () => void | Abort the active stream |
reset | () => void | Clear local state and create a fresh session |
Custom reducers
The default reducer returns { messages }. Custom reducers can project the
same event stream into any shape:
import { useEveAgent, type EveAgentReducer } from 'flue-eve/react'
type ToolStats = { toolCalls: number }
const reducer: EveAgentReducer<ToolStats> = {
initial: () => ({ toolCalls: 0 }),
reduce: (data, event) =>
event.type === 'actions.requested'
? { toolCalls: data.toolCalls + event.data.actions.length }
: data,
}
const agent = useEveAgent({ reducer })Reducer input includes both server events and client projection events such as
client.message.submitted, client.message.failed, and
client.input.responded.
Human-in-the-loop
HITL requests arrive as input.requested events. The default reducer exposes
them on dynamic-tool parts:
const request = agent.data.messages
.flatMap((message) => message.parts)
.find((part) => part.type === 'dynamic-tool' && part.toolMetadata?.eve?.inputRequest)
?.toolMetadata?.eve?.inputRequest
if (request) {
await agent.send({
inputResponses: [
{
requestId: request.requestId,
optionId: 'approve',
},
],
})
}The hook projects client.input.responded immediately, then updates the message
again when the server streams the resumed result.
Persistence
Use the helper for localStorage-backed session cursors:
import { createEveSessionPersistence, useEveAgent } from 'flue-eve/react'
const persistence = createEveSessionPersistence({ storage: localStorage })
const agent = useEveAgent({
...persistence,
host: 'https://agent.example.com',
})The saved state contains sessionId, continuationToken, and streamIndex.
Keep all three fields.
Client context
Attach ephemeral page state with prepareSend:
const agent = useEveAgent({
prepareSend: (input) => ({
...input,
clientContext: {
route: location.pathname,
selectedProjectId,
},
}),
})clientContext affects the next turn only and does not replace durable session
history.
Cleanup and stopping
The hook aborts an active stream when the component unmounts. Call stop() when
the user cancels a turn:
const busy = agent.status === 'submitted' || agent.status === 'streaming'
return busy ? <button onClick={agent.stop}>Stop generating</button> : nullAfter an abort, the hook returns to ready and keeps the partial message state
that was already reduced.
See getting started with React for a smaller end-to-end chat component.