@convex-dev/agent 0.1.18 → 0.2.0

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.
Files changed (122) hide show
  1. package/dist/client/createTool.d.ts +30 -11
  2. package/dist/client/createTool.d.ts.map +1 -1
  3. package/dist/client/createTool.js +24 -10
  4. package/dist/client/createTool.js.map +1 -1
  5. package/dist/client/defaultComponent.d.ts +11 -0
  6. package/dist/client/defaultComponent.d.ts.map +1 -0
  7. package/dist/client/defaultComponent.js +7 -0
  8. package/dist/client/defaultComponent.js.map +1 -0
  9. package/dist/client/definePlaygroundAPI.d.ts +68 -23
  10. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  11. package/dist/client/definePlaygroundAPI.js +7 -20
  12. package/dist/client/definePlaygroundAPI.js.map +1 -1
  13. package/dist/client/files.d.ts +14 -4
  14. package/dist/client/files.d.ts.map +1 -1
  15. package/dist/client/files.js +66 -11
  16. package/dist/client/files.js.map +1 -1
  17. package/dist/client/index.d.ts +323 -780
  18. package/dist/client/index.d.ts.map +1 -1
  19. package/dist/client/index.js +445 -543
  20. package/dist/client/index.js.map +1 -1
  21. package/dist/client/messages.d.ts +256 -0
  22. package/dist/client/messages.d.ts.map +1 -0
  23. package/dist/client/messages.js +97 -0
  24. package/dist/client/messages.js.map +1 -0
  25. package/dist/client/search.d.ts +35 -12
  26. package/dist/client/search.d.ts.map +1 -1
  27. package/dist/client/search.js +21 -4
  28. package/dist/client/search.js.map +1 -1
  29. package/dist/client/streaming.d.ts +21 -21
  30. package/dist/client/streaming.d.ts.map +1 -1
  31. package/dist/client/streaming.js +35 -40
  32. package/dist/client/streaming.js.map +1 -1
  33. package/dist/client/threads.d.ts +46 -0
  34. package/dist/client/threads.d.ts.map +1 -0
  35. package/dist/client/threads.js +49 -0
  36. package/dist/client/threads.js.map +1 -0
  37. package/dist/client/types.d.ts +159 -75
  38. package/dist/client/types.d.ts.map +1 -1
  39. package/dist/component/_generated/api.d.ts +261 -209
  40. package/dist/component/messages.d.ts +314 -105
  41. package/dist/component/messages.d.ts.map +1 -1
  42. package/dist/component/messages.js +141 -83
  43. package/dist/component/messages.js.map +1 -1
  44. package/dist/component/schema.d.ts +631 -870
  45. package/dist/component/schema.d.ts.map +1 -1
  46. package/dist/component/schema.js +11 -16
  47. package/dist/component/schema.js.map +1 -1
  48. package/dist/component/streams.d.ts +29 -149
  49. package/dist/component/streams.d.ts.map +1 -1
  50. package/dist/component/streams.js +104 -60
  51. package/dist/component/streams.js.map +1 -1
  52. package/dist/component/threads.d.ts +4 -4
  53. package/dist/component/users.d.ts +3 -3
  54. package/dist/mapping.d.ts +15 -16
  55. package/dist/mapping.d.ts.map +1 -1
  56. package/dist/mapping.js +208 -62
  57. package/dist/mapping.js.map +1 -1
  58. package/dist/parts.d.ts +3 -0
  59. package/dist/parts.d.ts.map +1 -0
  60. package/dist/parts.js +34 -0
  61. package/dist/parts.js.map +1 -0
  62. package/dist/react/SmoothText.d.ts +5 -0
  63. package/dist/react/SmoothText.d.ts.map +1 -0
  64. package/dist/react/SmoothText.js +6 -0
  65. package/dist/react/SmoothText.js.map +1 -0
  66. package/dist/react/deltas.d.ts +3 -5
  67. package/dist/react/deltas.d.ts.map +1 -1
  68. package/dist/react/deltas.js +89 -53
  69. package/dist/react/deltas.js.map +1 -1
  70. package/dist/react/index.d.ts +1 -0
  71. package/dist/react/index.d.ts.map +1 -1
  72. package/dist/react/index.js +31 -11
  73. package/dist/react/index.js.map +1 -1
  74. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  75. package/dist/react/optimisticallySendMessage.js +2 -8
  76. package/dist/react/optimisticallySendMessage.js.map +1 -1
  77. package/dist/react/toUIMessages.d.ts +5 -4
  78. package/dist/react/toUIMessages.d.ts.map +1 -1
  79. package/dist/react/toUIMessages.js +103 -40
  80. package/dist/react/toUIMessages.js.map +1 -1
  81. package/dist/react/useSmoothText.d.ts +13 -12
  82. package/dist/react/useSmoothText.d.ts.map +1 -1
  83. package/dist/react/useSmoothText.js +1 -1
  84. package/dist/react/useSmoothText.js.map +1 -1
  85. package/dist/validators.d.ts +751 -1464
  86. package/dist/validators.d.ts.map +1 -1
  87. package/dist/validators.js +39 -80
  88. package/dist/validators.js.map +1 -1
  89. package/package.json +34 -30
  90. package/src/client/createTool.ts +66 -40
  91. package/src/client/defaultComponent.ts +17 -0
  92. package/src/client/definePlaygroundAPI.ts +17 -31
  93. package/src/client/files.ts +94 -17
  94. package/src/client/index.test.ts +28 -38
  95. package/src/client/index.ts +785 -977
  96. package/src/client/messages.ts +205 -0
  97. package/src/client/search.ts +37 -10
  98. package/src/client/streaming.ts +53 -56
  99. package/src/client/threads.ts +78 -0
  100. package/src/client/types.ts +239 -131
  101. package/src/component/_generated/api.d.ts +261 -209
  102. package/src/component/messages.test.ts +182 -40
  103. package/src/component/messages.ts +165 -108
  104. package/src/component/schema.ts +11 -17
  105. package/src/component/streams.ts +180 -69
  106. package/src/mapping.ts +253 -96
  107. package/src/parts.ts +39 -0
  108. package/src/react/SmoothText.tsx +9 -0
  109. package/src/react/deltas.test.ts +38 -42
  110. package/src/react/deltas.ts +116 -65
  111. package/src/react/index.ts +35 -13
  112. package/src/react/optimisticallySendMessage.ts +2 -8
  113. package/src/react/toUIMessages.test.ts +154 -36
  114. package/src/react/toUIMessages.ts +136 -57
  115. package/src/react/useSmoothText.ts +14 -15
  116. package/src/validators.test.ts +1 -117
  117. package/src/validators.ts +57 -107
  118. package/dist/client/listMessages.d.ts +0 -22
  119. package/dist/client/listMessages.d.ts.map +0 -1
  120. package/dist/client/listMessages.js +0 -25
  121. package/dist/client/listMessages.js.map +0 -1
  122. package/src/client/listMessages.ts +0 -38
@@ -1,36 +1,24 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { mergeDeltas, applyDeltasToStreamMessage } from "./deltas.js";
3
- import type {
4
- StreamMessage,
5
- StreamDelta,
6
- TextStreamPart,
7
- } from "../validators.js";
3
+ import type { StreamMessage, StreamDelta } from "../validators.js";
8
4
  import { omit } from "convex-helpers";
5
+ import type { TextStreamPart, ToolSet } from "ai";
9
6
 
10
7
  function makeStreamMessage(
11
8
  streamId: string,
12
9
  order: number,
13
10
  stepOrder: number,
14
11
  ): StreamMessage {
15
- return {
16
- streamId,
17
- order,
18
- stepOrder,
19
- } as StreamMessage;
12
+ return { streamId, order, stepOrder } as StreamMessage;
20
13
  }
21
14
 
22
15
  function makeDelta(
23
16
  streamId: string,
24
17
  start: number,
25
18
  end: number,
26
- parts: TextStreamPart[],
19
+ parts: TextStreamPart<ToolSet>[],
27
20
  ): StreamDelta {
28
- return {
29
- streamId,
30
- start,
31
- end,
32
- parts,
33
- };
21
+ return { streamId, start, end, parts };
34
22
  }
35
23
 
36
24
  describe("mergeDeltas", () => {
@@ -38,7 +26,9 @@ describe("mergeDeltas", () => {
38
26
  const streamId = "s1";
39
27
  const streamMessages = [makeStreamMessage(streamId, 1, 0)];
40
28
  const deltas = [
41
- makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
29
+ makeDelta(streamId, 0, 5, [
30
+ { type: "text-delta", id: "1", text: "Hello" },
31
+ ]),
42
32
  ];
43
33
  const [messages, newStreams, changed] = mergeDeltas(
44
34
  "thread1",
@@ -57,9 +47,11 @@ describe("mergeDeltas", () => {
57
47
  const streamId = "s1";
58
48
  const streamMessages = [makeStreamMessage(streamId, 1, 0)];
59
49
  const deltas = [
60
- makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
50
+ makeDelta(streamId, 0, 5, [
51
+ { type: "text-delta", id: "1", text: "Hello" },
52
+ ]),
61
53
  makeDelta(streamId, 5, 11, [
62
- { type: "text-delta", textDelta: " World!" },
54
+ { type: "text-delta", id: "2", text: " World!" },
63
55
  ]),
64
56
  ];
65
57
  const [messages, newStreams, changed] = mergeDeltas(
@@ -83,7 +75,7 @@ describe("mergeDeltas", () => {
83
75
  type: "tool-call",
84
76
  toolCallId: "call1",
85
77
  toolName: "myTool",
86
- args: "",
78
+ input: "",
87
79
  },
88
80
  ]),
89
81
  makeDelta(streamId, 1, 2, [
@@ -91,7 +83,8 @@ describe("mergeDeltas", () => {
91
83
  type: "tool-result",
92
84
  toolCallId: "call1",
93
85
  toolName: "myTool",
94
- result: "42",
86
+ input: undefined,
87
+ output: "42",
95
88
  },
96
89
  ]),
97
90
  ];
@@ -106,12 +99,7 @@ describe("mergeDeltas", () => {
106
99
  expect(messages[0].tool).toBe(true);
107
100
  const content = messages[0].message?.content;
108
101
  expect(content).toEqual([
109
- {
110
- type: "tool-call",
111
- toolCallId: "call1",
112
- toolName: "myTool",
113
- args: "",
114
- },
102
+ { type: "tool-call", toolCallId: "call1", toolName: "myTool", args: "" },
115
103
  ]);
116
104
  expect(messages[1].message?.role).toBe("tool");
117
105
  expect(messages[1].tool).toBe(true);
@@ -145,8 +133,8 @@ describe("mergeDeltas", () => {
145
133
  const s1 = makeStreamMessage("s1", 1, 0);
146
134
  const s2 = makeStreamMessage("s2", 2, 0);
147
135
  const deltas = [
148
- makeDelta("s2", 0, 3, [{ type: "text-delta", textDelta: "B" }]),
149
- makeDelta("s1", 0, 3, [{ type: "text-delta", textDelta: "A" }]),
136
+ makeDelta("s2", 0, 3, [{ type: "text-delta", id: "1", text: "B" }]),
137
+ makeDelta("s1", 0, 3, [{ type: "text-delta", id: "2", text: "A" }]),
150
138
  ];
151
139
  const [messages, _, changed] = mergeDeltas("thread1", [s2, s1], [], deltas);
152
140
  expect(messages).toHaveLength(2);
@@ -162,11 +150,13 @@ describe("mergeDeltas", () => {
162
150
  const streamId = "s4";
163
151
  const streamMessages = [makeStreamMessage(streamId, 4, 0)];
164
152
  const deltas = [
165
- makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
153
+ makeDelta(streamId, 0, 5, [
154
+ { type: "text-delta", id: "1", text: "Hello" },
155
+ ]),
166
156
  makeDelta(streamId, 5, 11, [
167
- { type: "text-delta", textDelta: " World!" },
157
+ { type: "text-delta", id: "2", text: " World!" },
168
158
  ]),
169
- makeDelta(streamId, 11, 12, [{ type: "text-delta", textDelta: "!" }]),
159
+ makeDelta(streamId, 11, 12, [{ type: "text-delta", id: "3", text: "!" }]),
170
160
  ];
171
161
  const [messages] = mergeDeltas("thread1", streamMessages, [], deltas);
172
162
  expect(messages).toHaveLength(1);
@@ -185,10 +175,10 @@ describe("mergeDeltas", () => {
185
175
  const streamMessages = [makeStreamMessage(streamId, 6, 0)];
186
176
  const deltas = [
187
177
  makeDelta(streamId, 0, 1, [
188
- { type: "reasoning", textDelta: "I'm thinking..." },
178
+ { type: "reasoning-delta", id: "1", text: "I'm thinking..." },
189
179
  ]),
190
180
  makeDelta(streamId, 1, 2, [
191
- { type: "reasoning", textDelta: " Still thinking..." },
181
+ { type: "reasoning-delta", id: "2", text: " Still thinking..." },
192
182
  ]),
193
183
  ];
194
184
  const [messages] = mergeDeltas("thread1", streamMessages, [], deltas);
@@ -206,9 +196,11 @@ describe("mergeDeltas", () => {
206
196
  const streamId = "s7";
207
197
  const streamMessage = makeStreamMessage(streamId, 7, 0);
208
198
  const deltas = [
209
- makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
199
+ makeDelta(streamId, 0, 5, [
200
+ { type: "text-delta", id: "1", text: "Hello" },
201
+ ]),
210
202
  makeDelta(streamId, 5, 11, [
211
- { type: "text-delta", textDelta: " World!" },
203
+ { type: "text-delta", id: "2", text: " World!" },
212
204
  ]),
213
205
  ];
214
206
  // First call: apply both deltas
@@ -232,7 +224,7 @@ describe("mergeDeltas", () => {
232
224
  // Third call: add a new delta
233
225
  const moreDeltas = [
234
226
  ...deltas,
235
- makeDelta(streamId, 11, 12, [{ type: "text-delta", textDelta: "!" }]),
227
+ makeDelta(streamId, 11, 12, [{ type: "text-delta", id: "3", text: "!" }]),
236
228
  ];
237
229
  [result, changed] = applyDeltasToStreamMessage(
238
230
  "thread1",
@@ -259,9 +251,11 @@ describe("mergeDeltas", () => {
259
251
  const streamId = "s8";
260
252
  const streamMessages = [makeStreamMessage(streamId, 8, 0)];
261
253
  const deltas = [
262
- makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
254
+ makeDelta(streamId, 0, 5, [
255
+ { type: "text-delta", id: "1", text: "Hello" },
256
+ ]),
263
257
  makeDelta(streamId, 5, 11, [
264
- { type: "text-delta", textDelta: " World!" },
258
+ { type: "text-delta", id: "2", text: " World!" },
265
259
  ]),
266
260
  ];
267
261
  // Deep freeze inputs to catch mutation
@@ -306,9 +300,11 @@ describe("mergeDeltas", () => {
306
300
  // Inputs should remain unchanged
307
301
  expect(streamMessages).toEqual([makeStreamMessage(streamId, 8, 0)]);
308
302
  expect(deltas).toEqual([
309
- makeDelta(streamId, 0, 5, [{ type: "text-delta", textDelta: "Hello" }]),
303
+ makeDelta(streamId, 0, 5, [
304
+ { type: "text-delta", id: "1", text: "Hello" },
305
+ ]),
310
306
  makeDelta(streamId, 5, 11, [
311
- { type: "text-delta", textDelta: " World!" },
307
+ { type: "text-delta", id: "2", text: " World!" },
312
308
  ]),
313
309
  ]);
314
310
  });
@@ -1,16 +1,16 @@
1
- import type { TextPart, ToolCallPart, ToolResultPart } from "ai";
1
+ import type { TextStreamPart, ToolSet } from "ai";
2
2
  import type { MessageDoc } from "../client/index.js";
3
3
  import type {
4
4
  Message,
5
5
  MessageStatus,
6
6
  StreamDelta,
7
7
  StreamMessage,
8
- TextStreamPart,
8
+ vReasoningPart,
9
+ vTextPart,
10
+ vToolCallPart,
11
+ vToolResultPart,
9
12
  } from "../validators.js";
10
- import type { UIMessage } from "./toUIMessages.js";
11
- import { toUIMessages } from "./toUIMessages.js";
12
-
13
- export { toUIMessages, type UIMessage };
13
+ import type { Infer } from "convex/values";
14
14
 
15
15
  export function mergeDeltas(
16
16
  threadId: string,
@@ -73,7 +73,7 @@ export function applyDeltasToStreamMessage(
73
73
  ): [{ streamId: string; cursor: number; messages: MessageDoc[] }, boolean] {
74
74
  let changed = false;
75
75
  let cursor = existing?.cursor ?? 0;
76
- let parts: TextStreamPart[] = [];
76
+ let parts: TextStreamPart<ToolSet>[] = [];
77
77
  for (const delta of deltas.sort((a, b) => a.start - b.start)) {
78
78
  if (delta.parts.length === 0) {
79
79
  console.warn(`Got delta with no parts: ${JSON.stringify(delta)}`);
@@ -142,10 +142,10 @@ export function applyDeltasToStreamMessage(
142
142
  let lastContent = getLastContent(currentMessage);
143
143
  for (const part of parts) {
144
144
  let contentToAdd:
145
- | TextPart
146
- | ToolCallPart
147
- | { type: "reasoning"; text: string }
148
- | ToolResultPart
145
+ | Infer<typeof vTextPart>
146
+ | Infer<typeof vToolCallPart>
147
+ | Infer<typeof vToolResultPart>
148
+ | Infer<typeof vReasoningPart>
149
149
  | undefined;
150
150
  const isToolRole = part.type === "source" || part.type === "tool-result";
151
151
  if (isToolRole !== (currentMessage.message!.role === "tool")) {
@@ -160,27 +160,34 @@ export function applyDeltasToStreamMessage(
160
160
  continue;
161
161
  }
162
162
  switch (part.type) {
163
- case "text-delta":
164
- currentMessage.text = (currentMessage.text ?? "") + part.textDelta;
163
+ case "text-delta": {
164
+ const text = part.text;
165
+ currentMessage.text = (currentMessage.text ?? "") + text;
165
166
  if (lastContent?.type === "text") {
166
- lastContent.text = (lastContent.text ?? "") + part.textDelta;
167
+ lastContent.text = (lastContent.text ?? "") + text;
167
168
  } else {
168
- contentToAdd = {
169
- type: "text",
170
- text: part.textDelta,
171
- };
169
+ contentToAdd = { type: "text", text } satisfies Infer<
170
+ typeof vTextPart
171
+ >;
172
172
  }
173
173
  break;
174
- case "tool-call-streaming-start":
174
+ }
175
+ case "tool-input-start": {
176
+ const toolCallId = part.id;
175
177
  currentMessage.tool = true;
176
178
  contentToAdd = {
177
179
  type: "tool-call",
178
- toolCallId: part.toolCallId,
180
+ toolCallId,
179
181
  toolName: part.toolName,
180
182
  args: "",
181
- };
183
+ providerExecuted:
184
+ "providerExecuted" in part ? part.providerExecuted : undefined,
185
+ providerOptions:
186
+ "providerMetadata" in part ? part.providerMetadata : undefined,
187
+ } satisfies Infer<typeof vToolCallPart>;
182
188
  break;
183
- case "tool-call-delta":
189
+ }
190
+ case "tool-input-delta":
184
191
  {
185
192
  currentMessage.tool = true;
186
193
  if (lastContent?.type !== "tool-call") {
@@ -189,33 +196,43 @@ export function applyDeltasToStreamMessage(
189
196
  if (typeof lastContent.args !== "string") {
190
197
  throw new Error("Expected args to be a string");
191
198
  }
192
- lastContent.args = (lastContent.args ?? "") + part.argsTextDelta;
199
+ const delta =
200
+ "argsTextDelta" in part ? part.argsTextDelta : part.delta;
201
+ lastContent.args = (lastContent.args ?? "") + delta;
193
202
  }
194
203
  break;
195
- case "tool-call":
204
+ case "tool-call": {
196
205
  currentMessage.tool = true;
197
- contentToAdd = part;
206
+ contentToAdd = toolCallContent(part);
207
+ break;
208
+ }
209
+ case "tool-result": {
210
+ contentToAdd = toolResultContent(part);
198
211
  break;
199
- case "reasoning":
200
- currentMessage.reasoning =
201
- (currentMessage.reasoning ?? "") + part.textDelta;
212
+ }
213
+ case "reasoning-delta": {
214
+ currentMessage.reasoning = (currentMessage.reasoning ?? "") + part.text;
202
215
  if (lastContent?.type === "reasoning") {
203
- lastContent.text = (lastContent.text ?? "") + part.textDelta;
216
+ lastContent.text = (lastContent.text ?? "") + part.text;
204
217
  } else {
205
218
  contentToAdd = {
206
219
  type: "reasoning",
207
- text: part.textDelta,
208
- };
220
+ text: part.text,
221
+ providerOptions:
222
+ "providerMetadata" in part ? part.providerMetadata : undefined,
223
+ state: "streaming",
224
+ } satisfies Infer<typeof vReasoningPart>;
209
225
  }
210
226
  break;
227
+ }
211
228
  case "source":
212
229
  if (!currentMessage.sources) {
213
230
  currentMessage.sources = [];
214
231
  }
215
- currentMessage.sources.push(part.source);
232
+ currentMessage.sources.push(part);
216
233
  break;
217
- case "tool-result":
218
- contentToAdd = part;
234
+ case "raw":
235
+ // ignore
219
236
  break;
220
237
  default:
221
238
  console.warn(`Received unexpected part: ${JSON.stringify(part)}`);
@@ -236,6 +253,31 @@ export function applyDeltasToStreamMessage(
236
253
  return [newStream, true];
237
254
  }
238
255
 
256
+ function toolCallContent(
257
+ part: Extract<TextStreamPart<ToolSet>, { type: "tool-call" }>,
258
+ ): Infer<typeof vToolCallPart> {
259
+ const args = "args" in part ? part.args : part.input;
260
+ return {
261
+ type: "tool-call",
262
+ toolCallId: part.toolCallId,
263
+ toolName: part.toolName,
264
+ args,
265
+ providerExecuted: part.providerExecuted,
266
+ } satisfies Infer<typeof vToolCallPart>;
267
+ }
268
+
269
+ function toolResultContent(
270
+ part: Extract<TextStreamPart<ToolSet>, { type: "tool-result" }>,
271
+ ): Infer<typeof vToolResultPart> {
272
+ return {
273
+ type: "tool-result",
274
+ toolCallId: part.toolCallId,
275
+ toolName: part.toolName,
276
+ result: part.output,
277
+ args: part.input,
278
+ providerExecuted: part.providerExecuted,
279
+ } satisfies Infer<typeof vToolResultPart>;
280
+ }
239
281
  function cloneMessageAndContent(
240
282
  message: Message | undefined,
241
283
  ): Message | undefined {
@@ -272,10 +314,11 @@ function statusFromStreamStatus(
272
314
  }
273
315
  }
274
316
 
317
+ // TODO: share more code with applyDeltasToStreamMessage
275
318
  export function createStreamingMessage(
276
319
  threadId: string,
277
320
  message: StreamMessage,
278
- part: TextStreamPart,
321
+ part: TextStreamPart<ToolSet>,
279
322
  index: number,
280
323
  ): MessageDoc {
281
324
  const { streamId, ...rest } = message;
@@ -288,16 +331,15 @@ export function createStreamingMessage(
288
331
  tool: false,
289
332
  };
290
333
  switch (part.type) {
291
- case "text-delta":
334
+ case "text-delta": {
335
+ const text = part.text || "";
292
336
  return {
293
337
  ...metadata,
294
- message: {
295
- role: "assistant",
296
- content: [{ type: "text", text: part.textDelta }],
297
- },
298
- text: part.textDelta,
338
+ message: { role: "assistant", content: [{ type: "text", text }] },
339
+ text,
299
340
  };
300
- case "tool-call-streaming-start":
341
+ }
342
+ case "tool-input-start": {
301
343
  return {
302
344
  ...metadata,
303
345
  tool: true,
@@ -307,58 +349,67 @@ export function createStreamingMessage(
307
349
  {
308
350
  type: "tool-call",
309
351
  toolName: part.toolName,
310
- toolCallId: part.toolCallId,
352
+ toolCallId: part.id,
311
353
  args: "", // when it's a string, it's a partial call
354
+ providerExecuted:
355
+ "providerExecuted" in part ? part.providerExecuted : undefined,
356
+ providerOptions:
357
+ "providerMetadata" in part ? part.providerMetadata : undefined,
312
358
  },
313
359
  ],
314
360
  },
315
361
  };
316
- case "reasoning":
362
+ }
363
+ case "tool-input-delta": {
364
+ console.warn("Received tool call delta part first??");
365
+ const delta = part.delta;
366
+ const toolCallId = part.id;
367
+ const toolName = part.type.slice("tool-".length);
317
368
  return {
318
369
  ...metadata,
370
+ tool: true,
319
371
  message: {
320
372
  role: "assistant",
321
- content: [{ type: "reasoning", text: part.textDelta }],
373
+ content: [{ type: "tool-call", toolCallId, toolName, args: delta }],
322
374
  },
323
- reasoning: part.textDelta,
324
375
  };
325
- case "source":
326
- console.warn("Received source part first??");
376
+ }
377
+ case "tool-call": {
327
378
  return {
328
379
  ...metadata,
329
380
  tool: true,
330
- message: { role: "tool", content: [] },
331
- sources: [part.source],
381
+ message: { role: "assistant", content: [toolCallContent(part)] },
332
382
  };
333
- case "tool-call":
383
+ }
384
+ case "tool-result":
334
385
  return {
335
386
  ...metadata,
336
387
  tool: true,
337
- message: { role: "assistant", content: [part] },
388
+ message: { role: "tool", content: [toolResultContent(part)] },
338
389
  };
339
- case "tool-call-delta":
340
- console.warn("Received tool call delta part first??");
390
+ case "reasoning-delta": {
341
391
  return {
342
392
  ...metadata,
343
- tool: true,
344
393
  message: {
345
394
  role: "assistant",
346
- content: [
347
- {
348
- type: "tool-call",
349
- toolCallId: part.toolCallId,
350
- toolName: part.toolName,
351
- args: part.argsTextDelta,
352
- },
353
- ],
395
+ content: [{ type: "reasoning", text: part.text }],
354
396
  },
397
+ reasoning: part.text,
355
398
  };
356
- case "tool-result":
399
+ }
400
+ case "source":
401
+ console.warn("Received source part first??");
357
402
  return {
358
403
  ...metadata,
359
404
  tool: true,
360
- message: { role: "tool", content: [part] },
405
+ message: { role: "tool", content: [] },
406
+ sources: [part],
361
407
  };
408
+ // case "raw":
409
+ // return {
410
+ // ...metadata,
411
+ // message: { role: "assistant", content: [part.rawValue] },
412
+ // };
362
413
  default:
363
414
  throw new Error(`Unexpected part type: ${JSON.stringify(part)}`);
364
415
  }
@@ -7,7 +7,7 @@ import {
7
7
  } from "convex/react";
8
8
  import { usePaginatedQuery } from "convex-helpers/react";
9
9
  import type { FunctionArgs } from "convex/server";
10
- import { useMemo, useRef, useState } from "react";
10
+ import { useMemo, useState } from "react";
11
11
  import type { MessageDoc } from "../client/index.js";
12
12
  import type { SyncStreamsReturnValue } from "../client/types.js";
13
13
  import type { StreamArgs } from "../validators.js";
@@ -23,6 +23,7 @@ import type {
23
23
 
24
24
  export { optimisticallySendMessage } from "./optimisticallySendMessage.js";
25
25
  export { useSmoothText } from "./useSmoothText.js";
26
+ export { SmoothText } from "./SmoothText.js";
26
27
  export { toUIMessages, type UIMessage, type ThreadQuery };
27
28
 
28
29
  /**
@@ -97,6 +98,15 @@ export function useThreadMessages<
97
98
  { initialNumItems: options.initialNumItems },
98
99
  );
99
100
 
101
+ let startOrder = paginated.results.at(-1)?.order ?? 0;
102
+ for (let i = paginated.results.length - 1; i >= 0; i--) {
103
+ const m = paginated.results[i];
104
+ if (!m.streaming && m.status === "pending") {
105
+ // round down to the nearest 10 for some cache benefits
106
+ startOrder = m.order - (m.order % 10);
107
+ break;
108
+ }
109
+ }
100
110
  // These are streaming messages that will not include full messages.
101
111
  const streamMessages = useStreamingThreadMessages(
102
112
  query as ThreadStreamQuery<
@@ -107,7 +117,7 @@ export function useThreadMessages<
107
117
  args === "skip" ||
108
118
  paginated.status === "LoadingFirstPage"
109
119
  ? "skip"
110
- : { ...args, startOrder: paginated.results.at(-1)?.order },
120
+ : { ...args, startOrder },
111
121
  );
112
122
 
113
123
  const merged = useMemo(() => {
@@ -120,16 +130,32 @@ export function useThreadMessages<
120
130
  ...paginated,
121
131
  results: paginated.results
122
132
  .map((m) => ({ ...m, streaming: false }))
133
+ // Note: this is intentionally after paginated results.
123
134
  .concat(streamListMessages)
124
135
  .sort((a, b) =>
125
136
  a.order === b.order ? a.stepOrder - b.stepOrder : a.order - b.order,
126
137
  )
127
- // They shouldn't overlap, but check for duplicates just in case.
128
- .filter(
129
- (m, i, arr) =>
130
- !arr[i - 1] ||
131
- m.order !== arr[i - 1].order ||
132
- m.stepOrder !== arr[i - 1].stepOrder,
138
+ .reduce(
139
+ (msgs, msg) => {
140
+ const last = msgs.at(-1);
141
+ if (!last) {
142
+ return [msg];
143
+ }
144
+ if (last.order !== msg.order || last.stepOrder !== msg.stepOrder) {
145
+ return [...msgs, msg];
146
+ }
147
+ if (
148
+ last.status === "pending" &&
149
+ (msg.streaming || msg.status !== "pending")
150
+ ) {
151
+ // Let's prefer a streaming or finalized message over a pending
152
+ // one.
153
+ return [...msgs.slice(0, -1), msg];
154
+ }
155
+ // skip the new one if the previous one (listed) was finalized
156
+ return msgs;
157
+ },
158
+ [] as (ThreadMessagesResult<Query> & { streaming: boolean })[],
133
159
  ),
134
160
  };
135
161
  }, [paginated, streamMessages]);
@@ -162,11 +188,7 @@ export function useStreamingThreadMessages<
162
188
  const [streams, setStreams] = useState<
163
189
  Array<{ streamId: string; cursor: number; messages: MessageDoc[] }>
164
190
  >([]);
165
- const startOrderRef = useRef<number>(0);
166
191
  const queryArgs = args === "skip" ? args : omit(args, ["startOrder"]);
167
- if (args !== "skip" && !startOrderRef.current && args.startOrder) {
168
- startOrderRef.current = args.startOrder;
169
- }
170
192
  // Get all the active streams
171
193
  const streamList = useQuery(
172
194
  query,
@@ -177,7 +199,7 @@ export function useStreamingThreadMessages<
177
199
  paginationOpts: { cursor: null, numItems: 0 },
178
200
  streamArgs: {
179
201
  kind: "list",
180
- startOrder: startOrderRef.current,
202
+ startOrder: queryArgs.startOrder ?? 0,
181
203
  } as StreamArgs,
182
204
  } as FunctionArgs<Query>),
183
205
  ) as
@@ -12,17 +12,14 @@ export function optimisticallySendMessage(
12
12
  return (store, args) => {
13
13
  const queries = store.getAllQueries(query);
14
14
  let maxOrder = -1;
15
- let maxStepOrder = 0;
16
15
  for (const q of queries) {
17
16
  if (q.args?.threadId !== args.threadId) continue;
18
17
  if (q.args.streamArgs) continue;
19
18
  for (const m of q.value?.page ?? []) {
20
19
  maxOrder = Math.max(maxOrder, m.order);
21
- maxStepOrder = Math.max(maxStepOrder, m.stepOrder);
22
20
  }
23
21
  }
24
22
  const order = maxOrder + 1;
25
- const stepOrder = 0;
26
23
  insertAtTop({
27
24
  paginatedQuery: query,
28
25
  argsToMatch: { threadId: args.threadId, streamArgs: undefined },
@@ -30,14 +27,11 @@ export function optimisticallySendMessage(
30
27
  _creationTime: Date.now(),
31
28
  _id: randomUUID(),
32
29
  order,
33
- stepOrder,
30
+ stepOrder: 0,
34
31
  status: "pending",
35
32
  threadId: args.threadId,
36
33
  tool: false,
37
- message: {
38
- role: "user",
39
- content: args.prompt,
40
- },
34
+ message: { role: "user", content: args.prompt },
41
35
  text: args.prompt,
42
36
  },
43
37
  localQueryStore: store,