@convex-dev/agent 0.3.2 → 0.6.0-alpha.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 (99) hide show
  1. package/dist/UIMessages.d.ts +3 -2
  2. package/dist/UIMessages.d.ts.map +1 -1
  3. package/dist/UIMessages.js +100 -19
  4. package/dist/UIMessages.js.map +1 -1
  5. package/dist/client/createTool.d.ts +129 -25
  6. package/dist/client/createTool.d.ts.map +1 -1
  7. package/dist/client/createTool.js +65 -8
  8. package/dist/client/createTool.js.map +1 -1
  9. package/dist/client/definePlaygroundAPI.d.ts +560 -35
  10. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  11. package/dist/client/files.d.ts.map +1 -1
  12. package/dist/client/files.js +4 -2
  13. package/dist/client/files.js.map +1 -1
  14. package/dist/client/index.d.ts +462 -37
  15. package/dist/client/index.d.ts.map +1 -1
  16. package/dist/client/index.js +26 -7
  17. package/dist/client/index.js.map +1 -1
  18. package/dist/client/messages.d.ts +114 -9
  19. package/dist/client/messages.d.ts.map +1 -1
  20. package/dist/client/mockModel.d.ts +17 -17
  21. package/dist/client/mockModel.d.ts.map +1 -1
  22. package/dist/client/mockModel.js +8 -6
  23. package/dist/client/mockModel.js.map +1 -1
  24. package/dist/client/saveInputMessages.d.ts +1 -1
  25. package/dist/client/saveInputMessages.d.ts.map +1 -1
  26. package/dist/client/saveInputMessages.js +1 -1
  27. package/dist/client/saveInputMessages.js.map +1 -1
  28. package/dist/client/search.d.ts +132 -14
  29. package/dist/client/search.d.ts.map +1 -1
  30. package/dist/client/search.js +66 -19
  31. package/dist/client/search.js.map +1 -1
  32. package/dist/client/streamText.d.ts +4 -4
  33. package/dist/client/streamText.d.ts.map +1 -1
  34. package/dist/client/streamText.js.map +1 -1
  35. package/dist/client/streaming.d.ts +1491 -87
  36. package/dist/client/streaming.d.ts.map +1 -1
  37. package/dist/client/types.d.ts +26 -6
  38. package/dist/client/types.d.ts.map +1 -1
  39. package/dist/component/_generated/component.d.ts +862 -19
  40. package/dist/component/_generated/component.d.ts.map +1 -1
  41. package/dist/component/files.d.ts +12 -6
  42. package/dist/component/files.d.ts.map +1 -1
  43. package/dist/component/files.js +10 -2
  44. package/dist/component/files.js.map +1 -1
  45. package/dist/component/messages.d.ts +1247 -92
  46. package/dist/component/messages.d.ts.map +1 -1
  47. package/dist/component/schema.d.ts +2823 -217
  48. package/dist/component/schema.d.ts.map +1 -1
  49. package/dist/component/schema.js +3 -1
  50. package/dist/component/schema.js.map +1 -1
  51. package/dist/component/streams.js +1 -1
  52. package/dist/component/streams.js.map +1 -1
  53. package/dist/component/threads.d.ts +10 -10
  54. package/dist/component/vector/index.d.ts +1 -1
  55. package/dist/deltas.d.ts.map +1 -1
  56. package/dist/deltas.js +40 -5
  57. package/dist/deltas.js.map +1 -1
  58. package/dist/mapping.d.ts.map +1 -1
  59. package/dist/mapping.js +117 -25
  60. package/dist/mapping.js.map +1 -1
  61. package/dist/react/useThreadMessages.d.ts.map +1 -1
  62. package/dist/react/useThreadMessages.js +17 -9
  63. package/dist/react/useThreadMessages.js.map +1 -1
  64. package/dist/shared.d.ts +2 -2
  65. package/dist/shared.d.ts.map +1 -1
  66. package/dist/shared.js.map +1 -1
  67. package/dist/validators.d.ts +12196 -624
  68. package/dist/validators.d.ts.map +1 -1
  69. package/dist/validators.js +152 -9
  70. package/dist/validators.js.map +1 -1
  71. package/package.json +24 -22
  72. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  73. package/src/UIMessages.ts +184 -66
  74. package/src/client/createTool.ts +291 -67
  75. package/src/client/files.ts +4 -2
  76. package/src/client/index.test.ts +1 -0
  77. package/src/client/index.ts +46 -25
  78. package/src/client/mockModel.ts +36 -34
  79. package/src/client/saveInputMessages.ts +2 -2
  80. package/src/client/search.test.ts +166 -0
  81. package/src/client/search.ts +121 -38
  82. package/src/client/streamText.ts +5 -5
  83. package/src/client/types.ts +31 -18
  84. package/src/component/_generated/component.ts +2329 -338
  85. package/src/component/files.ts +11 -2
  86. package/src/component/messages.test.ts +7 -7
  87. package/src/component/schema.ts +3 -1
  88. package/src/component/setup.test.ts +7 -0
  89. package/src/component/streams.ts +1 -1
  90. package/src/deltas.test.ts +90 -0
  91. package/src/deltas.ts +47 -8
  92. package/src/fromUIMessages.test.ts +32 -27
  93. package/src/mapping.test.ts +48 -0
  94. package/src/mapping.ts +234 -142
  95. package/src/react/useThreadMessages.ts +21 -9
  96. package/src/shared.ts +2 -0
  97. package/src/toUIMessages.test.ts +539 -1
  98. package/src/validators.ts +179 -20
  99. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
package/src/mapping.ts CHANGED
@@ -25,7 +25,6 @@ import {
25
25
  type Message,
26
26
  type MessageWithMetadata,
27
27
  type Usage,
28
- type vContent,
29
28
  type vFilePart,
30
29
  type vImagePart,
31
30
  type vReasoningPart,
@@ -36,6 +35,8 @@ import {
36
35
  type SourcePart,
37
36
  vToolResultOutput,
38
37
  type MessageDoc,
38
+ vToolApprovalRequest,
39
+ vToolApprovalResponse,
39
40
  } from "./validators.js";
40
41
  import type { ActionCtx, AgentComponent } from "./client/types.js";
41
42
  import type { MutationCtx } from "./client/types.js";
@@ -155,6 +156,17 @@ export function toModelMessageUsage(usage: Usage): LanguageModelUsage {
155
156
  totalTokens: usage.totalTokens,
156
157
  reasoningTokens: usage.reasoningTokens,
157
158
  cachedInputTokens: usage.cachedInputTokens,
159
+ // These detail fields are required by LanguageModelUsage type but we don't
160
+ // have the granular data, so we provide empty objects with undefined values.
161
+ inputTokenDetails: {
162
+ cacheReadTokens: undefined,
163
+ cacheWriteTokens: undefined,
164
+ noCacheTokens: undefined,
165
+ },
166
+ outputTokenDetails: {
167
+ textTokens: undefined,
168
+ reasoningTokens: undefined,
169
+ },
158
170
  };
159
171
  }
160
172
 
@@ -165,18 +177,33 @@ export function serializeWarnings(
165
177
  return undefined;
166
178
  }
167
179
  return warnings.map((warning) => {
168
- if (warning.type !== "unsupported-setting") {
169
- return warning;
180
+ if (warning.type === "compatibility") {
181
+ return {
182
+ type: "unsupported-setting",
183
+ setting: warning.feature,
184
+ details: warning.details,
185
+ };
170
186
  }
171
- return { ...warning, setting: warning.setting.toString() };
172
- });
187
+ return warning;
188
+ }) as any;
173
189
  }
174
190
 
175
191
  export function toModelMessageWarnings(
176
192
  warnings: MessageWithMetadata["warnings"],
177
193
  ): CallWarning[] | undefined {
178
- // We don't need to do anythign here for now
179
- return warnings;
194
+ if (!warnings) {
195
+ return undefined;
196
+ }
197
+ return warnings.map((warning) => {
198
+ if (warning.type === "unsupported-setting") {
199
+ return {
200
+ type: "compatibility",
201
+ feature: warning.setting,
202
+ details: warning.details,
203
+ };
204
+ }
205
+ return warning;
206
+ }) as any;
180
207
  }
181
208
 
182
209
  export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
@@ -307,7 +334,7 @@ export async function serializeContent(
307
334
  }
308
335
  return {
309
336
  type: part.type,
310
- mimeType: getMimeOrMediaType(part),
337
+ mediaType: getMimeOrMediaType(part),
311
338
  ...metadata,
312
339
  image,
313
340
  } satisfies Infer<typeof vImagePart>;
@@ -327,15 +354,18 @@ export async function serializeContent(
327
354
  type: part.type,
328
355
  data,
329
356
  filename: part.filename,
330
- mimeType: getMimeOrMediaType(part)!,
357
+ mediaType: getMimeOrMediaType(part)!,
331
358
  ...metadata,
332
359
  } satisfies Infer<typeof vFilePart>;
333
360
  }
334
361
  case "tool-call": {
335
- const args = "input" in part ? part.input : part.args;
362
+ // Handle legacy data where only args field exists
363
+ const input = part.input ?? (part as any)?.args ?? {};
336
364
  return {
337
365
  type: part.type,
338
- args: args ?? null,
366
+ input,
367
+ /** @deprecated Use `input` instead. */
368
+ args: input,
339
369
  toolCallId: part.toolCallId,
340
370
  toolName: part.toolName,
341
371
  providerExecuted: part.providerExecuted,
@@ -363,13 +393,31 @@ export async function serializeContent(
363
393
  case "source": {
364
394
  return part satisfies Infer<typeof vSourcePart>;
365
395
  }
396
+ case "tool-approval-request": {
397
+ return {
398
+ type: part.type,
399
+ approvalId: part.approvalId,
400
+ toolCallId: part.toolCallId,
401
+ ...metadata,
402
+ } satisfies Infer<typeof vToolApprovalRequest>;
403
+ }
404
+ case "tool-approval-response": {
405
+ return {
406
+ type: part.type,
407
+ approvalId: part.approvalId,
408
+ approved: part.approved,
409
+ reason: part.reason,
410
+ providerExecuted: part.providerExecuted,
411
+ ...metadata,
412
+ } satisfies Infer<typeof vToolApprovalResponse>;
413
+ }
366
414
  default:
367
- return part satisfies Infer<typeof vContent>;
415
+ return null;
368
416
  }
369
417
  }),
370
418
  );
371
419
  return {
372
- content: serialized as SerializedContent,
420
+ content: serialized.filter((p) => p !== null) as SerializedContent,
373
421
  fileIds: fileIds.length > 0 ? fileIds : undefined,
374
422
  };
375
423
  }
@@ -378,57 +426,78 @@ export function fromModelMessageContent(content: Content): Message["content"] {
378
426
  if (typeof content === "string") {
379
427
  return content;
380
428
  }
381
- return content.map((part) => {
382
- const metadata: {
383
- providerOptions?: ProviderOptions;
384
- providerMetadata?: ProviderMetadata;
385
- } = {};
386
- if ("providerOptions" in part) {
387
- metadata.providerOptions = part.providerOptions as ProviderOptions;
388
- }
389
- if ("providerMetadata" in part) {
390
- metadata.providerMetadata = part.providerMetadata as ProviderMetadata;
391
- }
392
- switch (part.type) {
393
- case "text":
394
- return part satisfies Infer<typeof vTextPart>;
395
- case "image":
396
- return {
397
- type: part.type,
398
- mimeType: getMimeOrMediaType(part),
399
- ...metadata,
400
- image: serializeDataOrUrl(part.image),
401
- } satisfies Infer<typeof vImagePart>;
402
- case "file":
403
- return {
404
- type: part.type,
405
- data: serializeDataOrUrl(part.data),
406
- filename: part.filename,
407
- mimeType: getMimeOrMediaType(part)!,
408
- ...metadata,
409
- } satisfies Infer<typeof vFilePart>;
410
- case "tool-call":
411
- return {
412
- type: part.type,
413
- args: part.input ?? null,
414
- toolCallId: part.toolCallId,
415
- toolName: part.toolName,
416
- providerExecuted: part.providerExecuted,
417
- ...metadata,
418
- } satisfies Infer<typeof vToolCallPart>;
419
- case "tool-result":
420
- return normalizeToolResult(part, metadata);
421
- case "reasoning":
422
- return {
423
- type: part.type,
424
- text: part.text,
425
- ...metadata,
426
- } satisfies Infer<typeof vReasoningPart>;
427
- // Not in current generation output, but could be in historical messages
428
- default:
429
- return part satisfies Infer<typeof vContent>;
430
- }
431
- }) as Message["content"];
429
+ return content
430
+ .map((part) => {
431
+ const metadata: {
432
+ providerOptions?: ProviderOptions;
433
+ providerMetadata?: ProviderMetadata;
434
+ } = {};
435
+ if ("providerOptions" in part) {
436
+ metadata.providerOptions = part.providerOptions as ProviderOptions;
437
+ }
438
+ if ("providerMetadata" in part) {
439
+ metadata.providerMetadata = part.providerMetadata as ProviderMetadata;
440
+ }
441
+ switch (part.type) {
442
+ case "text":
443
+ return part satisfies Infer<typeof vTextPart>;
444
+ case "image":
445
+ return {
446
+ type: part.type,
447
+ mediaType: getMimeOrMediaType(part),
448
+ ...metadata,
449
+ image: serializeDataOrUrl(part.image),
450
+ } satisfies Infer<typeof vImagePart>;
451
+ case "file":
452
+ return {
453
+ type: part.type,
454
+ data: serializeDataOrUrl(part.data),
455
+ filename: part.filename,
456
+ mediaType: getMimeOrMediaType(part)!,
457
+ ...metadata,
458
+ } satisfies Infer<typeof vFilePart>;
459
+ case "tool-call":
460
+ // Handle legacy data where only args field exists
461
+ return {
462
+ type: part.type,
463
+ input: part.input ?? (part as any)?.args ?? {},
464
+ /** @deprecated Use `input` instead. */
465
+ args: part.input ?? (part as any)?.args ?? {},
466
+ toolCallId: part.toolCallId,
467
+ toolName: part.toolName,
468
+ providerExecuted: part.providerExecuted,
469
+ ...metadata,
470
+ } satisfies Infer<typeof vToolCallPart>;
471
+ case "tool-result":
472
+ return normalizeToolResult(part, metadata);
473
+ case "reasoning":
474
+ return {
475
+ type: part.type,
476
+ text: part.text,
477
+ ...metadata,
478
+ } satisfies Infer<typeof vReasoningPart>;
479
+ case "tool-approval-request":
480
+ return {
481
+ type: part.type,
482
+ approvalId: part.approvalId,
483
+ toolCallId: part.toolCallId,
484
+ ...metadata,
485
+ } satisfies Infer<typeof vToolApprovalRequest>;
486
+ case "tool-approval-response":
487
+ return {
488
+ type: part.type,
489
+ approvalId: part.approvalId,
490
+ approved: part.approved,
491
+ reason: part.reason,
492
+ providerExecuted: part.providerExecuted,
493
+ ...metadata,
494
+ } satisfies Infer<typeof vToolApprovalResponse>;
495
+ // Not in current generation output, but could be in historical messages
496
+ default:
497
+ return null;
498
+ }
499
+ })
500
+ .filter((p) => p !== null) as Message["content"];
432
501
  }
433
502
 
434
503
  export function toModelMessageContent(
@@ -437,84 +506,103 @@ export function toModelMessageContent(
437
506
  if (typeof content === "string") {
438
507
  return content;
439
508
  }
440
- return content.map((part) => {
441
- const metadata: {
442
- providerOptions?: ProviderOptions;
443
- providerMetadata?: ProviderMetadata;
444
- } = {};
445
- if ("providerOptions" in part) {
446
- metadata.providerOptions = part.providerOptions;
447
- }
448
- if ("providerMetadata" in part) {
449
- metadata.providerMetadata = part.providerMetadata;
450
- }
451
- switch (part.type) {
452
- case "text":
453
- return {
454
- type: part.type,
455
- text: part.text,
456
- ...metadata,
457
- } satisfies TextPart;
458
- case "image":
459
- return {
460
- type: part.type,
461
- image: toModelMessageDataOrUrl(part.image),
462
- mediaType: getMimeOrMediaType(part),
463
- ...metadata,
464
- } satisfies ImagePart;
465
- case "file":
466
- return {
467
- type: part.type,
468
- data: toModelMessageDataOrUrl(part.data),
469
- filename: part.filename,
470
- mediaType: getMimeOrMediaType(part)!,
471
- ...metadata,
472
- } satisfies FilePart;
473
- case "tool-call": {
474
- const input = "input" in part ? part.input : part.args;
475
- return {
476
- type: part.type,
477
- input: input ?? null,
478
- toolCallId: part.toolCallId,
479
- toolName: part.toolName,
480
- providerExecuted: part.providerExecuted,
481
- ...metadata,
482
- } satisfies ToolCallPart;
509
+ return content
510
+ .map((part) => {
511
+ const metadata: {
512
+ providerOptions?: ProviderOptions;
513
+ providerMetadata?: ProviderMetadata;
514
+ } = {};
515
+ if ("providerOptions" in part) {
516
+ metadata.providerOptions = part.providerOptions;
483
517
  }
484
- case "tool-result": {
485
- return normalizeToolResult(part, metadata);
518
+ if ("providerMetadata" in part) {
519
+ metadata.providerMetadata = part.providerMetadata;
486
520
  }
487
- case "reasoning":
488
- return {
489
- type: part.type,
490
- text: part.text,
491
- ...metadata,
492
- } satisfies ReasoningPart;
493
- case "redacted-reasoning":
494
- // TODO: should we just drop this?
495
- return {
496
- type: "reasoning",
497
- text: "",
498
- ...metadata,
499
- providerOptions: metadata.providerOptions
500
- ? {
501
- ...Object.fromEntries(
502
- Object.entries(metadata.providerOptions ?? {}).map(
503
- ([key, value]) => [
504
- key,
505
- { ...value, redactedData: part.data },
506
- ],
521
+ switch (part.type) {
522
+ case "text":
523
+ return {
524
+ type: part.type,
525
+ text: part.text,
526
+ ...metadata,
527
+ } satisfies TextPart;
528
+ case "image":
529
+ return {
530
+ type: part.type,
531
+ image: toModelMessageDataOrUrl(part.image),
532
+ mediaType: getMimeOrMediaType(part),
533
+ ...metadata,
534
+ } satisfies ImagePart;
535
+ case "file":
536
+ return {
537
+ type: part.type,
538
+ data: toModelMessageDataOrUrl(part.data),
539
+ filename: part.filename,
540
+ mediaType: getMimeOrMediaType(part)!,
541
+ ...metadata,
542
+ } satisfies FilePart;
543
+ case "tool-call": {
544
+ // Handle legacy data where only args field exists
545
+ const input = part.input ?? (part as any)?.args ?? {};
546
+ return {
547
+ type: part.type,
548
+ input,
549
+ toolCallId: part.toolCallId,
550
+ toolName: part.toolName,
551
+ providerExecuted: part.providerExecuted,
552
+ ...metadata,
553
+ } satisfies ToolCallPart;
554
+ }
555
+ case "tool-result": {
556
+ return normalizeToolResult(part, metadata);
557
+ }
558
+ case "reasoning":
559
+ return {
560
+ type: part.type,
561
+ text: part.text,
562
+ ...metadata,
563
+ } satisfies ReasoningPart;
564
+ case "redacted-reasoning":
565
+ // TODO: should we just drop this?
566
+ return {
567
+ type: "reasoning",
568
+ text: "",
569
+ ...metadata,
570
+ providerOptions: metadata.providerOptions
571
+ ? {
572
+ ...Object.fromEntries(
573
+ Object.entries(metadata.providerOptions ?? {}).map(
574
+ ([key, value]) => [
575
+ key,
576
+ { ...value, redactedData: part.data },
577
+ ],
578
+ ),
507
579
  ),
508
- ),
509
- }
510
- : undefined,
511
- } satisfies ReasoningPart;
512
- case "source":
513
- return part satisfies SourcePart;
514
- default:
515
- return part satisfies Content;
516
- }
517
- }) as Content;
580
+ }
581
+ : undefined,
582
+ } satisfies ReasoningPart;
583
+ case "source":
584
+ return part satisfies SourcePart;
585
+ case "tool-approval-request":
586
+ return {
587
+ type: part.type,
588
+ approvalId: part.approvalId,
589
+ toolCallId: part.toolCallId,
590
+ ...metadata,
591
+ } satisfies Infer<typeof vToolApprovalRequest>;
592
+ case "tool-approval-response":
593
+ return {
594
+ type: part.type,
595
+ approvalId: part.approvalId,
596
+ approved: part.approved,
597
+ reason: part.reason,
598
+ providerExecuted: part.providerExecuted,
599
+ ...metadata,
600
+ } satisfies Infer<typeof vToolApprovalResponse>;
601
+ default:
602
+ return null;
603
+ }
604
+ })
605
+ .filter((p) => p !== null) as Content;
518
606
  }
519
607
 
520
608
  export function normalizeToolOutput(
@@ -544,11 +632,15 @@ function normalizeToolResult(
544
632
  ): ToolResultPart & Infer<typeof vToolResultPart> {
545
633
  return {
546
634
  type: part.type,
547
- output:
548
- part.output ??
549
- normalizeToolOutput("result" in part ? part.result : undefined),
635
+ output: part.output
636
+ ? validate(vToolResultOutput, part.output)
637
+ ? (part.output as any)
638
+ : normalizeToolOutput(JSON.stringify(part.output))
639
+ : normalizeToolOutput("result" in part ? part.result : undefined),
550
640
  toolCallId: part.toolCallId,
551
641
  toolName: part.toolName,
642
+ // Preserve isError flag for error reporting
643
+ ...("isError" in part && part.isError ? { isError: true } : {}),
552
644
  ...metadata,
553
645
  } satisfies ToolResultPart;
554
646
  }
@@ -16,7 +16,7 @@ import type {
16
16
  PaginationOptions,
17
17
  PaginationResult,
18
18
  } from "convex/server";
19
- import { useMemo } from "react";
19
+ import { useMemo, useState, useEffect, useRef } from "react";
20
20
  import type { SyncStreamsReturnValue } from "../client/types.js";
21
21
  import { sorted } from "../shared.js";
22
22
  import { fromUIMessages } from "../UIMessages.js";
@@ -239,12 +239,24 @@ export function useStreamingThreadMessages<Query extends StreamQuery<any>>(
239
239
  args === "skip" ? undefined : (args.startOrder ?? undefined);
240
240
  const queryOptions = { startOrder, ...options };
241
241
  const uiMessages = useStreamingUIMessages(query, queryArgs, queryOptions);
242
- if (args === "skip") {
243
- return undefined;
244
- }
245
- // TODO: we aren't passing through as much metadata as we could here.
246
- // We could share the stream metadata logic with useStreamingUIMessages.
247
- return uiMessages
248
- ?.map((m) => fromUIMessages([m], { threadId: args.threadId }))
249
- .flat();
242
+ const [messages, setMessages] = useState<Array<MessageDoc> | undefined>();
243
+ const generationRef = useRef(0);
244
+
245
+ useEffect(() => {
246
+ if (args === "skip" || !uiMessages) {
247
+ setMessages(undefined);
248
+ return;
249
+ }
250
+ const currentGeneration = ++generationRef.current;
251
+ (async () => {
252
+ const nested = await Promise.all(
253
+ uiMessages.map((m) => fromUIMessages([m], { threadId: args.threadId })),
254
+ );
255
+ if (generationRef.current === currentGeneration) {
256
+ setMessages(nested.flat());
257
+ }
258
+ })();
259
+ }, [uiMessages, args === "skip" ? undefined : args.threadId]);
260
+
261
+ return messages;
250
262
  }
package/src/shared.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  ReasoningPart,
5
5
  ToolCallPart,
6
6
  ToolResultPart,
7
+ ToolApprovalRequest,
7
8
  } from "@ai-sdk/provider-utils";
8
9
  import type {
9
10
  ModelMessage,
@@ -55,6 +56,7 @@ export function joinText(
55
56
  | ToolCallPart
56
57
  | ToolResultPart
57
58
  | MessageContentParts
59
+ | ToolApprovalRequest
58
60
  )[],
59
61
  ) {
60
62
  return parts