@agent-native/toolkit 0.209.1-nightly-20261015153252 → 0.209.1
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/styles.css +1 -1
- package/dist/app/chat/AgentKitAssistantChat.d.ts +1 -0
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +96 -1
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.js +21 -0
- package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
- package/dist/app/chat/chat/surface-types.d.ts +3 -1
- package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
- package/dist/app/chat/chat/surface-types.js.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +2 -0
- 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 +2 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +2 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +2 -0
- 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 +2 -0
- 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 +2 -0
- 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 +2 -0
- 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 +2 -0
- 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 +2 -0
- 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 +2 -0
- 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 +2 -0
- 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 +2 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +2 -4
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/package.json +4 -4
- package/src/app/agentkit/react/styles.css +1 -1
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +137 -4
- package/src/app/chat/AgentKitAssistantChat.tsx +168 -0
- package/src/app/chat/MultiTabAssistantChat.spec.tsx +53 -0
- package/src/app/chat/MultiTabAssistantChat.tsx +36 -1
- package/src/app/chat/chat/surface-types.ts +6 -1
- package/src/app/i18n/catalogs/ar-SA.ts +4 -0
- package/src/app/i18n/catalogs/de-DE.ts +4 -0
- package/src/app/i18n/catalogs/en-US.ts +4 -0
- package/src/app/i18n/catalogs/es-ES.ts +4 -0
- package/src/app/i18n/catalogs/fr-FR.ts +4 -0
- package/src/app/i18n/catalogs/hi-IN.ts +4 -0
- package/src/app/i18n/catalogs/ja-JP.ts +4 -0
- package/src/app/i18n/catalogs/ko-KR.ts +4 -0
- package/src/app/i18n/catalogs/pt-BR.ts +4 -0
- package/src/app/i18n/catalogs/zh-CN.ts +3 -0
- package/src/app/i18n/catalogs/zh-TW.ts +3 -0
- package/src/composer/TiptapComposer.spec.ts +11 -12
- package/src/composer/TiptapComposer.tsx +2 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/toolkit",
|
|
3
|
-
"version": "0.209.1
|
|
3
|
+
"version": "0.209.1",
|
|
4
4
|
"description": "Reusable app-building UI and helpers for Agent-Native apps.",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -678,7 +678,7 @@
|
|
|
678
678
|
"y-protocols": "^1.0.7",
|
|
679
679
|
"yjs": "^13.6.33",
|
|
680
680
|
"zod": "^4.3.6",
|
|
681
|
-
"@agent-native/agentkit": "^0.209.1
|
|
681
|
+
"@agent-native/agentkit": "^0.209.1"
|
|
682
682
|
},
|
|
683
683
|
"devDependencies": {
|
|
684
684
|
"@excalidraw/excalidraw": "0.18.1",
|
|
@@ -702,10 +702,10 @@
|
|
|
702
702
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
703
703
|
"vite": "8.1.0",
|
|
704
704
|
"vitest": "^4.1.5",
|
|
705
|
-
"@agent-native/core": "^0.209.1
|
|
705
|
+
"@agent-native/core": "^0.209.1"
|
|
706
706
|
},
|
|
707
707
|
"peerDependencies": {
|
|
708
|
-
"@agent-native/core": "0.
|
|
708
|
+
"@agent-native/core": ">=0.205.1",
|
|
709
709
|
"@excalidraw/excalidraw": ">=0.18",
|
|
710
710
|
"@excalidraw/mermaid-to-excalidraw": ">=2",
|
|
711
711
|
"@tanstack/react-query": "^5.99.2",
|
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
AgentKitClient,
|
|
5
5
|
AgentKitUploadError,
|
|
6
6
|
} from "@agent-native/agentkit/client";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY,
|
|
9
|
+
MAX_AGENT_REQUEST_ATTACHMENT_DATA_CHARS,
|
|
10
|
+
} from "@agent-native/agentkit/protocol";
|
|
8
11
|
import type {
|
|
9
12
|
AgentInteraction,
|
|
10
13
|
FilePart,
|
|
@@ -4597,6 +4600,7 @@ describe("AgentKitAssistantChat host behavior", () => {
|
|
|
4597
4600
|
|
|
4598
4601
|
it("uploads inline images before storing a deferred submission", async () => {
|
|
4599
4602
|
chatMocks.history = { isRestoring: true };
|
|
4603
|
+
const submissionExpiresAt = Date.now() + 20_000;
|
|
4600
4604
|
chatMocks.control.uploadFiles.mockResolvedValueOnce([
|
|
4601
4605
|
{
|
|
4602
4606
|
type: "file",
|
|
@@ -4609,18 +4613,112 @@ describe("AgentKitAssistantChat host behavior", () => {
|
|
|
4609
4613
|
await mount(baseProps(), ref);
|
|
4610
4614
|
|
|
4611
4615
|
await act(async () => {
|
|
4612
|
-
await ref.current?.sendMessage(
|
|
4613
|
-
"
|
|
4614
|
-
|
|
4616
|
+
await ref.current?.sendMessage(
|
|
4617
|
+
"Use this image",
|
|
4618
|
+
["data:image/png;base64,aGVsbG8="],
|
|
4619
|
+
{ submissionExpiresAt },
|
|
4620
|
+
);
|
|
4615
4621
|
});
|
|
4616
4622
|
|
|
4617
4623
|
const stored = JSON.stringify([...chatMocks.appState.values()]);
|
|
4618
4624
|
expect(chatMocks.control.uploadFiles).toHaveBeenCalledOnce();
|
|
4619
4625
|
expect(stored).toContain("https://files.example.test/image.png");
|
|
4626
|
+
expect(stored).toContain(String(submissionExpiresAt));
|
|
4620
4627
|
expect(stored).not.toContain("base64,");
|
|
4621
4628
|
expect(stored).not.toContain("data:image");
|
|
4622
4629
|
});
|
|
4623
4630
|
|
|
4631
|
+
it("does not report a deferred retry as submitted if saving passes its deadline", async () => {
|
|
4632
|
+
chatMocks.history = { isRestoring: true };
|
|
4633
|
+
const threadId = chatMocks.threadId;
|
|
4634
|
+
const encodedThreadId = Array.from(threadId, (character) =>
|
|
4635
|
+
character.codePointAt(0)!.toString(16),
|
|
4636
|
+
).join("-");
|
|
4637
|
+
const stateKey = `agentkit-deferred-provider-submissions:${encodedThreadId}`;
|
|
4638
|
+
const now = vi.spyOn(Date, "now").mockReturnValue(1_000);
|
|
4639
|
+
const compareAndSet = vi.mocked(compareAndSetClientAppState);
|
|
4640
|
+
const originalCompareAndSet = compareAndSet.getMockImplementation();
|
|
4641
|
+
if (!originalCompareAndSet) {
|
|
4642
|
+
throw new Error("Expected the app-state compare-and-set mock.");
|
|
4643
|
+
}
|
|
4644
|
+
let advancedDeadline = false;
|
|
4645
|
+
compareAndSet.mockImplementation(async (...args) => {
|
|
4646
|
+
const saved = await originalCompareAndSet(...args);
|
|
4647
|
+
if (args[0] === stateKey && args[2] !== null && !advancedDeadline) {
|
|
4648
|
+
advancedDeadline = true;
|
|
4649
|
+
now.mockReturnValue(1_001);
|
|
4650
|
+
}
|
|
4651
|
+
return saved;
|
|
4652
|
+
});
|
|
4653
|
+
const results: CustomEvent[] = [];
|
|
4654
|
+
const listener = (event: Event) => results.push(event as CustomEvent);
|
|
4655
|
+
window.addEventListener(AGENT_CHAT_SUBMIT_RESULT_EVENT, listener);
|
|
4656
|
+
const ref = createRef<AssistantChatHandle>();
|
|
4657
|
+
|
|
4658
|
+
try {
|
|
4659
|
+
await mount(baseProps(), ref);
|
|
4660
|
+
await expect(
|
|
4661
|
+
ref.current?.sendMessage("Retry the same deck prompt", undefined, {
|
|
4662
|
+
submissionExpiresAt: 1_001,
|
|
4663
|
+
submitMessageId: "deadline-during-save",
|
|
4664
|
+
}),
|
|
4665
|
+
).resolves.toEqual({
|
|
4666
|
+
status: "rejected",
|
|
4667
|
+
reason: "submission-expired",
|
|
4668
|
+
});
|
|
4669
|
+
expect(chatMocks.appState.has(stateKey)).toBe(false);
|
|
4670
|
+
expect(chatMocks.control.sendMessage).not.toHaveBeenCalled();
|
|
4671
|
+
expect(results.map((event) => event.detail)).toContainEqual({
|
|
4672
|
+
submitMessageId: "deadline-during-save",
|
|
4673
|
+
delivered: false,
|
|
4674
|
+
reason: "submission-expired",
|
|
4675
|
+
});
|
|
4676
|
+
} finally {
|
|
4677
|
+
window.removeEventListener(AGENT_CHAT_SUBMIT_RESULT_EVENT, listener);
|
|
4678
|
+
compareAndSet.mockImplementation(originalCompareAndSet);
|
|
4679
|
+
now.mockRestore();
|
|
4680
|
+
}
|
|
4681
|
+
});
|
|
4682
|
+
|
|
4683
|
+
it("removes an expired deferred submission without dispatching it", async () => {
|
|
4684
|
+
const threadId = chatMocks.threadId;
|
|
4685
|
+
const encodedThreadId = Array.from(threadId, (character) =>
|
|
4686
|
+
character.codePointAt(0)!.toString(16),
|
|
4687
|
+
).join("-");
|
|
4688
|
+
const stateKey = `agentkit-deferred-provider-submissions:${encodedThreadId}`;
|
|
4689
|
+
chatMocks.appState.set(stateKey, {
|
|
4690
|
+
version: 1,
|
|
4691
|
+
threadId,
|
|
4692
|
+
submissions: [
|
|
4693
|
+
{
|
|
4694
|
+
id: "expired-deferred-submit",
|
|
4695
|
+
threadId,
|
|
4696
|
+
text: "Create a deck",
|
|
4697
|
+
submissionExpiresAt: Date.now() - 1,
|
|
4698
|
+
fileParts: [],
|
|
4699
|
+
references: [],
|
|
4700
|
+
composerOptions: {},
|
|
4701
|
+
options: {},
|
|
4702
|
+
},
|
|
4703
|
+
],
|
|
4704
|
+
});
|
|
4705
|
+
const results: CustomEvent[] = [];
|
|
4706
|
+
const listener = (event: Event) => results.push(event as CustomEvent);
|
|
4707
|
+
window.addEventListener(AGENT_CHAT_SUBMIT_RESULT_EVENT, listener);
|
|
4708
|
+
|
|
4709
|
+
await mount(baseProps());
|
|
4710
|
+
await flush();
|
|
4711
|
+
window.removeEventListener(AGENT_CHAT_SUBMIT_RESULT_EVENT, listener);
|
|
4712
|
+
|
|
4713
|
+
expect(chatMocks.control.sendMessage).not.toHaveBeenCalled();
|
|
4714
|
+
expect(chatMocks.appState.has(stateKey)).toBe(false);
|
|
4715
|
+
expect(results.map((event) => event.detail)).toContainEqual({
|
|
4716
|
+
submitMessageId: "expired-deferred-submit",
|
|
4717
|
+
delivered: false,
|
|
4718
|
+
reason: "submission-expired",
|
|
4719
|
+
});
|
|
4720
|
+
});
|
|
4721
|
+
|
|
4624
4722
|
it("rechecks only submission scope after upload and reports its reason", async () => {
|
|
4625
4723
|
const uploading = Promise.withResolvers<FilePart[]>();
|
|
4626
4724
|
chatMocks.control.uploadFiles.mockReturnValueOnce(uploading.promise);
|
|
@@ -6042,6 +6140,41 @@ describe("AgentKitAssistantChat host behavior", () => {
|
|
|
6042
6140
|
expect(chatMocks.control.queueMessage).not.toHaveBeenCalled();
|
|
6043
6141
|
});
|
|
6044
6142
|
|
|
6143
|
+
it("keeps a deadline-bound retry out of the queue and forwards its expiry", async () => {
|
|
6144
|
+
chatMocks.thread.activeRunIds = ["run-1"];
|
|
6145
|
+
const ref = createRef<AssistantChatHandle>();
|
|
6146
|
+
container = document.createElement("div");
|
|
6147
|
+
document.body.appendChild(container);
|
|
6148
|
+
root = createRoot(container);
|
|
6149
|
+
await act(async () => {
|
|
6150
|
+
root.render(<AgentKitAssistantChat ref={ref} {...baseProps()} />);
|
|
6151
|
+
});
|
|
6152
|
+
const submissionExpiresAt = Date.now() + 20_000;
|
|
6153
|
+
|
|
6154
|
+
await act(async () => {
|
|
6155
|
+
await expect(
|
|
6156
|
+
ref.current?.sendMessage("Retry the same deck prompt", undefined, {
|
|
6157
|
+
submissionExpiresAt,
|
|
6158
|
+
submitMessageId: "slides-retry",
|
|
6159
|
+
}),
|
|
6160
|
+
).resolves.toEqual({ status: "submitted" });
|
|
6161
|
+
});
|
|
6162
|
+
|
|
6163
|
+
expect(chatMocks.control.sendMessage).toHaveBeenCalledOnce();
|
|
6164
|
+
expect(chatMocks.control.sendMessage.mock.calls[0]?.[0]).toMatchObject({
|
|
6165
|
+
text: "Retry the same deck prompt",
|
|
6166
|
+
queuedWhileRunActive: true,
|
|
6167
|
+
queueWhileRunning: false,
|
|
6168
|
+
metadata: {
|
|
6169
|
+
custom: {
|
|
6170
|
+
[AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY]:
|
|
6171
|
+
submissionExpiresAt,
|
|
6172
|
+
},
|
|
6173
|
+
},
|
|
6174
|
+
});
|
|
6175
|
+
expect(chatMocks.control.queueMessage).not.toHaveBeenCalled();
|
|
6176
|
+
});
|
|
6177
|
+
|
|
6045
6178
|
it("hides the shared guided-question card when the host owns that flow", async () => {
|
|
6046
6179
|
chatMocks.guidedQuestions = [{ id: "question-1", question: "Title?" }];
|
|
6047
6180
|
await mount(baseProps({ showGuidedQuestions: false }));
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
hasActiveAgentRuns,
|
|
4
4
|
selectAgentSuggestions,
|
|
5
5
|
isCurrentAgentSuggestion,
|
|
6
|
+
AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY,
|
|
6
7
|
splitAgentKitMessageContext,
|
|
7
8
|
type AgentKitUploadDriver,
|
|
8
9
|
type AgentThreadState,
|
|
@@ -643,10 +644,22 @@ type AgentKitInternalSendOptions = AssistantChatSendOptions & {
|
|
|
643
644
|
deferredSubmissionId?: string;
|
|
644
645
|
};
|
|
645
646
|
|
|
647
|
+
function isSubmissionExpired(submissionExpiresAt: number | undefined): boolean {
|
|
648
|
+
return (
|
|
649
|
+
submissionExpiresAt !== undefined &&
|
|
650
|
+
(!Number.isFinite(submissionExpiresAt) || submissionExpiresAt <= Date.now())
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function isSubmissionExpiredError(error: unknown): boolean {
|
|
655
|
+
return asRecord(error)?.code === "submission_expired";
|
|
656
|
+
}
|
|
657
|
+
|
|
646
658
|
interface PendingProviderSubmission {
|
|
647
659
|
id: string;
|
|
648
660
|
threadId: string;
|
|
649
661
|
text: string;
|
|
662
|
+
submissionExpiresAt?: number;
|
|
650
663
|
fileParts: FilePart[];
|
|
651
664
|
requestAttachments?: AgentRequestAttachment[];
|
|
652
665
|
references: Reference[];
|
|
@@ -931,6 +944,9 @@ function parseDeferredProviderSubmissions(
|
|
|
931
944
|
!Array.isArray(submission.references) ||
|
|
932
945
|
!composerOptions ||
|
|
933
946
|
!options ||
|
|
947
|
+
(submission.submissionExpiresAt !== undefined &&
|
|
948
|
+
(typeof submission.submissionExpiresAt !== "number" ||
|
|
949
|
+
!Number.isFinite(submission.submissionExpiresAt))) ||
|
|
934
950
|
(submission.attempts !== undefined &&
|
|
935
951
|
(!Number.isSafeInteger(submission.attempts) ||
|
|
936
952
|
(submission.attempts as number) < 0)) ||
|
|
@@ -950,6 +966,9 @@ function parseDeferredProviderSubmissions(
|
|
|
950
966
|
id: submission.id,
|
|
951
967
|
threadId,
|
|
952
968
|
text: submission.text,
|
|
969
|
+
...(typeof submission.submissionExpiresAt === "number"
|
|
970
|
+
? { submissionExpiresAt: submission.submissionExpiresAt }
|
|
971
|
+
: {}),
|
|
953
972
|
fileParts: fileParts as FilePart[],
|
|
954
973
|
...(Array.isArray(requestAttachments)
|
|
955
974
|
? {
|
|
@@ -3536,6 +3555,12 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
3536
3555
|
: props.selectedAgent;
|
|
3537
3556
|
const actionScope = options.actionScope ?? contextScope;
|
|
3538
3557
|
const customMetadata = {
|
|
3558
|
+
...(typeof options.submissionExpiresAt === "number"
|
|
3559
|
+
? {
|
|
3560
|
+
[AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY]:
|
|
3561
|
+
options.submissionExpiresAt,
|
|
3562
|
+
}
|
|
3563
|
+
: {}),
|
|
3539
3564
|
...(options.recoveryAction
|
|
3540
3565
|
? { agentNativeRecoveryAction: options.recoveryAction }
|
|
3541
3566
|
: {}),
|
|
@@ -3596,6 +3621,12 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
3596
3621
|
requestMode,
|
|
3597
3622
|
};
|
|
3598
3623
|
if (!queuedMessageReservationId) localSubmissionRef.current = true;
|
|
3624
|
+
if (isSubmissionExpired(options.submissionExpiresAt)) {
|
|
3625
|
+
throw Object.assign(
|
|
3626
|
+
new Error("The prompt expired before it could be sent."),
|
|
3627
|
+
{ code: "submission_expired", retryable: false },
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3599
3630
|
await control.sendMessage({
|
|
3600
3631
|
text: message,
|
|
3601
3632
|
attachments: fileParts,
|
|
@@ -3603,6 +3634,9 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
3603
3634
|
queuedWhileRunActive:
|
|
3604
3635
|
composerOptions.queuedWhileRunActive ||
|
|
3605
3636
|
composerOptions.intent === "queued",
|
|
3637
|
+
...(options.submissionExpiresAt !== undefined
|
|
3638
|
+
? { queueWhileRunning: false }
|
|
3639
|
+
: {}),
|
|
3606
3640
|
interruptActiveRun: composerOptions.steer,
|
|
3607
3641
|
options: {
|
|
3608
3642
|
model,
|
|
@@ -3684,12 +3718,28 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
3684
3718
|
composerOptions: AgentKitSuggestionSubmitOptions,
|
|
3685
3719
|
options: AgentKitInternalSendOptions = {},
|
|
3686
3720
|
): Promise<AssistantChatSubmitResult> => {
|
|
3721
|
+
if (isSubmissionExpired(options.submissionExpiresAt)) {
|
|
3722
|
+
reportAgentChatSubmitResult(
|
|
3723
|
+
options.submitMessageId,
|
|
3724
|
+
false,
|
|
3725
|
+
"submission-expired",
|
|
3726
|
+
);
|
|
3727
|
+
return { status: "rejected", reason: "submission-expired" };
|
|
3728
|
+
}
|
|
3687
3729
|
const submittedComposerOptions = captureQueuedRunState(
|
|
3688
3730
|
composerOptions,
|
|
3689
3731
|
isThreadRunning(),
|
|
3690
3732
|
);
|
|
3691
3733
|
const release = await acquireSubmission();
|
|
3692
3734
|
if (!release) {
|
|
3735
|
+
if (isSubmissionExpired(options.submissionExpiresAt)) {
|
|
3736
|
+
reportAgentChatSubmitResult(
|
|
3737
|
+
options.submitMessageId,
|
|
3738
|
+
false,
|
|
3739
|
+
"submission-expired",
|
|
3740
|
+
);
|
|
3741
|
+
return { status: "rejected", reason: "submission-expired" };
|
|
3742
|
+
}
|
|
3693
3743
|
if (isRestoring) {
|
|
3694
3744
|
try {
|
|
3695
3745
|
const selectedEngine =
|
|
@@ -3738,6 +3788,14 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
3738
3788
|
) {
|
|
3739
3789
|
throw new Error(t("onboarding.fileStorage.title"));
|
|
3740
3790
|
}
|
|
3791
|
+
if (isSubmissionExpired(options.submissionExpiresAt)) {
|
|
3792
|
+
reportAgentChatSubmitResult(
|
|
3793
|
+
options.submitMessageId,
|
|
3794
|
+
false,
|
|
3795
|
+
"submission-expired",
|
|
3796
|
+
);
|
|
3797
|
+
return { status: "rejected", reason: "submission-expired" };
|
|
3798
|
+
}
|
|
3741
3799
|
const requestAttachments = normalizeRequestAttachmentReferences(
|
|
3742
3800
|
options.deferredRequestAttachments ??
|
|
3743
3801
|
(await uploadRequestAttachments(
|
|
@@ -3746,6 +3804,14 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
3746
3804
|
)),
|
|
3747
3805
|
);
|
|
3748
3806
|
submittedComposerOptions.validateSubmissionScope?.();
|
|
3807
|
+
if (isSubmissionExpired(options.submissionExpiresAt)) {
|
|
3808
|
+
reportAgentChatSubmitResult(
|
|
3809
|
+
options.submitMessageId,
|
|
3810
|
+
false,
|
|
3811
|
+
"submission-expired",
|
|
3812
|
+
);
|
|
3813
|
+
return { status: "rejected", reason: "submission-expired" };
|
|
3814
|
+
}
|
|
3749
3815
|
const selectionChangedDuringUpload =
|
|
3750
3816
|
selectionRevision !== selectionRevisionRef.current;
|
|
3751
3817
|
const context = options.recoveryAction
|
|
@@ -3794,12 +3860,23 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
3794
3860
|
id: deferredOptions.deferredSubmissionId,
|
|
3795
3861
|
threadId,
|
|
3796
3862
|
text: appendAgentChatContextToMessage(text, context),
|
|
3863
|
+
...(typeof options.submissionExpiresAt === "number"
|
|
3864
|
+
? { submissionExpiresAt: options.submissionExpiresAt }
|
|
3865
|
+
: {}),
|
|
3797
3866
|
fileParts,
|
|
3798
3867
|
...(requestAttachments.length ? { requestAttachments } : {}),
|
|
3799
3868
|
references: [...references],
|
|
3800
3869
|
composerOptions: deferredComposerOptions,
|
|
3801
3870
|
options: deferredOptions,
|
|
3802
3871
|
};
|
|
3872
|
+
if (isSubmissionExpired(options.submissionExpiresAt)) {
|
|
3873
|
+
reportAgentChatSubmitResult(
|
|
3874
|
+
submitMessageId,
|
|
3875
|
+
false,
|
|
3876
|
+
"submission-expired",
|
|
3877
|
+
);
|
|
3878
|
+
return { status: "rejected", reason: "submission-expired" };
|
|
3879
|
+
}
|
|
3803
3880
|
const submissions = await updateDeferredProviderSubmissions(
|
|
3804
3881
|
threadId,
|
|
3805
3882
|
(current) =>
|
|
@@ -3807,11 +3884,32 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
3807
3884
|
? current
|
|
3808
3885
|
: [...current, submission],
|
|
3809
3886
|
);
|
|
3887
|
+
if (isSubmissionExpired(options.submissionExpiresAt)) {
|
|
3888
|
+
pendingProviderSubmissionsRef.current =
|
|
3889
|
+
await updateDeferredProviderSubmissions(threadId, (current) =>
|
|
3890
|
+
current.filter(({ id }) => id !== submission.id),
|
|
3891
|
+
);
|
|
3892
|
+
setPendingProviderSubmissionVersion((version) => version + 1);
|
|
3893
|
+
reportAgentChatSubmitResult(
|
|
3894
|
+
submitMessageId,
|
|
3895
|
+
false,
|
|
3896
|
+
"submission-expired",
|
|
3897
|
+
);
|
|
3898
|
+
return { status: "rejected", reason: "submission-expired" };
|
|
3899
|
+
}
|
|
3810
3900
|
pendingProviderSubmissionsRef.current = submissions;
|
|
3811
3901
|
setDeferredSubmissionsLoadedThread(threadId);
|
|
3812
3902
|
reportAgentChatSubmitResult(submitMessageId, true);
|
|
3813
3903
|
return { status: "submitted" };
|
|
3814
3904
|
} catch (error) {
|
|
3905
|
+
if (isSubmissionExpiredError(error)) {
|
|
3906
|
+
reportAgentChatSubmitResult(
|
|
3907
|
+
options.submitMessageId,
|
|
3908
|
+
false,
|
|
3909
|
+
"submission-expired",
|
|
3910
|
+
);
|
|
3911
|
+
return { status: "rejected", reason: "submission-expired" };
|
|
3912
|
+
}
|
|
3815
3913
|
reportAgentChatSubmitResult(
|
|
3816
3914
|
options.submitMessageId,
|
|
3817
3915
|
false,
|
|
@@ -3841,6 +3939,14 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
3841
3939
|
}
|
|
3842
3940
|
}
|
|
3843
3941
|
try {
|
|
3942
|
+
if (isSubmissionExpired(options.submissionExpiresAt)) {
|
|
3943
|
+
reportAgentChatSubmitResult(
|
|
3944
|
+
options.submitMessageId,
|
|
3945
|
+
false,
|
|
3946
|
+
"submission-expired",
|
|
3947
|
+
);
|
|
3948
|
+
return { status: "rejected", reason: "submission-expired" };
|
|
3949
|
+
}
|
|
3844
3950
|
await dispatch(
|
|
3845
3951
|
text,
|
|
3846
3952
|
files,
|
|
@@ -3850,6 +3956,14 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
3850
3956
|
);
|
|
3851
3957
|
return { status: "submitted" };
|
|
3852
3958
|
} catch (error) {
|
|
3959
|
+
if (isSubmissionExpiredError(error)) {
|
|
3960
|
+
reportAgentChatSubmitResult(
|
|
3961
|
+
options.submitMessageId,
|
|
3962
|
+
false,
|
|
3963
|
+
"submission-expired",
|
|
3964
|
+
);
|
|
3965
|
+
return { status: "rejected", reason: "submission-expired" };
|
|
3966
|
+
}
|
|
3853
3967
|
reportAgentChatSubmitResult(
|
|
3854
3968
|
options.submitMessageId,
|
|
3855
3969
|
false,
|
|
@@ -4232,6 +4346,20 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
4232
4346
|
let claimToken: string | undefined;
|
|
4233
4347
|
let claimRenewalTimer: number | undefined;
|
|
4234
4348
|
try {
|
|
4349
|
+
if (isSubmissionExpired(submission.submissionExpiresAt)) {
|
|
4350
|
+
const submissions = await updateDeferredProviderSubmissions(
|
|
4351
|
+
threadId,
|
|
4352
|
+
(current) => current.filter(({ id }) => id !== submission.id),
|
|
4353
|
+
);
|
|
4354
|
+
pendingProviderSubmissionsRef.current = submissions;
|
|
4355
|
+
reportAgentChatSubmitResult(
|
|
4356
|
+
submission.id,
|
|
4357
|
+
false,
|
|
4358
|
+
"submission-expired",
|
|
4359
|
+
);
|
|
4360
|
+
setPendingProviderSubmissionVersion((version) => version + 1);
|
|
4361
|
+
return;
|
|
4362
|
+
}
|
|
4235
4363
|
release = await acquireSubmission();
|
|
4236
4364
|
if (!release) {
|
|
4237
4365
|
if (active) scheduleProviderSubmissionRetry();
|
|
@@ -4301,6 +4429,20 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
4301
4429
|
);
|
|
4302
4430
|
return;
|
|
4303
4431
|
}
|
|
4432
|
+
if (isSubmissionExpired(currentSubmission.submissionExpiresAt)) {
|
|
4433
|
+
const submissions = await updateDeferredProviderSubmissions(
|
|
4434
|
+
threadId,
|
|
4435
|
+
(current) => current.filter(({ id }) => id !== submission.id),
|
|
4436
|
+
);
|
|
4437
|
+
pendingProviderSubmissionsRef.current = submissions;
|
|
4438
|
+
reportAgentChatSubmitResult(
|
|
4439
|
+
submission.id,
|
|
4440
|
+
false,
|
|
4441
|
+
"submission-expired",
|
|
4442
|
+
);
|
|
4443
|
+
setPendingProviderSubmissionVersion((version) => version + 1);
|
|
4444
|
+
return;
|
|
4445
|
+
}
|
|
4304
4446
|
|
|
4305
4447
|
claimRenewalTimer = window.setInterval(() => {
|
|
4306
4448
|
void updateDeferredProviderSubmissions(threadId, (current) =>
|
|
@@ -4345,6 +4487,29 @@ const AgentKitAssistantChatBody = forwardRef<
|
|
|
4345
4487
|
setDeferredProviderSubmissionFailureId(null);
|
|
4346
4488
|
setPendingProviderSubmissionVersion((version) => version + 1);
|
|
4347
4489
|
} catch (error) {
|
|
4490
|
+
if (isSubmissionExpiredError(error)) {
|
|
4491
|
+
const submissions = await updateDeferredProviderSubmissions(
|
|
4492
|
+
threadId,
|
|
4493
|
+
(current) =>
|
|
4494
|
+
current.filter(
|
|
4495
|
+
(candidate) =>
|
|
4496
|
+
candidate.id !== submission.id ||
|
|
4497
|
+
!claimToken ||
|
|
4498
|
+
candidate.claim?.token !== claimToken,
|
|
4499
|
+
),
|
|
4500
|
+
);
|
|
4501
|
+
pendingProviderSubmissionsRef.current = submissions;
|
|
4502
|
+
reportAgentChatSubmitResult(
|
|
4503
|
+
submission.id,
|
|
4504
|
+
false,
|
|
4505
|
+
"submission-expired",
|
|
4506
|
+
);
|
|
4507
|
+
if (active) {
|
|
4508
|
+
setDeferredProviderSubmissionFailureId(null);
|
|
4509
|
+
setPendingProviderSubmissionVersion((version) => version + 1);
|
|
4510
|
+
}
|
|
4511
|
+
return;
|
|
4512
|
+
}
|
|
4348
4513
|
let snapshotReadFailed = false;
|
|
4349
4514
|
const latestThread = await props
|
|
4350
4515
|
.getThreadSnapshot(threadId)
|
|
@@ -7353,6 +7518,9 @@ const ATTACHMENT_UNREADABLE_SUBMIT_REASON = "attachment-unreadable";
|
|
|
7353
7518
|
|
|
7354
7519
|
function submitFailureReason(error: unknown): string {
|
|
7355
7520
|
const record = asRecord(error);
|
|
7521
|
+
if (record?.code === "submission_expired") {
|
|
7522
|
+
return "submission-expired";
|
|
7523
|
+
}
|
|
7356
7524
|
if (record?.code === ATTACHMENT_UNREADABLE_SUBMIT_REASON) {
|
|
7357
7525
|
return ATTACHMENT_UNREADABLE_SUBMIT_REASON;
|
|
7358
7526
|
}
|
|
@@ -6627,6 +6627,7 @@ describe("MultiTabAssistantChat cold-start delivery (Mode B)", () => {
|
|
|
6627
6627
|
"fetch",
|
|
6628
6628
|
vi.fn(async () => Response.json({ value: null })),
|
|
6629
6629
|
);
|
|
6630
|
+
ensureLocalStorage();
|
|
6630
6631
|
window.localStorage.clear();
|
|
6631
6632
|
window.localStorage.setItem(
|
|
6632
6633
|
openTabsStorageKey("mode-b"),
|
|
@@ -6669,6 +6670,58 @@ describe("MultiTabAssistantChat cold-start delivery (Mode B)", () => {
|
|
|
6669
6670
|
);
|
|
6670
6671
|
});
|
|
6671
6672
|
|
|
6673
|
+
it("rejects a submission whose retry window has expired", async () => {
|
|
6674
|
+
const submissionExpiresAt = Date.now() - 1;
|
|
6675
|
+
const results: unknown[] = [];
|
|
6676
|
+
const onResult = (event: Event) =>
|
|
6677
|
+
results.push((event as CustomEvent).detail);
|
|
6678
|
+
window.addEventListener(AGENT_CHAT_SUBMIT_RESULT_EVENT, onResult);
|
|
6679
|
+
|
|
6680
|
+
try {
|
|
6681
|
+
await act(async () => {
|
|
6682
|
+
root.render(<MultiTabAssistantChat storageKey="mode-b" />);
|
|
6683
|
+
await Promise.resolve();
|
|
6684
|
+
});
|
|
6685
|
+
dispatchSubmitChat({
|
|
6686
|
+
message: "Create a deck",
|
|
6687
|
+
submissionExpiresAt,
|
|
6688
|
+
submitMessageId: "expired-submit",
|
|
6689
|
+
});
|
|
6690
|
+
|
|
6691
|
+
expect(chatHandleMocks.sendMessage).not.toHaveBeenCalled();
|
|
6692
|
+
expect(results).toContainEqual({
|
|
6693
|
+
submitMessageId: "expired-submit",
|
|
6694
|
+
delivered: false,
|
|
6695
|
+
reason: "submission-expired",
|
|
6696
|
+
});
|
|
6697
|
+
} finally {
|
|
6698
|
+
window.removeEventListener(AGENT_CHAT_SUBMIT_RESULT_EVENT, onResult);
|
|
6699
|
+
}
|
|
6700
|
+
});
|
|
6701
|
+
|
|
6702
|
+
it("passes a submission expiry to the chat that receives the message", async () => {
|
|
6703
|
+
const submissionExpiresAt = Date.now() + 20_000;
|
|
6704
|
+
await act(async () => {
|
|
6705
|
+
root.render(<MultiTabAssistantChat storageKey="mode-b" />);
|
|
6706
|
+
await Promise.resolve();
|
|
6707
|
+
});
|
|
6708
|
+
|
|
6709
|
+
dispatchSubmitChat({
|
|
6710
|
+
message: "Create a deck",
|
|
6711
|
+
submissionExpiresAt,
|
|
6712
|
+
submitMessageId: "deadline-submit",
|
|
6713
|
+
});
|
|
6714
|
+
|
|
6715
|
+
expect(chatHandleMocks.sendMessage).toHaveBeenCalledWith(
|
|
6716
|
+
"Create a deck",
|
|
6717
|
+
undefined,
|
|
6718
|
+
{
|
|
6719
|
+
submitMessageId: "deadline-submit",
|
|
6720
|
+
submissionExpiresAt,
|
|
6721
|
+
},
|
|
6722
|
+
);
|
|
6723
|
+
});
|
|
6724
|
+
|
|
6672
6725
|
it("ignores a duplicate submit with the same submitMessageId", async () => {
|
|
6673
6726
|
await act(async () => {
|
|
6674
6727
|
root.render(<MultiTabAssistantChat storageKey="mode-b" />);
|
|
@@ -121,6 +121,7 @@ interface PendingSend {
|
|
|
121
121
|
requestMode?: "act" | "plan";
|
|
122
122
|
/** Correlates with `AGENT_CHAT_SUBMIT_RESULT_EVENT` — see agent-chat.ts. */
|
|
123
123
|
submitMessageId?: string;
|
|
124
|
+
submissionExpiresAt?: number;
|
|
124
125
|
/** See `AgentChatMessage.usageLabel`. */
|
|
125
126
|
usageLabel?: string;
|
|
126
127
|
actionScope?: AgentActionScope;
|
|
@@ -227,6 +228,18 @@ function deliverPendingSend(
|
|
|
227
228
|
onHandoff: (delivered: boolean) => boolean,
|
|
228
229
|
): void | Promise<void> {
|
|
229
230
|
if (isAgentChatSubmitCancelled(send.submitMessageId)) return;
|
|
231
|
+
if (
|
|
232
|
+
send.submissionExpiresAt !== undefined &&
|
|
233
|
+
send.submissionExpiresAt <= Date.now()
|
|
234
|
+
) {
|
|
235
|
+
if (onHandoff(false))
|
|
236
|
+
reportAgentChatSubmitResult(
|
|
237
|
+
send.submitMessageId,
|
|
238
|
+
false,
|
|
239
|
+
"submission-expired",
|
|
240
|
+
);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
230
243
|
if (!send.submit) {
|
|
231
244
|
return deliverPendingPrefill(ref, send, isCurrent, onHandoff).catch(
|
|
232
245
|
(error: unknown) => {
|
|
@@ -250,6 +263,9 @@ function deliverPendingSend(
|
|
|
250
263
|
...(send.requestMode ? { requestMode: send.requestMode } : {}),
|
|
251
264
|
...(send.attachments ? { attachments: send.attachments } : {}),
|
|
252
265
|
...(send.submitMessageId ? { submitMessageId: send.submitMessageId } : {}),
|
|
266
|
+
...(send.submissionExpiresAt !== undefined
|
|
267
|
+
? { submissionExpiresAt: send.submissionExpiresAt }
|
|
268
|
+
: {}),
|
|
253
269
|
...(send.usageLabel ? { usageLabel: send.usageLabel } : {}),
|
|
254
270
|
...(send.actionScope ? { actionScope: send.actionScope } : {}),
|
|
255
271
|
// An approval resume is a protocol continuation, not a visible prompt.
|
|
@@ -2259,6 +2275,23 @@ export function MultiTabAssistantChat({
|
|
|
2259
2275
|
(onlyThreadId?: string, immediate?: boolean) => void
|
|
2260
2276
|
>(
|
|
2261
2277
|
(onlyThreadId, immediate = false) => {
|
|
2278
|
+
for (const delivery of [...pendingDeliveries.current]) {
|
|
2279
|
+
if (
|
|
2280
|
+
delivery.send.submissionExpiresAt === undefined ||
|
|
2281
|
+
delivery.send.submissionExpiresAt > Date.now()
|
|
2282
|
+
) {
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
pendingDeliveries.current = pendingDeliveries.current.filter(
|
|
2286
|
+
(pending) => pending !== delivery,
|
|
2287
|
+
);
|
|
2288
|
+
reportAgentChatSubmitResult(
|
|
2289
|
+
delivery.send.submitMessageId,
|
|
2290
|
+
false,
|
|
2291
|
+
"submission-expired",
|
|
2292
|
+
);
|
|
2293
|
+
cancelAgentChatSubmit(delivery.send.submitMessageId);
|
|
2294
|
+
}
|
|
2262
2295
|
const active = activeThreadIdRef.current;
|
|
2263
2296
|
for (const delivery of pendingDeliveries.current) {
|
|
2264
2297
|
if (delivery.threadId === null && active) delivery.threadId = active;
|
|
@@ -2524,9 +2557,11 @@ export function MultiTabAssistantChat({
|
|
|
2524
2557
|
...(submitMessageId ? { submitMessageId } : {}),
|
|
2525
2558
|
...(usageLabel ? { usageLabel } : {}),
|
|
2526
2559
|
...(actionScope ? { actionScope } : {}),
|
|
2560
|
+
...(typeof parsed.submissionExpiresAt === "number"
|
|
2561
|
+
? { submissionExpiresAt: parsed.submissionExpiresAt }
|
|
2562
|
+
: {}),
|
|
2527
2563
|
...(approvedToolCalls ? { approvedToolCalls } : {}),
|
|
2528
2564
|
};
|
|
2529
|
-
|
|
2530
2565
|
// Resolved once, up front, and carried with the send until a thread
|
|
2531
2566
|
// exists to key it by. Applying it only when `model` matched
|
|
2532
2567
|
// `availableModels` dropped every override that arrived before the
|
|
@@ -46,6 +46,8 @@ export interface AssistantChatSendOptions {
|
|
|
46
46
|
attachments?: AgentChatAttachment[];
|
|
47
47
|
/** Correlates with `AGENT_CHAT_SUBMIT_RESULT_EVENT` — see agent-chat.ts. */
|
|
48
48
|
submitMessageId?: string;
|
|
49
|
+
/** Drop a queued submit instead of handing it to chat after this time. */
|
|
50
|
+
submissionExpiresAt?: number;
|
|
49
51
|
/** See `AgentChatMessage.usageLabel`. */
|
|
50
52
|
usageLabel?: string;
|
|
51
53
|
actionScope?: AgentActionScope;
|
|
@@ -59,7 +61,10 @@ export type AssistantChatSubmitResult =
|
|
|
59
61
|
| { status: "submitted" }
|
|
60
62
|
| {
|
|
61
63
|
status: "rejected";
|
|
62
|
-
reason:
|
|
64
|
+
reason:
|
|
65
|
+
| "engine-not-configured"
|
|
66
|
+
| "submission-unavailable"
|
|
67
|
+
| "submission-expired";
|
|
63
68
|
};
|
|
64
69
|
|
|
65
70
|
export interface AssistantChatHandle {
|