flue-eve

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-eve
import { 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:

PropertyTypeDescription
data{ messages } by defaultReducer output for rendering
status"ready" | "submitted" | "streaming" | "error"Composer and error state
errorError | undefinedLast non-recoverable error
eventsEveEvent[]Authoritative server events received by the hook
sessionSessionStateSerializable cursor with sessionId, token, and index
send(input) => Promise<void>Send a message or HITL response
stop() => voidAbort the current HTTP stream
reset() => voidClear 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.