Skip to content

Streaming chat ergonomics: typed streaming-query helpers, validators, and known-good AI SDK versions - #295

Open
sethconvex wants to merge 1 commit into
mainfrom
magicseth/streaming-chat-ergonomics
Open

Streaming chat ergonomics: typed streaming-query helpers, validators, and known-good AI SDK versions#295
sethconvex wants to merge 1 commit into
mainfrom
magicseth/streaming-chat-ergonomics

Conversation

@sethconvex

@sethconvex sethconvex commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

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 useUIMessages hook steer you to listUIMessages (which returns a page of UIMessages), but the only exported returns validator, vStreamMessagesReturnValue, has a vMessageDoc-shaped page. Combining them, as convex best practices ("always add return validators") encourage, produces:

error TS2719: Type '(ctx: ...) => ...' is not assignable to type '(ctx: ...) => ...'. Two different types with this name exist, but they are unrelated.
  Type 'Promise<{ streams: SyncStreamsReturnValue; page: UIMessage[]; ... splitCursor?: string | null | undefined; pageStatus?: "SplitRecommended" | ... }>' is not assignable to type 'ValidatorTypeToReturnType<{ splitCursor?: ...; pageStatus?: "SplitRecommended" | "SplitRequired" | null | undefined; streams?: ...; page: ...MessageDoc... }>'

The eval agent burned ~20 turns grep/sed-ing .d.ts files 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 returns validator was rejected by the hooks, because v.optional(streams) infers an optional property (streams?:) while StreamQuery (the type behind useUIMessages(..., { stream: true })) required a non-optional streams, yielding:

error TS2322: Type 'true' is not assignable to type 'ErrorMessage<"To enable streaming, your query must take in streamArgs: vStreamArgs and return a streams object returned from syncStreams. See docs.">'

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 what useUIMessages(..., { stream: true }) expects.
  • listMessagesWithStreams(...) - same for MessageDocs / useThreadMessages.

New validators:

  • vUIMessage (src/UIMessages.ts) - validator matching the UIMessage type (AI SDK parts/metadata are validated as any since they're generic over the app's tools).
  • vStreamUIMessagesReturnValue - the returns validator matching listUIMessagesWithStreams.
  • vSyncStreamsReturnValue - the streams field alone, for hand-rolled queries. (vStreamMessagesReturnValue is unchanged in shape, now built from it.)

One targeted fix (src/react/types.ts): StreamQuery's return type now has streams?: optional, so queries declaring a returns validator satisfy the stream: true guard. Queries that don't return streams at 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.mdx document the version pairing: ai@^6 pairs 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.mdx and the useUIMessages docstring now lead with the helper.

Example: example/convex/chat/streaming.ts's listThreadMessages now uses the helper + returns validator (so the example exercises the exact new path end-to-end, including optimisticallySendMessage); the manual composition is preserved as listThreadMessagesCustom.

Before / after for a from-scratch user

Before (what the eval agent kept trying, TS2719):

export const listThreadMessages = query({
  args: { threadId: v.string(), paginationOpts: paginationOptsValidator, streamArgs: vStreamArgs },
  returns: vStreamMessagesReturnValue, // MessageDoc-shaped: mismatch
  handler: async (ctx, args) => {
    const paginated = await listUIMessages(ctx, components.agent, args); // UIMessage-shaped
    const streams = await syncStreams(ctx, components.agent, args);
    return { ...paginated, streams }; // TS2719
  },
});

After:

export const listThreadMessages = query({
  args: { threadId: v.string(), paginationOpts: paginationOptsValidator, streamArgs: vStreamArgs },
  returns: vStreamUIMessagesReturnValue,
  handler: async (ctx, args) => {
    await authorizeThreadAccess(ctx, args.threadId);
    return listUIMessagesWithStreams(ctx, components.agent, args);
  },
});
const { results: messages } = useUIMessages(
  api.chat.listThreadMessages,
  { threadId },
  { initialNumItems: 10, stream: true },
);

Verification

  • npm run build - clean
  • npm run typecheck (tsc --noEmit && tsc -p example && tsc -p example/convex) - clean
  • npm run test (vitest run --typecheck) - 25 files, 275 tests passed, no type errors (was 24 files / 272 tests before; adds src/client/listWithStreams.test.ts with runtime parse() validation of both helpers against both validators for kind: "list", kind: "deltas", and no-streamArgs calls, plus type-level assertions that the helper return types satisfy the validators, that both validator-typed and inferred-typed queries extend StreamQuery/UIMessagesQuery, and that a non-streaming query still does not extend StreamQuery)
  • npm run lint - clean
  • Reproduced the original TS2719 verbatim on this codebase before the fix, and confirmed the same query now compiles.

Surfaced by a Convex plugin coding-eval session.

🤖 Generated with Claude Code

https://claude.ai/code/session_016XVeiChqZyhVRXu34va31k

…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
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added 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
Loading

Suggested reviewers: ianmacartney, ianmacartney

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main changes: streaming-query helpers, validators, and AI SDK version guidance.
Description check ✅ Passed The description matches the changeset and explains the streaming fixes, docs, version pairing, and tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch magicseth/streaming-chat-ergonomics

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
README.md (2)

88-93: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use 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 value

Use 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

📥 Commits

Reviewing files that changed from the base of the PR and between b3034d7 and b29ffe6.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • README.md
  • docs/getting-started.mdx
  • docs/streaming.mdx
  • example/convex/chat/streaming.ts
  • src/UIMessages.ts
  • src/client/index.ts
  • src/client/listWithStreams.test.ts
  • src/client/streaming.ts
  • src/react/types.ts
  • src/react/useUIMessages.ts

sethconvex added a commit to get-convex/convex-backend-skill that referenced this pull request Jul 15, 2026
…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>
sethconvex added a commit to get-convex/convex-codex-plugin that referenced this pull request Jul 15, 2026
… (interim) (#26)

Forged from convex-agents#34. Reversible when get-convex/agent#295 ships.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@ianmacartney
ianmacartney requested a review from robelest July 16, 2026 01:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant