@convex-dev/agent 0.1.9-alpha.1 → 0.1.9-alpha.11

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 (91) hide show
  1. package/README.md +3 -3
  2. package/dist/commonjs/client/index.d.ts +1 -0
  3. package/dist/commonjs/client/index.d.ts.map +1 -1
  4. package/dist/commonjs/client/index.js +5 -1
  5. package/dist/commonjs/client/index.js.map +1 -1
  6. package/dist/commonjs/client/streaming.d.ts +3 -0
  7. package/dist/commonjs/client/streaming.d.ts.map +1 -1
  8. package/dist/commonjs/client/streaming.js +29 -1
  9. package/dist/commonjs/client/streaming.js.map +1 -1
  10. package/dist/commonjs/client/types.d.ts +0 -2
  11. package/dist/commonjs/client/types.d.ts.map +1 -1
  12. package/dist/commonjs/component/_generated/api.d.ts +24 -5
  13. package/dist/commonjs/component/schema.d.ts +26 -26
  14. package/dist/commonjs/component/schema.d.ts.map +1 -1
  15. package/dist/commonjs/component/schema.js +2 -2
  16. package/dist/commonjs/component/schema.js.map +1 -1
  17. package/dist/commonjs/component/streams.d.ts +11 -4
  18. package/dist/commonjs/component/streams.d.ts.map +1 -1
  19. package/dist/commonjs/component/streams.js +63 -22
  20. package/dist/commonjs/component/streams.js.map +1 -1
  21. package/dist/commonjs/component/threads.d.ts +8 -2
  22. package/dist/commonjs/component/threads.d.ts.map +1 -1
  23. package/dist/commonjs/component/threads.js +44 -18
  24. package/dist/commonjs/component/threads.js.map +1 -1
  25. package/dist/commonjs/component/vector/tables.d.ts +4 -2
  26. package/dist/commonjs/component/vector/tables.d.ts.map +1 -1
  27. package/dist/commonjs/react/deltas.d.ts.map +1 -1
  28. package/dist/commonjs/react/deltas.js +3 -2
  29. package/dist/commonjs/react/deltas.js.map +1 -1
  30. package/dist/commonjs/react/index.d.ts +4 -2
  31. package/dist/commonjs/react/index.d.ts.map +1 -1
  32. package/dist/commonjs/react/index.js +22 -9
  33. package/dist/commonjs/react/index.js.map +1 -1
  34. package/dist/commonjs/react/optimisticallySendMessage.js +1 -1
  35. package/dist/commonjs/react/optimisticallySendMessage.js.map +1 -1
  36. package/dist/commonjs/validators.d.ts +8 -3
  37. package/dist/commonjs/validators.d.ts.map +1 -1
  38. package/dist/commonjs/validators.js +2 -0
  39. package/dist/commonjs/validators.js.map +1 -1
  40. package/dist/commonjs.tsbuildinfo +1 -1
  41. package/dist/esm/client/index.d.ts +1 -0
  42. package/dist/esm/client/index.d.ts.map +1 -1
  43. package/dist/esm/client/index.js +5 -1
  44. package/dist/esm/client/index.js.map +1 -1
  45. package/dist/esm/client/streaming.d.ts +3 -0
  46. package/dist/esm/client/streaming.d.ts.map +1 -1
  47. package/dist/esm/client/streaming.js +29 -1
  48. package/dist/esm/client/streaming.js.map +1 -1
  49. package/dist/esm/client/types.d.ts +0 -2
  50. package/dist/esm/client/types.d.ts.map +1 -1
  51. package/dist/esm/component/_generated/api.d.ts +24 -5
  52. package/dist/esm/component/schema.d.ts +26 -26
  53. package/dist/esm/component/schema.d.ts.map +1 -1
  54. package/dist/esm/component/schema.js +2 -2
  55. package/dist/esm/component/schema.js.map +1 -1
  56. package/dist/esm/component/streams.d.ts +11 -4
  57. package/dist/esm/component/streams.d.ts.map +1 -1
  58. package/dist/esm/component/streams.js +63 -22
  59. package/dist/esm/component/streams.js.map +1 -1
  60. package/dist/esm/component/threads.d.ts +8 -2
  61. package/dist/esm/component/threads.d.ts.map +1 -1
  62. package/dist/esm/component/threads.js +44 -18
  63. package/dist/esm/component/threads.js.map +1 -1
  64. package/dist/esm/component/vector/tables.d.ts +4 -2
  65. package/dist/esm/component/vector/tables.d.ts.map +1 -1
  66. package/dist/esm/react/deltas.d.ts.map +1 -1
  67. package/dist/esm/react/deltas.js +3 -2
  68. package/dist/esm/react/deltas.js.map +1 -1
  69. package/dist/esm/react/index.d.ts +4 -2
  70. package/dist/esm/react/index.d.ts.map +1 -1
  71. package/dist/esm/react/index.js +22 -9
  72. package/dist/esm/react/index.js.map +1 -1
  73. package/dist/esm/react/optimisticallySendMessage.js +1 -1
  74. package/dist/esm/react/optimisticallySendMessage.js.map +1 -1
  75. package/dist/esm/validators.d.ts +8 -3
  76. package/dist/esm/validators.d.ts.map +1 -1
  77. package/dist/esm/validators.js +2 -0
  78. package/dist/esm/validators.js.map +1 -1
  79. package/dist/esm.tsbuildinfo +1 -1
  80. package/package.json +1 -1
  81. package/src/client/index.ts +7 -3
  82. package/src/client/streaming.ts +32 -1
  83. package/src/client/types.ts +0 -2
  84. package/src/component/_generated/api.d.ts +24 -5
  85. package/src/component/schema.ts +2 -2
  86. package/src/component/streams.ts +100 -37
  87. package/src/component/threads.ts +48 -26
  88. package/src/react/deltas.ts +5 -2
  89. package/src/react/index.ts +23 -11
  90. package/src/react/optimisticallySendMessage.ts +1 -1
  91. package/src/validators.ts +6 -0
@@ -1414,6 +1414,12 @@ export type Mounts = {
1414
1414
  >;
1415
1415
  };
1416
1416
  streams: {
1417
+ abort: FunctionReference<
1418
+ "mutation",
1419
+ "public",
1420
+ { reason: string; streamId: string },
1421
+ null
1422
+ >;
1417
1423
  addDelta: FunctionReference<
1418
1424
  "mutation",
1419
1425
  "public",
@@ -1467,7 +1473,7 @@ export type Mounts = {
1467
1473
  start: number;
1468
1474
  streamId: string;
1469
1475
  },
1470
- null
1476
+ boolean
1471
1477
  >;
1472
1478
  create: FunctionReference<
1473
1479
  "mutation",
@@ -1569,13 +1575,18 @@ export type Mounts = {
1569
1575
  list: FunctionReference<
1570
1576
  "query",
1571
1577
  "public",
1572
- { threadId: string },
1578
+ {
1579
+ startOrder?: number;
1580
+ statuses?: Array<"streaming" | "finished" | "aborted">;
1581
+ threadId: string;
1582
+ },
1573
1583
  Array<{
1574
1584
  agentName?: string;
1575
1585
  model?: string;
1576
1586
  order: number;
1577
1587
  provider?: string;
1578
1588
  providerOptions?: Record<string, Record<string, any>>;
1589
+ status: "streaming" | "finished" | "aborted";
1579
1590
  stepOrder: number;
1580
1591
  streamId: string;
1581
1592
  userId?: string;
@@ -1663,13 +1674,21 @@ export type Mounts = {
1663
1674
  deleteAllForThreadIdAsync: FunctionReference<
1664
1675
  "mutation",
1665
1676
  "public",
1666
- { cursor?: string; limit?: number; threadId: string },
1667
- { cursor: string; isDone: boolean }
1677
+ {
1678
+ cursor?: string;
1679
+ deltaCursor?: string;
1680
+ limit?: number;
1681
+ messagesDone?: boolean;
1682
+ streamOrder?: number;
1683
+ streamsDone?: boolean;
1684
+ threadId: string;
1685
+ },
1686
+ { isDone: boolean }
1668
1687
  >;
1669
1688
  deleteAllForThreadIdSync: FunctionReference<
1670
1689
  "action",
1671
1690
  "public",
1672
- { cursor?: string; limit?: number; threadId: string },
1691
+ { limit?: number; threadId: string },
1673
1692
  null
1674
1693
  >;
1675
1694
  getThread: FunctionReference<
@@ -110,8 +110,8 @@ export const schema = defineSchema({
110
110
  endedAt: v.number(),
111
111
  }),
112
112
  v.object({
113
- kind: v.literal("error"),
114
- error: v.string(),
113
+ kind: v.literal("aborted"),
114
+ reason: v.string(),
115
115
  })
116
116
  ),
117
117
  })
@@ -6,7 +6,7 @@ import {
6
6
  vStreamMessage,
7
7
  } from "../validators.js";
8
8
  import { api, internal } from "./_generated/api.js";
9
- import type { Id } from "./_generated/dataModel.js";
9
+ import type { Doc, Id } from "./_generated/dataModel.js";
10
10
  import {
11
11
  internalMutation,
12
12
  mutation,
@@ -17,20 +17,30 @@ import {
17
17
  import schema from "./schema.js";
18
18
  import { stream } from "convex-helpers/server/stream";
19
19
  import { mergedStream } from "convex-helpers/server/stream";
20
+ import { paginator } from "convex-helpers/server/pagination";
21
+
22
+ const SECOND = 1000;
23
+ const MINUTE = 60 * SECOND;
20
24
 
21
25
  const MAX_DELTAS_PER_REQUEST = 1000;
22
26
  const MAX_DELTAS_PER_STREAM = 100;
23
- const TIMEOUT_INTERVAL = 1000 * 60; // 1 minute
24
- const DELETE_STREAM_DELAY = 1000 * 60 * 5; // 5 minutes
27
+ const TIMEOUT_INTERVAL = 10 * MINUTE;
28
+ const DELETE_STREAM_DELAY = MINUTE * 5; // 5 minutes
25
29
 
26
30
  const deltaValidator = schema.tables.streamDeltas.validator;
27
31
 
28
32
  export const addDelta = mutation({
29
33
  args: deltaValidator,
30
- returns: v.null(),
34
+ returns: v.boolean(),
31
35
  handler: async (ctx, args) => {
32
36
  await ctx.db.insert("streamDeltas", args);
33
37
  await heartbeatStream(ctx, { streamId: args.streamId });
38
+ const stream = await ctx.db.get(args.streamId);
39
+ if (stream?.state.kind !== "streaming") {
40
+ console.warn(`Stream is not streaming: ${args.streamId}`);
41
+ return false;
42
+ }
43
+ return true;
34
44
  },
35
45
  });
36
46
 
@@ -96,33 +106,87 @@ export const create = mutation({
96
106
  export const list = query({
97
107
  args: {
98
108
  threadId: v.id("threads"),
109
+ startOrder: v.optional(v.number()),
110
+ statuses: v.optional(
111
+ v.array(
112
+ v.union(
113
+ v.literal("streaming"),
114
+ v.literal("finished"),
115
+ v.literal("aborted")
116
+ )
117
+ )
118
+ ),
99
119
  },
100
120
  returns: v.array(vStreamMessage),
101
121
  handler: async (ctx, args) => {
102
- return ctx.db
103
- .query("streamingMessages")
104
- .withIndex("threadId_state_order_stepOrder", (q) =>
105
- q.eq("threadId", args.threadId).eq("state.kind", "streaming")
106
- )
107
- .order("desc")
108
- .take(100)
109
- .then((msgs) =>
110
- msgs.map((m) => ({
111
- streamId: m._id,
112
- ...pick(m, [
113
- "order",
114
- "stepOrder",
115
- "userId",
116
- "agentName",
117
- "model",
118
- "provider",
119
- "providerOptions",
120
- ]),
121
- }))
122
+ const statuses = args.statuses ?? ["streaming"];
123
+ const messages = await mergedStream(
124
+ statuses.map((status) =>
125
+ stream(ctx.db, schema)
126
+ .query("streamingMessages")
127
+ .withIndex("threadId_state_order_stepOrder", (q) =>
128
+ q
129
+ .eq("threadId", args.threadId)
130
+ .eq("state.kind", status)
131
+ .gte("order", args.startOrder ?? 0)
132
+ )
133
+ .order("desc")
134
+ ),
135
+ ["order", "stepOrder"]
136
+ ).take(100);
137
+
138
+ return messages.map((m) => ({
139
+ streamId: m._id,
140
+ status: m.state.kind,
141
+ ...pick(m, [
142
+ "order",
143
+ "stepOrder",
144
+ "userId",
145
+ "agentName",
146
+ "model",
147
+ "provider",
148
+ "providerOptions",
149
+ ]),
150
+ }));
151
+ },
152
+ });
153
+
154
+ export const abort = mutation({
155
+ args: {
156
+ streamId: v.id("streamingMessages"),
157
+ reason: v.string(),
158
+ },
159
+ returns: v.null(),
160
+ handler: async (ctx, args) => {
161
+ const stream = await ctx.db.get(args.streamId);
162
+ if (!stream) {
163
+ throw new Error(`Stream not found: ${args.streamId}`);
164
+ }
165
+ if (stream.state.kind !== "streaming") {
166
+ console.warn(
167
+ `Stream trying to abort but not currently streaming (${stream.state.kind}): ${args.streamId}`
122
168
  );
169
+ return;
170
+ }
171
+ await cleanupTimeoutFn(ctx, stream);
172
+ await ctx.db.patch(args.streamId, {
173
+ state: { kind: "aborted", reason: args.reason },
174
+ });
123
175
  },
124
176
  });
125
177
 
178
+ async function cleanupTimeoutFn(
179
+ ctx: MutationCtx,
180
+ stream: Doc<"streamingMessages">
181
+ ) {
182
+ if (stream.state.kind === "streaming" && stream.state.timeoutFnId) {
183
+ const timeoutFn = await ctx.db.system.get(stream.state.timeoutFnId);
184
+ if (timeoutFn?.state.kind === "pending") {
185
+ await ctx.scheduler.cancel(stream.state.timeoutFnId);
186
+ }
187
+ }
188
+ }
189
+
126
190
  export const finish = mutation({
127
191
  args: {
128
192
  streamId: v.id("streamingMessages"),
@@ -138,14 +202,12 @@ export const finish = mutation({
138
202
  throw new Error(`Stream not found: ${args.streamId}`);
139
203
  }
140
204
  if (stream.state.kind !== "streaming") {
141
- throw new Error(`Stream is not streaming: ${args.streamId}`);
142
- }
143
- if (stream.state.timeoutFnId) {
144
- const timeoutFn = await ctx.db.system.get(stream.state.timeoutFnId);
145
- if (timeoutFn?.state.kind === "pending") {
146
- await ctx.scheduler.cancel(stream.state.timeoutFnId);
147
- }
205
+ console.warn(
206
+ `Stream trying to finish but not currently streaming: ${args.streamId}`
207
+ );
208
+ return;
148
209
  }
210
+ await cleanupTimeoutFn(ctx, stream);
149
211
  await ctx.db.patch(args.streamId, {
150
212
  state: { kind: "finished", endedAt: Date.now() },
151
213
  });
@@ -210,8 +272,8 @@ export const timeoutStream = internalMutation({
210
272
  }
211
273
  await ctx.db.patch(args.streamId, {
212
274
  state: {
213
- kind: "finished",
214
- endedAt: Date.now(),
275
+ kind: "aborted",
276
+ reason: "timeout",
215
277
  },
216
278
  });
217
279
  },
@@ -221,7 +283,7 @@ async function deletePageForStreamId(
221
283
  ctx: MutationCtx,
222
284
  args: { streamId: Id<"streamingMessages">; cursor?: string }
223
285
  ) {
224
- const deltas = await ctx.db
286
+ const deltas = await paginator(ctx.db, schema)
225
287
  .query("streamDeltas")
226
288
  .withIndex("streamId_start_end", (q) => q.eq("streamId", args.streamId))
227
289
  .paginate({
@@ -229,6 +291,9 @@ async function deletePageForStreamId(
229
291
  cursor: args.cursor ?? null,
230
292
  });
231
293
  await Promise.all(deltas.page.map((d) => ctx.db.delete(d._id)));
294
+ if (deltas.isDone) {
295
+ await ctx.db.delete(args.streamId);
296
+ }
232
297
  return deltas;
233
298
  }
234
299
 
@@ -268,13 +333,12 @@ export async function deleteStreamsPageForThreadId(
268
333
  cursor: deltaCursor,
269
334
  });
270
335
  if (result.isDone) {
271
- await ctx.db.delete(streamMessage._id);
272
336
  deltaCursor = undefined;
273
337
  }
274
338
  return {
275
339
  isDone: false,
276
340
  streamOrder: streamMessage.order,
277
- deltaCursor: result.continueCursor,
341
+ deltaCursor,
278
342
  };
279
343
  }
280
344
 
@@ -333,7 +397,6 @@ export const deleteStreamSync = mutation({
333
397
  cursor: deltas.continueCursor,
334
398
  });
335
399
  }
336
- await ctx.db.delete(args.streamId);
337
400
  },
338
401
  });
339
402
 
@@ -16,6 +16,7 @@ import {
16
16
  } from "./_generated/server.js";
17
17
  import { deleteMessage } from "./messages.js";
18
18
  import { schema, v } from "./schema.js";
19
+ import { deleteStreamsPageForThreadId } from "./streams.js";
19
20
 
20
21
  function publicThreadOrNull(thread: Doc<"threads"> | null): ThreadDoc | null {
21
22
  if (thread === null) {
@@ -100,29 +101,17 @@ export const updateThread = mutation({
100
101
 
101
102
  // TODO: delete thread
102
103
 
103
- const deleteThreadArgs = {
104
- threadId: v.id("threads"),
105
- cursor: v.optional(v.string()),
106
- limit: v.optional(v.number()),
107
- };
108
- type DeleteThreadArgs = ObjectType<typeof deleteThreadArgs>;
109
- const deleteThreadReturns = {
110
- cursor: v.string(),
111
- isDone: v.boolean(),
112
- };
113
- type DeleteThreadReturns = ObjectType<typeof deleteThreadReturns>;
114
-
115
104
  /**
116
105
  * Use this to delete a thread and everything it contains.
117
106
  * It will try to delete all pages synchronously.
118
107
  * If it times out or fails, you'll have to run it again.
119
108
  */
120
109
  export const deleteAllForThreadIdSync = action({
121
- args: deleteThreadArgs,
110
+ args: { threadId: v.id("threads"), limit: v.optional(v.number()) },
122
111
  handler: async (ctx, args) => {
123
- let cursor = args.cursor;
112
+ let cursor: string | undefined = undefined;
124
113
  while (true) {
125
- const result = await ctx.runMutation(
114
+ const result: DeleteThreadReturns = await ctx.runMutation(
126
115
  internal.threads._deletePageForThreadId,
127
116
  { threadId: args.threadId, cursor, limit: args.limit }
128
117
  );
@@ -138,6 +127,22 @@ export const deleteAllForThreadIdSync = action({
138
127
  returns: v.null(),
139
128
  });
140
129
 
130
+ const deleteThreadArgs = {
131
+ threadId: v.id("threads"),
132
+ cursor: v.optional(v.string()),
133
+ messagesDone: v.optional(v.boolean()),
134
+ streamsDone: v.optional(v.boolean()),
135
+ streamOrder: v.optional(v.number()),
136
+ deltaCursor: v.optional(v.string()),
137
+ limit: v.optional(v.number()),
138
+ };
139
+ type DeleteThreadArgs = ObjectType<typeof deleteThreadArgs>;
140
+ const deleteThreadReturns = {
141
+ cursor: v.string(),
142
+ isDone: v.boolean(),
143
+ };
144
+ type DeleteThreadReturns = ObjectType<typeof deleteThreadReturns>;
145
+
141
146
  export const _deletePageForThreadId = internalMutation({
142
147
  args: deleteThreadArgs,
143
148
  handler: deletePageForThreadIdHandler,
@@ -151,22 +156,39 @@ export const _deletePageForThreadId = internalMutation({
151
156
  export const deleteAllForThreadIdAsync = mutation({
152
157
  args: deleteThreadArgs,
153
158
  handler: async (ctx, args) => {
154
- const result = await deletePageForThreadIdHandler(ctx, args);
155
- if (!result.isDone) {
159
+ let messagesResult = {
160
+ isDone: args.messagesDone ?? false,
161
+ cursor: args.cursor,
162
+ };
163
+ if (!args.messagesDone) {
164
+ messagesResult = await deletePageForThreadIdHandler(ctx, args);
165
+ }
166
+ let streamResult = {
167
+ isDone: args.streamsDone ?? false,
168
+ streamOrder: args.streamOrder,
169
+ deltaCursor: args.deltaCursor,
170
+ };
171
+ if (!args.streamsDone) {
172
+ streamResult = await deleteStreamsPageForThreadId(ctx, {
173
+ threadId: args.threadId,
174
+ streamOrder: args.streamOrder,
175
+ deltaCursor: args.deltaCursor,
176
+ });
177
+ }
178
+ const isDone = messagesResult.isDone && streamResult.isDone;
179
+ if (!isDone) {
156
180
  await ctx.scheduler.runAfter(0, api.threads.deleteAllForThreadIdAsync, {
157
181
  threadId: args.threadId,
158
- cursor: result.cursor,
182
+ cursor: messagesResult.cursor,
183
+ messagesDone: messagesResult.isDone,
184
+ streamsDone: streamResult.isDone,
185
+ streamOrder: streamResult.streamOrder,
186
+ deltaCursor: streamResult.deltaCursor,
159
187
  });
160
188
  }
161
- // Kick off the streams deletion
162
- await ctx.scheduler.runAfter(
163
- 0,
164
- api.streams.deleteAllStreamsForThreadIdSync,
165
- { threadId: args.threadId }
166
- );
167
- return result;
189
+ return { isDone };
168
190
  },
169
- returns: deleteThreadReturns,
191
+ returns: v.object({ isDone: v.boolean() }),
170
192
  });
171
193
 
172
194
  async function deletePageForThreadIdHandler(
@@ -189,6 +189,7 @@ export function applyDeltasToStreamMessage(
189
189
  contentToAdd = part;
190
190
  break;
191
191
  case "reasoning":
192
+ currentMessage.reasoning += part.textDelta;
192
193
  if (lastContent?.type === "reasoning") {
193
194
  lastContent.text += part.textDelta;
194
195
  } else {
@@ -255,12 +256,14 @@ export function createStreamingMessage(
255
256
  ): MessageDoc {
256
257
  const { streamId, ...rest } = message;
257
258
  const metadata: MessageDoc = {
259
+ ...rest,
258
260
  _id: `${streamId}-${index}`,
259
261
  _creationTime: Date.now(),
260
- status: "pending",
262
+ status: (
263
+ { streaming: "pending", finished: "success", aborted: "failed" } as const
264
+ )[message.status],
261
265
  threadId,
262
266
  tool: false,
263
- ...rest,
264
267
  };
265
268
  switch (part.type) {
266
269
  case "text-delta":
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import type { ErrorMessage } from "convex-helpers";
2
+ import { omit, type ErrorMessage } from "convex-helpers";
3
3
  import {
4
4
  type PaginatedQueryArgs,
5
5
  type UsePaginatedQueryResult,
@@ -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, useState } from "react";
10
+ import { useMemo, useRef, 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";
@@ -103,14 +103,18 @@ export function useThreadMessages<
103
103
  ThreadMessagesArgs<Query>,
104
104
  ThreadMessagesResult<Query>
105
105
  >,
106
- !options.stream ? "skip" : args
106
+ !options.stream ||
107
+ args === "skip" ||
108
+ paginated.status === "LoadingFirstPage"
109
+ ? "skip"
110
+ : { ...args, startOrder: paginated.results.at(-1)?.order }
107
111
  );
108
112
 
109
113
  const merged = useMemo(() => {
110
114
  const streamListMessages =
111
115
  streamMessages?.map((m) => ({
112
116
  ...m,
113
- streaming: true,
117
+ streaming: !m.status || m.status === "streaming",
114
118
  })) ?? [];
115
119
  return {
116
120
  ...paginated,
@@ -150,7 +154,7 @@ export function useStreamingThreadMessages<
150
154
  Query extends ThreadStreamQuery<any, any>,
151
155
  >(
152
156
  query: Query,
153
- args: ThreadMessagesArgs<Query> | "skip"
157
+ args: (ThreadMessagesArgs<Query> & { startOrder?: number }) | "skip"
154
158
  ): Array<ThreadMessagesResult<Query>> | undefined {
155
159
  // Invariant: streamMessages[streamId] is comprised of all deltas up to the
156
160
  // cursor. There can be multiple messages in the same stream, e.g. for tool
@@ -158,15 +162,23 @@ export function useStreamingThreadMessages<
158
162
  const [streams, setStreams] = useState<
159
163
  Array<{ streamId: string; cursor: number; messages: MessageDoc[] }>
160
164
  >([]);
165
+ const startOrderRef = useRef<number>(0);
166
+ const queryArgs = args === "skip" ? args : omit(args, ["startOrder"]);
167
+ if (args !== "skip" && !startOrderRef.current && args.startOrder) {
168
+ startOrderRef.current = args.startOrder;
169
+ }
161
170
  // Get all the active streams
162
171
  const streamList = useQuery(
163
172
  query,
164
- args === "skip"
165
- ? args
173
+ queryArgs === "skip"
174
+ ? queryArgs
166
175
  : ({
167
- ...args,
176
+ ...queryArgs,
168
177
  paginationOpts: { cursor: null, numItems: 0 },
169
- streamArgs: { kind: "list" } as StreamArgs,
178
+ streamArgs: {
179
+ kind: "list",
180
+ startOrder: startOrderRef.current,
181
+ } as StreamArgs,
170
182
  } as FunctionArgs<Query>)
171
183
  ) as
172
184
  | { streams: Extract<SyncStreamsReturnValue, { kind: "list" }> }
@@ -186,10 +198,10 @@ export function useStreamingThreadMessages<
186
198
  // Get the deltas for all the active streams, if any.
187
199
  const cursorQuery = useQuery(
188
200
  query,
189
- args === "skip" || !streamList
201
+ queryArgs === "skip" || !streamList
190
202
  ? ("skip" as const)
191
203
  : ({
192
- ...args,
204
+ ...queryArgs,
193
205
  paginationOpts: { cursor: null, numItems: 0 },
194
206
  streamArgs: { kind: "deltas", cursors } as StreamArgs,
195
207
  } as FunctionArgs<Query>)
@@ -11,7 +11,7 @@ export function optimisticallySendMessage(
11
11
  ) => void {
12
12
  return (store, args) => {
13
13
  const queries = store.getAllQueries(query);
14
- let maxOrder = 0;
14
+ let maxOrder = -1;
15
15
  let maxStepOrder = 0;
16
16
  for (const q of queries) {
17
17
  if (q.args?.threadId !== args.threadId) continue;
package/src/validators.ts CHANGED
@@ -460,6 +460,7 @@ export const vStreamArgs = v.optional(
460
460
  v.union(
461
461
  v.object({
462
462
  kind: v.literal("list"),
463
+ startOrder: v.optional(v.number()),
463
464
  }),
464
465
  v.object({
465
466
  kind: v.literal("deltas"),
@@ -471,6 +472,11 @@ export type StreamArgs = Infer<typeof vStreamArgs>;
471
472
 
472
473
  export const vStreamMessage = v.object({
473
474
  streamId: v.string(),
475
+ status: v.union(
476
+ v.literal("streaming"),
477
+ v.literal("finished"),
478
+ v.literal("aborted")
479
+ ),
474
480
  order: v.number(),
475
481
  stepOrder: v.number(),
476
482
  // metadata