flue-eve

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

PropertyTypeDescription
dataReducer output{ messages } by default
status"ready" | "submitted" | "streaming" | "error"Current turn state
errorError | undefinedLast fatal error
eventsEveEvent[]Raw authoritative stream events
sessionSessionStateCursor for persistence
send(input) => Promise<void>Send message, context, output schema, or HITL responses
stop() => voidAbort the active stream
reset() => voidClear 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> : null

After 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.