Streaming chat ergonomics: typed streaming-query helpers, validators, and known-good AI SDK versions - #295
Streaming chat ergonomics: typed streaming-query helpers, validators, and known-good AI SDK versions#295sethconvex wants to merge 1 commit into
Conversation
…docs Building a from-scratch streaming chat query previously required reconstructing the paginated + streams return shape by hand, which hit TS2719 (the only exported returns validator, vStreamMessagesReturnValue, is MessageDoc-shaped while the docs and hooks steer users to listUIMessages, which returns UIMessages). Additionally, queries that declared a returns validator were rejected by the hooks' stream: true guard because StreamQuery required a non-optional streams property. - Add listUIMessagesWithStreams / listMessagesWithStreams: one-call helpers that return exactly what useUIMessages / useThreadMessages expect, typed to satisfy the exported returns validators. - Add vUIMessage, vStreamUIMessagesReturnValue, vSyncStreamsReturnValue validators (additive; vStreamMessagesReturnValue unchanged in shape). - Loosen StreamQuery's streams to optional so returns-validator queries count as stream queries; queries without streams still fail the guard. - Document the compatible AI SDK versions (ai@^6 pairs with @ai-sdk/* providers at ^3) in the README and getting-started docs, and add a complete streaming chat quickstart (server + client) to the README. - Example: listThreadMessages now uses the helper + returns validator; the manual composition is kept as listThreadMessagesCustom. - Tests: type-level + runtime coverage that the helpers satisfy the validators and hook contracts (src/client/listWithStreams.test.ts). Surfaced by a Convex plugin coding-eval where an agent burned ~20 turns reverse-engineering the validator shape and never compiled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XVeiChqZyhVRXu34va31k
📝 WalkthroughWalkthroughAdded shared validators and helpers for returning paginated messages with synchronized stream data. Updated stream query typing and public exports, added compile-time and runtime tests, and revised documentation and examples to use the consolidated APIs. Added a streaming chat quickstart and AI SDK/provider version guidance. Sequence Diagram(s)sequenceDiagram
participant Client
participant ConvexQuery
participant MessageHelper
participant StreamSync
Client->>ConvexQuery: Request messages with streamArgs
ConvexQuery->>MessageHelper: Call listUIMessagesWithStreams
MessageHelper->>StreamSync: Call syncStreams
MessageHelper-->>ConvexQuery: Combine messages and streams
ConvexQuery-->>Client: Return validated streaming result
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
README.md (2)
88-93: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse a known valid model identifier for the quickstart example.
The model string
"claude-sonnet-4-5"is likely a typo for"claude-3-5-sonnet-20240620"(or another valid Anthropic model identifier). Using an invalid model name in the quickstart can cause runtime validation errors for users who copy and paste the code directly.💡 Proposed change
const agent = new Agent(components.agent, { name: "Chat Agent", - languageModel: anthropic("claude-sonnet-4-5"), + languageModel: anthropic("claude-3-5-sonnet-20240620"), });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 88 - 93, Update the languageModel configuration in the Agent quickstart example to use a known valid Anthropic model identifier, such as claude-3-5-sonnet-20240620, instead of claude-sonnet-4-5. Keep the anthropic provider usage and surrounding example unchanged.
90-93: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueUse a known valid model identifier for the example.
The model string
"claude-sonnet-4-5"is likely a typo for"claude-3-5-sonnet-20240620"(or another valid Anthropic model identifier). Using an invalid model name in the quickstart can cause runtime errors for users who copy and paste the code.💡 Proposed change
const agent = new Agent(components.agent, { name: "Chat Agent", - languageModel: anthropic("claude-sonnet-4-5"), + languageModel: anthropic("claude-3-5-sonnet-20240620"), });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 90 - 93, Update the Chat Agent example’s anthropic model identifier to a known valid Anthropic model string, replacing "claude-sonnet-4-5" with the established valid identifier "claude-3-5-sonnet-20240620" or another currently supported equivalent. Keep the surrounding Agent configuration unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@README.md`:
- Around line 88-93: Update the languageModel configuration in the Agent
quickstart example to use a known valid Anthropic model identifier, such as
claude-3-5-sonnet-20240620, instead of claude-sonnet-4-5. Keep the anthropic
provider usage and surrounding example unchanged.
- Around line 90-93: Update the Chat Agent example’s anthropic model identifier
to a known valid Anthropic model string, replacing "claude-sonnet-4-5" with the
established valid identifier "claude-3-5-sonnet-20240620" or another currently
supported equivalent. Keep the surrounding Agent configuration unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: get-convex/coderabbit/.coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ef7b275b-b0a9-4f69-995a-afe4f2c3913b
📒 Files selected for processing (11)
CHANGELOG.mdREADME.mddocs/getting-started.mdxdocs/streaming.mdxexample/convex/chat/streaming.tssrc/UIMessages.tssrc/client/index.tssrc/client/listWithStreams.test.tssrc/client/streaming.tssrc/react/types.tssrc/react/useUIMessages.ts
…erim) (#47) Forged from convex-agents#34. The 'any chat/LLM → @convex-dev/agent' directive dragged simple streaming chats onto the component's from-scratch-broken streaming wiring (TS2719); an eval showed a 69-turn never-compiled build. Reversible via the AGENT-STREAMING-INTERIM markers when get-convex/agent#295 ships listUIMessagesWithStreams. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… (interim) (#26) Forged from convex-agents#34. Reversible when get-convex/agent#295 ships. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Why
An AI coding-eval for the Convex plugin asked an agent to build a streaming chat app on
@convex-dev/agent. It took 69 turns and never compiled, hitting two concrete walls that any from-scratch user can hit:1. The streaming return type is a landmine (TS2719)
The docs and the
useUIMessageshook steer you tolistUIMessages(which returns a page ofUIMessages), but the only exportedreturnsvalidator,vStreamMessagesReturnValue, has avMessageDoc-shaped page. Combining them, as convex best practices ("always add return validators") encourage, produces:The eval agent burned ~20 turns grep/sed-ing
.d.tsfiles trying to reconstruct the exact split-pagination validator shape. There was no exported UIMessage validator to reach for.A second, hidden trap compounded it: even a correct streaming query with a
returnsvalidator was rejected by the hooks, becausev.optional(streams)infers an optional property (streams?:) whileStreamQuery(the type behinduseUIMessages(..., { stream: true })) required a non-optionalstreams, yielding:2. Peer-dependency guessing
The component requires
ai@^6+@ai-sdk/provider-utils@^4, but nothing documented which provider majors pair with it. The eval agent installed@ai-sdk/anthropic@4(wrong AI SDK generation), got an unresolvable peer conflict, and thrashed reinstalling before stumbling onto@ai-sdk/anthropic@^3.What changed (all additive)
New server helpers (
src/client/streaming.ts, exported from the package root):listUIMessagesWithStreams(ctx, component, { threadId, paginationOpts, streamArgs })- fetches the paginated UIMessages and the stream deltas, returning exactly whatuseUIMessages(..., { stream: true })expects.listMessagesWithStreams(...)- same forMessageDocs /useThreadMessages.New validators:
vUIMessage(src/UIMessages.ts) - validator matching theUIMessagetype (AI SDKparts/metadataare validated asanysince they're generic over the app's tools).vStreamUIMessagesReturnValue- thereturnsvalidator matchinglistUIMessagesWithStreams.vSyncStreamsReturnValue- thestreamsfield alone, for hand-rolled queries. (vStreamMessagesReturnValueis unchanged in shape, now built from it.)One targeted fix (
src/react/types.ts):StreamQuery's return type now hasstreams?:optional, so queries declaring areturnsvalidator satisfy thestream: trueguard. Queries that don't returnstreamsat all still fail it (no properties in common), and a type test pins that.Docs: README gains a complete "Streaming chat quickstart" (server query + mutation + action + React hook that all type-check), and README +
docs/getting-started.mdxdocument the version pairing:ai@^6pairs with@ai-sdk/*providers at^3(e.g.@ai-sdk/anthropic@^3), matching this repo's own pinned devDeps (ai@6.0.35+@ai-sdk/anthropic@3.0.13).docs/streaming.mdxand theuseUIMessagesdocstring now lead with the helper.Example:
example/convex/chat/streaming.ts'slistThreadMessagesnow uses the helper +returnsvalidator (so the example exercises the exact new path end-to-end, includingoptimisticallySendMessage); the manual composition is preserved aslistThreadMessagesCustom.Before / after for a from-scratch user
Before (what the eval agent kept trying, TS2719):
After:
Verification
npm run build- cleannpm run typecheck(tsc --noEmit && tsc -p example && tsc -p example/convex) - cleannpm run test(vitest run --typecheck) - 25 files, 275 tests passed, no type errors (was 24 files / 272 tests before; addssrc/client/listWithStreams.test.tswith runtimeparse()validation of both helpers against both validators forkind: "list",kind: "deltas", and no-streamArgscalls, plus type-level assertions that the helper return types satisfy the validators, that both validator-typed and inferred-typed queries extendStreamQuery/UIMessagesQuery, and that a non-streaming query still does not extendStreamQuery)npm run lint- cleanSurfaced by a Convex plugin coding-eval session.
🤖 Generated with Claude Code
https://claude.ai/code/session_016XVeiChqZyhVRXu34va31k