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

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 (72) 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/chat/AgentKitAssistantChat.d.ts.map +1 -1
  9. package/dist/app/chat/AgentKitAssistantChat.js +213 -67
  10. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  11. package/dist/app/chat/agentkit-chat/guided-questions.d.ts +18 -0
  12. package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
  13. package/dist/app/chat/agentkit-chat/guided-questions.js +12 -4
  14. package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
  15. package/dist/app/chat/chat/run-recovery.d.ts +6 -1
  16. package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
  17. package/dist/app/chat/chat/run-recovery.js +6 -3
  18. package/dist/app/chat/chat/run-recovery.js.map +1 -1
  19. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  20. package/dist/app/i18n/catalogs/ar-SA.js +8 -2
  21. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  22. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  23. package/dist/app/i18n/catalogs/de-DE.js +8 -2
  24. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  25. package/dist/app/i18n/catalogs/en-US.d.ts +8 -2
  26. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  27. package/dist/app/i18n/catalogs/en-US.js +8 -2
  28. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  29. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  30. package/dist/app/i18n/catalogs/es-ES.js +8 -2
  31. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  32. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  33. package/dist/app/i18n/catalogs/fr-FR.js +8 -2
  34. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  35. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  36. package/dist/app/i18n/catalogs/hi-IN.js +8 -2
  37. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  38. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  39. package/dist/app/i18n/catalogs/ja-JP.js +8 -2
  40. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  41. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  42. package/dist/app/i18n/catalogs/ko-KR.js +8 -2
  43. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  44. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  45. package/dist/app/i18n/catalogs/pt-BR.js +8 -2
  46. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  47. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  48. package/dist/app/i18n/catalogs/zh-CN.js +8 -2
  49. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  50. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  51. package/dist/app/i18n/catalogs/zh-TW.js +8 -2
  52. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  53. package/package.json +4 -4
  54. package/src/app/agentkit/react/components.tsx +6 -3
  55. package/src/app/agentkit/react/context.tsx +2 -0
  56. package/src/app/chat/AgentKitAssistantChat.spec.tsx +367 -7
  57. package/src/app/chat/AgentKitAssistantChat.tsx +265 -72
  58. package/src/app/chat/agentkit-chat/guided-questions.flow.spec.tsx +89 -5
  59. package/src/app/chat/agentkit-chat/guided-questions.tsx +17 -6
  60. package/src/app/chat/chat/run-recovery.spec.tsx +48 -0
  61. package/src/app/chat/chat/run-recovery.tsx +23 -1
  62. package/src/app/i18n/catalogs/ar-SA.ts +14 -2
  63. package/src/app/i18n/catalogs/de-DE.ts +12 -2
  64. package/src/app/i18n/catalogs/en-US.ts +13 -2
  65. package/src/app/i18n/catalogs/es-ES.ts +12 -2
  66. package/src/app/i18n/catalogs/fr-FR.ts +12 -2
  67. package/src/app/i18n/catalogs/hi-IN.ts +12 -2
  68. package/src/app/i18n/catalogs/ja-JP.ts +12 -2
  69. package/src/app/i18n/catalogs/ko-KR.ts +12 -2
  70. package/src/app/i18n/catalogs/pt-BR.ts +12 -2
  71. package/src/app/i18n/catalogs/zh-CN.ts +8 -2
  72. package/src/app/i18n/catalogs/zh-TW.ts +8 -2
@@ -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,
@@ -563,6 +569,7 @@ type AgentKitComposerSubmit = (
563
569
  type AgentKitSuggestionSubmitOptions = PromptComposerSubmitOptions & {
564
570
  suggestion?: Exclude<AgentSuggestionInput, string>;
565
571
  validateSubmission?: () => void;
572
+ validateSubmissionScope?: () => void;
566
573
  queuedWhileRunActive?: boolean;
567
574
  };
568
575
 
@@ -929,6 +936,7 @@ export const AgentKitAssistantChat = forwardRef<
929
936
  name: "{{name}}",
930
937
  }),
931
938
  pastedText: t("agentChat.pastedText.title"),
939
+ attachmentNotSaved: t("agentChat.composer.attachmentNotSaved"),
932
940
  imagePreview: t("agentChat.composer.imagePreview"),
933
941
  closePreview: t("agentChat.composer.closePreview"),
934
942
  dropFilesToAttach: t("agentChat.composer.dropToAttach"),
@@ -970,46 +978,32 @@ export const AgentKitAssistantChat = forwardRef<
970
978
  form.set(key, value);
971
979
  }
972
980
  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,
981
+ let response: Response;
982
+ try {
983
+ response = await fetch(target.url, {
984
+ method: target.method,
985
+ headers: target.headers,
986
+ body: form,
987
+ signal: context?.signal,
1006
988
  });
1007
- throw error;
989
+ } catch (error) {
990
+ if (context?.signal?.aborted) throw error;
991
+ throw Object.assign(
992
+ new Error(translatorRef.current("agentChat.composer.uploadOffline")),
993
+ { code: "upload_network_error", retryable: true, cause: error },
994
+ );
995
+ }
996
+ // The server's error body is never shown: its text is untranslated.
997
+ if (!response.ok) {
998
+ throw uploadHttpError(response.status, translatorRef.current);
1008
999
  }
1009
1000
  const result: unknown = await response.json();
1010
1001
  const uploaded = asRecord(result);
1011
1002
  if (typeof uploaded?.url !== "string" || !uploaded.url) {
1012
- throw new TypeError("File upload response did not include a URL.");
1003
+ throw Object.assign(
1004
+ new Error(translatorRef.current("agentChat.composer.uploadFailed")),
1005
+ { code: "upload_invalid_response", retryable: false },
1006
+ );
1013
1007
  }
1014
1008
  uploadedFilesRef.current.set(target.uploadId, {
1015
1009
  type: "file",
@@ -2144,6 +2138,7 @@ const AgentKitAssistantChatBody = forwardRef<
2144
2138
  : await uploadAgentChatAttachments(control, attachments, files, {
2145
2139
  storageConfigured: fileStorageConfigured,
2146
2140
  storageUnavailableMessage: t("onboarding.fileStorage.title"),
2141
+ fileTooLargeMessage: uploadTooLargeMessage(t),
2147
2142
  });
2148
2143
  const fileParts = uploadedAttachments.fileParts;
2149
2144
  let requestAttachments =
@@ -2192,7 +2187,9 @@ const AgentKitAssistantChatBody = forwardRef<
2192
2187
  if (inlineImageDataChars > MAX_AGENT_REQUEST_ATTACHMENT_DATA_CHARS) {
2193
2188
  throw new Error(t("agentChat.composer.requestTooLarge"));
2194
2189
  }
2195
- composerOptions.validateSubmission?.();
2190
+ // Readiness was gated before the upload; a provider status refresh during
2191
+ // it must not discard the upload, only a change of thread or scope.
2192
+ composerOptions.validateSubmissionScope?.();
2196
2193
  const selectionChangedDuringSubmission =
2197
2194
  selectionRevision !== selectionRevisionRef.current ||
2198
2195
  (options.pendingSelectionCapturedAt !== undefined &&
@@ -2393,8 +2390,16 @@ const AgentKitAssistantChatBody = forwardRef<
2393
2390
  : await uploadAgentChatAttachments(control, attachments, files, {
2394
2391
  storageConfigured: fileStorageConfigured,
2395
2392
  storageUnavailableMessage: t("onboarding.fileStorage.title"),
2393
+ fileTooLargeMessage: uploadTooLargeMessage(t),
2396
2394
  });
2397
- const fileParts = uploadedAttachments.fileParts;
2395
+ const fileParts = await durableFileParts(
2396
+ control,
2397
+ uploadedAttachments.fileParts,
2398
+ {
2399
+ storageConfigured: fileStorageConfigured,
2400
+ storageUnavailableMessage: t("onboarding.fileStorage.title"),
2401
+ },
2402
+ );
2398
2403
  if (
2399
2404
  !fileStorageConfigured &&
2400
2405
  uploadedAttachments.requestAttachments.some(
@@ -2448,6 +2453,7 @@ const AgentKitAssistantChatBody = forwardRef<
2448
2453
  delete deferredComposerOptions.attachments;
2449
2454
  delete deferredComposerOptions.onLocalSubmit;
2450
2455
  delete deferredComposerOptions.validateSubmission;
2456
+ delete deferredComposerOptions.validateSubmissionScope;
2451
2457
  deferredComposerOptions.model ??= props.selectedModel;
2452
2458
  deferredComposerOptions.engine ??= props.selectedEngine;
2453
2459
  deferredComposerOptions.effort ??= props.selectedEffort;
@@ -3916,24 +3922,36 @@ function AgentKitComposerSurface({
3916
3922
  : {}),
3917
3923
  };
3918
3924
  setComposerError(null);
3919
- const assertCurrentSubmission = () => {
3925
+ const assertSubmissionScope = () => {
3926
+ const latest = controller.getThread(threadId);
3920
3927
  if (
3921
- !submissionAllowed.current ||
3922
3928
  !mounted.current ||
3923
- currentSubmissionScope.current !== submissionScope
3929
+ currentSubmissionScope.current !== submissionScope ||
3930
+ (options.suggestion &&
3931
+ (latest.messages.length || Object.keys(latest.runs).length) &&
3932
+ !isCurrentAgentSuggestion(latest, options.suggestion))
3924
3933
  ) {
3925
- throw new Error(t("agentChat.error.failed"));
3934
+ throw Object.assign(
3935
+ new Error(t("agentChat.composer.submissionScopeChanged")),
3936
+ { code: "submission_scope_changed" },
3937
+ );
3926
3938
  }
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"));
3939
+ };
3940
+ const assertCurrentSubmission = () => {
3941
+ if (!submissionAllowed.current) {
3942
+ throw Object.assign(
3943
+ new Error(t("agentChat.composer.submissionNotReady")),
3944
+ { code: "submission_not_ready" },
3945
+ );
3946
+ }
3947
+ assertSubmissionScope();
3934
3948
  };
3935
3949
  assertCurrentSubmission();
3936
- options = { ...options, validateSubmission: assertCurrentSubmission };
3950
+ options = {
3951
+ ...options,
3952
+ validateSubmission: assertCurrentSubmission,
3953
+ validateSubmissionScope: assertSubmissionScope,
3954
+ };
3937
3955
  const captured = snapshotComposerContextItems(
3938
3956
  options.contextItems ?? visibleContextItems,
3939
3957
  );
@@ -4728,6 +4746,21 @@ function AgentKitRunFailure({
4728
4746
  lastUserMessage?.id,
4729
4747
  );
4730
4748
  const retryFailedTurn = () => sendRetryRequest(surface, retryRequest, runId);
4749
+ const retryWithoutAttachments =
4750
+ retryRequest.fileParts.length || retryRequest.requestAttachments.length
4751
+ ? () =>
4752
+ void sendRetryRequest(
4753
+ surface,
4754
+ {
4755
+ ...retryRequest,
4756
+ text: retryRequest.textWithContext,
4757
+ fileParts: [],
4758
+ requestAttachments: [],
4759
+ hasUnavailableAttachment: false,
4760
+ },
4761
+ runId,
4762
+ )
4763
+ : undefined;
4731
4764
  const resumeAfterSetup = useResumeAfterAiSetup(
4732
4765
  `${threadId}:${(failedPrompt ?? lastUserMessage)?.id ?? runId}`,
4733
4766
  setupFailure &&
@@ -4802,6 +4835,7 @@ function AgentKitRunFailure({
4802
4835
  continueFailed ? t("agentChat.recovery.continueUnavailable") : null
4803
4836
  }
4804
4837
  onRetry={() => void retryFailedTurn()}
4838
+ onRetryWithoutAttachments={retryWithoutAttachments}
4805
4839
  retryHasUnavailableAttachment={retryRequest.hasUnavailableAttachment}
4806
4840
  onFork={async () => {
4807
4841
  if (!lastUserMessage) return surface.props.onForkChat?.();
@@ -4870,6 +4904,7 @@ function retryRequestFrom(message: AgentMessage | undefined) {
4870
4904
  mode === "plan" || mode === "act" ? mode : undefined;
4871
4905
  return {
4872
4906
  text: message ? agentMessageText(message) : "",
4907
+ textWithContext: message ? rawAgentMessageText(message) : "",
4873
4908
  fileParts,
4874
4909
  requestAttachments: retryRequestAttachmentsFrom(
4875
4910
  custom?.agentNativeRetryRequestAttachments,
@@ -5411,6 +5446,52 @@ interface UploadedAgentChatAttachments {
5411
5446
  requestAttachments: AgentRequestAttachment[];
5412
5447
  }
5413
5448
 
5449
+ /** The `/_agent-native/file-upload` ceiling (core's `DEFAULT_UPLOAD_MAX_FILE_BYTES`). */
5450
+ const AGENT_CHAT_UPLOAD_MAX_FILE_BYTES = 25 * 1024 * 1024;
5451
+
5452
+ function uploadTooLargeMessage(t: ReturnType<typeof useT>): string {
5453
+ return t("agentChat.composer.fileTooLarge", {
5454
+ size: AGENT_CHAT_UPLOAD_MAX_FILE_BYTES / 1024 / 1024,
5455
+ });
5456
+ }
5457
+
5458
+ function uploadHttpError(status: number, t: ReturnType<typeof useT>): Error {
5459
+ const [code, message, retryable]: [string, string, boolean] =
5460
+ status === 413
5461
+ ? ["upload_too_large", uploadTooLargeMessage(t), false]
5462
+ : status === 401 || status === 403
5463
+ ? [
5464
+ "upload_session_expired",
5465
+ t("agentChat.composer.sessionExpired"),
5466
+ false,
5467
+ ]
5468
+ : status === 415
5469
+ ? [
5470
+ "upload_unsupported_type",
5471
+ t("agentChat.composer.unsupportedFileType"),
5472
+ false,
5473
+ ]
5474
+ : status === 408 || status === 429 || status >= 500
5475
+ ? [
5476
+ "upload_unavailable",
5477
+ t("agentChat.composer.uploadUnavailable"),
5478
+ true,
5479
+ ]
5480
+ : [
5481
+ `upload_http_${status}`,
5482
+ t("agentChat.composer.uploadFailed"),
5483
+ false,
5484
+ ];
5485
+ return Object.assign(new Error(message), { code, status, retryable });
5486
+ }
5487
+
5488
+ /**
5489
+ * Durable uploads keyed by the attachment or file they came from, so a send
5490
+ * that failed on one file reuses its siblings' uploads instead of orphaning
5491
+ * them and uploading again.
5492
+ */
5493
+ const uploadedAgentChatSources = new WeakMap<object, FilePart>();
5494
+
5414
5495
  const OPTIMIZABLE_RASTER_IMAGE_TYPES = new Set([
5415
5496
  "image/gif",
5416
5497
  "image/jpeg",
@@ -5472,9 +5553,11 @@ async function uploadAgentChatAttachments(
5472
5553
  options: {
5473
5554
  storageConfigured: boolean;
5474
5555
  storageUnavailableMessage: string;
5556
+ fileTooLargeMessage: string;
5475
5557
  },
5476
5558
  ): Promise<UploadedAgentChatAttachments> {
5477
5559
  const entries: Array<FilePart | File> = [];
5560
+ const sourceByFile = new Map<File, object>();
5478
5561
  for (const attachment of attachments) {
5479
5562
  if (attachment.displayOnly) continue;
5480
5563
  if (attachment.url) {
@@ -5488,13 +5571,17 @@ async function uploadAgentChatAttachments(
5488
5571
  });
5489
5572
  } else {
5490
5573
  const file = await attachmentToFile(attachment);
5491
- if (file) entries.push(file);
5574
+ if (file) {
5575
+ entries.push(file);
5576
+ sourceByFile.set(file, attachment);
5577
+ }
5492
5578
  }
5493
5579
  }
5494
5580
  entries.push(...files);
5495
5581
  const pending = entries.filter(
5496
5582
  (entry): entry is File => entry instanceof File,
5497
5583
  );
5584
+ const sourceOf = (file: File): object => sourceByFile.get(file) ?? file;
5498
5585
  const optimizedImages = new Map<
5499
5586
  File,
5500
5587
  { data: string; contentType: string }
@@ -5523,25 +5610,62 @@ async function uploadAgentChatAttachments(
5523
5610
  if (!options.storageConfigured && filesToUploadNormally.length > 0) {
5524
5611
  throw new Error(options.storageUnavailableMessage);
5525
5612
  }
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) => {
5613
+ const oversized = filesToUploadNormally.flatMap((file, index) =>
5614
+ file.size > AGENT_CHAT_UPLOAD_MAX_FILE_BYTES
5615
+ ? [
5616
+ {
5617
+ index,
5618
+ name: file.name,
5619
+ error: Object.assign(new Error(options.fileTooLargeMessage), {
5620
+ code: "upload_too_large",
5621
+ retryable: false,
5622
+ }),
5623
+ },
5624
+ ]
5625
+ : [],
5626
+ );
5627
+ if (oversized.length) throw new AgentKitUploadError(oversized, []);
5628
+ const filesToUpload = filesToUploadNormally.filter((file) => {
5629
+ const reused = uploadedAgentChatSources.get(sourceOf(file));
5630
+ if (reused) uploadedByFile.set(file, reused);
5631
+ return !reused;
5632
+ });
5633
+ if (filesToUpload.length) {
5634
+ let uploaded: FilePart[];
5635
+ try {
5636
+ uploaded = await control.uploadFiles(
5637
+ filesToUpload.map((file) => ({
5638
+ name: file.name,
5639
+ mediaType: file.type || "application/octet-stream",
5640
+ size: file.size,
5641
+ body: file,
5642
+ })),
5643
+ );
5644
+ } catch (error) {
5645
+ if (error instanceof AgentKitUploadError) {
5646
+ for (const { index, part } of error.uploaded) {
5647
+ uploadedAgentChatSources.set(sourceOf(filesToUpload[index]!), part);
5648
+ }
5649
+ }
5650
+ throw error;
5651
+ }
5652
+ filesToUpload.forEach((file, index) => {
5536
5653
  const part = uploaded[index];
5537
5654
  if (!part) {
5538
5655
  throw new TypeError("File upload did not return every uploaded file.");
5539
5656
  }
5540
5657
  uploadedByFile.set(file, part);
5658
+ uploadedAgentChatSources.set(sourceOf(file), part);
5541
5659
  });
5542
5660
  }
5543
5661
  for (const file of pending) {
5544
- if (!optimizedImages.has(file) || !options.storageConfigured) continue;
5662
+ if (
5663
+ !optimizedImages.has(file) ||
5664
+ !options.storageConfigured ||
5665
+ file.size > AGENT_CHAT_UPLOAD_MAX_FILE_BYTES
5666
+ ) {
5667
+ continue;
5668
+ }
5545
5669
  try {
5546
5670
  const [part] = await control.uploadFiles([
5547
5671
  {
@@ -5618,6 +5742,42 @@ async function uploadRequestAttachments(
5618
5742
  });
5619
5743
  }
5620
5744
 
5745
+ /** Stored submissions carry durable references, so inline file bytes upload first. */
5746
+ async function durableFileParts(
5747
+ control: ReturnType<typeof useAgentKitControl>,
5748
+ parts: readonly FilePart[],
5749
+ options: { storageConfigured: boolean; storageUnavailableMessage: string },
5750
+ ): Promise<FilePart[]> {
5751
+ const inline = parts.filter((part) => isInlineDataUrl(part.url));
5752
+ if (inline.length === 0) return [...parts];
5753
+ if (!options.storageConfigured) {
5754
+ throw new Error(options.storageUnavailableMessage);
5755
+ }
5756
+ const uploaded = await control.uploadFiles(
5757
+ await Promise.all(
5758
+ inline.map(async (part) => {
5759
+ const body = await (await fetch(part.url!)).blob();
5760
+ return {
5761
+ name: part.name,
5762
+ mediaType:
5763
+ part.mediaType ?? (body.type || "application/octet-stream"),
5764
+ size: body.size,
5765
+ body,
5766
+ };
5767
+ }),
5768
+ ),
5769
+ );
5770
+ let uploadIndex = 0;
5771
+ return parts.map((part) => {
5772
+ if (!isInlineDataUrl(part.url)) return part;
5773
+ const durable = uploaded[uploadIndex++];
5774
+ if (!durable?.url && !durable?.fileId) {
5775
+ throw new TypeError(`${part.name} did not receive a durable reference.`);
5776
+ }
5777
+ return durable;
5778
+ });
5779
+ }
5780
+
5621
5781
  function rawAgentMessageTextFromParts(parts: AgentMessage["parts"]): string {
5622
5782
  return parts
5623
5783
  .filter((part) => part.type === "text")
@@ -5639,24 +5799,57 @@ function agentMessageText(message: AgentMessage): string {
5639
5799
  return agentMessageTextFromParts(message.parts);
5640
5800
  }
5641
5801
 
5802
+ /**
5803
+ * The live message keeps inline image bytes for display; what is stored keeps
5804
+ * only a durable reference, or a named `omitted` marker when there is none.
5805
+ */
5642
5806
  function persistedAgentMessage(message: AgentMessage): AgentMessage {
5643
5807
  return {
5644
5808
  ...message,
5645
5809
  parts: message.parts.map((part) => {
5646
5810
  const record = asRecord(part);
5647
5811
  if (
5648
- (record?.type === "file" || record?.type === "image") &&
5649
- typeof record.data === "string" &&
5650
- record.data.startsWith("data:")
5812
+ !record ||
5813
+ (!isInlineDataUrl(record.data) && !isInlineDataUrl(record.url))
5651
5814
  ) {
5652
- const { data: _data, ...withoutInlineBody } = record;
5653
- return withoutInlineBody as unknown as AgentMessage["parts"][number];
5815
+ return part;
5654
5816
  }
5655
- return part;
5817
+ const { data: _data, url, ...rest } = record;
5818
+ const durableUrl = isInlineDataUrl(url) ? undefined : url;
5819
+ return {
5820
+ ...rest,
5821
+ ...(durableUrl ? { url: durableUrl } : {}),
5822
+ ...(durableUrl || rest.fileId ? {} : { omitted: "inline-bytes" }),
5823
+ } as unknown as AgentMessage["parts"][number];
5656
5824
  }),
5657
5825
  };
5658
5826
  }
5659
5827
 
5828
+ function persistedAgentEvent(event: AgentEvent): AgentEvent {
5829
+ return "message" in event && event.message
5830
+ ? { ...event, message: persistedAgentMessage(event.message) }
5831
+ : event;
5832
+ }
5833
+
5834
+ function persistedQueuedMessage(
5835
+ message: AgentQueuedMessage,
5836
+ ): AgentQueuedMessage {
5837
+ return {
5838
+ ...message,
5839
+ ...(message.attachments
5840
+ ? { attachments: message.attachments.map(persistableFilePart) }
5841
+ : {}),
5842
+ ...(message.requestAttachments
5843
+ ? {
5844
+ requestAttachments: message.requestAttachments.flatMap(
5845
+ ({ data: _data, ...reference }) =>
5846
+ reference.url ? [reference] : [],
5847
+ ),
5848
+ }
5849
+ : {}),
5850
+ };
5851
+ }
5852
+
5660
5853
  function createAgentKitThreadSnapshot(thread: AgentThreadState) {
5661
5854
  const messages = thread.messages.map(persistedAgentMessage);
5662
5855
  const repositoryMessages = messages.map((message, index) => ({
@@ -5690,7 +5883,7 @@ function createAgentKitThreadSnapshot(thread: AgentThreadState) {
5690
5883
  }));
5691
5884
  const agentKit = {
5692
5885
  messages,
5693
- events: thread.events,
5886
+ events: thread.events.map(persistedAgentEvent),
5694
5887
  runs,
5695
5888
  activeRunIds: thread.activeRunIds,
5696
5889
  toolCalls: Object.values(thread.tools),
@@ -5700,7 +5893,7 @@ function createAgentKitThreadSnapshot(thread: AgentThreadState) {
5700
5893
  threadData: JSON.stringify({
5701
5894
  headId: messages.at(-1)?.id ?? null,
5702
5895
  messages: repositoryMessages,
5703
- queuedMessages: thread.queuedMessages,
5896
+ queuedMessages: thread.queuedMessages.map(persistedQueuedMessage),
5704
5897
  agentKit,
5705
5898
  }),
5706
5899
  title,
@@ -5772,8 +5965,8 @@ function storeAgentKitThreadHandoffSnapshot(
5772
5965
  createdAt: thread.thread?.createdAt ?? messages[0]?.createdAt ?? now,
5773
5966
  updatedAt: thread.thread?.updatedAt ?? messages.at(-1)?.createdAt ?? now,
5774
5967
  messages,
5775
- queuedMessages: thread.queuedMessages,
5776
- events: thread.events,
5968
+ queuedMessages: thread.queuedMessages.map(persistedQueuedMessage),
5969
+ events: thread.events.map(persistedAgentEvent),
5777
5970
  runs: Object.entries(thread.runs).map(([id, run]) => ({
5778
5971
  ...run,
5779
5972
  id,
@@ -314,6 +314,10 @@ describe("useGuidedQuestionFlow scoped reads", () => {
314
314
  fetchMock.mock.calls.filter(([input]) =>
315
315
  isApplicationStateRequest(input),
316
316
  );
317
+ const unrelatedFetches = () =>
318
+ fetchMock.mock.calls
319
+ .filter(([input]) => !isApplicationStateRequest(input))
320
+ .map(([input]) => String(input));
317
321
 
318
322
  const result = await renderFlow({
319
323
  stateKey: "guided-questions-refresh",
@@ -321,6 +325,7 @@ describe("useGuidedQuestionFlow scoped reads", () => {
321
325
  });
322
326
  expect(result.current().questions).toBeNull();
323
327
  const initialReads = applicationStateReads().length;
328
+ const initialUnrelatedFetches = unrelatedFetches();
324
329
 
325
330
  hasQuestion = true;
326
331
  await act(async () => {
@@ -333,6 +338,10 @@ describe("useGuidedQuestionFlow scoped reads", () => {
333
338
 
334
339
  expect(result.current().questions?.length).toBe(1);
335
340
  expect(applicationStateReads()).toHaveLength(initialReads + 1);
341
+ expect(unrelatedFetches()).toEqual([
342
+ ...initialUnrelatedFetches,
343
+ expect.stringContaining("/_agent-native/agent-engine/status"),
344
+ ]);
336
345
  });
337
346
 
338
347
  it("confirms a question written after the caller's own trigger, without a DB-sync wakeup", async () => {
@@ -354,18 +363,93 @@ describe("useGuidedQuestionFlow scoped reads", () => {
354
363
  expect(result.current().questions).toBeNull();
355
364
 
356
365
  hasQuestion = true;
357
- let stillWaiting = false;
366
+ let questionCheck: unknown;
358
367
  await act(async () => {
359
- stillWaiting = await result.current().refetchPendingQuestion();
368
+ questionCheck = await result.current().refetchPendingQuestionStatus();
360
369
  });
361
370
 
362
- expect(stillWaiting).toBe(true);
371
+ expect(questionCheck).toEqual({ status: "pending" });
363
372
  for (let i = 0; i < 20 && !result.current().questions; i += 1) {
364
373
  await flush();
365
374
  }
366
375
  expect(result.current().questions?.length).toBe(1);
367
376
  });
368
377
 
378
+ it("preserves application-state read errors when checking for pending questions", async () => {
379
+ let failReads = false;
380
+ vi.stubGlobal(
381
+ "fetch",
382
+ vi.fn(async (input: RequestInfo | URL) => {
383
+ if (!isApplicationStateRequest(input)) {
384
+ return new Response(JSON.stringify({ chatEligible: true }), {
385
+ status: 200,
386
+ });
387
+ }
388
+ if (failReads) {
389
+ return new Response("state unavailable", { status: 503 });
390
+ }
391
+ return readResponse(String(input), () => "");
392
+ }),
393
+ );
394
+
395
+ const result = await renderFlow({
396
+ stateKey: "guided-questions",
397
+ queryKey: ["guided-questions"],
398
+ refetchInterval: false,
399
+ });
400
+ expect(result.current().questions).toBeNull();
401
+
402
+ failReads = true;
403
+ let questionCheck: unknown;
404
+ await act(async () => {
405
+ questionCheck = await result.current().refetchPendingQuestionStatus();
406
+ });
407
+
408
+ expect(questionCheck).toMatchObject({
409
+ status: "error",
410
+ error: expect.anything(),
411
+ });
412
+ });
413
+
414
+ it("preserves the boolean refetchPendingQuestion contract", async () => {
415
+ let hasQuestion = false;
416
+ let failReads = false;
417
+ vi.stubGlobal(
418
+ "fetch",
419
+ vi.fn(async (input: RequestInfo | URL) => {
420
+ if (failReads && isApplicationStateRequest(input)) {
421
+ return new Response("state unavailable", { status: 503 });
422
+ }
423
+ return readResponse(String(input), () =>
424
+ hasQuestion ? JSON.stringify(payload) : "",
425
+ );
426
+ }),
427
+ );
428
+
429
+ const result = await renderFlow({
430
+ stateKey: "guided-questions",
431
+ queryKey: ["guided-questions"],
432
+ refetchInterval: false,
433
+ });
434
+ let isPending = true;
435
+ await act(async () => {
436
+ isPending = await result.current().refetchPendingQuestion();
437
+ });
438
+ expect(isPending).toBe(false);
439
+
440
+ hasQuestion = true;
441
+ await act(async () => {
442
+ isPending = await result.current().refetchPendingQuestion();
443
+ });
444
+ expect(isPending).toBe(true);
445
+
446
+ failReads = true;
447
+ await act(async () => {
448
+ isPending = await result.current().refetchPendingQuestion();
449
+ });
450
+ expect(isPending).toBe(true);
451
+ });
452
+
369
453
  it("keeps active questions visible while a DB-sync refresh is pending", async () => {
370
454
  let reads = 0;
371
455
  let resolveRefresh: (() => void) | null = null;
@@ -398,11 +482,11 @@ describe("useGuidedQuestionFlow scoped reads", () => {
398
482
  await new Promise((resolve) => setTimeout(resolve, 0));
399
483
  });
400
484
 
401
- for (let i = 0; i < 20 && fetchMock.mock.calls.length < 2; i += 1) {
485
+ for (let i = 0; i < 20 && reads < 2; i += 1) {
402
486
  await flush();
403
487
  }
404
488
 
405
- expect(fetchMock).toHaveBeenCalledTimes(2);
489
+ expect(reads).toBe(2);
406
490
  expect(result.current().questions).toEqual(payload.questions);
407
491
 
408
492
  await act(async () => {