@convex-dev/agent 0.2.8-alpha.2 → 0.2.8-alpha.4

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 (50) hide show
  1. package/dist/UIMessages.d.ts +2 -1
  2. package/dist/UIMessages.d.ts.map +1 -1
  3. package/dist/UIMessages.js +12 -17
  4. package/dist/UIMessages.js.map +1 -1
  5. package/dist/client/createTool.d.ts +1 -1
  6. package/dist/client/createTool.d.ts.map +1 -1
  7. package/dist/client/createTool.js +1 -1
  8. package/dist/client/createTool.js.map +1 -1
  9. package/dist/client/mockModel.d.ts +10 -0
  10. package/dist/client/mockModel.d.ts.map +1 -1
  11. package/dist/client/mockModel.js +73 -62
  12. package/dist/client/mockModel.js.map +1 -1
  13. package/dist/component/streams.d.ts +8 -8
  14. package/dist/component/streams.d.ts.map +1 -1
  15. package/dist/component/streams.js +18 -21
  16. package/dist/component/streams.js.map +1 -1
  17. package/dist/deltas.d.ts.map +1 -1
  18. package/dist/deltas.js +1 -0
  19. package/dist/deltas.js.map +1 -1
  20. package/dist/react/types.d.ts +1 -1
  21. package/dist/react/types.d.ts.map +1 -1
  22. package/dist/react/useStreamingUIMessages.d.ts +2 -2
  23. package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
  24. package/dist/react/useStreamingUIMessages.js +22 -18
  25. package/dist/react/useStreamingUIMessages.js.map +1 -1
  26. package/dist/react/useThreadMessages.d.ts +2 -2
  27. package/dist/react/useThreadMessages.d.ts.map +1 -1
  28. package/dist/react/useThreadMessages.js +3 -2
  29. package/dist/react/useThreadMessages.js.map +1 -1
  30. package/dist/react/useUIMessages.d.ts +10 -6
  31. package/dist/react/useUIMessages.d.ts.map +1 -1
  32. package/dist/react/useUIMessages.js +24 -34
  33. package/dist/react/useUIMessages.js.map +1 -1
  34. package/dist/shared.d.ts.map +1 -1
  35. package/dist/shared.js +7 -4
  36. package/dist/shared.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/UIMessages.ts +16 -21
  39. package/src/client/createTool.ts +2 -2
  40. package/src/client/mockModel.ts +108 -76
  41. package/src/component/streams.ts +20 -21
  42. package/src/deltas.test.ts +311 -97
  43. package/src/deltas.ts +4 -0
  44. package/src/react/types.ts +1 -1
  45. package/src/react/useStreamingUIMessages.ts +36 -28
  46. package/src/react/useThreadMessages.ts +13 -8
  47. package/src/react/useUIMessages.test.ts +255 -0
  48. package/src/react/useUIMessages.ts +38 -52
  49. package/src/shared.ts +6 -4
  50. package/src/toUIMessages.test.ts +1 -1
package/src/UIMessages.ts CHANGED
@@ -27,6 +27,8 @@ import type {
27
27
  } from "./validators.js";
28
28
  import { omit, pick } from "convex-helpers";
29
29
 
30
+ export type UIStatus = "streaming" | MessageStatus;
31
+
30
32
  export type UIMessage<
31
33
  METADATA = unknown,
32
34
  DATA_PARTS extends UIDataTypes = UIDataTypes,
@@ -35,7 +37,7 @@ export type UIMessage<
35
37
  key: string;
36
38
  order: number;
37
39
  stepOrder: number;
38
- status: "streaming" | MessageStatus;
40
+ status: UIStatus;
39
41
  agentName?: string;
40
42
  text: string;
41
43
  _creationTime: number;
@@ -160,7 +162,7 @@ export function toUIMessages<
160
162
  messages: (MessageDoc & ExtraFields<METADATA>)[],
161
163
  ): UIMessage<METADATA, DATA_PARTS, TOOLS>[] {
162
164
  // Group assistant and tool messages together
163
- const assistantGroups = groupAssistantMessages(messages);
165
+ const assistantGroups = groupAssistantMessages(sorted(messages));
164
166
 
165
167
  const uiMessages: UIMessage<METADATA, DATA_PARTS, TOOLS>[] = [];
166
168
  for (const group of assistantGroups) {
@@ -196,13 +198,10 @@ function groupAssistantMessages<METADATA = unknown>(
196
198
  ): Group<METADATA>[] {
197
199
  const groups: Group<METADATA>[] = [];
198
200
 
199
- // Sort messages by order and stepOrder first to handle out-of-order arrivals
200
- const sortedMessages = sorted(messages);
201
-
202
201
  let currentAssistantGroup: (MessageDoc & ExtraFields<METADATA>)[] = [];
203
202
  let currentOrder: number | undefined;
204
203
 
205
- for (const message of sortedMessages) {
204
+ for (const message of messages) {
206
205
  const coreMessage = message.message && deserializeMessage(message.message);
207
206
  if (!coreMessage) continue;
208
207
 
@@ -239,7 +238,6 @@ function groupAssistantMessages<METADATA = unknown>(
239
238
  currentAssistantGroup.push(message);
240
239
 
241
240
  // End group if this is an assistant message without tool calls
242
- // But only if we're processing messages in order (which we are now due to sorting)
243
241
  if (coreMessage.role === "assistant" && !message.tool) {
244
242
  groups.push({
245
243
  role: "assistant",
@@ -294,7 +292,7 @@ function createSystemUIMessage<
294
292
 
295
293
  function extractTextFromMessageDoc(message: MessageDoc): string {
296
294
  return (
297
- message.text || (message.message && extractText(message.message)) || ""
295
+ (message.message && extractText(message.message)) || message.text || ""
298
296
  );
299
297
  }
300
298
 
@@ -373,9 +371,9 @@ function createAssistantUIMessage<
373
371
 
374
372
  // Concatenate text from all messages in group
375
373
  const allText = group
376
- .map((msg) => msg.text || ((msg.message && extractText(msg.message)) ?? ""))
374
+ .map((msg) => extractTextFromMessageDoc(msg))
377
375
  .filter(Boolean)
378
- .join("");
376
+ .join(" ");
379
377
 
380
378
  // Get status from last message
381
379
  const lastMessage = group[group.length - 1];
@@ -393,8 +391,7 @@ function createAssistantUIMessage<
393
391
  const content = coreMessage.content;
394
392
  const nonStringContent =
395
393
  content && typeof content !== "string" ? content : [];
396
- const text =
397
- message.text || ((message.message && extractText(message.message)) ?? "");
394
+ const text = extractTextFromMessageDoc(message);
398
395
 
399
396
  const partCommon = {
400
397
  state: message.streaming ? ("streaming" as const) : ("done" as const),
@@ -465,6 +462,10 @@ function createAssistantUIMessage<
465
462
  break;
466
463
  }
467
464
  case "tool-result": {
465
+ const output =
466
+ contentPart.output?.type === "json"
467
+ ? contentPart.output.value
468
+ : contentPart.output;
468
469
  const call = allParts.find(
469
470
  (part) =>
470
471
  part.type === `tool-${contentPart.toolName}` &&
@@ -475,13 +476,10 @@ function createAssistantUIMessage<
475
476
  if (message.error) {
476
477
  call.state = "output-error";
477
478
  call.errorText = message.error;
478
- call.output = contentPart.output;
479
+ call.output = output;
479
480
  } else {
480
481
  call.state = "output-available";
481
- call.output =
482
- contentPart.output?.type === "json"
483
- ? contentPart.output.value
484
- : contentPart.output;
482
+ call.output = output;
485
483
  }
486
484
  } else {
487
485
  console.warn(
@@ -503,10 +501,7 @@ function createAssistantUIMessage<
503
501
  toolCallId: contentPart.toolCallId,
504
502
  state: "output-available",
505
503
  input: undefined,
506
- output:
507
- contentPart.output?.type === "json"
508
- ? contentPart.output.value
509
- : contentPart.output,
504
+ output,
510
505
  callProviderMetadata: message.providerMetadata,
511
506
  } satisfies ToolUIPart<TOOLS>);
512
507
  }
@@ -44,7 +44,7 @@ export function createTool<INPUT, OUTPUT, Ctx extends ToolCtx = ToolCtx>(def: {
44
44
  ctx: Ctx,
45
45
  args: INPUT,
46
46
  options: ToolCallOptions,
47
- ) => PromiseLike<OUTPUT>;
47
+ ) => PromiseLike<OUTPUT> | AsyncIterable<OUTPUT>;
48
48
  /**
49
49
  * Provide the context to use, e.g. when defining the tool at runtime.
50
50
  */
@@ -85,7 +85,7 @@ export function createTool<INPUT, OUTPUT, Ctx extends ToolCtx = ToolCtx>(def: {
85
85
  ctx: def.ctx,
86
86
  description: def.description,
87
87
  inputSchema: def.args,
88
- async execute(args: INPUT, options: ToolCallOptions) {
88
+ execute(args: INPUT, options: ToolCallOptions) {
89
89
  if (!getCtx(this)) {
90
90
  throw new Error(
91
91
  "To use a Convex tool, you must either provide the ctx" +
@@ -4,7 +4,7 @@ import type {
4
4
  LanguageModelV2StreamPart,
5
5
  } from "@ai-sdk/provider";
6
6
  import { simulateReadableStream, type ProviderMetadata } from "ai";
7
- import { pick } from "convex-helpers";
7
+ import { assert, pick } from "convex-helpers";
8
8
 
9
9
  export const DEFAULT_TEXT = `
10
10
  A A A A A A A A A A A A A A A
@@ -22,8 +22,18 @@ export type MockModelArgs = {
22
22
  | (() => LanguageModelV2["supportedUrls"]);
23
23
  chunkDelayInMs?: number;
24
24
  initialDelayInMs?: number;
25
- // provide either content or doGenerate & doStream
25
+ /** A list of the responses for multiple steps.
26
+ * For tool calls, the first list would include a tool call part,
27
+ * then the next list would be after the tool response or another tool call.
28
+ * Tool responses come from actual tool calls!
29
+ */
30
+ contentSteps?: LanguageModelV2Content[][];
31
+ /** A single list of content responded from each step.
32
+ * Provide contentSteps instead if you want to do multi-step responses with
33
+ * tool calls.
34
+ */
26
35
  content?: LanguageModelV2Content[];
36
+ // provide either content, contentResponses or doGenerate & doStream
27
37
  doGenerate?: LanguageModelV2["doGenerate"];
28
38
  doStream?: LanguageModelV2["doStream"];
29
39
  providerMetadata?: ProviderMetadata;
@@ -35,6 +45,10 @@ export type MockModelArgs = {
35
45
  };
36
46
  };
37
47
 
48
+ function atMostOneOf(...args: unknown[]) {
49
+ return args.filter(Boolean).length <= 1;
50
+ }
51
+
38
52
  export function mockModel(args?: MockModelArgs): LanguageModelV2 {
39
53
  return new MockLanguageModel(args ?? {});
40
54
  }
@@ -54,10 +68,19 @@ export class MockLanguageModel implements LanguageModelV2 {
54
68
  doStreamCalls: Parameters<LanguageModelV2["doStream"]>[0][] = [];
55
69
 
56
70
  constructor(args: MockModelArgs) {
71
+ assert(
72
+ atMostOneOf(
73
+ args.content,
74
+ args.contentSteps,
75
+ args.doGenerate && args.doStream,
76
+ ),
77
+ "Expected only one of content, contentSteps, or doGenerate and doStream",
78
+ );
57
79
  this.provider = args.provider || "mock-provider";
58
80
  this.modelId = args.modelId || "mock-model-id";
59
81
  const {
60
82
  content = [{ type: "text", text: DEFAULT_TEXT }],
83
+ contentSteps = [content],
61
84
  chunkDelayInMs = 0,
62
85
  initialDelayInMs = 0,
63
86
  supportedUrls = {},
@@ -72,77 +95,83 @@ export class MockLanguageModel implements LanguageModelV2 {
72
95
  "Mock error message";
73
96
  const metadata = pick(args, ["providerMetadata"]);
74
97
 
75
- const chunks: LanguageModelV2StreamPart[] = [
76
- { type: "stream-start", warnings: [] },
77
- ];
78
- chunks.push(
79
- ...content.flatMap((c, ci): LanguageModelV2StreamPart[] => {
80
- if (c.type !== "text" && c.type !== "reasoning") {
81
- return [c];
82
- }
83
- const metadata = pick(c, ["providerMetadata"]);
84
- const deltas = c.text.split(" ");
85
- const parts: LanguageModelV2StreamPart[] = [];
86
- if (c.type === "reasoning") {
87
- parts.push({
88
- type: "reasoning-start",
89
- id: `reasoning-${ci}`,
90
- ...metadata,
91
- });
92
- parts.push(
93
- ...deltas.map(
94
- (delta, di) =>
95
- ({
96
- type: "reasoning-delta",
97
- delta: (di ? " " : "") + delta,
98
- id: `reasoning-${ci}`,
99
- ...metadata,
100
- }) satisfies LanguageModelV2StreamPart,
101
- ),
102
- );
103
- parts.push({
104
- type: "reasoning-end",
105
- id: `reasoning-${ci}`,
106
- ...metadata,
107
- });
108
- } else if (c.type === "text") {
109
- parts.push({
110
- type: "text-start",
111
- id: `txt-${ci}`,
112
- ...metadata,
113
- });
114
- parts.push(
115
- ...deltas.map(
116
- (delta, di) =>
117
- ({
118
- type: "text-delta",
119
- delta: (di ? " " : "") + delta,
120
- id: `txt-${ci}`,
121
- ...metadata,
122
- }) satisfies LanguageModelV2StreamPart,
123
- ),
124
- );
125
- parts.push({
126
- type: "text-end",
127
- id: `txt-${ci}`,
128
- ...metadata,
98
+ const chunkResponses: LanguageModelV2StreamPart[][] = contentSteps.map(
99
+ (content) => {
100
+ const chunks: LanguageModelV2StreamPart[] = [
101
+ { type: "stream-start", warnings: [] },
102
+ ];
103
+ chunks.push(
104
+ ...content.flatMap((c, ci): LanguageModelV2StreamPart[] => {
105
+ if (c.type !== "text" && c.type !== "reasoning") {
106
+ return [c];
107
+ }
108
+ const metadata = pick(c, ["providerMetadata"]);
109
+ const deltas = c.text.split(" ");
110
+ const parts: LanguageModelV2StreamPart[] = [];
111
+ if (c.type === "reasoning") {
112
+ parts.push({
113
+ type: "reasoning-start",
114
+ id: `reasoning-${ci}`,
115
+ ...metadata,
116
+ });
117
+ parts.push(
118
+ ...deltas.map(
119
+ (delta, di) =>
120
+ ({
121
+ type: "reasoning-delta",
122
+ delta: (di ? " " : "") + delta,
123
+ id: `reasoning-${ci}`,
124
+ ...metadata,
125
+ }) satisfies LanguageModelV2StreamPart,
126
+ ),
127
+ );
128
+ parts.push({
129
+ type: "reasoning-end",
130
+ id: `reasoning-${ci}`,
131
+ ...metadata,
132
+ });
133
+ } else if (c.type === "text") {
134
+ parts.push({
135
+ type: "text-start",
136
+ id: `txt-${ci}`,
137
+ ...metadata,
138
+ });
139
+ parts.push(
140
+ ...deltas.map(
141
+ (delta, di) =>
142
+ ({
143
+ type: "text-delta",
144
+ delta: (di ? " " : "") + delta,
145
+ id: `txt-${ci}`,
146
+ ...metadata,
147
+ }) satisfies LanguageModelV2StreamPart,
148
+ ),
149
+ );
150
+ parts.push({
151
+ type: "text-end",
152
+ id: `txt-${ci}`,
153
+ ...metadata,
154
+ });
155
+ }
156
+ return parts;
157
+ }),
158
+ );
159
+ if (fail) {
160
+ chunks.push({
161
+ type: "error",
162
+ error,
129
163
  });
130
164
  }
131
- return parts;
132
- }),
165
+ chunks.push({
166
+ type: "finish",
167
+ finishReason: fail ? "error" : "stop",
168
+ usage: DEFAULT_USAGE,
169
+ ...metadata,
170
+ });
171
+ return chunks;
172
+ },
133
173
  );
134
- if (fail) {
135
- chunks.push({
136
- type: "error",
137
- error,
138
- });
139
- }
140
- chunks.push({
141
- type: "finish",
142
- finishReason: fail ? "error" : "stop",
143
- usage: DEFAULT_USAGE,
144
- ...metadata,
145
- });
174
+ let callIndex = 0;
146
175
  this.doGenerate = async (options) => {
147
176
  this.doGenerateCalls.push(options);
148
177
 
@@ -153,14 +182,16 @@ export class MockLanguageModel implements LanguageModelV2 {
153
182
  return args.doGenerate(options);
154
183
  } else if (Array.isArray(args.doGenerate)) {
155
184
  return args.doGenerate[this.doGenerateCalls.length];
156
- } else if (content) {
157
- return {
158
- content,
159
- finishReason: "stop",
185
+ } else if (contentSteps.length) {
186
+ const result = {
187
+ content: contentSteps[callIndex % contentSteps.length],
188
+ finishReason: "stop" as const,
160
189
  usage: DEFAULT_USAGE,
161
190
  ...metadata,
162
191
  warnings: [],
163
192
  };
193
+ callIndex++;
194
+ return result;
164
195
  } else {
165
196
  throw new Error("Unexpected: no content or doGenerate");
166
197
  }
@@ -176,12 +207,13 @@ export class MockLanguageModel implements LanguageModelV2 {
176
207
  return args.doStream(options);
177
208
  } else if (Array.isArray(args.doStream)) {
178
209
  return args.doStream[this.doStreamCalls.length];
179
- } else if (content) {
210
+ } else if (contentSteps) {
180
211
  const stream = simulateReadableStream({
181
- chunks,
212
+ chunks: chunkResponses[callIndex % chunkResponses.length],
182
213
  initialDelayInMs,
183
214
  chunkDelayInMs,
184
215
  });
216
+ callIndex++;
185
217
 
186
218
  if (options.abortSignal) {
187
219
  options.abortSignal.addEventListener("abort", () => {
@@ -3,6 +3,7 @@ import { v } from "convex/values";
3
3
  import {
4
4
  type MessageWithMetadataInternal,
5
5
  type StreamDelta,
6
+ type StreamMessage,
6
7
  vStreamDelta,
7
8
  vStreamMessage,
8
9
  } from "../validators.js";
@@ -136,23 +137,27 @@ export const list = query({
136
137
  ["order", "stepOrder"],
137
138
  ).take(100);
138
139
 
139
- return messages.map((m) => ({
140
- streamId: m._id,
141
- status: m.state.kind,
142
- ...pick(m, [
143
- "format",
144
- "order",
145
- "stepOrder",
146
- "userId",
147
- "agentName",
148
- "model",
149
- "provider",
150
- "providerOptions",
151
- ]),
152
- }));
140
+ return messages.map((m) => publicStreamMessage(m));
153
141
  },
154
142
  });
155
143
 
144
+ function publicStreamMessage(m: Doc<"streamingMessages">): StreamMessage {
145
+ return {
146
+ streamId: m._id,
147
+ status: m.state.kind,
148
+ ...pick(m, [
149
+ "format",
150
+ "order",
151
+ "stepOrder",
152
+ "userId",
153
+ "agentName",
154
+ "model",
155
+ "provider",
156
+ "providerOptions",
157
+ ]),
158
+ };
159
+ }
160
+
156
161
  export const abortByOrder = mutation({
157
162
  args: { threadId: v.id("threads"), order: v.number(), reason: v.string() },
158
163
  returns: v.boolean(),
@@ -526,13 +531,7 @@ export async function getStreamingMessagesWithMetadata(
526
531
  .take(1000);
527
532
  const uiMessages = await deriveUIMessagesFromDeltas(
528
533
  threadId,
529
- [
530
- {
531
- ...streamingMessage,
532
- status: "streaming",
533
- streamId: streamingMessage._id,
534
- },
535
- ],
534
+ [publicStreamMessage(streamingMessage)],
536
535
  deltas,
537
536
  );
538
537
  // We don't save messages that have already been saved