feat(api-client): conversation + agent hooks + pure SSE streaming transport #344
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol#344
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The built-in agent's full API surface now exists (conversations CRUD #338, streaming SSE #340, proposal read #343, commit/reject #51) but
@carol/api-clienthas no hooks for it — the chat UI (next ticket) needs them. This adds the data layer: TanStack Query hooks over the typed client for everything non-streaming, plus a pure, injectable SSE streaming transport the UI will wire to token-by-token output.Scope boundary: the runtime base-URL resolution + auth + platform fetch (
expo/fetchon native) live in the app (apps/client/lib/apiClient.ts), and the package must stay platform-neutral (web bundles can't pull react-native). So the streaming transport here is a standalone function with an injectedfetch— the app wiresexpo/fetch+ the runtime URL in the chat-UI ticket. Noexpo/react-nativeimport in the package.Scope
packages/api-client/src/hooks/conversations.ts), viauseApiClient()exactly likehooks/notes.ts:useConversations()(first page) +useConversationsInfinite()—GET /api/conversationsis cursor-paginated ({ data, next_cursor }).useConversation(id)—GET /api/conversations/{id}→ConversationDetailDto.useCreateConversation()—POST /api/conversations(201).useSendMessageJson()—POST /api/conversations/{id}/messagesnon-streaming (the existing JSON turn; works on every platform) →TurnResponseDto; invalidates the conversation on success.useResumeConversation()—POST /api/conversations/{id}/resume→TurnResponseDto.useProposal(id)—GET /api/agent/proposals/{id}→ProposalDto(#343).useCommitProposal()/useRejectProposal()— the existing commit/reject endpoints.packages/api-client/src/agent-stream.ts):streamConversationTurn(opts)taking{ url, getAuthHeader?, fetchImpl?, body, signal, onEvent, onError? }(or a clean equivalent) that POSTs withAccept: text/event-stream, readsresponse.bodyviagetReader(), parsesevent: <type>\ndata: <json>\n\nframes, and invokesonEvent(ev)per parsedConversationStreamEvent. Tolerates frames split across reads, comment/keepalive lines, and a malformeddataline (skip it, don't throw). Declares aConversationStreamEventunion mirroring the backend wire shape (text_delta,tool_call,tool_result,messagecarryingMessageDto,awaiting_confirmation,done,error) — these aren't OpenAPI DTOs, so the type lives in the package; reusecomponents["schemas"]["MessageDto"]for the message payload. InjectablefetchImpldefaults to globalfetch; no react-native/expo import.keys.ts):conversations: { all, detail(id), infinite },proposals: { detail(id) }.hooks/index.ts+ packageindex.ts): export the hooks,streamConversationTurn, and theConversationStreamEventtype.Tests (package vitest —
nodeenv, no React render)tests/contracts.test.ts(each hook exported from the top barrel; key namespaces registered).fetchImplreturning aResponsewhosebodyis aReadableStreamof canned SSE chunks. Assert: orderedtext_deltas; atool_call/tool_result; anawaiting_confirmation;messageevents; terminaldone; anerrorevent; a frame split across two reads reassembles; comment/blank lines ignored; a malformeddata:line is skipped without throwing. No real network.Out of scope
expo/fetch, runtime URL, the streaminguseSendMessage-from-context) — chat-UI ticket.No
openapi.jsonchange (all endpoints already in the committed spec). Part of epic #47; unblocks the chat UI.