@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.
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +3 -3
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/agentkit/react/context.d.ts +1 -0
- package/dist/app/agentkit/react/context.d.ts.map +1 -1
- package/dist/app/agentkit/react/context.js +1 -0
- package/dist/app/agentkit/react/context.js.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +213 -67
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/agentkit-chat/guided-questions.d.ts +18 -0
- package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/guided-questions.js +12 -4
- package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
- package/dist/app/chat/chat/run-recovery.d.ts +6 -1
- package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
- package/dist/app/chat/chat/run-recovery.js +6 -3
- package/dist/app/chat/chat/run-recovery.js.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +8 -2
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +8 -2
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +8 -2
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +8 -2
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +8 -2
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +8 -2
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +8 -2
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +8 -2
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +8 -2
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +8 -2
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +8 -2
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +8 -2
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/package.json +4 -4
- package/src/app/agentkit/react/components.tsx +6 -3
- package/src/app/agentkit/react/context.tsx +2 -0
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +367 -7
- package/src/app/chat/AgentKitAssistantChat.tsx +265 -72
- package/src/app/chat/agentkit-chat/guided-questions.flow.spec.tsx +89 -5
- package/src/app/chat/agentkit-chat/guided-questions.tsx +17 -6
- package/src/app/chat/chat/run-recovery.spec.tsx +48 -0
- package/src/app/chat/chat/run-recovery.tsx +23 -1
- package/src/app/i18n/catalogs/ar-SA.ts +14 -2
- package/src/app/i18n/catalogs/de-DE.ts +12 -2
- package/src/app/i18n/catalogs/en-US.ts +13 -2
- package/src/app/i18n/catalogs/es-ES.ts +12 -2
- package/src/app/i18n/catalogs/fr-FR.ts +12 -2
- package/src/app/i18n/catalogs/hi-IN.ts +12 -2
- package/src/app/i18n/catalogs/ja-JP.ts +12 -2
- package/src/app/i18n/catalogs/ko-KR.ts +12 -2
- package/src/app/i18n/catalogs/pt-BR.ts +12 -2
- package/src/app/i18n/catalogs/zh-CN.ts +8 -2
- 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 {
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
3934
|
+
throw Object.assign(
|
|
3935
|
+
new Error(t("agentChat.composer.submissionScopeChanged")),
|
|
3936
|
+
{ code: "submission_scope_changed" },
|
|
3937
|
+
);
|
|
3926
3938
|
}
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
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 = {
|
|
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)
|
|
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
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
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 (
|
|
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
|
-
|
|
5649
|
-
|
|
5650
|
-
record.data.startsWith("data:")
|
|
5812
|
+
!record ||
|
|
5813
|
+
(!isInlineDataUrl(record.data) && !isInlineDataUrl(record.url))
|
|
5651
5814
|
) {
|
|
5652
|
-
|
|
5653
|
-
return withoutInlineBody as unknown as AgentMessage["parts"][number];
|
|
5815
|
+
return part;
|
|
5654
5816
|
}
|
|
5655
|
-
|
|
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
|
|
366
|
+
let questionCheck: unknown;
|
|
358
367
|
await act(async () => {
|
|
359
|
-
|
|
368
|
+
questionCheck = await result.current().refetchPendingQuestionStatus();
|
|
360
369
|
});
|
|
361
370
|
|
|
362
|
-
expect(
|
|
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 &&
|
|
485
|
+
for (let i = 0; i < 20 && reads < 2; i += 1) {
|
|
402
486
|
await flush();
|
|
403
487
|
}
|
|
404
488
|
|
|
405
|
-
expect(
|
|
489
|
+
expect(reads).toBe(2);
|
|
406
490
|
expect(result.current().questions).toEqual(payload.questions);
|
|
407
491
|
|
|
408
492
|
await act(async () => {
|