@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-alpha.1
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/MIGRATION.md +153 -0
- package/README.md +32 -27
- package/dist/UIMessages.d.ts +46 -0
- package/dist/UIMessages.d.ts.map +1 -0
- package/dist/UIMessages.js +546 -0
- package/dist/UIMessages.js.map +1 -0
- package/dist/client/createTool.d.ts +126 -27
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +67 -12
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/defaultComponent.d.ts +11 -0
- package/dist/client/defaultComponent.d.ts.map +1 -0
- package/dist/client/defaultComponent.js +7 -0
- package/dist/client/defaultComponent.js.map +1 -0
- package/dist/client/definePlaygroundAPI.d.ts +1335 -204
- package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/client/definePlaygroundAPI.js +52 -28
- package/dist/client/definePlaygroundAPI.js.map +1 -1
- package/dist/client/files.d.ts +20 -7
- package/dist/client/files.d.ts.map +1 -1
- package/dist/client/files.js +68 -11
- package/dist/client/files.js.map +1 -1
- package/dist/client/index.d.ts +1116 -978
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +332 -747
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +461 -0
- package/dist/client/messages.d.ts.map +1 -0
- package/dist/client/messages.js +106 -0
- package/dist/client/messages.js.map +1 -0
- package/dist/client/mockModel.d.ts +42 -0
- package/dist/client/mockModel.d.ts.map +1 -0
- package/dist/client/mockModel.js +182 -0
- package/dist/client/mockModel.js.map +1 -0
- package/dist/client/saveInputMessages.d.ts +20 -0
- package/dist/client/saveInputMessages.d.ts.map +1 -0
- package/dist/client/saveInputMessages.js +58 -0
- package/dist/client/saveInputMessages.js.map +1 -0
- package/dist/client/search.d.ts +350 -39
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +350 -39
- package/dist/client/search.js.map +1 -1
- package/dist/client/start.d.ts +84 -0
- package/dist/client/start.d.ts.map +1 -0
- package/dist/client/start.js +185 -0
- package/dist/client/start.js.map +1 -0
- package/dist/client/streamText.d.ts +46 -0
- package/dist/client/streamText.d.ts.map +1 -0
- package/dist/client/streamText.js +117 -0
- package/dist/client/streamText.js.map +1 -0
- package/dist/client/streaming.d.ts +3716 -32
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +161 -59
- package/dist/client/streaming.js.map +1 -1
- package/dist/client/threads.d.ts +46 -0
- package/dist/client/threads.d.ts.map +1 -0
- package/dist/client/threads.js +49 -0
- package/dist/client/threads.js.map +1 -0
- package/dist/client/types.d.ts +266 -128
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/utils.d.ts +4 -0
- package/dist/client/utils.d.ts.map +1 -0
- package/dist/client/utils.js +21 -0
- package/dist/client/utils.js.map +1 -0
- package/dist/component/_generated/api.d.ts +24 -2178
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js +10 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +3120 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +4 -18
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +10 -38
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +9 -5
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/files.d.ts +16 -10
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +10 -2
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts +2578 -366
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +397 -154
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +5697 -3584
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +18 -41
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.d.ts +39 -339
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +114 -73
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts +13 -13
- package/dist/component/users.d.ts +7 -7
- package/dist/component/vector/index.d.ts +1 -1
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/index.js +1 -3
- package/dist/component/vector/index.js.map +1 -1
- package/dist/deltas.d.ts +43 -0
- package/dist/deltas.d.ts.map +1 -0
- package/dist/deltas.js +446 -0
- package/dist/deltas.js.map +1 -0
- package/dist/mapping.d.ts +38 -20
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +365 -97
- package/dist/mapping.js.map +1 -1
- package/dist/react/SmoothText.d.ts +5 -0
- package/dist/react/SmoothText.d.ts.map +1 -0
- package/dist/react/SmoothText.js +6 -0
- package/dist/react/SmoothText.js.map +1 -0
- package/dist/react/index.d.ts +5 -77
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +6 -160
- package/dist/react/index.js.map +1 -1
- package/dist/react/optimisticallySendMessage.d.ts +36 -3
- package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
- package/dist/react/optimisticallySendMessage.js +35 -9
- package/dist/react/optimisticallySendMessage.js.map +1 -1
- package/dist/react/types.d.ts +4 -18
- package/dist/react/types.d.ts.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 +106 -0
- package/dist/react/useDeltaStreams.js.map +1 -0
- package/dist/react/useSmoothText.d.ts +13 -12
- package/dist/react/useSmoothText.d.ts.map +1 -1
- package/dist/react/useSmoothText.js +32 -15
- package/dist/react/useSmoothText.js.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts +22 -0
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/react/useStreamingUIMessages.js +92 -0
- package/dist/react/useStreamingUIMessages.js.map +1 -0
- package/dist/react/useThreadMessages.d.ts +104 -0
- package/dist/react/useThreadMessages.d.ts.map +1 -0
- package/dist/react/useThreadMessages.js +148 -0
- package/dist/react/useThreadMessages.js.map +1 -0
- package/dist/react/useUIMessages.d.ts +96 -0
- package/dist/react/useUIMessages.d.ts.map +1 -0
- package/dist/react/useUIMessages.js +108 -0
- package/dist/react/useUIMessages.js.map +1 -0
- package/dist/shared.d.ts +20 -4
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +45 -8
- package/dist/shared.js.map +1 -1
- package/dist/validators.d.ts +22981 -5666
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +245 -137
- package/dist/validators.js.map +1 -1
- package/package.json +101 -51
- package/src/UIMessages.combineUIMessages.test.ts +239 -0
- package/src/UIMessages.test.ts +273 -0
- package/src/UIMessages.ts +739 -0
- package/src/client/approval.test.ts +350 -0
- package/src/client/createTool.ts +291 -76
- package/src/client/defaultComponent.ts +17 -0
- package/src/client/definePlaygroundAPI.ts +67 -31
- package/src/client/files.ts +100 -20
- package/src/client/index.test.ts +40 -85
- package/src/client/index.ts +638 -1289
- package/src/client/messages.ts +237 -0
- package/src/client/mockModel.ts +252 -0
- package/src/client/saveInputMessages.test.ts +583 -0
- package/src/client/saveInputMessages.ts +101 -0
- package/src/client/search.test.ts +1207 -0
- package/src/client/search.ts +581 -70
- package/src/client/start.ts +327 -0
- package/src/client/streamText.ts +187 -0
- package/src/client/streaming.test.ts +186 -0
- package/src/client/streaming.ts +241 -97
- package/src/client/threads.ts +83 -0
- package/src/client/types.ts +370 -219
- package/src/client/utils.ts +27 -0
- package/src/component/_generated/api.ts +64 -0
- package/src/component/_generated/component.ts +4902 -0
- package/src/component/_generated/{server.d.ts → server.ts} +33 -21
- package/src/component/files.ts +11 -2
- package/src/component/messages.test.ts +195 -51
- package/src/component/messages.ts +500 -201
- package/src/component/schema.ts +20 -46
- package/src/component/setup.test.ts +7 -0
- package/src/component/streams.ts +184 -83
- package/src/component/users.test.ts +0 -1
- package/src/component/vector/index.ts +1 -3
- package/src/deltas.test.ts +626 -0
- package/src/deltas.ts +569 -0
- package/src/fromUIMessages.test.ts +497 -0
- package/src/mapping.test.ts +180 -6
- package/src/mapping.ts +479 -162
- package/src/react/SmoothText.tsx +9 -0
- package/src/react/index.ts +10 -230
- package/src/react/optimisticallySendMessage.ts +55 -12
- package/src/react/types.ts +6 -39
- package/src/react/useDeltaStreams.ts +160 -0
- package/src/react/useSmoothText.ts +56 -36
- package/src/react/useStreamingUIMessages.ts +143 -0
- package/src/react/useThreadMessages.ts +262 -0
- package/src/react/useUIMessages.test.ts +255 -0
- package/src/react/useUIMessages.ts +195 -0
- package/src/shared.ts +88 -12
- package/src/test.ts +18 -0
- package/src/toUIMessages.test.ts +1269 -0
- package/src/validators.test.ts +18 -19
- package/src/validators.ts +325 -185
- package/dist/client/_generated/_ignore.d.ts +0 -1
- package/dist/client/_generated/_ignore.d.ts.map +0 -1
- package/dist/client/_generated/_ignore.js +0 -3
- package/dist/client/_generated/_ignore.js.map +0 -1
- package/dist/client/listMessages.d.ts +0 -22
- package/dist/client/listMessages.d.ts.map +0 -1
- package/dist/client/listMessages.js +0 -25
- package/dist/client/listMessages.js.map +0 -1
- package/dist/package.json +0 -3
- package/dist/react/deltas.d.ts +0 -26
- package/dist/react/deltas.d.ts.map +0 -1
- package/dist/react/deltas.js +0 -384
- package/dist/react/deltas.js.map +0 -1
- package/dist/react/toUIMessages.d.ts +0 -15
- package/dist/react/toUIMessages.d.ts.map +0 -1
- package/dist/react/toUIMessages.js +0 -211
- package/dist/react/toUIMessages.js.map +0 -1
- package/src/client/listMessages.ts +0 -38
- package/src/component/_generated/api.d.ts +0 -2202
- package/src/component/_generated/api.js +0 -23
- package/src/component/_generated/server.js +0 -90
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/src/react/deltas.test.ts +0 -315
- package/src/react/deltas.ts +0 -478
- package/src/react/toUIMessages.test.ts +0 -420
- package/src/react/toUIMessages.ts +0 -253
- package/src/vitest.config.ts +0 -7
- /package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +0 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
blankUIMessage,
|
|
4
|
+
deriveUIMessagesFromTextStreamParts,
|
|
5
|
+
updateFromTextStreamParts,
|
|
6
|
+
updateFromUIMessageChunks,
|
|
7
|
+
} from "./deltas.js";
|
|
8
|
+
import type { StreamMessage, StreamDelta } from "./validators.js";
|
|
9
|
+
import { omit } from "convex-helpers";
|
|
10
|
+
import type { Tool, ToolUIPart, TypedToolResult } from "ai";
|
|
11
|
+
|
|
12
|
+
describe("UIMessageChunks", () => {
|
|
13
|
+
it("updates a UIMessage with a tool call and follow up", async () => {
|
|
14
|
+
const uiMessage = blankUIMessage(
|
|
15
|
+
{
|
|
16
|
+
streamId: "s1",
|
|
17
|
+
status: "streaming",
|
|
18
|
+
order: 0,
|
|
19
|
+
stepOrder: 1,
|
|
20
|
+
format: "UIMessageChunk",
|
|
21
|
+
agentName: "agent1",
|
|
22
|
+
},
|
|
23
|
+
"thread1",
|
|
24
|
+
);
|
|
25
|
+
expect(uiMessage.text).toBe("");
|
|
26
|
+
expect(uiMessage.parts).toEqual([]);
|
|
27
|
+
const updatedMessage = await updateFromUIMessageChunks(uiMessage, [
|
|
28
|
+
{ type: "start" },
|
|
29
|
+
{ type: "start-step" },
|
|
30
|
+
{ type: "reasoning-start", id: "reasoning-0" },
|
|
31
|
+
{ type: "reasoning-delta", id: "reasoning-0", delta: "Okay" },
|
|
32
|
+
{
|
|
33
|
+
type: "reasoning-delta",
|
|
34
|
+
id: "reasoning-0",
|
|
35
|
+
delta: ", the user is asking...",
|
|
36
|
+
},
|
|
37
|
+
{ type: "text-start", id: "txt-1" },
|
|
38
|
+
{
|
|
39
|
+
type: "text-delta",
|
|
40
|
+
id: "txt-1",
|
|
41
|
+
delta: "Hey ho.",
|
|
42
|
+
},
|
|
43
|
+
{ type: "reasoning-end", id: "reasoning-0" },
|
|
44
|
+
{ type: "text-end", id: "txt-1" },
|
|
45
|
+
{ type: "tool-input-start", toolCallId: "0ychh9k6f", toolName: "say" },
|
|
46
|
+
{
|
|
47
|
+
type: "tool-input-delta",
|
|
48
|
+
toolCallId: "0ychh9k6f",
|
|
49
|
+
inputTextDelta:
|
|
50
|
+
'{"question":"What is your favorite flavor of ice cream?"}',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "tool-input-available",
|
|
54
|
+
toolCallId: "0ychh9k6f",
|
|
55
|
+
toolName: "say",
|
|
56
|
+
input: { question: "What is your favorite flavor of ice cream?" },
|
|
57
|
+
providerMetadata: { openai: { itemId: "123" } },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: "tool-output-available",
|
|
61
|
+
toolCallId: "0ychh9k6f",
|
|
62
|
+
output: "I'm sorry I can't help you. Stop asking me questions.",
|
|
63
|
+
},
|
|
64
|
+
{ type: "finish-step" },
|
|
65
|
+
{ type: "start-step" },
|
|
66
|
+
{ type: "tool-input-start", toolCallId: "1ychh9k6f", toolName: "say" },
|
|
67
|
+
{
|
|
68
|
+
type: "tool-input-delta",
|
|
69
|
+
toolCallId: "1ychh9k6f",
|
|
70
|
+
inputTextDelta:
|
|
71
|
+
'{"question":"What is your favorite flavor of ice cream?"}',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: "tool-input-available",
|
|
75
|
+
toolCallId: "1ychh9k6f",
|
|
76
|
+
toolName: "say",
|
|
77
|
+
input: { question: "What is your favorite flavor of ice cream?" },
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "tool-output-available",
|
|
81
|
+
toolCallId: "1ychh9k6f",
|
|
82
|
+
output: "I'm serious.",
|
|
83
|
+
},
|
|
84
|
+
{ type: "finish-step" },
|
|
85
|
+
{ type: "start-step" },
|
|
86
|
+
{ type: "text-start", id: "msg_0" },
|
|
87
|
+
{
|
|
88
|
+
type: "text-delta",
|
|
89
|
+
id: "msg_0",
|
|
90
|
+
delta: "The best ice cream flavor is vanilla",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: "text-delta",
|
|
94
|
+
id: "msg_0",
|
|
95
|
+
delta: ".",
|
|
96
|
+
},
|
|
97
|
+
{ type: "text-end", id: "msg_0" },
|
|
98
|
+
{ type: "finish-step" },
|
|
99
|
+
{ type: "finish" },
|
|
100
|
+
]);
|
|
101
|
+
expect(updatedMessage.text).toBe(
|
|
102
|
+
"Hey ho. The best ice cream flavor is vanilla.",
|
|
103
|
+
);
|
|
104
|
+
const expectedParts = [
|
|
105
|
+
{
|
|
106
|
+
type: "step-start",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
state: "done",
|
|
110
|
+
text: "Okay, the user is asking...",
|
|
111
|
+
type: "reasoning",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
state: "done",
|
|
115
|
+
text: "Hey ho.",
|
|
116
|
+
type: "text",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
callProviderMetadata: {
|
|
120
|
+
openai: {
|
|
121
|
+
itemId: "123",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
input: {
|
|
125
|
+
question: "What is your favorite flavor of ice cream?",
|
|
126
|
+
},
|
|
127
|
+
output: "I'm sorry I can't help you. Stop asking me questions.",
|
|
128
|
+
|
|
129
|
+
state: "output-available",
|
|
130
|
+
toolCallId: "0ychh9k6f",
|
|
131
|
+
type: "tool-say",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: "step-start",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
input: {
|
|
138
|
+
question: "What is your favorite flavor of ice cream?",
|
|
139
|
+
},
|
|
140
|
+
output: "I'm serious.",
|
|
141
|
+
state: "output-available",
|
|
142
|
+
toolCallId: "1ychh9k6f",
|
|
143
|
+
type: "tool-say",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: "step-start",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
state: "done",
|
|
150
|
+
text: "The best ice cream flavor is vanilla.",
|
|
151
|
+
type: "text",
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
expect(updatedMessage.parts).toEqual(expectedParts);
|
|
155
|
+
expect(updatedMessage.parts).toHaveLength(8);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("UIMessageChunks - continuation stream", () => {
|
|
160
|
+
it("gracefully handles tool-result without tool-call in continuation stream after approval", async () => {
|
|
161
|
+
// This simulates what happens after tool approval:
|
|
162
|
+
// Stream A: tool-call, tool-approval-request -> finishes
|
|
163
|
+
// User approves
|
|
164
|
+
// Stream B: tool-result (referencing tool-call from Stream A) -> this test
|
|
165
|
+
//
|
|
166
|
+
// The AI SDK's readUIMessageStream expects tool-call before tool-result,
|
|
167
|
+
// but they're in different streams. The onError handler should gracefully
|
|
168
|
+
// ignore this error since stored messages provide the fallback.
|
|
169
|
+
const uiMessage = blankUIMessage(
|
|
170
|
+
{
|
|
171
|
+
streamId: "continuation-stream",
|
|
172
|
+
status: "streaming",
|
|
173
|
+
order: 1,
|
|
174
|
+
stepOrder: 0,
|
|
175
|
+
format: "UIMessageChunk",
|
|
176
|
+
agentName: "agent1",
|
|
177
|
+
},
|
|
178
|
+
"thread1",
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Send a tool-result without the corresponding tool-call in this stream
|
|
182
|
+
// This would normally throw "No tool invocation found" error
|
|
183
|
+
const updatedMessage = await updateFromUIMessageChunks(uiMessage, [
|
|
184
|
+
{ type: "start" },
|
|
185
|
+
{ type: "start-step" },
|
|
186
|
+
{
|
|
187
|
+
type: "tool-output-available",
|
|
188
|
+
toolCallId: "call_from_previous_stream",
|
|
189
|
+
output: "Tool execution result",
|
|
190
|
+
},
|
|
191
|
+
{ type: "finish-step" },
|
|
192
|
+
{ type: "finish" },
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
// The message should NOT be marked as failed - the error should be suppressed
|
|
196
|
+
expect(updatedMessage.status).not.toBe("failed");
|
|
197
|
+
// The stream still processes (even if tool-output isn't reflected without tool-input)
|
|
198
|
+
expect(updatedMessage).toBeDefined();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("mergeDeltas", () => {
|
|
203
|
+
it("merges a single text-delta into a message", () => {
|
|
204
|
+
const streamId = "s1";
|
|
205
|
+
const deltas = [
|
|
206
|
+
{
|
|
207
|
+
streamId,
|
|
208
|
+
start: 0,
|
|
209
|
+
end: 5,
|
|
210
|
+
parts: [{ type: "text-delta", id: "1", text: "Hello" }],
|
|
211
|
+
} satisfies StreamDelta,
|
|
212
|
+
];
|
|
213
|
+
const [messages, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
|
|
214
|
+
"thread1",
|
|
215
|
+
[{ streamId, order: 1, stepOrder: 0, status: "streaming" }],
|
|
216
|
+
[],
|
|
217
|
+
deltas,
|
|
218
|
+
);
|
|
219
|
+
expect(messages).toHaveLength(1);
|
|
220
|
+
expect(messages[0].text).toBe("Hello");
|
|
221
|
+
expect(messages[0].role).toBe("assistant");
|
|
222
|
+
expect(changed).toBe(true);
|
|
223
|
+
expect(newStreams[0].cursor).toBe(5);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("merges multiple deltas for the same stream", () => {
|
|
227
|
+
const streamId = "s1";
|
|
228
|
+
const deltas = [
|
|
229
|
+
{
|
|
230
|
+
streamId,
|
|
231
|
+
start: 0,
|
|
232
|
+
end: 5,
|
|
233
|
+
parts: [{ type: "text-delta", id: "1", text: "Hello" }],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
streamId,
|
|
237
|
+
start: 5,
|
|
238
|
+
end: 11,
|
|
239
|
+
parts: [{ type: "text-delta", id: "2", text: " World!" }],
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
const [messages, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
|
|
243
|
+
"thread1",
|
|
244
|
+
[{ streamId, order: 1, stepOrder: 0, status: "streaming" }],
|
|
245
|
+
[],
|
|
246
|
+
deltas,
|
|
247
|
+
);
|
|
248
|
+
expect(messages).toHaveLength(1);
|
|
249
|
+
expect(messages[0].text).toBe("Hello World!");
|
|
250
|
+
expect(changed).toBe(true);
|
|
251
|
+
expect(newStreams[0].cursor).toBe(11);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("handles tool-call and tool-result parts", () => {
|
|
255
|
+
const streamId = "s2";
|
|
256
|
+
const deltas = [
|
|
257
|
+
{
|
|
258
|
+
streamId,
|
|
259
|
+
start: 0,
|
|
260
|
+
end: 1,
|
|
261
|
+
parts: [
|
|
262
|
+
{
|
|
263
|
+
type: "tool-call",
|
|
264
|
+
toolCallId: "call1",
|
|
265
|
+
toolName: "myTool",
|
|
266
|
+
input: "What's the meaning of life?",
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
} satisfies StreamDelta,
|
|
270
|
+
{
|
|
271
|
+
streamId,
|
|
272
|
+
start: 1,
|
|
273
|
+
end: 2,
|
|
274
|
+
parts: [
|
|
275
|
+
{
|
|
276
|
+
type: "tool-result",
|
|
277
|
+
toolCallId: "call1",
|
|
278
|
+
toolName: "myTool",
|
|
279
|
+
input: undefined,
|
|
280
|
+
output: "42",
|
|
281
|
+
} satisfies TypedToolResult<{ myTool: Tool }>,
|
|
282
|
+
],
|
|
283
|
+
} satisfies StreamDelta,
|
|
284
|
+
];
|
|
285
|
+
const [[message], _, changed] = deriveUIMessagesFromTextStreamParts(
|
|
286
|
+
"thread1",
|
|
287
|
+
[{ streamId, order: 2, stepOrder: 0, status: "streaming" }],
|
|
288
|
+
[],
|
|
289
|
+
deltas,
|
|
290
|
+
);
|
|
291
|
+
expect(message).toBeDefined();
|
|
292
|
+
expect(message.role).toBe("assistant");
|
|
293
|
+
const content = message.parts;
|
|
294
|
+
expect(content).toEqual([
|
|
295
|
+
{
|
|
296
|
+
type: "tool-myTool",
|
|
297
|
+
toolCallId: "call1",
|
|
298
|
+
input: "What's the meaning of life?",
|
|
299
|
+
output: "42",
|
|
300
|
+
state: "output-available",
|
|
301
|
+
} satisfies ToolUIPart,
|
|
302
|
+
]);
|
|
303
|
+
expect(changed).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("returns changed=false if no new deltas", () => {
|
|
307
|
+
const streamId = "s3";
|
|
308
|
+
const deltas: StreamDelta[] = [];
|
|
309
|
+
const [, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
|
|
310
|
+
"thread1",
|
|
311
|
+
[{ streamId, order: 3, stepOrder: 0, status: "streaming" }],
|
|
312
|
+
[],
|
|
313
|
+
deltas,
|
|
314
|
+
);
|
|
315
|
+
expect(changed).toBe(false);
|
|
316
|
+
expect(newStreams[0].cursor).toBe(0);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("handles multiple streams and sorts by order/stepOrder", () => {
|
|
320
|
+
const deltas = [
|
|
321
|
+
{
|
|
322
|
+
streamId: "s2",
|
|
323
|
+
start: 0,
|
|
324
|
+
end: 3,
|
|
325
|
+
parts: [{ type: "text-delta", id: "1", text: "B" }],
|
|
326
|
+
} satisfies StreamDelta,
|
|
327
|
+
{
|
|
328
|
+
streamId: "s1",
|
|
329
|
+
start: 0,
|
|
330
|
+
end: 3,
|
|
331
|
+
parts: [{ type: "text-delta", id: "2", text: "A" }],
|
|
332
|
+
} satisfies StreamDelta,
|
|
333
|
+
];
|
|
334
|
+
const [messages, _, changed] = deriveUIMessagesFromTextStreamParts(
|
|
335
|
+
"thread1",
|
|
336
|
+
[
|
|
337
|
+
{ streamId: "s1", order: 1, stepOrder: 0, status: "streaming" },
|
|
338
|
+
{ streamId: "s2", order: 2, stepOrder: 0, status: "streaming" },
|
|
339
|
+
],
|
|
340
|
+
[],
|
|
341
|
+
deltas,
|
|
342
|
+
);
|
|
343
|
+
expect(messages).toHaveLength(2);
|
|
344
|
+
expect(messages[0].text).toBe("A");
|
|
345
|
+
expect(messages[1].text).toBe("B");
|
|
346
|
+
expect(changed).toBe(true);
|
|
347
|
+
// Sorted by order
|
|
348
|
+
expect(messages[0].order).toBe(1);
|
|
349
|
+
expect(messages[1].order).toBe(2);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("does not duplicate text content when merging sequential text-deltas", () => {
|
|
353
|
+
const streamId = "s4";
|
|
354
|
+
const deltas = [
|
|
355
|
+
{
|
|
356
|
+
streamId,
|
|
357
|
+
start: 0,
|
|
358
|
+
end: 5,
|
|
359
|
+
parts: [{ type: "text-delta", id: "1", text: "Hello" }],
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
streamId,
|
|
363
|
+
start: 5,
|
|
364
|
+
end: 11,
|
|
365
|
+
parts: [{ type: "text-delta", id: "2", text: " World!" }],
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
streamId,
|
|
369
|
+
start: 11,
|
|
370
|
+
end: 12,
|
|
371
|
+
parts: [{ type: "text-delta", id: "3", text: "!" }],
|
|
372
|
+
},
|
|
373
|
+
] satisfies StreamDelta[];
|
|
374
|
+
const [messages] = deriveUIMessagesFromTextStreamParts(
|
|
375
|
+
"thread1",
|
|
376
|
+
[{ streamId, order: 4, stepOrder: 0, status: "streaming" }],
|
|
377
|
+
[],
|
|
378
|
+
deltas,
|
|
379
|
+
);
|
|
380
|
+
expect(messages).toHaveLength(1);
|
|
381
|
+
expect(messages[0].text).toBe("Hello World!!");
|
|
382
|
+
// There should only be one text part per message
|
|
383
|
+
const content = messages[0].parts;
|
|
384
|
+
if (Array.isArray(content)) {
|
|
385
|
+
const textParts = content.filter((p) => p.type === "text");
|
|
386
|
+
expect(textParts).toHaveLength(1);
|
|
387
|
+
expect(textParts[0].text).toBe("Hello World!!");
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("does not duplicate reasoning parts", () => {
|
|
392
|
+
const streamId = "s6";
|
|
393
|
+
const deltas = [
|
|
394
|
+
{
|
|
395
|
+
streamId,
|
|
396
|
+
start: 0,
|
|
397
|
+
end: 1,
|
|
398
|
+
parts: [
|
|
399
|
+
{ type: "reasoning-start", id: "1" },
|
|
400
|
+
{ type: "reasoning-delta", id: "1", text: "I'm thinking..." },
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
streamId,
|
|
405
|
+
start: 1,
|
|
406
|
+
end: 2,
|
|
407
|
+
parts: [
|
|
408
|
+
{ type: "reasoning-delta", id: "1", text: " Still thinking..." },
|
|
409
|
+
],
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
streamId,
|
|
413
|
+
start: 2,
|
|
414
|
+
end: 3,
|
|
415
|
+
parts: [{ type: "reasoning-end", id: "1" }],
|
|
416
|
+
},
|
|
417
|
+
];
|
|
418
|
+
const [messages] = deriveUIMessagesFromTextStreamParts(
|
|
419
|
+
"thread1",
|
|
420
|
+
[{ streamId, order: 6, stepOrder: 0, status: "streaming" }],
|
|
421
|
+
[],
|
|
422
|
+
deltas,
|
|
423
|
+
);
|
|
424
|
+
expect(messages).toHaveLength(1);
|
|
425
|
+
if (Array.isArray(messages[0].parts)) {
|
|
426
|
+
const reasoningParts = messages[0].parts.filter(
|
|
427
|
+
(p) => p.type === "reasoning",
|
|
428
|
+
);
|
|
429
|
+
expect(reasoningParts).toHaveLength(1);
|
|
430
|
+
expect(reasoningParts[0].text).toBe("I'm thinking... Still thinking...");
|
|
431
|
+
expect(reasoningParts[0].state).toBe("done");
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("applyDeltasToStreamMessage is idempotent and does not duplicate content", () => {
|
|
436
|
+
const streamId = "s7";
|
|
437
|
+
const streamMessage = {
|
|
438
|
+
streamId,
|
|
439
|
+
order: 7,
|
|
440
|
+
stepOrder: 0,
|
|
441
|
+
status: "streaming",
|
|
442
|
+
} satisfies StreamMessage;
|
|
443
|
+
const deltas = [
|
|
444
|
+
{
|
|
445
|
+
streamId,
|
|
446
|
+
start: 0,
|
|
447
|
+
end: 5,
|
|
448
|
+
parts: [{ type: "text-delta", id: "1", text: "Hello" }],
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
streamId,
|
|
452
|
+
start: 5,
|
|
453
|
+
end: 11,
|
|
454
|
+
parts: [{ type: "text-delta", id: "2", text: " World!" }],
|
|
455
|
+
},
|
|
456
|
+
];
|
|
457
|
+
// First call: apply both deltas
|
|
458
|
+
let [result, changed] = updateFromTextStreamParts(
|
|
459
|
+
"thread1",
|
|
460
|
+
streamMessage,
|
|
461
|
+
undefined,
|
|
462
|
+
deltas,
|
|
463
|
+
);
|
|
464
|
+
expect(result.message.text).toBe("Hello World!");
|
|
465
|
+
// Second call: re-apply the same deltas (should not duplicate)
|
|
466
|
+
[result, changed] = updateFromTextStreamParts(
|
|
467
|
+
"thread1",
|
|
468
|
+
streamMessage,
|
|
469
|
+
result,
|
|
470
|
+
deltas,
|
|
471
|
+
);
|
|
472
|
+
expect(result.message.text).toBe("Hello World!");
|
|
473
|
+
// Third call: add a new delta
|
|
474
|
+
const moreDeltas = [
|
|
475
|
+
...deltas,
|
|
476
|
+
{
|
|
477
|
+
streamId,
|
|
478
|
+
start: 11,
|
|
479
|
+
end: 12,
|
|
480
|
+
parts: [{ type: "text-delta", id: "3", text: "!" }],
|
|
481
|
+
},
|
|
482
|
+
];
|
|
483
|
+
[result, changed] = updateFromTextStreamParts(
|
|
484
|
+
"thread1",
|
|
485
|
+
streamMessage,
|
|
486
|
+
result,
|
|
487
|
+
moreDeltas,
|
|
488
|
+
);
|
|
489
|
+
expect(changed).toBe(true);
|
|
490
|
+
expect(result.message.text).toBe("Hello World!!");
|
|
491
|
+
// Re-apply all deltas again (should still not duplicate)
|
|
492
|
+
[result, changed] = updateFromTextStreamParts(
|
|
493
|
+
"thread1",
|
|
494
|
+
streamMessage,
|
|
495
|
+
result,
|
|
496
|
+
moreDeltas,
|
|
497
|
+
);
|
|
498
|
+
expect(changed).toBe(false);
|
|
499
|
+
expect(result.message.text).toBe("Hello World!!");
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("mergeDeltas is pure and does not mutate inputs", () => {
|
|
503
|
+
const streamId = "s8";
|
|
504
|
+
const streamMessages = [
|
|
505
|
+
{ streamId, order: 8, stepOrder: 0, status: "streaming" },
|
|
506
|
+
] satisfies StreamMessage[];
|
|
507
|
+
const deltas = [
|
|
508
|
+
{
|
|
509
|
+
streamId,
|
|
510
|
+
start: 0,
|
|
511
|
+
end: 5,
|
|
512
|
+
parts: [{ type: "text-delta", id: "1", text: "Hello" }],
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
streamId,
|
|
516
|
+
start: 5,
|
|
517
|
+
end: 11,
|
|
518
|
+
parts: [{ type: "text-delta", id: "2", text: " World!" }],
|
|
519
|
+
},
|
|
520
|
+
];
|
|
521
|
+
// Deep freeze inputs to catch mutation
|
|
522
|
+
function deepFreeze(obj: unknown): unknown {
|
|
523
|
+
if (obj && typeof obj === "object" && !Object.isFrozen(obj)) {
|
|
524
|
+
Object.freeze(obj);
|
|
525
|
+
for (const key of Object.keys(obj)) {
|
|
526
|
+
deepFreeze((obj as Record<string, unknown>)[key]);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return obj;
|
|
530
|
+
}
|
|
531
|
+
deepFreeze(streamMessages);
|
|
532
|
+
deepFreeze(deltas);
|
|
533
|
+
const [messages1, streams1, changed1] = deriveUIMessagesFromTextStreamParts(
|
|
534
|
+
"thread1",
|
|
535
|
+
streamMessages,
|
|
536
|
+
[],
|
|
537
|
+
deltas,
|
|
538
|
+
);
|
|
539
|
+
const [messages2, streams2, changed2] = deriveUIMessagesFromTextStreamParts(
|
|
540
|
+
"thread1",
|
|
541
|
+
streamMessages,
|
|
542
|
+
[],
|
|
543
|
+
deltas,
|
|
544
|
+
);
|
|
545
|
+
expect(messages1.map((m) => omit(m, ["_creationTime"]))).toEqual(
|
|
546
|
+
messages2.map((m) => omit(m, ["_creationTime"])),
|
|
547
|
+
);
|
|
548
|
+
expect(
|
|
549
|
+
streams1.map((s) => ({
|
|
550
|
+
...s,
|
|
551
|
+
message: omit(s.message, ["_creationTime"]),
|
|
552
|
+
})),
|
|
553
|
+
).toEqual(
|
|
554
|
+
streams2.map((s) => ({
|
|
555
|
+
...s,
|
|
556
|
+
message: omit(s.message, ["_creationTime"]),
|
|
557
|
+
})),
|
|
558
|
+
);
|
|
559
|
+
expect(changed1).toBe(changed2);
|
|
560
|
+
// Inputs should remain unchanged
|
|
561
|
+
expect(streamMessages).toMatchObject([
|
|
562
|
+
{ streamId, order: 8, stepOrder: 0, status: "streaming" },
|
|
563
|
+
]);
|
|
564
|
+
expect(deltas).toEqual([
|
|
565
|
+
{
|
|
566
|
+
streamId,
|
|
567
|
+
start: 0,
|
|
568
|
+
end: 5,
|
|
569
|
+
parts: [{ type: "text-delta", id: "1", text: "Hello" }],
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
streamId,
|
|
573
|
+
start: 5,
|
|
574
|
+
end: 11,
|
|
575
|
+
parts: [{ type: "text-delta", id: "2", text: " World!" }],
|
|
576
|
+
},
|
|
577
|
+
]);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("handles streaming tool-approval-request and updates tool state", () => {
|
|
581
|
+
const streamId = "s10";
|
|
582
|
+
const deltas = [
|
|
583
|
+
{
|
|
584
|
+
streamId,
|
|
585
|
+
start: 0,
|
|
586
|
+
end: 1,
|
|
587
|
+
parts: [
|
|
588
|
+
{
|
|
589
|
+
type: "tool-call",
|
|
590
|
+
toolCallId: "call1",
|
|
591
|
+
toolName: "dangerousTool",
|
|
592
|
+
input: { action: "delete" },
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
} satisfies StreamDelta,
|
|
596
|
+
{
|
|
597
|
+
streamId,
|
|
598
|
+
start: 1,
|
|
599
|
+
end: 2,
|
|
600
|
+
parts: [
|
|
601
|
+
{
|
|
602
|
+
type: "tool-approval-request",
|
|
603
|
+
toolCallId: "call1",
|
|
604
|
+
approvalId: "approval1",
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
} satisfies StreamDelta,
|
|
608
|
+
];
|
|
609
|
+
const [[message], _, changed] = deriveUIMessagesFromTextStreamParts(
|
|
610
|
+
"thread1",
|
|
611
|
+
[{ streamId, order: 10, stepOrder: 0, status: "streaming" }],
|
|
612
|
+
[],
|
|
613
|
+
deltas,
|
|
614
|
+
);
|
|
615
|
+
expect(message).toBeDefined();
|
|
616
|
+
expect(message.role).toBe("assistant");
|
|
617
|
+
expect(changed).toBe(true);
|
|
618
|
+
|
|
619
|
+
const toolPart = message.parts.find(
|
|
620
|
+
(p) => p.type === "tool-dangerousTool",
|
|
621
|
+
) as any;
|
|
622
|
+
expect(toolPart).toBeDefined();
|
|
623
|
+
expect(toolPart.state).toBe("approval-requested");
|
|
624
|
+
expect(toolPart.approval).toEqual({ id: "approval1" });
|
|
625
|
+
});
|
|
626
|
+
});
|