Skip to content

Commit 77aa359

Browse files
committed
fix(react): assemble messages by order across pages and streams
1 parent 6988674 commit 77aa359

17 files changed

Lines changed: 395 additions & 141 deletions

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Assemble `useUIMessages` and `useThreadMessages` results by order across pages and streams (#193)
6+
- `useUIMessages` accepts `MessageDoc` pages from `listMessages` (#193)
7+
- Bound the context order-completion query by bytes (#193)
8+
- Require `convex@^1.32.0`, the first version with `maximumBytesRead` (#193)
9+
310
## 0.7.5
411

512
- Allow tool approvals to be handled atomically and more robust (#367)

‎docs/messages.mdx‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,34 @@ example.
2424
```ts
2525
import { paginationOptsValidator } from "convex/server";
2626
import { v } from "convex/values";
27-
import { listUIMessages } from "@convex-dev/agent";
27+
import { listMessages } from "@convex-dev/agent";
2828
import { components } from "./_generated/api";
2929

3030
export const listThreadMessages = query({
3131
args: { threadId: v.string(), paginationOpts: paginationOptsValidator },
3232
handler: async (ctx, args) => {
3333
await authorizeThreadAccess(ctx, threadId);
3434

35-
const paginated = await listUIMessages(ctx, components.agent, args);
35+
const paginated = await listMessages(ctx, components.agent, args);
3636

3737
// Here you could filter out / modify the documents
3838
return paginated;
3939
},
4040
});
4141
```
4242

43-
Note: Above we used `listUIMessages`, which returns UIMessages, specifically the
44-
Agent extension that includes some extra fields like order, status, etc.
43+
Note: Above we used `listMessages`, which returns MessageDocs: one per stored
44+
message, with fields like order, stepOrder and status. The React hooks below
45+
take this query and assemble the documents into UIMessages on the client.
4546
UIMessages combine multiple MessageDocs into a single UIMessage when there are
4647
multiple tool calls followed by an assistant message, making it easy to build
4748
UIs that work with the various "parts" on the UIMessage.
4849

49-
If you want to get MessageDocs, you can use `listMessages` instead.
50+
If you need UIMessages on the server, `listUIMessages` converts each page
51+
separately. Returning those pages to `useUIMessages` is still supported but
52+
legacy: a page boundary can fall inside one UIMessage, and a tool approval
53+
response whose tool call is on another page is lost. Return MessageDocs from
54+
`listMessages` instead.
5055

5156
## Showing messages in React
5257

@@ -56,8 +61,14 @@ non-streaming example.
5661

5762
### `useUIMessages` hook
5863

59-
The crux is to use the `useUIMessages` hook. For streaming, pass in
60-
`stream: true` to the hook.
64+
The crux is to use the `useUIMessages` hook with the query above. For streaming,
65+
pass in `stream: true` to the hook.
66+
67+
Pagination can end a page partway through one UIMessage. The hook loads the
68+
rest of the oldest message before it settles (reporting `LoadingMore`), so
69+
`results` always start at a whole message, and may hold more rows than
70+
`initialNumItems`. Rows of older messages read along the way are shown on the
71+
next `loadMore`.
6172

6273
```tsx
6374
import { api } from "../convex/_generated/api";
@@ -80,7 +91,7 @@ function MyComponent({ threadId }: { threadId: string }) {
8091
```
8192

8293
Note: If you want to work with MessageDocs instead of UIMessages, you can use
83-
the older `useThreadMessages` hook instead. However, working with UIMessages
94+
the older `useThreadMessages` hook with the same query. However, working with UIMessages
8495
enables richer streaming capabilities, such as status on whether the agent is
8596
actively reasoning.
8697

‎docs/streaming.mdx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ stream deltas. This is very similar to
6868
```ts
6969
import { paginationOptsValidator } from "convex/server";
7070
// highlight-next-line
71-
import { vStreamArgs, listUIMessages, syncStreams } from "@convex-dev/agent";
71+
import { vStreamArgs, listMessages, syncStreams } from "@convex-dev/agent";
7272
import { components } from "./_generated/api";
7373

7474
export const listThreadMessages = query({
@@ -83,7 +83,7 @@ export const listThreadMessages = query({
8383
await authorizeThreadAccess(ctx, threadId);
8484

8585
// Fetches the regular non-streaming messages.
86-
const paginated = await listUIMessages(ctx, components.agent, args);
86+
const paginated = await listMessages(ctx, components.agent, args);
8787

8888
// highlight-next-line
8989
const streams = await syncStreams(ctx, components.agent, args);
@@ -274,6 +274,8 @@ export const listStreams = query({
274274
// By default syncStreams only returns streaming messages. However, if
275275
// your messages aren't saved in the same transaction as the streaming
276276
// ends, you might want to include them here to avoid UI flashes.
277+
// useUIMessages and useThreadMessages show a finished or aborted stream
278+
// only where its order has no saved message at or after its start.
277279
includeStatuses: ["streaming", "aborted", "finished"],
278280
});
279281
return { streams };

‎docs/threads.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,9 @@ const messages = await listMessages(ctx, components.agent, {
154154
});
155155
```
156156

157-
Or for the UIMessage type (easier for rendering UIs):
157+
Or for the UIMessage type (each page is converted on its own, so a UIMessage can
158+
be cut at a page boundary; for React, return MessageDocs and use
159+
`useUIMessages`):
158160

159161
```ts
160162
import { listUIMessages } from "@convex-dev/agent";

‎docs/tool-approval.mdx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,10 @@ histories where tool calls lack results.
177177
## Client-side flow
178178

179179
On the client, use `useUIMessages` to detect pending approvals and show
180-
Approve/Deny buttons. Tool parts with `state === "approval-requested"` are
180+
Approve/Deny buttons. Its query should return MessageDocs from `listMessages`
181+
(see [retrieving messages](./messages.mdx#retrieving-messages)): with pages of
182+
`listUIMessages`, an approval response on a different page from its tool call
183+
is lost. Tool parts with `state === "approval-requested"` are
181184
waiting for a decision.
182185

183186
```tsx

‎example/convex/chat/approval.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// 5. AI SDK automatically handles the approval: executes tool (if approved)
99
// or creates execution-denied result (if denied), then continues generation
1010
import { paginationOptsValidator } from "convex/server";
11-
import { listUIMessages, syncStreams, vStreamArgs } from "@convex-dev/agent";
11+
import { listMessages, syncStreams, vStreamArgs } from "@convex-dev/agent";
1212
import { components, internal } from "../_generated/api";
1313
import { internalAction, mutation, query } from "../_generated/server";
1414
import { v } from "convex/values";
@@ -135,7 +135,7 @@ export const listThreadMessages = query({
135135
threadId,
136136
streamArgs,
137137
});
138-
const paginated = await listUIMessages(ctx, components.agent, args);
138+
const paginated = await listMessages(ctx, components.agent, args);
139139
return { ...paginated, streams };
140140
},
141141
});

‎example/convex/chat/human.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
saveMessage,
44
syncStreams,
55
vStreamArgs,
6-
listUIMessages,
6+
listMessages,
77
} from "@convex-dev/agent";
88
import {
99
action,
@@ -168,7 +168,7 @@ export const getMessages = query({
168168
streamArgs: vStreamArgs,
169169
},
170170
handler: async (ctx, args) => {
171-
const messages = await listUIMessages(ctx, components.agent, {
171+
const messages = await listMessages(ctx, components.agent, {
172172
threadId: args.threadId,
173173
paginationOpts: args.paginationOpts,
174174
});

‎example/convex/chat/streaming.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { paginationOptsValidator } from "convex/server";
33
import {
44
createThread,
5-
listUIMessages,
5+
listMessages,
66
syncStreams,
77
vStreamArgs,
88
} from "@convex-dev/agent";
@@ -99,7 +99,9 @@ export const listThreadMessages = query({
9999
// Here you could filter out / modify the stream of deltas / filter out
100100
// deltas.
101101

102-
const paginated = await listUIMessages(ctx, components.agent, args);
102+
// MessageDocs, which useUIMessages assembles into UIMessages across
103+
// pages and streams.
104+
const paginated = await listMessages(ctx, components.agent, args);
103105

104106
// Here you could filter out metadata that you don't want from any optional
105107
// fields on the messages.

‎example/ui/chat/ChatStreaming.tsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ function Story({ threadId, reset }: { threadId: string; reset: () => void }) {
4848
{ threadId },
4949
{ initialNumItems: 10, stream: true },
5050
);
51-
// If you don't want to use UIMessages, you can use this hook:
52-
// (note: you'll need to return MessageDoc from listThreadMessages)
51+
// If you want the MessageDocs themselves, use this hook with the same query:
5352
/*
5453
const { results } = useThreadMessages(
5554
api.chat.streaming.listThreadMessages,

‎package-lock.json‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)