React
useEveAgent is the React layer over the Eve session protocol. It creates or
resumes a session, sends turns, streams events, and reduces those events into
renderable message data.
Basic usage
npm install flue-eveimport { useEveAgent } from 'flue-eve/react'
function Chat() {
const agent = useEveAgent()
const busy = agent.status === 'submitted' || agent.status === 'streaming'
return (
<form
onSubmit={(event) => {
event.preventDefault()
const form = new FormData(event.currentTarget)
const message = String(form.get('message') ?? '').trim()
if (message) void agent.send({ message })
}}
>
{agent.data.messages.map((message) => (
<article key={message.id}>
<header>{message.role}</header>
{message.parts.map((part, index) =>
part.type === 'text' ? <p key={index}>{part.text}</p> : null,
)}
</article>
))}
<input name="message" disabled={busy} />
<button disabled={busy} type="submit">Send</button>
</form>
)
}Returned state
The hook returns a session snapshot plus commands:
| Property | Type | Description |
|---|---|---|
data | { messages } by default | Reducer output for rendering |
status | "ready" | "submitted" | "streaming" | "error" | Composer and error state |
error | Error | undefined | Last non-recoverable error |
events | EveEvent[] | Authoritative server events received by the hook |
session | SessionState | Serializable cursor with sessionId, token, and index |
send | (input) => Promise<void> | Send a message or HITL response |
stop | () => void | Abort the current HTTP stream |
reset | () => void | Clear local state and start a fresh session |
The default reducer returns { messages }. Each message has a role and typed
parts:
type EveMessage = {
id: string
role: 'user' | 'assistant'
parts: EveMessagePart[]
metadata?: {
status?: 'submitted' | 'streaming' | 'complete' | 'failed'
turnId?: string
}
}Persist sessions
Persist the full cursor so the next page load can resume from the right stream index:
import { useState } from 'react'
import { loadSessionState, saveSessionState } from 'flue-eve/client'
import { useEveAgent } from 'flue-eve/react'
function Chat() {
const [initialSession] = useState(() => loadSessionState(localStorage))
const agent = useEveAgent({
initialSession,
onSessionChange: (session) => saveSessionState(localStorage, session),
})
// ...
}Human-in-the-loop
When the agent pauses for input, the stream emits input.requested. The default
reducer attaches the request to a dynamic-tool part under
part.toolMetadata?.eve?.inputRequest:
const request = agent.data.messages
.at(-1)
?.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' }],
})
}Custom host, auth, and headers
By default, the hook calls same-origin /eve/v1/*. Pass host, auth, or
headers for split-origin or protected deployments:
const agent = useEveAgent({
host: 'https://api.example.com',
auth: {
bearer: async () => await getAccessToken(),
},
headers: async () => ({
'x-request-id': crypto.randomUUID(),
}),
})Client context per turn
Use prepareSend to attach ephemeral context to each turn:
const agent = useEveAgent({
prepareSend: (input) => ({
...input,
clientContext: {
route: location.pathname,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
}),
})clientContext is sent with the next turn only. It is useful for current page
state, selected records, or tenant hints that should not become durable
conversation history.
See React guide for advanced patterns including custom reducers and lifecycle callbacks.