@agent-native/toolkit 0.206.0 → 0.206.1-nightly-20261009213000

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 (82) hide show
  1. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  2. package/dist/app/agentkit/react/components.js +3 -3
  3. package/dist/app/agentkit/react/components.js.map +1 -1
  4. package/dist/app/agentkit/react/context.d.ts +1 -0
  5. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  6. package/dist/app/agentkit/react/context.js +1 -0
  7. package/dist/app/agentkit/react/context.js.map +1 -1
  8. package/dist/app/agentkit/react/styles.css +43 -0
  9. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  10. package/dist/app/chat/AgentKitAssistantChat.js +219 -75
  11. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  12. package/dist/app/chat/agentkit-chat/guided-questions.d.ts +18 -0
  13. package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
  14. package/dist/app/chat/agentkit-chat/guided-questions.js +12 -4
  15. package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
  16. package/dist/app/chat/chat/run-recovery.d.ts +6 -1
  17. package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
  18. package/dist/app/chat/chat/run-recovery.js +6 -3
  19. package/dist/app/chat/chat/run-recovery.js.map +1 -1
  20. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  21. package/dist/app/i18n/catalogs/ar-SA.js +8 -2
  22. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  23. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  24. package/dist/app/i18n/catalogs/de-DE.js +8 -2
  25. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  26. package/dist/app/i18n/catalogs/en-US.d.ts +8 -2
  27. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  28. package/dist/app/i18n/catalogs/en-US.js +8 -2
  29. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  30. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  31. package/dist/app/i18n/catalogs/es-ES.js +8 -2
  32. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  33. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  34. package/dist/app/i18n/catalogs/fr-FR.js +8 -2
  35. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  36. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  37. package/dist/app/i18n/catalogs/hi-IN.js +8 -2
  38. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  39. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  40. package/dist/app/i18n/catalogs/ja-JP.js +8 -2
  41. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  42. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  43. package/dist/app/i18n/catalogs/ko-KR.js +8 -2
  44. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  45. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  46. package/dist/app/i18n/catalogs/pt-BR.js +8 -2
  47. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  48. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  49. package/dist/app/i18n/catalogs/zh-CN.js +8 -2
  50. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  51. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/zh-TW.js +8 -2
  53. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  54. package/dist/app/sharing/ShareButton.d.ts +9 -0
  55. package/dist/app/sharing/ShareButton.d.ts.map +1 -1
  56. package/dist/app/sharing/ShareButton.js +23 -11
  57. package/dist/app/sharing/ShareButton.js.map +1 -1
  58. package/package.json +4 -4
  59. package/src/app/agentkit/react/components.tsx +6 -3
  60. package/src/app/agentkit/react/context.tsx +2 -0
  61. package/src/app/agentkit/react/styles.css +43 -0
  62. package/src/app/chat/AgentKitAssistantChat.spec.tsx +425 -15
  63. package/src/app/chat/AgentKitAssistantChat.tsx +306 -95
  64. package/src/app/chat/agentkit-chat/guided-questions.flow.spec.tsx +89 -5
  65. package/src/app/chat/agentkit-chat/guided-questions.tsx +17 -6
  66. package/src/app/chat/chat/run-recovery.spec.tsx +48 -0
  67. package/src/app/chat/chat/run-recovery.tsx +23 -1
  68. package/src/app/i18n/catalogs/ar-SA.ts +14 -2
  69. package/src/app/i18n/catalogs/de-DE.ts +12 -2
  70. package/src/app/i18n/catalogs/en-US.ts +13 -2
  71. package/src/app/i18n/catalogs/es-ES.ts +12 -2
  72. package/src/app/i18n/catalogs/fr-FR.ts +12 -2
  73. package/src/app/i18n/catalogs/hi-IN.ts +12 -2
  74. package/src/app/i18n/catalogs/ja-JP.ts +12 -2
  75. package/src/app/i18n/catalogs/ko-KR.ts +12 -2
  76. package/src/app/i18n/catalogs/pt-BR.ts +12 -2
  77. package/src/app/i18n/catalogs/zh-CN.ts +8 -2
  78. package/src/app/i18n/catalogs/zh-TW.ts +8 -2
  79. package/src/app/settings/model/ProviderDialog.spec.tsx +1 -0
  80. package/src/app/sharing/ShareButton.spec.tsx +148 -0
  81. package/src/app/sharing/ShareButton.tsx +44 -15
  82. package/src/app/sharing/useShareButtonController.spec.tsx +25 -1
@@ -1,17 +1,23 @@
1
1
  import {
2
+ AgentKitUploadError,
2
3
  hasActiveAgentRuns,
3
4
  selectAgentSuggestions,
4
5
  isCurrentAgentSuggestion,
5
6
  type AgentKitUploadDriver,
6
7
  type AgentThreadState,
7
8
  } from "@agent-native/agentkit";
8
- import { MAX_AGENT_REQUEST_ATTACHMENT_DATA_CHARS } from "@agent-native/agentkit/protocol";
9
+ import {
10
+ MAX_AGENT_REQUEST_ATTACHMENT_DATA_CHARS,
11
+ isInlineDataUrl,
12
+ persistableFilePart,
13
+ } from "@agent-native/agentkit/protocol";
9
14
  import type {
10
15
  AgentActionResult,
11
16
  AgentApprovalRequest,
12
17
  AgentConnectionRequest,
13
18
  AgentEvent,
14
19
  AgentMessage,
20
+ AgentQueuedMessage,
15
21
  AgentRequestAttachment,
16
22
  AgentStreamIntegrityReport,
17
23
  AgentTransport,
@@ -94,11 +100,18 @@ import {
94
100
  type RealtimeVoiceTranscriptMessage,
95
101
  } from "@agent-native/toolkit/composer/realtime-voice-transcript";
96
102
  import { IconButton } from "@agent-native/toolkit/design-system";
103
+ import {
104
+ DropdownMenu,
105
+ DropdownMenuContent,
106
+ DropdownMenuItem,
107
+ DropdownMenuTrigger,
108
+ } from "@agent-native/toolkit/ui/dropdown-menu";
97
109
  import { cn } from "@agent-native/toolkit/utils";
98
110
  import {
99
111
  IconAlertTriangle,
100
112
  IconCircleCheck,
101
113
  IconCopy,
114
+ IconDotsVertical,
102
115
  IconMessage,
103
116
  IconPlayerStopFilled,
104
117
  IconQuote,
@@ -563,6 +576,7 @@ type AgentKitComposerSubmit = (
563
576
  type AgentKitSuggestionSubmitOptions = PromptComposerSubmitOptions & {
564
577
  suggestion?: Exclude<AgentSuggestionInput, string>;
565
578
  validateSubmission?: () => void;
579
+ validateSubmissionScope?: () => void;
566
580
  queuedWhileRunActive?: boolean;
567
581
  };
568
582
 
@@ -929,6 +943,7 @@ export const AgentKitAssistantChat = forwardRef<
929
943
  name: "{{name}}",
930
944
  }),
931
945
  pastedText: t("agentChat.pastedText.title"),
946
+ attachmentNotSaved: t("agentChat.composer.attachmentNotSaved"),
932
947
  imagePreview: t("agentChat.composer.imagePreview"),
933
948
  closePreview: t("agentChat.composer.closePreview"),
934
949
  dropFilesToAttach: t("agentChat.composer.dropToAttach"),
@@ -970,46 +985,32 @@ export const AgentKitAssistantChat = forwardRef<
970
985
  form.set(key, value);
971
986
  }
972
987
  form.set("file", file.body, file.name);
973
- const response = await fetch(target.url, {
974
- method: target.method,
975
- headers: target.headers,
976
- body: form,
977
- signal: context?.signal,
978
- });
979
- if (!response.ok) {
980
- const messageKey =
981
- response.status === 415
982
- ? "agentChat.composer.unsupportedFileType"
983
- : response.status === 413
984
- ? "agentChat.composer.fileTooLarge"
985
- : response.status === 401
986
- ? "agentChat.composer.sessionExpired"
987
- : undefined;
988
- const message = messageKey
989
- ? translatorRef.current(messageKey)
990
- : translatorRef.current("agentChat.composer.uploadFailed");
991
- const error = new Error(message);
992
- Object.assign(error, {
993
- code:
994
- response.status === 413
995
- ? "upload_too_large"
996
- : response.status === 401
997
- ? "upload_session_expired"
998
- : response.status === 415
999
- ? "upload_unsupported_type"
1000
- : `upload_http_${response.status}`,
1001
- status: response.status,
1002
- retryable:
1003
- response.status === 408 ||
1004
- response.status === 429 ||
1005
- response.status >= 500,
988
+ let response: Response;
989
+ try {
990
+ response = await fetch(target.url, {
991
+ method: target.method,
992
+ headers: target.headers,
993
+ body: form,
994
+ signal: context?.signal,
1006
995
  });
1007
- throw error;
996
+ } catch (error) {
997
+ if (context?.signal?.aborted) throw error;
998
+ throw Object.assign(
999
+ new Error(translatorRef.current("agentChat.composer.uploadOffline")),
1000
+ { code: "upload_network_error", retryable: true, cause: error },
1001
+ );
1002
+ }
1003
+ // The server's error body is never shown: its text is untranslated.
1004
+ if (!response.ok) {
1005
+ throw uploadHttpError(response.status, translatorRef.current);
1008
1006
  }
1009
1007
  const result: unknown = await response.json();
1010
1008
  const uploaded = asRecord(result);
1011
1009
  if (typeof uploaded?.url !== "string" || !uploaded.url) {
1012
- throw new TypeError("File upload response did not include a URL.");
1010
+ throw Object.assign(
1011
+ new Error(translatorRef.current("agentChat.composer.uploadFailed")),
1012
+ { code: "upload_invalid_response", retryable: false },
1013
+ );
1013
1014
  }
1014
1015
  uploadedFilesRef.current.set(target.uploadId, {
1015
1016
  type: "file",
@@ -2144,6 +2145,7 @@ const AgentKitAssistantChatBody = forwardRef<
2144
2145
  : await uploadAgentChatAttachments(control, attachments, files, {
2145
2146
  storageConfigured: fileStorageConfigured,
2146
2147
  storageUnavailableMessage: t("onboarding.fileStorage.title"),
2148
+ fileTooLargeMessage: uploadTooLargeMessage(t),
2147
2149
  });
2148
2150
  const fileParts = uploadedAttachments.fileParts;
2149
2151
  let requestAttachments =
@@ -2192,7 +2194,9 @@ const AgentKitAssistantChatBody = forwardRef<
2192
2194
  if (inlineImageDataChars > MAX_AGENT_REQUEST_ATTACHMENT_DATA_CHARS) {
2193
2195
  throw new Error(t("agentChat.composer.requestTooLarge"));
2194
2196
  }
2195
- composerOptions.validateSubmission?.();
2197
+ // Readiness was gated before the upload; a provider status refresh during
2198
+ // it must not discard the upload, only a change of thread or scope.
2199
+ composerOptions.validateSubmissionScope?.();
2196
2200
  const selectionChangedDuringSubmission =
2197
2201
  selectionRevision !== selectionRevisionRef.current ||
2198
2202
  (options.pendingSelectionCapturedAt !== undefined &&
@@ -2393,8 +2397,16 @@ const AgentKitAssistantChatBody = forwardRef<
2393
2397
  : await uploadAgentChatAttachments(control, attachments, files, {
2394
2398
  storageConfigured: fileStorageConfigured,
2395
2399
  storageUnavailableMessage: t("onboarding.fileStorage.title"),
2400
+ fileTooLargeMessage: uploadTooLargeMessage(t),
2396
2401
  });
2397
- const fileParts = uploadedAttachments.fileParts;
2402
+ const fileParts = await durableFileParts(
2403
+ control,
2404
+ uploadedAttachments.fileParts,
2405
+ {
2406
+ storageConfigured: fileStorageConfigured,
2407
+ storageUnavailableMessage: t("onboarding.fileStorage.title"),
2408
+ },
2409
+ );
2398
2410
  if (
2399
2411
  !fileStorageConfigured &&
2400
2412
  uploadedAttachments.requestAttachments.some(
@@ -2448,6 +2460,7 @@ const AgentKitAssistantChatBody = forwardRef<
2448
2460
  delete deferredComposerOptions.attachments;
2449
2461
  delete deferredComposerOptions.onLocalSubmit;
2450
2462
  delete deferredComposerOptions.validateSubmission;
2463
+ delete deferredComposerOptions.validateSubmissionScope;
2451
2464
  deferredComposerOptions.model ??= props.selectedModel;
2452
2465
  deferredComposerOptions.engine ??= props.selectedEngine;
2453
2466
  deferredComposerOptions.effort ??= props.selectedEffort;
@@ -3589,42 +3602,53 @@ function AgentKitTranscript({ children, threadId }: AgentKitRegionRenderProps) {
3589
3602
  {children}
3590
3603
  {activeRunId ? (
3591
3604
  <div
3592
- className="agentkit-activities-static"
3605
+ className="agentkit-activities-static agentkit-active-run-actions"
3593
3606
  data-agentkit-active-run-id-copy="true"
3594
3607
  >
3595
- <IconButton
3596
- label={
3597
- activeRunRequestIdCopyStatus === "copied"
3598
- ? t("agentChat.common.copied")
3599
- : activeRunRequestIdCopyStatus === "failed"
3600
- ? t("agentChat.recovery.copyFailed")
3601
- : t("agentChat.message.copyRequestId")
3602
- }
3603
- title={t("agentChat.message.copyRequestId")}
3604
- icon={
3605
- activeRunRequestIdCopyStatus === "copied" ? (
3606
- <IconCircleCheck aria-hidden="true" />
3607
- ) : activeRunRequestIdCopyStatus === "failed" ? (
3608
- <IconAlertTriangle aria-hidden="true" />
3609
- ) : (
3610
- <IconCopy aria-hidden="true" />
3611
- )
3612
- }
3613
- size="compact"
3614
- onPress={() => void copyActiveRunRequestId()}
3615
- />
3616
3608
  {activeRunRequestIdCopyStatus ? (
3617
3609
  <span
3618
- className="sr-only"
3610
+ className={cn(
3611
+ "agentkit-active-run-actions-feedback",
3612
+ activeRunRequestIdCopyStatus === "failed" &&
3613
+ "agentkit-active-run-actions-feedback-error",
3614
+ )}
3619
3615
  role={
3620
3616
  activeRunRequestIdCopyStatus === "failed" ? "alert" : "status"
3621
3617
  }
3622
3618
  >
3619
+ {activeRunRequestIdCopyStatus === "copied" ? (
3620
+ <IconCircleCheck size={14} aria-hidden="true" />
3621
+ ) : (
3622
+ <IconAlertTriangle size={14} aria-hidden="true" />
3623
+ )}
3623
3624
  {activeRunRequestIdCopyStatus === "copied"
3624
3625
  ? t("agentChat.common.copied")
3625
3626
  : t("agentChat.recovery.copyFailed")}
3626
3627
  </span>
3627
3628
  ) : null}
3629
+ <DropdownMenu>
3630
+ <DropdownMenuTrigger asChild>
3631
+ <IconButton
3632
+ label={t("agentChat.message.actions")}
3633
+ title={t("agentChat.message.actions")}
3634
+ icon={<IconDotsVertical aria-hidden="true" />}
3635
+ size="compact"
3636
+ className="agentkit-active-run-actions-trigger"
3637
+ />
3638
+ </DropdownMenuTrigger>
3639
+ <DropdownMenuContent align="end" sideOffset={6}>
3640
+ <DropdownMenuItem onSelect={() => void copyActiveRunRequestId()}>
3641
+ {activeRunRequestIdCopyStatus === "copied" ? (
3642
+ <IconCircleCheck size={14} aria-hidden="true" />
3643
+ ) : activeRunRequestIdCopyStatus === "failed" ? (
3644
+ <IconAlertTriangle size={14} aria-hidden="true" />
3645
+ ) : (
3646
+ <IconCopy size={14} aria-hidden="true" />
3647
+ )}
3648
+ {t("agentChat.message.copyRequestId")}
3649
+ </DropdownMenuItem>
3650
+ </DropdownMenuContent>
3651
+ </DropdownMenu>
3628
3652
  </div>
3629
3653
  ) : null}
3630
3654
  {handoffMessages.map((message) => (
@@ -3916,24 +3940,36 @@ function AgentKitComposerSurface({
3916
3940
  : {}),
3917
3941
  };
3918
3942
  setComposerError(null);
3919
- const assertCurrentSubmission = () => {
3943
+ const assertSubmissionScope = () => {
3944
+ const latest = controller.getThread(threadId);
3920
3945
  if (
3921
- !submissionAllowed.current ||
3922
3946
  !mounted.current ||
3923
- currentSubmissionScope.current !== submissionScope
3947
+ currentSubmissionScope.current !== submissionScope ||
3948
+ (options.suggestion &&
3949
+ (latest.messages.length || Object.keys(latest.runs).length) &&
3950
+ !isCurrentAgentSuggestion(latest, options.suggestion))
3924
3951
  ) {
3925
- throw new Error(t("agentChat.error.failed"));
3952
+ throw Object.assign(
3953
+ new Error(t("agentChat.composer.submissionScopeChanged")),
3954
+ { code: "submission_scope_changed" },
3955
+ );
3926
3956
  }
3927
- const latest = controller.getThread(threadId);
3928
- if (
3929
- options.suggestion &&
3930
- (latest.messages.length || Object.keys(latest.runs).length) &&
3931
- !isCurrentAgentSuggestion(latest, options.suggestion)
3932
- )
3933
- throw new Error(t("agentChat.error.failed"));
3957
+ };
3958
+ const assertCurrentSubmission = () => {
3959
+ if (!submissionAllowed.current) {
3960
+ throw Object.assign(
3961
+ new Error(t("agentChat.composer.submissionNotReady")),
3962
+ { code: "submission_not_ready" },
3963
+ );
3964
+ }
3965
+ assertSubmissionScope();
3934
3966
  };
3935
3967
  assertCurrentSubmission();
3936
- options = { ...options, validateSubmission: assertCurrentSubmission };
3968
+ options = {
3969
+ ...options,
3970
+ validateSubmission: assertCurrentSubmission,
3971
+ validateSubmissionScope: assertSubmissionScope,
3972
+ };
3937
3973
  const captured = snapshotComposerContextItems(
3938
3974
  options.contextItems ?? visibleContextItems,
3939
3975
  );
@@ -4728,6 +4764,21 @@ function AgentKitRunFailure({
4728
4764
  lastUserMessage?.id,
4729
4765
  );
4730
4766
  const retryFailedTurn = () => sendRetryRequest(surface, retryRequest, runId);
4767
+ const retryWithoutAttachments =
4768
+ retryRequest.fileParts.length || retryRequest.requestAttachments.length
4769
+ ? () =>
4770
+ void sendRetryRequest(
4771
+ surface,
4772
+ {
4773
+ ...retryRequest,
4774
+ text: retryRequest.textWithContext,
4775
+ fileParts: [],
4776
+ requestAttachments: [],
4777
+ hasUnavailableAttachment: false,
4778
+ },
4779
+ runId,
4780
+ )
4781
+ : undefined;
4731
4782
  const resumeAfterSetup = useResumeAfterAiSetup(
4732
4783
  `${threadId}:${(failedPrompt ?? lastUserMessage)?.id ?? runId}`,
4733
4784
  setupFailure &&
@@ -4802,6 +4853,7 @@ function AgentKitRunFailure({
4802
4853
  continueFailed ? t("agentChat.recovery.continueUnavailable") : null
4803
4854
  }
4804
4855
  onRetry={() => void retryFailedTurn()}
4856
+ onRetryWithoutAttachments={retryWithoutAttachments}
4805
4857
  retryHasUnavailableAttachment={retryRequest.hasUnavailableAttachment}
4806
4858
  onFork={async () => {
4807
4859
  if (!lastUserMessage) return surface.props.onForkChat?.();
@@ -4870,6 +4922,7 @@ function retryRequestFrom(message: AgentMessage | undefined) {
4870
4922
  mode === "plan" || mode === "act" ? mode : undefined;
4871
4923
  return {
4872
4924
  text: message ? agentMessageText(message) : "",
4925
+ textWithContext: message ? rawAgentMessageText(message) : "",
4873
4926
  fileParts,
4874
4927
  requestAttachments: retryRequestAttachmentsFrom(
4875
4928
  custom?.agentNativeRetryRequestAttachments,
@@ -5411,6 +5464,52 @@ interface UploadedAgentChatAttachments {
5411
5464
  requestAttachments: AgentRequestAttachment[];
5412
5465
  }
5413
5466
 
5467
+ /** The `/_agent-native/file-upload` ceiling (core's `DEFAULT_UPLOAD_MAX_FILE_BYTES`). */
5468
+ const AGENT_CHAT_UPLOAD_MAX_FILE_BYTES = 25 * 1024 * 1024;
5469
+
5470
+ function uploadTooLargeMessage(t: ReturnType<typeof useT>): string {
5471
+ return t("agentChat.composer.fileTooLarge", {
5472
+ size: AGENT_CHAT_UPLOAD_MAX_FILE_BYTES / 1024 / 1024,
5473
+ });
5474
+ }
5475
+
5476
+ function uploadHttpError(status: number, t: ReturnType<typeof useT>): Error {
5477
+ const [code, message, retryable]: [string, string, boolean] =
5478
+ status === 413
5479
+ ? ["upload_too_large", uploadTooLargeMessage(t), false]
5480
+ : status === 401 || status === 403
5481
+ ? [
5482
+ "upload_session_expired",
5483
+ t("agentChat.composer.sessionExpired"),
5484
+ false,
5485
+ ]
5486
+ : status === 415
5487
+ ? [
5488
+ "upload_unsupported_type",
5489
+ t("agentChat.composer.unsupportedFileType"),
5490
+ false,
5491
+ ]
5492
+ : status === 408 || status === 429 || status >= 500
5493
+ ? [
5494
+ "upload_unavailable",
5495
+ t("agentChat.composer.uploadUnavailable"),
5496
+ true,
5497
+ ]
5498
+ : [
5499
+ `upload_http_${status}`,
5500
+ t("agentChat.composer.uploadFailed"),
5501
+ false,
5502
+ ];
5503
+ return Object.assign(new Error(message), { code, status, retryable });
5504
+ }
5505
+
5506
+ /**
5507
+ * Durable uploads keyed by the attachment or file they came from, so a send
5508
+ * that failed on one file reuses its siblings' uploads instead of orphaning
5509
+ * them and uploading again.
5510
+ */
5511
+ const uploadedAgentChatSources = new WeakMap<object, FilePart>();
5512
+
5414
5513
  const OPTIMIZABLE_RASTER_IMAGE_TYPES = new Set([
5415
5514
  "image/gif",
5416
5515
  "image/jpeg",
@@ -5472,9 +5571,11 @@ async function uploadAgentChatAttachments(
5472
5571
  options: {
5473
5572
  storageConfigured: boolean;
5474
5573
  storageUnavailableMessage: string;
5574
+ fileTooLargeMessage: string;
5475
5575
  },
5476
5576
  ): Promise<UploadedAgentChatAttachments> {
5477
5577
  const entries: Array<FilePart | File> = [];
5578
+ const sourceByFile = new Map<File, object>();
5478
5579
  for (const attachment of attachments) {
5479
5580
  if (attachment.displayOnly) continue;
5480
5581
  if (attachment.url) {
@@ -5488,13 +5589,17 @@ async function uploadAgentChatAttachments(
5488
5589
  });
5489
5590
  } else {
5490
5591
  const file = await attachmentToFile(attachment);
5491
- if (file) entries.push(file);
5592
+ if (file) {
5593
+ entries.push(file);
5594
+ sourceByFile.set(file, attachment);
5595
+ }
5492
5596
  }
5493
5597
  }
5494
5598
  entries.push(...files);
5495
5599
  const pending = entries.filter(
5496
5600
  (entry): entry is File => entry instanceof File,
5497
5601
  );
5602
+ const sourceOf = (file: File): object => sourceByFile.get(file) ?? file;
5498
5603
  const optimizedImages = new Map<
5499
5604
  File,
5500
5605
  { data: string; contentType: string }
@@ -5523,25 +5628,62 @@ async function uploadAgentChatAttachments(
5523
5628
  if (!options.storageConfigured && filesToUploadNormally.length > 0) {
5524
5629
  throw new Error(options.storageUnavailableMessage);
5525
5630
  }
5526
- if (filesToUploadNormally.length) {
5527
- const uploaded = await control.uploadFiles(
5528
- filesToUploadNormally.map((file) => ({
5529
- name: file.name,
5530
- mediaType: file.type || "application/octet-stream",
5531
- size: file.size,
5532
- body: file,
5533
- })),
5534
- );
5535
- filesToUploadNormally.forEach((file, index) => {
5631
+ const oversized = filesToUploadNormally.flatMap((file, index) =>
5632
+ file.size > AGENT_CHAT_UPLOAD_MAX_FILE_BYTES
5633
+ ? [
5634
+ {
5635
+ index,
5636
+ name: file.name,
5637
+ error: Object.assign(new Error(options.fileTooLargeMessage), {
5638
+ code: "upload_too_large",
5639
+ retryable: false,
5640
+ }),
5641
+ },
5642
+ ]
5643
+ : [],
5644
+ );
5645
+ if (oversized.length) throw new AgentKitUploadError(oversized, []);
5646
+ const filesToUpload = filesToUploadNormally.filter((file) => {
5647
+ const reused = uploadedAgentChatSources.get(sourceOf(file));
5648
+ if (reused) uploadedByFile.set(file, reused);
5649
+ return !reused;
5650
+ });
5651
+ if (filesToUpload.length) {
5652
+ let uploaded: FilePart[];
5653
+ try {
5654
+ uploaded = await control.uploadFiles(
5655
+ filesToUpload.map((file) => ({
5656
+ name: file.name,
5657
+ mediaType: file.type || "application/octet-stream",
5658
+ size: file.size,
5659
+ body: file,
5660
+ })),
5661
+ );
5662
+ } catch (error) {
5663
+ if (error instanceof AgentKitUploadError) {
5664
+ for (const { index, part } of error.uploaded) {
5665
+ uploadedAgentChatSources.set(sourceOf(filesToUpload[index]!), part);
5666
+ }
5667
+ }
5668
+ throw error;
5669
+ }
5670
+ filesToUpload.forEach((file, index) => {
5536
5671
  const part = uploaded[index];
5537
5672
  if (!part) {
5538
5673
  throw new TypeError("File upload did not return every uploaded file.");
5539
5674
  }
5540
5675
  uploadedByFile.set(file, part);
5676
+ uploadedAgentChatSources.set(sourceOf(file), part);
5541
5677
  });
5542
5678
  }
5543
5679
  for (const file of pending) {
5544
- if (!optimizedImages.has(file) || !options.storageConfigured) continue;
5680
+ if (
5681
+ !optimizedImages.has(file) ||
5682
+ !options.storageConfigured ||
5683
+ file.size > AGENT_CHAT_UPLOAD_MAX_FILE_BYTES
5684
+ ) {
5685
+ continue;
5686
+ }
5545
5687
  try {
5546
5688
  const [part] = await control.uploadFiles([
5547
5689
  {
@@ -5618,6 +5760,42 @@ async function uploadRequestAttachments(
5618
5760
  });
5619
5761
  }
5620
5762
 
5763
+ /** Stored submissions carry durable references, so inline file bytes upload first. */
5764
+ async function durableFileParts(
5765
+ control: ReturnType<typeof useAgentKitControl>,
5766
+ parts: readonly FilePart[],
5767
+ options: { storageConfigured: boolean; storageUnavailableMessage: string },
5768
+ ): Promise<FilePart[]> {
5769
+ const inline = parts.filter((part) => isInlineDataUrl(part.url));
5770
+ if (inline.length === 0) return [...parts];
5771
+ if (!options.storageConfigured) {
5772
+ throw new Error(options.storageUnavailableMessage);
5773
+ }
5774
+ const uploaded = await control.uploadFiles(
5775
+ await Promise.all(
5776
+ inline.map(async (part) => {
5777
+ const body = await (await fetch(part.url!)).blob();
5778
+ return {
5779
+ name: part.name,
5780
+ mediaType:
5781
+ part.mediaType ?? (body.type || "application/octet-stream"),
5782
+ size: body.size,
5783
+ body,
5784
+ };
5785
+ }),
5786
+ ),
5787
+ );
5788
+ let uploadIndex = 0;
5789
+ return parts.map((part) => {
5790
+ if (!isInlineDataUrl(part.url)) return part;
5791
+ const durable = uploaded[uploadIndex++];
5792
+ if (!durable?.url && !durable?.fileId) {
5793
+ throw new TypeError(`${part.name} did not receive a durable reference.`);
5794
+ }
5795
+ return durable;
5796
+ });
5797
+ }
5798
+
5621
5799
  function rawAgentMessageTextFromParts(parts: AgentMessage["parts"]): string {
5622
5800
  return parts
5623
5801
  .filter((part) => part.type === "text")
@@ -5639,24 +5817,57 @@ function agentMessageText(message: AgentMessage): string {
5639
5817
  return agentMessageTextFromParts(message.parts);
5640
5818
  }
5641
5819
 
5820
+ /**
5821
+ * The live message keeps inline image bytes for display; what is stored keeps
5822
+ * only a durable reference, or a named `omitted` marker when there is none.
5823
+ */
5642
5824
  function persistedAgentMessage(message: AgentMessage): AgentMessage {
5643
5825
  return {
5644
5826
  ...message,
5645
5827
  parts: message.parts.map((part) => {
5646
5828
  const record = asRecord(part);
5647
5829
  if (
5648
- (record?.type === "file" || record?.type === "image") &&
5649
- typeof record.data === "string" &&
5650
- record.data.startsWith("data:")
5830
+ !record ||
5831
+ (!isInlineDataUrl(record.data) && !isInlineDataUrl(record.url))
5651
5832
  ) {
5652
- const { data: _data, ...withoutInlineBody } = record;
5653
- return withoutInlineBody as unknown as AgentMessage["parts"][number];
5833
+ return part;
5654
5834
  }
5655
- return part;
5835
+ const { data: _data, url, ...rest } = record;
5836
+ const durableUrl = isInlineDataUrl(url) ? undefined : url;
5837
+ return {
5838
+ ...rest,
5839
+ ...(durableUrl ? { url: durableUrl } : {}),
5840
+ ...(durableUrl || rest.fileId ? {} : { omitted: "inline-bytes" }),
5841
+ } as unknown as AgentMessage["parts"][number];
5656
5842
  }),
5657
5843
  };
5658
5844
  }
5659
5845
 
5846
+ function persistedAgentEvent(event: AgentEvent): AgentEvent {
5847
+ return "message" in event && event.message
5848
+ ? { ...event, message: persistedAgentMessage(event.message) }
5849
+ : event;
5850
+ }
5851
+
5852
+ function persistedQueuedMessage(
5853
+ message: AgentQueuedMessage,
5854
+ ): AgentQueuedMessage {
5855
+ return {
5856
+ ...message,
5857
+ ...(message.attachments
5858
+ ? { attachments: message.attachments.map(persistableFilePart) }
5859
+ : {}),
5860
+ ...(message.requestAttachments
5861
+ ? {
5862
+ requestAttachments: message.requestAttachments.flatMap(
5863
+ ({ data: _data, ...reference }) =>
5864
+ reference.url ? [reference] : [],
5865
+ ),
5866
+ }
5867
+ : {}),
5868
+ };
5869
+ }
5870
+
5660
5871
  function createAgentKitThreadSnapshot(thread: AgentThreadState) {
5661
5872
  const messages = thread.messages.map(persistedAgentMessage);
5662
5873
  const repositoryMessages = messages.map((message, index) => ({
@@ -5690,7 +5901,7 @@ function createAgentKitThreadSnapshot(thread: AgentThreadState) {
5690
5901
  }));
5691
5902
  const agentKit = {
5692
5903
  messages,
5693
- events: thread.events,
5904
+ events: thread.events.map(persistedAgentEvent),
5694
5905
  runs,
5695
5906
  activeRunIds: thread.activeRunIds,
5696
5907
  toolCalls: Object.values(thread.tools),
@@ -5700,7 +5911,7 @@ function createAgentKitThreadSnapshot(thread: AgentThreadState) {
5700
5911
  threadData: JSON.stringify({
5701
5912
  headId: messages.at(-1)?.id ?? null,
5702
5913
  messages: repositoryMessages,
5703
- queuedMessages: thread.queuedMessages,
5914
+ queuedMessages: thread.queuedMessages.map(persistedQueuedMessage),
5704
5915
  agentKit,
5705
5916
  }),
5706
5917
  title,
@@ -5772,8 +5983,8 @@ function storeAgentKitThreadHandoffSnapshot(
5772
5983
  createdAt: thread.thread?.createdAt ?? messages[0]?.createdAt ?? now,
5773
5984
  updatedAt: thread.thread?.updatedAt ?? messages.at(-1)?.createdAt ?? now,
5774
5985
  messages,
5775
- queuedMessages: thread.queuedMessages,
5776
- events: thread.events,
5986
+ queuedMessages: thread.queuedMessages.map(persistedQueuedMessage),
5987
+ events: thread.events.map(persistedAgentEvent),
5777
5988
  runs: Object.entries(thread.runs).map(([id, run]) => ({
5778
5989
  ...run,
5779
5990
  id,