@convex-dev/agent 0.2.8-alpha.5 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/definePlaygroundAPI.d.ts +5 -5
- package/dist/client/index.d.ts +5 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +1 -1
- package/dist/client/search.d.ts +1 -1
- package/dist/client/streaming.d.ts +17 -12
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +49 -2
- package/dist/client/streaming.js.map +1 -1
- package/dist/component/_generated/api.d.ts +8 -8
- package/dist/component/messages.d.ts +11 -11
- package/dist/component/schema.d.ts +20 -20
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +1 -0
- package/dist/component/streams.js.map +1 -1
- package/dist/deltas.d.ts +2 -6
- package/dist/deltas.d.ts.map +1 -1
- package/dist/deltas.js +52 -49
- package/dist/deltas.js.map +1 -1
- package/dist/react/useDeltaStreams.d.ts +10 -0
- package/dist/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/react/useDeltaStreams.js +101 -0
- package/dist/react/useDeltaStreams.js.map +1 -0
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
- package/dist/react/useStreamingUIMessages.js +66 -181
- package/dist/react/useStreamingUIMessages.js.map +1 -1
- package/dist/react/useUIMessages.d.ts.map +1 -1
- package/dist/react/useUIMessages.js +4 -1
- package/dist/react/useUIMessages.js.map +1 -1
- package/dist/validators.d.ts +78 -78
- package/dist/validators.js +1 -1
- package/dist/validators.js.map +1 -1
- package/package.json +7 -5
- package/src/client/index.ts +2 -1
- package/src/client/streaming.test.ts +2 -2
- package/src/client/streaming.ts +58 -5
- package/src/component/_generated/api.d.ts +8 -8
- package/src/component/streams.ts +1 -0
- package/src/deltas.test.ts +262 -0
- package/src/deltas.ts +62 -54
- package/src/react/useDeltaStreams.ts +155 -0
- package/src/react/useStreamingUIMessages.ts +94 -251
- package/src/react/useUIMessages.ts +5 -2
- package/src/validators.ts +1 -1
- package/dist/package.json +0 -3
package/src/deltas.ts
CHANGED
|
@@ -18,57 +18,6 @@ import {
|
|
|
18
18
|
type StreamMessage,
|
|
19
19
|
} from "./validators.js";
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* Compressing parts when streaming to save bandwidth in deltas.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
export function compressUIMessageChunks(
|
|
26
|
-
parts: UIMessageChunk[],
|
|
27
|
-
): UIMessageChunk[] {
|
|
28
|
-
const compressed: UIMessageChunk[] = [];
|
|
29
|
-
for (const part of parts) {
|
|
30
|
-
const last = compressed.at(-1);
|
|
31
|
-
if (part.type === "text-delta" || part.type === "reasoning-delta") {
|
|
32
|
-
if (last?.type === part.type && part.id === last.id) {
|
|
33
|
-
last.delta += part.delta;
|
|
34
|
-
} else {
|
|
35
|
-
compressed.push(part);
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
38
|
-
compressed.push(part);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return compressed;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function compressTextStreamParts(
|
|
45
|
-
parts: TextStreamPart<ToolSet>[],
|
|
46
|
-
): TextStreamPart<ToolSet>[] {
|
|
47
|
-
const compressed: TextStreamPart<ToolSet>[] = [];
|
|
48
|
-
for (const part of parts) {
|
|
49
|
-
const last = compressed.at(-1);
|
|
50
|
-
if (part.type === "text-delta" || part.type === "reasoning-delta") {
|
|
51
|
-
if (last?.type === part.type && part.id === last.id) {
|
|
52
|
-
last.text += part.text;
|
|
53
|
-
} else {
|
|
54
|
-
compressed.push(part);
|
|
55
|
-
}
|
|
56
|
-
} else {
|
|
57
|
-
if (part.type === "file") {
|
|
58
|
-
compressed.push({
|
|
59
|
-
type: "file",
|
|
60
|
-
file: {
|
|
61
|
-
...part.file,
|
|
62
|
-
uint8Array: undefined as unknown as Uint8Array,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
compressed.push(part);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return compressed;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
21
|
export function blankUIMessage<METADATA = unknown>(
|
|
73
22
|
streamMessage: StreamMessage & { metadata?: METADATA },
|
|
74
23
|
threadId: string,
|
|
@@ -238,9 +187,6 @@ export function getParts<T extends StreamDelta["parts"][number]>(
|
|
|
238
187
|
}
|
|
239
188
|
if (cursor !== delta.start) {
|
|
240
189
|
if (cursor >= delta.end) {
|
|
241
|
-
console.debug(
|
|
242
|
-
`Got duplicate delta for stream ${delta.streamId} at ${delta.start}`,
|
|
243
|
-
);
|
|
244
190
|
continue;
|
|
245
191
|
} else if (cursor < delta.start) {
|
|
246
192
|
console.warn(
|
|
@@ -591,3 +537,65 @@ function mergeProviderMetadata(
|
|
|
591
537
|
}
|
|
592
538
|
return merged;
|
|
593
539
|
}
|
|
540
|
+
|
|
541
|
+
export function combineUIMessages(messages: UIMessage[]): UIMessage[] {
|
|
542
|
+
const combined = messages.reduce((acc, message) => {
|
|
543
|
+
if (!acc.length) {
|
|
544
|
+
return [message];
|
|
545
|
+
}
|
|
546
|
+
const previous = acc.at(-1)!;
|
|
547
|
+
if (previous.role !== message.role) {
|
|
548
|
+
acc.push(message);
|
|
549
|
+
return acc;
|
|
550
|
+
}
|
|
551
|
+
// We will replace it with a combined message
|
|
552
|
+
acc.pop();
|
|
553
|
+
const newParts = [...previous.parts];
|
|
554
|
+
for (const part of message.parts) {
|
|
555
|
+
const toolCallId = getToolCallId(part);
|
|
556
|
+
if (!toolCallId) {
|
|
557
|
+
newParts.push(part);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const previousPartIndex = newParts.findIndex(
|
|
561
|
+
(p) => getToolCallId(p) === toolCallId,
|
|
562
|
+
);
|
|
563
|
+
const previousPart = newParts.splice(previousPartIndex, 1)[0];
|
|
564
|
+
if (!previousPart) {
|
|
565
|
+
newParts.push(part);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
newParts.push(mergeParts(previousPart, part));
|
|
569
|
+
}
|
|
570
|
+
acc.push({
|
|
571
|
+
...previous,
|
|
572
|
+
...pick(message, ["status", "metadata", "agentName"]),
|
|
573
|
+
parts: newParts,
|
|
574
|
+
text: newParts
|
|
575
|
+
.filter((p) => p.type === "text")
|
|
576
|
+
.map((p) => p.text)
|
|
577
|
+
.join(""),
|
|
578
|
+
});
|
|
579
|
+
return acc;
|
|
580
|
+
}, [] as UIMessage[]);
|
|
581
|
+
return combined;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function getToolCallId(
|
|
585
|
+
part: UIMessage["parts"][number] & { toolCallId?: string },
|
|
586
|
+
) {
|
|
587
|
+
return part.toolCallId;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function mergeParts(
|
|
591
|
+
previousPart: UIMessage["parts"][number],
|
|
592
|
+
part: UIMessage["parts"][number],
|
|
593
|
+
): UIMessage["parts"][number] {
|
|
594
|
+
const merged: Record<string, unknown> = { ...previousPart };
|
|
595
|
+
for (const [key, value] of Object.entries(part)) {
|
|
596
|
+
if (value !== undefined) {
|
|
597
|
+
merged[key] = value;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return merged as ToolUIPart | DynamicToolUIPart;
|
|
601
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { StreamQuery, StreamQueryArgs } from "./types.js";
|
|
4
|
+
import type { SyncStreamsReturnValue } from "../client/types.js";
|
|
5
|
+
import type { FunctionArgs } from "convex/server";
|
|
6
|
+
import type { StreamArgs, StreamDelta, StreamMessage } from "../validators.js";
|
|
7
|
+
import { sorted } from "../shared.js";
|
|
8
|
+
import { useQuery } from "convex/react";
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { assert } from "convex-helpers";
|
|
11
|
+
|
|
12
|
+
export function useDeltaStreams<
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
Query extends StreamQuery<any> = StreamQuery<object>,
|
|
15
|
+
>(
|
|
16
|
+
query: Query,
|
|
17
|
+
args: StreamQueryArgs<Query> | "skip",
|
|
18
|
+
options?: {
|
|
19
|
+
startOrder?: number;
|
|
20
|
+
skipStreamIds?: string[];
|
|
21
|
+
},
|
|
22
|
+
): { streamMessage: StreamMessage; deltas: StreamDelta[] }[] | undefined {
|
|
23
|
+
// We hold onto and modify state directly to avoid re-running unnecessarily.
|
|
24
|
+
const [state] = useState<{
|
|
25
|
+
startOrder: number;
|
|
26
|
+
threadId: string | undefined;
|
|
27
|
+
deltaStreams:
|
|
28
|
+
| Array<{
|
|
29
|
+
streamMessage: StreamMessage;
|
|
30
|
+
deltas: StreamDelta[];
|
|
31
|
+
}>
|
|
32
|
+
| undefined;
|
|
33
|
+
}>({
|
|
34
|
+
startOrder: options?.startOrder ?? 0,
|
|
35
|
+
deltaStreams: undefined,
|
|
36
|
+
threadId: args === "skip" ? undefined : args.threadId,
|
|
37
|
+
});
|
|
38
|
+
const [cursors, setCursors] = useState<Record<string, number>>({});
|
|
39
|
+
if (args !== "skip" && state.threadId !== args.threadId) {
|
|
40
|
+
state.threadId = args.threadId;
|
|
41
|
+
state.deltaStreams = undefined;
|
|
42
|
+
state.startOrder = options?.startOrder ?? 0;
|
|
43
|
+
setCursors({});
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
state.deltaStreams?.length ||
|
|
47
|
+
(options?.startOrder && options.startOrder < state.startOrder)
|
|
48
|
+
) {
|
|
49
|
+
const cacheFriendlyStartOrder = options?.startOrder
|
|
50
|
+
? // round down to the nearest 10 for some cache benefits
|
|
51
|
+
options.startOrder - (options.startOrder % 10)
|
|
52
|
+
: 0;
|
|
53
|
+
if (cacheFriendlyStartOrder !== state.startOrder) {
|
|
54
|
+
state.startOrder = cacheFriendlyStartOrder;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Get all the active streams
|
|
59
|
+
const streamList = useQuery(
|
|
60
|
+
query,
|
|
61
|
+
args === "skip"
|
|
62
|
+
? args
|
|
63
|
+
: ({
|
|
64
|
+
...args,
|
|
65
|
+
streamArgs: {
|
|
66
|
+
kind: "list",
|
|
67
|
+
startOrder: state.startOrder,
|
|
68
|
+
} as StreamArgs,
|
|
69
|
+
} as FunctionArgs<Query>),
|
|
70
|
+
) as
|
|
71
|
+
| { streams: Extract<SyncStreamsReturnValue, { kind: "list" }> }
|
|
72
|
+
| undefined;
|
|
73
|
+
|
|
74
|
+
const streamMessages =
|
|
75
|
+
args === "skip"
|
|
76
|
+
? undefined
|
|
77
|
+
: !streamList
|
|
78
|
+
? state.deltaStreams?.map(({ streamMessage }) => streamMessage)
|
|
79
|
+
: sorted(
|
|
80
|
+
streamList.streams.messages.filter(
|
|
81
|
+
({ streamId, order }) =>
|
|
82
|
+
!options?.skipStreamIds?.includes(streamId) &&
|
|
83
|
+
(!options?.startOrder || order >= options.startOrder),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Get the deltas for all the active streams, if any.
|
|
88
|
+
const cursorQuery = useQuery(
|
|
89
|
+
query,
|
|
90
|
+
args === "skip" || !streamMessages?.length
|
|
91
|
+
? ("skip" as const)
|
|
92
|
+
: ({
|
|
93
|
+
...args,
|
|
94
|
+
streamArgs: {
|
|
95
|
+
kind: "deltas",
|
|
96
|
+
cursors: streamMessages.map(({ streamId }) => ({
|
|
97
|
+
streamId,
|
|
98
|
+
cursor: cursors[streamId] ?? 0,
|
|
99
|
+
})),
|
|
100
|
+
} as StreamArgs,
|
|
101
|
+
} as FunctionArgs<Query>),
|
|
102
|
+
) as
|
|
103
|
+
| { streams: Extract<SyncStreamsReturnValue, { kind: "deltas" }> }
|
|
104
|
+
| undefined;
|
|
105
|
+
|
|
106
|
+
const newDeltas = cursorQuery?.streams.deltas;
|
|
107
|
+
if (newDeltas?.length && streamMessages) {
|
|
108
|
+
const newDeltasByStreamId = new Map<string, StreamDelta[]>();
|
|
109
|
+
for (const delta of newDeltas) {
|
|
110
|
+
const oldCursor = cursors[delta.streamId];
|
|
111
|
+
if (oldCursor && delta.start < oldCursor) continue;
|
|
112
|
+
const existing = newDeltasByStreamId.get(delta.streamId);
|
|
113
|
+
if (existing) {
|
|
114
|
+
const previousEnd = existing.at(-1)!.end;
|
|
115
|
+
assert(
|
|
116
|
+
previousEnd === delta.start,
|
|
117
|
+
`Gap found in deltas for ${delta.streamId} jumping to ${delta.start} from ${previousEnd}`,
|
|
118
|
+
);
|
|
119
|
+
existing.push(delta);
|
|
120
|
+
} else {
|
|
121
|
+
assert(
|
|
122
|
+
!oldCursor || oldCursor === delta.start,
|
|
123
|
+
`Gap found - first delta after ${oldCursor} is ${delta.start} for stream ${delta.streamId}`,
|
|
124
|
+
);
|
|
125
|
+
newDeltasByStreamId.set(delta.streamId, [delta]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const newCursors: Record<string, number> = {};
|
|
129
|
+
for (const { streamId } of streamMessages) {
|
|
130
|
+
const cursor =
|
|
131
|
+
newDeltasByStreamId.get(streamId)?.at(-1)?.end ?? cursors[streamId];
|
|
132
|
+
if (cursor !== undefined) {
|
|
133
|
+
newCursors[streamId] = cursor;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
setCursors(newCursors);
|
|
137
|
+
|
|
138
|
+
// we defensively create a new object so object identity matches contents
|
|
139
|
+
state.deltaStreams = streamMessages.map((streamMessage) => {
|
|
140
|
+
const streamId = streamMessage.streamId;
|
|
141
|
+
const old = state.deltaStreams?.find(
|
|
142
|
+
(ds) => ds.streamMessage.streamId === streamId,
|
|
143
|
+
);
|
|
144
|
+
const newDeltas = newDeltasByStreamId.get(streamId);
|
|
145
|
+
if (!newDeltas && streamMessage === old?.streamMessage) {
|
|
146
|
+
return old;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
streamMessage,
|
|
150
|
+
deltas: [...(old?.deltas ?? []), ...(newDeltas ?? [])],
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return state.deltaStreams;
|
|
155
|
+
}
|
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import type { FunctionArgs } from "convex/server";
|
|
5
|
-
import { useMemo, useState, useEffect, useRef } from "react";
|
|
6
|
-
import {
|
|
7
|
-
readUIMessageStream,
|
|
8
|
-
type TextUIPart,
|
|
9
|
-
type UIDataTypes,
|
|
10
|
-
type UIMessageChunk,
|
|
11
|
-
type UITools,
|
|
12
|
-
} from "ai";
|
|
13
|
-
import type { SyncStreamsReturnValue } from "../client/types.js";
|
|
14
|
-
import type { StreamArgs } from "../validators.js";
|
|
2
|
+
import { useMemo, useState, useEffect } from "react";
|
|
3
|
+
import { type UIDataTypes, type UIMessageChunk, type UITools } from "ai";
|
|
15
4
|
import type { StreamQuery, StreamQueryArgs } from "./types.js";
|
|
16
5
|
import { type UIMessage } from "../UIMessages.js";
|
|
17
6
|
import {
|
|
18
7
|
blankUIMessage,
|
|
19
8
|
getParts,
|
|
9
|
+
updateFromUIMessageChunks,
|
|
20
10
|
deriveUIMessagesFromTextStreamParts,
|
|
21
|
-
statusFromStreamStatus,
|
|
22
11
|
} from "../deltas.js";
|
|
23
|
-
import {
|
|
12
|
+
import { useDeltaStreams } from "./useDeltaStreams.js";
|
|
13
|
+
|
|
14
|
+
// Polyfill structuredClone to support readUIMessageStream on ReactNative
|
|
15
|
+
if (!("structuredClone" in globalThis)) {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
void import("@ungap/structured-clone" as any).then(
|
|
18
|
+
({ default: structuredClone }) =>
|
|
19
|
+
(globalThis.structuredClone = structuredClone),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
24
|
* A hook that fetches streaming messages from a thread and converts them to UIMessages
|
|
@@ -51,252 +49,97 @@ export function useStreamingUIMessages<
|
|
|
51
49
|
},
|
|
52
50
|
// TODO: make generic on metadata, etc.
|
|
53
51
|
): UIMessage<METADATA, DATA_PARTS, TOOLS>[] | undefined {
|
|
54
|
-
const [
|
|
55
|
-
Record<
|
|
52
|
+
const [messageState, setMessageState] = useState<
|
|
53
|
+
Record<
|
|
54
|
+
string,
|
|
55
|
+
{
|
|
56
|
+
uiMessage: UIMessage<METADATA, DATA_PARTS, TOOLS>;
|
|
57
|
+
cursor: number;
|
|
58
|
+
}
|
|
59
|
+
>
|
|
56
60
|
>({});
|
|
57
|
-
const [streamCursors, setStreamCursors] = useState<Record<string, number>>(
|
|
58
|
-
{},
|
|
59
|
-
);
|
|
60
|
-
const uiMessageStreamControllers = useRef<
|
|
61
|
-
Map<string, ReadableStreamDefaultController<UIMessageChunk>>
|
|
62
|
-
>(new Map());
|
|
63
|
-
|
|
64
|
-
const startOrder = options?.startOrder
|
|
65
|
-
? // round down to the nearest 10 for some cache benefits
|
|
66
|
-
options.startOrder - (options.startOrder % 10)
|
|
67
|
-
: 0;
|
|
68
61
|
|
|
69
|
-
|
|
70
|
-
const streamList = useQuery(
|
|
71
|
-
query,
|
|
72
|
-
args === "skip"
|
|
73
|
-
? args
|
|
74
|
-
: ({
|
|
75
|
-
...args,
|
|
76
|
-
streamArgs: {
|
|
77
|
-
kind: "list",
|
|
78
|
-
startOrder,
|
|
79
|
-
} as StreamArgs,
|
|
80
|
-
} as FunctionArgs<Query>),
|
|
81
|
-
) as
|
|
82
|
-
| { streams: Extract<SyncStreamsReturnValue, { kind: "list" }> }
|
|
83
|
-
| undefined;
|
|
84
|
-
|
|
85
|
-
const minOrder = Math.max(
|
|
86
|
-
options?.startOrder ?? 0,
|
|
87
|
-
streamList?.streams.messages.length
|
|
88
|
-
? Math.min(...streamList.streams.messages.map(({ order }) => order))
|
|
89
|
-
: 0,
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
// Get the cursors for all the active streams
|
|
93
|
-
const cursors = useMemo(() => {
|
|
94
|
-
if (!streamList?.streams) return [];
|
|
95
|
-
if (streamList.streams.kind !== "list") {
|
|
96
|
-
throw new Error("Expected list streams");
|
|
97
|
-
}
|
|
98
|
-
return streamList.streams.messages
|
|
99
|
-
.filter(({ streamId }) => !options?.skipStreamIds?.includes(streamId))
|
|
100
|
-
.filter(({ order }) => order >= minOrder)
|
|
101
|
-
.map(({ streamId }) => {
|
|
102
|
-
const cursor = streamCursors[streamId] ?? 0;
|
|
103
|
-
return { streamId, cursor };
|
|
104
|
-
});
|
|
105
|
-
}, [streamList, streamCursors, options?.skipStreamIds, minOrder]);
|
|
106
|
-
|
|
107
|
-
// Get the deltas for all the active streams, if any.
|
|
108
|
-
const cursorQuery = useQuery(
|
|
109
|
-
query,
|
|
110
|
-
args === "skip" || !streamList
|
|
111
|
-
? ("skip" as const)
|
|
112
|
-
: ({
|
|
113
|
-
...args,
|
|
114
|
-
streamArgs: { kind: "deltas", cursors } as StreamArgs,
|
|
115
|
-
} as FunctionArgs<Query>),
|
|
116
|
-
) as
|
|
117
|
-
| { streams: Extract<SyncStreamsReturnValue, { kind: "deltas" }> }
|
|
118
|
-
| undefined;
|
|
62
|
+
const streams = useDeltaStreams(query, args, options);
|
|
119
63
|
|
|
120
64
|
const threadId = args === "skip" ? undefined : args.threadId;
|
|
121
65
|
|
|
122
66
|
useEffect(() => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
for (const [
|
|
134
|
-
streamId,
|
|
135
|
-
controller,
|
|
136
|
-
] of uiMessageStreamControllers.current.entries()) {
|
|
137
|
-
if (!activeStreamIds.has(streamId)) {
|
|
138
|
-
uiMessageStreamControllers.current.delete(streamId);
|
|
139
|
-
controller.close();
|
|
67
|
+
if (!streams) return;
|
|
68
|
+
// return if there are no new deltas beyond the cursors
|
|
69
|
+
let noNewDeltas = true;
|
|
70
|
+
for (const stream of streams) {
|
|
71
|
+
const lastDelta = stream.deltas.at(-1);
|
|
72
|
+
const cursor = messageState[stream.streamMessage.streamId]?.cursor;
|
|
73
|
+
if (!cursor) {
|
|
74
|
+
noNewDeltas = false;
|
|
75
|
+
break;
|
|
140
76
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const deltasByStream = new Map<string, typeof cursorQuery.streams.deltas>();
|
|
145
|
-
|
|
146
|
-
// Group deltas by streamId
|
|
147
|
-
for (const delta of cursorQuery.streams.deltas) {
|
|
148
|
-
if (!deltasByStream.has(delta.streamId)) {
|
|
149
|
-
deltasByStream.set(delta.streamId, []);
|
|
77
|
+
if (lastDelta && lastDelta.start >= cursor) {
|
|
78
|
+
noNewDeltas = false;
|
|
79
|
+
break;
|
|
150
80
|
}
|
|
151
|
-
deltasByStream.get(delta.streamId)!.push(delta);
|
|
152
81
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
for (const [streamId, deltas] of deltasByStream.entries()) {
|
|
156
|
-
const streamMessage = streamList?.streams?.messages.find(
|
|
157
|
-
(m) => m.streamId === streamId,
|
|
158
|
-
);
|
|
159
|
-
if (!streamMessage) continue;
|
|
160
|
-
if (streamMessage.format === "UIMessageChunk") {
|
|
161
|
-
setStreamCursors((streamCursors) => {
|
|
162
|
-
const currentCursor = streamCursors[streamId] ?? 0;
|
|
163
|
-
|
|
164
|
-
const { parts, cursor } = getParts<UIMessageChunk>(
|
|
165
|
-
deltas,
|
|
166
|
-
currentCursor,
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
const newStreamCursors =
|
|
170
|
-
cursor === currentCursor
|
|
171
|
-
? streamCursors
|
|
172
|
-
: { ...streamCursors, [streamId]: cursor };
|
|
173
|
-
|
|
174
|
-
if (parts.length === 0) return newStreamCursors;
|
|
175
|
-
|
|
176
|
-
// Get existing message for this stream as starting point
|
|
177
|
-
const existingStream =
|
|
178
|
-
uiMessageStreamControllers.current.get(streamId);
|
|
179
|
-
if (existingStream) {
|
|
180
|
-
for (const part of parts) {
|
|
181
|
-
existingStream.enqueue(part);
|
|
182
|
-
}
|
|
183
|
-
} else {
|
|
184
|
-
const stream = new ReadableStream<UIMessageChunk>({
|
|
185
|
-
start(controller) {
|
|
186
|
-
uiMessageStreamControllers.current.set(streamId, controller);
|
|
187
|
-
for (const part of parts) {
|
|
188
|
-
controller.enqueue(part);
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
const initialMessage = blankUIMessage(streamMessage, threadId);
|
|
193
|
-
setUIMessages((prev) => ({
|
|
194
|
-
...prev,
|
|
195
|
-
[streamId]: initialMessage as UIMessage<
|
|
196
|
-
METADATA,
|
|
197
|
-
DATA_PARTS,
|
|
198
|
-
TOOLS
|
|
199
|
-
>,
|
|
200
|
-
}));
|
|
201
|
-
|
|
202
|
-
const messageStream = readUIMessageStream({
|
|
203
|
-
message: initialMessage,
|
|
204
|
-
stream,
|
|
205
|
-
onError: (error) => {
|
|
206
|
-
setUIMessages((prev) => ({
|
|
207
|
-
...prev,
|
|
208
|
-
[streamId]: {
|
|
209
|
-
...prev[streamId],
|
|
210
|
-
status: "failed",
|
|
211
|
-
},
|
|
212
|
-
}));
|
|
213
|
-
console.error(`Error in stream ${streamId}:`, error);
|
|
214
|
-
},
|
|
215
|
-
terminateOnError: false,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Process the async iterator
|
|
219
|
-
void (async () => {
|
|
220
|
-
for await (const message of messageStream) {
|
|
221
|
-
message.text = message.parts
|
|
222
|
-
.filter((p) => p.type === "text")
|
|
223
|
-
.map((p) => p.text)
|
|
224
|
-
.join("");
|
|
225
|
-
setUIMessages((prev) =>
|
|
226
|
-
// If we don't have a ui message assume we've aborted.
|
|
227
|
-
prev[streamId]
|
|
228
|
-
? {
|
|
229
|
-
...prev,
|
|
230
|
-
[streamId]: message as UIMessage<
|
|
231
|
-
METADATA,
|
|
232
|
-
DATA_PARTS,
|
|
233
|
-
TOOLS
|
|
234
|
-
>,
|
|
235
|
-
}
|
|
236
|
-
: prev,
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
})().catch((error) => {
|
|
240
|
-
console.error(`Error processing stream ${streamId}:`, error);
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
return newStreamCursors;
|
|
244
|
-
});
|
|
245
|
-
} else {
|
|
246
|
-
setUIMessages((uiMessages) => {
|
|
247
|
-
const existingUIMessage = uiMessages[streamId];
|
|
248
|
-
const [[uiMessage], [streamMetadata], changed] =
|
|
249
|
-
deriveUIMessagesFromTextStreamParts(
|
|
250
|
-
threadId,
|
|
251
|
-
[streamMessage],
|
|
252
|
-
existingUIMessage
|
|
253
|
-
? [
|
|
254
|
-
{
|
|
255
|
-
streamId,
|
|
256
|
-
cursor: streamCursors[streamId] ?? 0,
|
|
257
|
-
message: existingUIMessage,
|
|
258
|
-
},
|
|
259
|
-
]
|
|
260
|
-
: [],
|
|
261
|
-
deltas,
|
|
262
|
-
);
|
|
263
|
-
if (changed) {
|
|
264
|
-
setStreamCursors((prev) => ({
|
|
265
|
-
...prev,
|
|
266
|
-
[streamId]: streamMetadata.cursor,
|
|
267
|
-
}));
|
|
268
|
-
return {
|
|
269
|
-
...uiMessages,
|
|
270
|
-
[streamId]: uiMessage as UIMessage<METADATA, DATA_PARTS, TOOLS>,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
return uiMessages;
|
|
274
|
-
});
|
|
275
|
-
}
|
|
82
|
+
if (noNewDeltas) {
|
|
83
|
+
return;
|
|
276
84
|
}
|
|
277
|
-
|
|
85
|
+
const abortController = new AbortController();
|
|
86
|
+
void (async () => {
|
|
87
|
+
const newMessageState: Record<
|
|
88
|
+
string,
|
|
89
|
+
{
|
|
90
|
+
uiMessage: UIMessage<METADATA, DATA_PARTS, TOOLS>;
|
|
91
|
+
cursor: number;
|
|
92
|
+
}
|
|
93
|
+
> = Object.fromEntries(
|
|
94
|
+
await Promise.all(
|
|
95
|
+
streams.map(async ({ deltas, streamMessage }) => {
|
|
96
|
+
const { parts, cursor } = getParts<UIMessageChunk>(deltas, 0);
|
|
97
|
+
if (streamMessage.format === "UIMessageChunk") {
|
|
98
|
+
// Unfortunately this can't handle resuming from a UIMessage and
|
|
99
|
+
// adding more chunks, so we re-create it from scratch each time.
|
|
100
|
+
const uiMessage = await updateFromUIMessageChunks(
|
|
101
|
+
blankUIMessage(streamMessage, threadId),
|
|
102
|
+
parts,
|
|
103
|
+
);
|
|
104
|
+
return [
|
|
105
|
+
streamMessage.streamId,
|
|
106
|
+
{
|
|
107
|
+
uiMessage,
|
|
108
|
+
cursor,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
} else {
|
|
112
|
+
const [uiMessages] = deriveUIMessagesFromTextStreamParts(
|
|
113
|
+
threadId,
|
|
114
|
+
[streamMessage],
|
|
115
|
+
[],
|
|
116
|
+
deltas,
|
|
117
|
+
);
|
|
118
|
+
return [
|
|
119
|
+
streamMessage.streamId,
|
|
120
|
+
{
|
|
121
|
+
uiMessage: uiMessages[0],
|
|
122
|
+
cursor,
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
if (abortController.signal.aborted) return;
|
|
130
|
+
setMessageState(newMessageState);
|
|
131
|
+
})();
|
|
132
|
+
return () => {
|
|
133
|
+
abortController.abort();
|
|
134
|
+
};
|
|
135
|
+
}, [messageState, streams, threadId]);
|
|
278
136
|
|
|
279
137
|
return useMemo(() => {
|
|
280
|
-
if (!
|
|
281
|
-
return
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
);
|
|
288
|
-
if (!streamMessage) return undefined;
|
|
289
|
-
uiMessage.status = statusFromStreamStatus(
|
|
290
|
-
streamMessage?.status ?? "finished",
|
|
291
|
-
);
|
|
292
|
-
uiMessage.text = uiMessage.parts
|
|
293
|
-
.filter((p): p is TextUIPart => p.type === "text")
|
|
294
|
-
.map((p) => p.text)
|
|
295
|
-
.join("");
|
|
296
|
-
return uiMessage;
|
|
297
|
-
})
|
|
298
|
-
.filter((uiMessage) => uiMessage !== undefined),
|
|
299
|
-
),
|
|
300
|
-
);
|
|
301
|
-
}, [uiMessages, streamList]);
|
|
138
|
+
if (!streams) return undefined;
|
|
139
|
+
return streams
|
|
140
|
+
.map(
|
|
141
|
+
({ streamMessage }) => messageState[streamMessage.streamId]?.uiMessage,
|
|
142
|
+
)
|
|
143
|
+
.filter((uiMessage) => uiMessage !== undefined);
|
|
144
|
+
}, [messageState, streams]);
|
|
302
145
|
}
|
|
@@ -17,11 +17,12 @@ import type {
|
|
|
17
17
|
} from "convex/server";
|
|
18
18
|
import { useMemo } from "react";
|
|
19
19
|
import type { SyncStreamsReturnValue } from "../client/types.js";
|
|
20
|
-
import type {
|
|
20
|
+
import type { StreamArgs } from "../validators.js";
|
|
21
21
|
import type { StreamQuery } from "./types.js";
|
|
22
22
|
import { type UIMessage, type UIStatus } from "../UIMessages.js";
|
|
23
23
|
import { sorted } from "../shared.js";
|
|
24
24
|
import { useStreamingUIMessages } from "./useStreamingUIMessages.js";
|
|
25
|
+
import { combineUIMessages } from "../deltas.js";
|
|
25
26
|
|
|
26
27
|
export type UIMessageLike = {
|
|
27
28
|
order: number;
|
|
@@ -155,9 +156,11 @@ export function useUIMessages<
|
|
|
155
156
|
);
|
|
156
157
|
|
|
157
158
|
const merged = useMemo(() => {
|
|
159
|
+
// Messages may have been split by pagination. Re-combine them here.
|
|
160
|
+
const combined = combineUIMessages(sorted(paginated.results));
|
|
158
161
|
return {
|
|
159
162
|
...paginated,
|
|
160
|
-
results: dedupeMessages(
|
|
163
|
+
results: dedupeMessages(combined, streamMessages ?? []),
|
|
161
164
|
};
|
|
162
165
|
}, [paginated, streamMessages]);
|
|
163
166
|
|
package/src/validators.ts
CHANGED
package/dist/package.json
DELETED