# From monorepo root:
pnpm --filter @openuidev/react-headless run build # tsc → dist/
pnpm --filter @openuidev/react-headless run test # vitest
pnpm --filter @openuidev/react-headless run ci # lint:check + format:check
# Or from this directory:
pnpm build && pnpm testBuild order: react-headless → react-lang → react-ui. This package has no upstream workspace deps (only @ag-ui/core from npm), so it can always build independently.
| Path | Purpose |
|---|---|
src/index.ts |
Public API surface — every export consumers see. Check here first when adding/removing exports. |
src/store/createChatStore.ts |
Zustand store factory. All state + actions live here. This is the most critical file. |
src/store/ChatProvider.tsx |
React provider — thin wrapper that creates the store once via useState. |
src/store/ChatContext.ts |
React context + useChatStore() internal hook. |
src/store/hooks.ts |
useThread() / useThreadList() — selector hooks over the store. |
src/store/types.ts |
All store types: ChatStore, ChatProviderProps, Thread, state/action slices. |
src/store/__tests__/createChatStore.test.ts |
Comprehensive test suite for the store (thread CRUD, message CRUD, streaming, cancellation, URL-based defaults, messageFormat round-trips). |
src/stream/processStreamedMessage.ts |
Consumes AsyncIterable<AGUIEvent> and drives message create/update/delete callbacks. |
src/stream/adapters/ag-ui.ts |
Default SSE adapter — parses data: {json}\n lines. |
src/stream/adapters/openai-completions.ts |
Adapter for OpenAI Chat Completions streaming (ChatCompletionChunk). |
src/stream/adapters/openai-responses.ts |
Adapter for OpenAI Responses API streaming (ResponseStreamEvent). |
src/stream/adapters/openai-readable-stream.ts |
Adapter for OpenAI SDK's Stream.toReadableStream() — parses NDJSON (no SSE prefix) ChatCompletionChunk objects. |
src/stream/adapters/openai-message-format.ts |
MessageFormat for OpenAI Completions (ChatCompletionMessageParam[] ↔ AG-UI). |
src/stream/adapters/openai-conversation-message-format.ts |
MessageFormat for OpenAI Responses/Conversations API (ResponseInputItem[] ↔ AG-UI). |
src/types/ |
Shared types: message.ts (re-exports from @ag-ui/core), messageFormat.ts, stream.ts. |
src/hooks/useMessage.tsx |
MessageContext / MessageProvider / useMessage — per-message React context used by react-ui. |
- Add the type to the appropriate slice in
src/store/types.ts(ThreadActionsorThreadListActions). - Implement it in
createChatStore.tsinside thecreateStore<ChatStore>(...)call. - Add it to the selector in
src/store/hooks.ts(threadSelectororthreadListSelector). - Add tests in
src/store/__tests__/createChatStore.test.ts. - If it should be public, export the type from
src/index.ts.
- Create
src/stream/adapters/my-adapter.tsimplementingStreamProtocolAdapter(must haveasync *parse(response): AsyncIterable<AGUIEvent>). - Export it from
src/stream/adapters/index.ts. - Export it from
src/index.ts.
- Create a file in
src/stream/adapters/implementing theMessageFormatinterface (toApi+fromApi). - Export it from
src/stream/adapters/index.tsandsrc/index.ts.
ChatProviderProps uses a discriminated union so TypeScript enforces "URL string OR custom functions, not both":
ThreadApiConfig: providethreadApiUrlstring or individual functions (fetchThreadList,createThread, etc.)ChatApiConfig: provideapiUrlstring or aprocessMessagefunction
When threadApiUrl is given, createChatStore generates default REST implementations (/get, /create, /delete/:id, /update/:id, /get/:id).
processMessage() in the store → fetch(apiUrl) → StreamProtocolAdapter.parse(response) → yields AGUIEvents → processStreamedMessage() accumulates text deltas and tool calls into an AssistantMessage, calling createMessage on first event and updateMessage on subsequent events.
The openai package is a devDependency only (for type imports). It is not bundled. The adapter files import types like ChatCompletionChunk, ResponseStreamEvent, etc. using import type so there is no runtime dependency.
Tests use Vitest and mock fetch via vi.stubGlobal. Streaming tests create ReadableStream instances with hand-crafted SSE payloads. Use flushPromises() (a setTimeout(0) wrapper) to await async store updates.
pnpm test # run all tests
pnpm vitest run --reporter verbose # verbose output- Message types — all message types (
Message,UserMessage,AssistantMessage, etc.) come from@ag-ui/core. Do not redefine them; only re-export. - Store shape —
ChatStoreis a flat Zustand store. Do not nest slices into sub-objects; hooks rely on the flat structure. identityMessageFormat— this is the default no-op format. It must remain{ toApi: (m) => m, fromApi: (d) => d as Message[] }._abortController/_nextCursor— these are internal fields prefixed with_. Do not expose them in hooks or types.ChatProviderstore creation — the store is created once viauseState(() => createChatStore(config)). It intentionally does not react to config changes after mount.