@assistant-ui/ai-sdk 0.0.6 → 0.0.8
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/README.md +1 -1
- package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -2
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts +2 -5
- package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
- package/dist/adapters/vercelAttachmentAdapter.js +3 -3
- package/dist/adapters/vercelAttachmentAdapter.js.map +1 -1
- package/dist/aiSDKExtras.d.ts +6 -9
- package/dist/aiSDKExtras.d.ts.map +1 -1
- package/dist/converters/convertMessage.d.ts +34 -25
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +78 -8
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/converters/modelContentEnvelope.d.ts +10 -12
- package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.d.ts +3 -6
- package/dist/converters/toCreateMessage.d.ts.map +1 -1
- package/dist/converters/toolOutputConversion.d.ts +22 -25
- package/dist/converters/toolOutputConversion.d.ts.map +1 -1
- package/dist/hooks.d.ts +4 -7
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +17 -14
- package/dist/index.d.ts.map +1 -0
- package/dist/index.native.d.ts +15 -12
- package/dist/index.native.d.ts.map +1 -0
- package/dist/model-context/injectInteractableContext.d.ts +3 -6
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +2 -5
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.d.ts +4 -7
- package/dist/runtime/AISDKChat.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.js +1 -1
- package/dist/runtime/AISDKThreads.d.ts +29 -32
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +26 -33
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.d.ts +2 -5
- package/dist/runtime/sdkIdentity.d.ts.map +1 -1
- package/dist/runtime/sdkIdentity.js +1 -1
- package/dist/runtime/useAISDKRuntime.d.ts +72 -61
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +170 -56
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts +8 -11
- package/dist/runtime/useChatRuntime.d.ts.map +1 -1
- package/dist/runtime/useChatRuntime.js +14 -10
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.d.ts +100 -81
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +33 -13
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts +8 -9
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +236 -58
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.d.ts +1 -4
- package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
- package/dist/runtime/useResourceCleanup.js +38 -14
- package/dist/runtime/useResourceCleanup.js.map +1 -1
- package/dist/runtime/useStreamingTiming.d.ts +5 -8
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
- package/dist/runtime/useStreamingTiming.js +1 -1
- package/dist/tools/frontendTools.d.ts +24 -27
- package/dist/tools/frontendTools.d.ts.map +1 -1
- package/dist/tools/generativeTools.d.ts +29 -32
- package/dist/tools/generativeTools.d.ts.map +1 -1
- package/dist/tools/mcp-stdio.node.d.ts +2 -2
- package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts +2 -5
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.d.ts +17 -20
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
- package/dist/transport/resumable.d.ts +29 -32
- package/dist/transport/resumable.d.ts.map +1 -1
- package/dist/usage.d.ts +12 -15
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js.map +1 -1
- package/dist/utils/getVercelAIMessages.d.ts +3 -6
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
- package/dist/utils/sliceMessagesUntil.d.ts +2 -5
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
- package/package.json +16 -14
- package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
- package/src/adapters/vercelAttachmentAdapter.ts +2 -2
- package/src/converters/convertMessage.test.ts +616 -3
- package/src/converters/convertMessage.ts +187 -12
- package/src/index.native.ts +1 -1
- package/src/index.ts +1 -1
- package/src/runtime/AISDKChat.test.ts +4 -5
- package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
- package/src/runtime/AISDKThreads.test.ts +88 -51
- package/src/runtime/AISDKThreads.ts +4 -13
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +338 -0
- package/src/runtime/useAISDKRuntime.approval.test.tsx +476 -4
- package/src/runtime/useAISDKRuntime.test.ts +282 -8
- package/src/runtime/useAISDKRuntime.ts +274 -46
- package/src/runtime/useAISDKRuntime.voice.test.tsx +319 -0
- package/src/runtime/useChatRuntime.integration.test.tsx +56 -0
- package/src/runtime/useChatRuntime.test.ts +77 -5
- package/src/runtime/useChatRuntime.ts +6 -2
- package/src/runtime/useChatThread.transport.test.tsx +149 -0
- package/src/runtime/useChatThread.ts +77 -10
- package/src/runtime/useExternalHistory.test.ts +989 -3
- package/src/runtime/useExternalHistory.ts +390 -24
- package/src/runtime/useResourceCleanup.ts +41 -15
- package/src/tools/generativeTools.test.ts +7 -1
- package/src/usage.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
getPartialJsonObjectFieldState,
|
|
4
4
|
type ReadonlyJSONObject,
|
|
@@ -8,6 +8,16 @@ import {
|
|
|
8
8
|
type AISDKMessageConverterMetadata,
|
|
9
9
|
} from "./convertMessage";
|
|
10
10
|
|
|
11
|
+
const { stableStringifySpy } = vi.hoisted(() => ({
|
|
12
|
+
stableStringifySpy: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("@assistant-ui/core/internal", async (importOriginal) => {
|
|
15
|
+
const actual =
|
|
16
|
+
await importOriginal<typeof import("@assistant-ui/core/internal")>();
|
|
17
|
+
stableStringifySpy.mockImplementation(actual.stableStringifyToolArgs);
|
|
18
|
+
return { ...actual, stableStringifyToolArgs: stableStringifySpy };
|
|
19
|
+
});
|
|
20
|
+
|
|
11
21
|
describe("AISDKMessageConverter", () => {
|
|
12
22
|
it("flags the streaming assistant message as optimistic", () => {
|
|
13
23
|
const metadata: AISDKMessageConverterMetadata = {
|
|
@@ -34,7 +44,7 @@ describe("AISDKMessageConverter", () => {
|
|
|
34
44
|
parts: [{ type: "text", text: "yo" }],
|
|
35
45
|
metadata: {
|
|
36
46
|
usage: { inputTokens: 40, outputTokens: 2 },
|
|
37
|
-
modelId: "gpt-
|
|
47
|
+
modelId: "gpt-6-luna",
|
|
38
48
|
custom: { source: "route" },
|
|
39
49
|
},
|
|
40
50
|
},
|
|
@@ -42,12 +52,31 @@ describe("AISDKMessageConverter", () => {
|
|
|
42
52
|
|
|
43
53
|
expect(converted[0]?.metadata.custom).toEqual({
|
|
44
54
|
usage: { inputTokens: 40, outputTokens: 2 },
|
|
45
|
-
modelId: "gpt-
|
|
55
|
+
modelId: "gpt-6-luna",
|
|
46
56
|
source: "route",
|
|
47
57
|
});
|
|
48
58
|
expect(converted[0]?.metadata).not.toHaveProperty("usage");
|
|
49
59
|
});
|
|
50
60
|
|
|
61
|
+
it("preserves prototype-named custom metadata", () => {
|
|
62
|
+
const metadata = JSON.parse(
|
|
63
|
+
'{"__proto__":{"source":"server"},"constructor":"model"}',
|
|
64
|
+
);
|
|
65
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
66
|
+
{
|
|
67
|
+
id: "a1",
|
|
68
|
+
role: "assistant",
|
|
69
|
+
parts: [{ type: "text", text: "yo" }],
|
|
70
|
+
metadata,
|
|
71
|
+
},
|
|
72
|
+
] as any);
|
|
73
|
+
const custom = converted[0]?.metadata.custom;
|
|
74
|
+
|
|
75
|
+
expect(Object.hasOwn(custom!, "__proto__")).toBe(true);
|
|
76
|
+
expect(custom!["__proto__"]).toEqual({ source: "server" });
|
|
77
|
+
expect(custom!["constructor"]).toBe("model");
|
|
78
|
+
});
|
|
79
|
+
|
|
51
80
|
it("keeps modality metadata at the top level", () => {
|
|
52
81
|
const converted = AISDKMessageConverter.toThreadMessages([
|
|
53
82
|
{
|
|
@@ -487,6 +516,415 @@ describe("AISDKMessageConverter", () => {
|
|
|
487
516
|
});
|
|
488
517
|
});
|
|
489
518
|
|
|
519
|
+
it("preserves rich approval fields for a custom response channel", () => {
|
|
520
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
521
|
+
supportsRichToolApprovalResponses: true,
|
|
522
|
+
};
|
|
523
|
+
const converted = AISDKMessageConverter.toThreadMessages(
|
|
524
|
+
[
|
|
525
|
+
{
|
|
526
|
+
id: "a1",
|
|
527
|
+
role: "assistant",
|
|
528
|
+
parts: [
|
|
529
|
+
{
|
|
530
|
+
type: "tool-deploy",
|
|
531
|
+
toolCallId: "tc-1",
|
|
532
|
+
state: "approval-responded",
|
|
533
|
+
input: {},
|
|
534
|
+
approval: {
|
|
535
|
+
id: "approval-1",
|
|
536
|
+
display: "select",
|
|
537
|
+
allowFreeform: true,
|
|
538
|
+
options: [
|
|
539
|
+
{
|
|
540
|
+
id: "once",
|
|
541
|
+
kind: "allow-once",
|
|
542
|
+
label: "Only once",
|
|
543
|
+
grants: ["repository", 42],
|
|
544
|
+
confirm: {
|
|
545
|
+
title: "Confirm access",
|
|
546
|
+
description: { invalid: true },
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
"invalid",
|
|
550
|
+
{ id: 1, kind: "allow-always" },
|
|
551
|
+
{ id: "always", kind: 2 },
|
|
552
|
+
],
|
|
553
|
+
optionId: "once",
|
|
554
|
+
text: "an answer",
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
],
|
|
558
|
+
} as any,
|
|
559
|
+
],
|
|
560
|
+
false,
|
|
561
|
+
metadata,
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
const toolCall = converted[0]?.content.find(
|
|
565
|
+
(part): part is any => part.type === "tool-call",
|
|
566
|
+
);
|
|
567
|
+
expect(toolCall?.approval).toEqual({
|
|
568
|
+
id: "approval-1",
|
|
569
|
+
display: "select",
|
|
570
|
+
allowFreeform: true,
|
|
571
|
+
options: [
|
|
572
|
+
{
|
|
573
|
+
id: "once",
|
|
574
|
+
kind: "allow-once",
|
|
575
|
+
label: "Only once",
|
|
576
|
+
grants: ["repository"],
|
|
577
|
+
confirm: { title: "Confirm access" },
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
optionId: "once",
|
|
581
|
+
text: "an answer",
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it("reads the request from the approval descriptor for a custom response channel", () => {
|
|
586
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
587
|
+
supportsRichToolApprovalResponses: true,
|
|
588
|
+
};
|
|
589
|
+
const descriptor = {
|
|
590
|
+
prompt: "Which environment?",
|
|
591
|
+
display: "select",
|
|
592
|
+
allowFreeform: true,
|
|
593
|
+
dismissible: true,
|
|
594
|
+
options: [{ id: "staging", kind: "_target", label: "Staging" }],
|
|
595
|
+
scope: "deploy",
|
|
596
|
+
};
|
|
597
|
+
const converted = AISDKMessageConverter.toThreadMessages(
|
|
598
|
+
[
|
|
599
|
+
{
|
|
600
|
+
id: "a1",
|
|
601
|
+
role: "assistant",
|
|
602
|
+
parts: [
|
|
603
|
+
{
|
|
604
|
+
type: "tool-deploy",
|
|
605
|
+
toolCallId: "tc-1",
|
|
606
|
+
state: "approval-requested",
|
|
607
|
+
input: {},
|
|
608
|
+
approval: {
|
|
609
|
+
id: "approval-1",
|
|
610
|
+
descriptor,
|
|
611
|
+
requestReason: "Production access requires approval",
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
type: "tool-deploy",
|
|
616
|
+
toolCallId: "tc-2",
|
|
617
|
+
state: "approval-responded",
|
|
618
|
+
input: {},
|
|
619
|
+
approval: {
|
|
620
|
+
id: "approval-2",
|
|
621
|
+
approved: true,
|
|
622
|
+
prompt: "Deploy?",
|
|
623
|
+
descriptor: {
|
|
624
|
+
prompt: "Which environment?",
|
|
625
|
+
display: "text",
|
|
626
|
+
optionId: "staging",
|
|
627
|
+
text: "staging only",
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
} as any,
|
|
633
|
+
],
|
|
634
|
+
false,
|
|
635
|
+
metadata,
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
const approvals = converted[0]?.content.map(
|
|
639
|
+
(part) => (part as { approval?: unknown }).approval,
|
|
640
|
+
);
|
|
641
|
+
expect(approvals).toEqual([
|
|
642
|
+
{
|
|
643
|
+
id: "approval-1",
|
|
644
|
+
prompt: "Which environment?",
|
|
645
|
+
display: "select",
|
|
646
|
+
allowFreeform: true,
|
|
647
|
+
dismissible: true,
|
|
648
|
+
options: [{ id: "staging", kind: "_target", label: "Staging" }],
|
|
649
|
+
descriptor,
|
|
650
|
+
requestReason: "Production access requires approval",
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
id: "approval-2",
|
|
654
|
+
approved: true,
|
|
655
|
+
prompt: "Deploy?",
|
|
656
|
+
display: "text",
|
|
657
|
+
optionId: "staging",
|
|
658
|
+
text: "staging only",
|
|
659
|
+
descriptor: {
|
|
660
|
+
prompt: "Which environment?",
|
|
661
|
+
display: "text",
|
|
662
|
+
optionId: "staging",
|
|
663
|
+
text: "staging only",
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
]);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it("ignores a non-boolean dismissible field from the approval descriptor", () => {
|
|
670
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
671
|
+
supportsRichToolApprovalResponses: true,
|
|
672
|
+
};
|
|
673
|
+
const descriptor = {
|
|
674
|
+
prompt: "Which environment?",
|
|
675
|
+
display: "select",
|
|
676
|
+
dismissible: "yes",
|
|
677
|
+
options: [{ id: "staging", kind: "_target", label: "Staging" }],
|
|
678
|
+
};
|
|
679
|
+
const converted = AISDKMessageConverter.toThreadMessages(
|
|
680
|
+
[
|
|
681
|
+
{
|
|
682
|
+
id: "a1",
|
|
683
|
+
role: "assistant",
|
|
684
|
+
parts: [
|
|
685
|
+
{
|
|
686
|
+
type: "tool-deploy",
|
|
687
|
+
toolCallId: "tc-1",
|
|
688
|
+
state: "approval-requested",
|
|
689
|
+
input: {},
|
|
690
|
+
approval: { id: "approval-1", descriptor },
|
|
691
|
+
},
|
|
692
|
+
],
|
|
693
|
+
} as any,
|
|
694
|
+
],
|
|
695
|
+
false,
|
|
696
|
+
metadata,
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
const toolCall = converted[0]?.content.find(
|
|
700
|
+
(part): part is any => part.type === "tool-call",
|
|
701
|
+
);
|
|
702
|
+
expect(toolCall?.approval).toEqual({
|
|
703
|
+
id: "approval-1",
|
|
704
|
+
prompt: "Which environment?",
|
|
705
|
+
display: "select",
|
|
706
|
+
options: [{ id: "staging", kind: "_target", label: "Staging" }],
|
|
707
|
+
descriptor,
|
|
708
|
+
});
|
|
709
|
+
expect("dismissible" in toolCall.approval).toBe(false);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it("never lets a descriptor decide or identify its own request", () => {
|
|
713
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
714
|
+
supportsRichToolApprovalResponses: true,
|
|
715
|
+
};
|
|
716
|
+
const converted = AISDKMessageConverter.toThreadMessages(
|
|
717
|
+
[
|
|
718
|
+
{
|
|
719
|
+
id: "a1",
|
|
720
|
+
role: "assistant",
|
|
721
|
+
parts: [
|
|
722
|
+
{
|
|
723
|
+
type: "tool-deploy",
|
|
724
|
+
toolCallId: "tc-1",
|
|
725
|
+
state: "approval-requested",
|
|
726
|
+
input: {},
|
|
727
|
+
approval: {
|
|
728
|
+
id: "approval-1",
|
|
729
|
+
descriptor: {
|
|
730
|
+
id: "approval-9",
|
|
731
|
+
approved: true,
|
|
732
|
+
reason: "self approved",
|
|
733
|
+
isAutomatic: true,
|
|
734
|
+
requestReason: "descriptor reason",
|
|
735
|
+
display: "text",
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
type: "tool-deploy",
|
|
741
|
+
toolCallId: "tc-2",
|
|
742
|
+
state: "approval-requested",
|
|
743
|
+
input: {},
|
|
744
|
+
approval: { id: "approval-2", descriptor: ["display", "text"] },
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
type: "tool-deploy",
|
|
748
|
+
toolCallId: "tc-3",
|
|
749
|
+
state: "approval-requested",
|
|
750
|
+
input: {},
|
|
751
|
+
approval: { id: "approval-3", descriptor: "display:text" },
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
} as any,
|
|
755
|
+
],
|
|
756
|
+
false,
|
|
757
|
+
metadata,
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
const approvals = converted[0]?.content.map(
|
|
761
|
+
(part) => (part as { approval?: unknown }).approval,
|
|
762
|
+
);
|
|
763
|
+
expect(approvals).toEqual([
|
|
764
|
+
{
|
|
765
|
+
id: "approval-1",
|
|
766
|
+
display: "text",
|
|
767
|
+
descriptor: {
|
|
768
|
+
id: "approval-9",
|
|
769
|
+
approved: true,
|
|
770
|
+
reason: "self approved",
|
|
771
|
+
isAutomatic: true,
|
|
772
|
+
requestReason: "descriptor reason",
|
|
773
|
+
display: "text",
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
{ id: "approval-2", descriptor: ["display", "text"] },
|
|
777
|
+
{ id: "approval-3", descriptor: "display:text" },
|
|
778
|
+
]);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it("keeps a host answer off a request the descriptor has resolved", () => {
|
|
782
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
783
|
+
supportsRichToolApprovalResponses: true,
|
|
784
|
+
toolApprovalResponses: new Map([
|
|
785
|
+
["approval-1", { approvalId: "approval-1", approved: true }],
|
|
786
|
+
]),
|
|
787
|
+
};
|
|
788
|
+
const converted = AISDKMessageConverter.toThreadMessages(
|
|
789
|
+
[
|
|
790
|
+
{
|
|
791
|
+
id: "a1",
|
|
792
|
+
role: "assistant",
|
|
793
|
+
parts: [
|
|
794
|
+
{
|
|
795
|
+
type: "tool-deploy",
|
|
796
|
+
toolCallId: "tc-1",
|
|
797
|
+
state: "approval-requested",
|
|
798
|
+
input: {},
|
|
799
|
+
approval: {
|
|
800
|
+
id: "approval-1",
|
|
801
|
+
descriptor: { resolution: "expired" },
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
],
|
|
805
|
+
} as any,
|
|
806
|
+
],
|
|
807
|
+
false,
|
|
808
|
+
metadata,
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
const toolCall = converted[0]?.content.find(
|
|
812
|
+
(part): part is any => part.type === "tool-call",
|
|
813
|
+
);
|
|
814
|
+
expect(toolCall?.approval).toEqual({
|
|
815
|
+
id: "approval-1",
|
|
816
|
+
resolution: "expired",
|
|
817
|
+
descriptor: { resolution: "expired" },
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it("drops descriptor fields the AI SDK cannot answer without a custom response channel", () => {
|
|
822
|
+
const descriptor = {
|
|
823
|
+
prompt: "Which environment?",
|
|
824
|
+
display: "select",
|
|
825
|
+
allowFreeform: true,
|
|
826
|
+
options: [{ id: "staging", kind: "_target" }],
|
|
827
|
+
resolution: "expired",
|
|
828
|
+
};
|
|
829
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
830
|
+
{
|
|
831
|
+
id: "a1",
|
|
832
|
+
role: "assistant",
|
|
833
|
+
parts: [
|
|
834
|
+
{
|
|
835
|
+
type: "tool-deploy",
|
|
836
|
+
toolCallId: "tc-1",
|
|
837
|
+
state: "approval-requested",
|
|
838
|
+
input: {},
|
|
839
|
+
approval: { id: "approval-1", descriptor },
|
|
840
|
+
},
|
|
841
|
+
],
|
|
842
|
+
} as any,
|
|
843
|
+
]);
|
|
844
|
+
|
|
845
|
+
const toolCall = converted[0]?.content.find(
|
|
846
|
+
(part): part is any => part.type === "tool-call",
|
|
847
|
+
);
|
|
848
|
+
expect(toolCall?.approval).toEqual({
|
|
849
|
+
id: "approval-1",
|
|
850
|
+
prompt: "Which environment?",
|
|
851
|
+
resolution: "expired",
|
|
852
|
+
descriptor,
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it("applies a host answer to an approval the message has not recorded", () => {
|
|
857
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
858
|
+
supportsRichToolApprovalResponses: true,
|
|
859
|
+
toolApprovalResponses: new Map([
|
|
860
|
+
[
|
|
861
|
+
"approval-1",
|
|
862
|
+
{
|
|
863
|
+
approvalId: "approval-1",
|
|
864
|
+
approved: true,
|
|
865
|
+
optionId: "staging",
|
|
866
|
+
text: "only staging",
|
|
867
|
+
},
|
|
868
|
+
],
|
|
869
|
+
["approval-2", { approvalId: "approval-2", approved: true }],
|
|
870
|
+
["approval-3", { approvalId: "approval-3", approved: true }],
|
|
871
|
+
]),
|
|
872
|
+
};
|
|
873
|
+
const converted = AISDKMessageConverter.toThreadMessages(
|
|
874
|
+
[
|
|
875
|
+
{
|
|
876
|
+
id: "a1",
|
|
877
|
+
role: "assistant",
|
|
878
|
+
parts: [
|
|
879
|
+
{
|
|
880
|
+
type: "tool-deploy",
|
|
881
|
+
toolCallId: "tc-1",
|
|
882
|
+
state: "approval-requested",
|
|
883
|
+
input: {},
|
|
884
|
+
approval: {
|
|
885
|
+
id: "approval-1",
|
|
886
|
+
display: "select",
|
|
887
|
+
options: [{ id: "staging", kind: "_target" }],
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
type: "tool-deploy",
|
|
892
|
+
toolCallId: "tc-2",
|
|
893
|
+
state: "approval-responded",
|
|
894
|
+
input: {},
|
|
895
|
+
approval: { id: "approval-2", approved: false, reason: "no" },
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
type: "tool-deploy",
|
|
899
|
+
toolCallId: "tc-3",
|
|
900
|
+
state: "approval-requested",
|
|
901
|
+
input: {},
|
|
902
|
+
approval: { id: "approval-3", resolution: "expired" },
|
|
903
|
+
},
|
|
904
|
+
],
|
|
905
|
+
} as any,
|
|
906
|
+
],
|
|
907
|
+
false,
|
|
908
|
+
metadata,
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
const approvals = converted[0]?.content.map(
|
|
912
|
+
(part) => (part as { approval?: unknown }).approval,
|
|
913
|
+
);
|
|
914
|
+
expect(approvals).toEqual([
|
|
915
|
+
{
|
|
916
|
+
id: "approval-1",
|
|
917
|
+
display: "select",
|
|
918
|
+
options: [{ id: "staging", kind: "_target" }],
|
|
919
|
+
approved: true,
|
|
920
|
+
optionId: "staging",
|
|
921
|
+
text: "only staging",
|
|
922
|
+
},
|
|
923
|
+
{ id: "approval-2", approved: false, reason: "no" },
|
|
924
|
+
{ id: "approval-3", resolution: "expired" },
|
|
925
|
+
]);
|
|
926
|
+
});
|
|
927
|
+
|
|
490
928
|
it("drops a resolution the core contract does not declare", () => {
|
|
491
929
|
const converted = AISDKMessageConverter.toThreadMessages([
|
|
492
930
|
{
|
|
@@ -848,6 +1286,37 @@ describe("AISDKMessageConverter", () => {
|
|
|
848
1286
|
expect(call?.modelContent).toBeUndefined();
|
|
849
1287
|
});
|
|
850
1288
|
|
|
1289
|
+
it.each([
|
|
1290
|
+
["preliminary", true, true],
|
|
1291
|
+
["final", undefined, undefined],
|
|
1292
|
+
])(
|
|
1293
|
+
"marks a %s output-available part on the tool call",
|
|
1294
|
+
(_label, preliminary, isPreliminary) => {
|
|
1295
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
1296
|
+
{
|
|
1297
|
+
id: "a1",
|
|
1298
|
+
role: "assistant",
|
|
1299
|
+
parts: [
|
|
1300
|
+
{
|
|
1301
|
+
type: "tool-weather",
|
|
1302
|
+
toolCallId: "tc-1",
|
|
1303
|
+
state: "output-available",
|
|
1304
|
+
input: { city: "NYC" },
|
|
1305
|
+
output: { temp: 72 },
|
|
1306
|
+
...(preliminary !== undefined && { preliminary }),
|
|
1307
|
+
},
|
|
1308
|
+
],
|
|
1309
|
+
} as any,
|
|
1310
|
+
]);
|
|
1311
|
+
|
|
1312
|
+
const call = converted[0]?.content.find(
|
|
1313
|
+
(part): part is any => part.type === "tool-call",
|
|
1314
|
+
);
|
|
1315
|
+
expect(call?.result).toEqual({ temp: 72 });
|
|
1316
|
+
expect(call?.isPreliminary).toBe(isPreliminary);
|
|
1317
|
+
},
|
|
1318
|
+
);
|
|
1319
|
+
|
|
851
1320
|
it("forwards callProviderMetadata.mcp.app onto ToolCallMessagePart.mcp.app", () => {
|
|
852
1321
|
const converted = AISDKMessageConverter.toThreadMessages([
|
|
853
1322
|
{
|
|
@@ -1265,4 +1734,148 @@ describe("AISDKMessageConverter", () => {
|
|
|
1265
1734
|
data: { acme: { foo: "bar" } },
|
|
1266
1735
|
});
|
|
1267
1736
|
});
|
|
1737
|
+
|
|
1738
|
+
it("preserves failed tool-call arguments from rawInput in the error snapshot", () => {
|
|
1739
|
+
// A tool that streamed complete arguments then failed schema validation
|
|
1740
|
+
// keeps those arguments in `rawInput`, not `input`. Converting from `input`
|
|
1741
|
+
// alone yields `{}`, hiding the real failed input from the UI.
|
|
1742
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
1743
|
+
toolArgsKeyOrderCache: new Map(),
|
|
1744
|
+
};
|
|
1745
|
+
const converted = AISDKMessageConverter.toThreadMessages(
|
|
1746
|
+
[
|
|
1747
|
+
{
|
|
1748
|
+
id: "a1",
|
|
1749
|
+
role: "assistant",
|
|
1750
|
+
parts: [
|
|
1751
|
+
{
|
|
1752
|
+
type: "tool-weather",
|
|
1753
|
+
toolCallId: "tc-1",
|
|
1754
|
+
state: "output-error",
|
|
1755
|
+
rawInput: { city: "NYC", units: "F" },
|
|
1756
|
+
errorText: "arguments failed schema validation",
|
|
1757
|
+
},
|
|
1758
|
+
],
|
|
1759
|
+
},
|
|
1760
|
+
] as any,
|
|
1761
|
+
false,
|
|
1762
|
+
metadata,
|
|
1763
|
+
);
|
|
1764
|
+
|
|
1765
|
+
const toolCall = converted[0]?.content.find(
|
|
1766
|
+
(part): part is any => part.type === "tool-call",
|
|
1767
|
+
);
|
|
1768
|
+
expect(toolCall?.args).toEqual({ city: "NYC", units: "F" });
|
|
1769
|
+
expect(toolCall?.argsText).toBe('{"city":"NYC","units":"F"}');
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
it("releases the key-order entry once the tool call settles", () => {
|
|
1773
|
+
// Arrival order only matters while args stream; a settled input's own key
|
|
1774
|
+
// order is already deterministic, so the entry is released at settlement.
|
|
1775
|
+
const toolArgsKeyOrderCache: NonNullable<
|
|
1776
|
+
AISDKMessageConverterMetadata["toolArgsKeyOrderCache"]
|
|
1777
|
+
> = new Map();
|
|
1778
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
1779
|
+
toolArgsKeyOrderCache,
|
|
1780
|
+
toolArgsTextCache: new WeakMap(),
|
|
1781
|
+
};
|
|
1782
|
+
const part = (state: string) => ({
|
|
1783
|
+
type: "tool-weather",
|
|
1784
|
+
toolCallId: "tc-1",
|
|
1785
|
+
state,
|
|
1786
|
+
input: { a: 1 },
|
|
1787
|
+
...(state === "output-available" && { output: { ok: true } }),
|
|
1788
|
+
});
|
|
1789
|
+
const message = (state: string) =>
|
|
1790
|
+
[{ id: "a1", role: "assistant", parts: [part(state)] }] as any;
|
|
1791
|
+
|
|
1792
|
+
AISDKMessageConverter.toThreadMessages(
|
|
1793
|
+
message("input-streaming"),
|
|
1794
|
+
true,
|
|
1795
|
+
metadata,
|
|
1796
|
+
);
|
|
1797
|
+
expect(toolArgsKeyOrderCache.size).toBe(1);
|
|
1798
|
+
|
|
1799
|
+
AISDKMessageConverter.toThreadMessages(
|
|
1800
|
+
message("output-available"),
|
|
1801
|
+
false,
|
|
1802
|
+
metadata,
|
|
1803
|
+
);
|
|
1804
|
+
expect(toolArgsKeyOrderCache.size).toBe(0);
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
it("keeps frozen argsText per tool call when two calls settle on one shared input object", () => {
|
|
1808
|
+
// The frozen text depends on the call's streamed key order, so a shared
|
|
1809
|
+
// input object must not hand one call the other's text: that would turn
|
|
1810
|
+
// its streamed prefix into a non-prefix snapshot the tracker cannot close.
|
|
1811
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
1812
|
+
toolArgsKeyOrderCache: new Map(),
|
|
1813
|
+
toolArgsTextCache: new WeakMap(),
|
|
1814
|
+
};
|
|
1815
|
+
const tool = (toolCallId: string, state: string, input: object) => ({
|
|
1816
|
+
type: "tool-weather",
|
|
1817
|
+
toolCallId,
|
|
1818
|
+
state,
|
|
1819
|
+
input,
|
|
1820
|
+
...(state === "output-available" && { output: { ok: true } }),
|
|
1821
|
+
});
|
|
1822
|
+
const convert = (parts: object[]) =>
|
|
1823
|
+
AISDKMessageConverter.toThreadMessages(
|
|
1824
|
+
[{ id: "a1", role: "assistant", parts }] as any,
|
|
1825
|
+
false,
|
|
1826
|
+
metadata,
|
|
1827
|
+
)[0]!.content.filter((part): part is any => part.type === "tool-call");
|
|
1828
|
+
|
|
1829
|
+
convert([
|
|
1830
|
+
tool("tc-a", "input-streaming", { a: 1, b: 2 }),
|
|
1831
|
+
tool("tc-b", "input-streaming", { b: 2, a: 1 }),
|
|
1832
|
+
]);
|
|
1833
|
+
const shared = { a: 1, b: 2 };
|
|
1834
|
+
const settled = [
|
|
1835
|
+
tool("tc-a", "output-available", shared),
|
|
1836
|
+
tool("tc-b", "output-available", shared),
|
|
1837
|
+
];
|
|
1838
|
+
const [a, b] = convert(settled);
|
|
1839
|
+
|
|
1840
|
+
expect(a.argsText).toBe('{"a":1,"b":2}');
|
|
1841
|
+
expect(b.argsText).toBe('{"b":2,"a":1}');
|
|
1842
|
+
|
|
1843
|
+
// Both entries must survive a reconversion: the key-order entries are gone
|
|
1844
|
+
// by now, so a cache miss would re-serialize B in raw key order.
|
|
1845
|
+
stableStringifySpy.mockClear();
|
|
1846
|
+
const [a2, b2] = convert(settled);
|
|
1847
|
+
expect(a2.argsText).toBe('{"a":1,"b":2}');
|
|
1848
|
+
expect(b2.argsText).toBe('{"b":2,"a":1}');
|
|
1849
|
+
expect(stableStringifySpy).not.toHaveBeenCalled();
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
it("serializes a settled tool call's argsText once when its input identity is stable", () => {
|
|
1853
|
+
// The frozen text is keyed weakly by the input object itself, so it becomes
|
|
1854
|
+
// collectible once that object is unreachable instead of outliving it.
|
|
1855
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
1856
|
+
toolArgsKeyOrderCache: new Map(),
|
|
1857
|
+
toolArgsTextCache: new WeakMap(),
|
|
1858
|
+
};
|
|
1859
|
+
const messages = [
|
|
1860
|
+
{
|
|
1861
|
+
id: "a1",
|
|
1862
|
+
role: "assistant",
|
|
1863
|
+
parts: [
|
|
1864
|
+
{
|
|
1865
|
+
type: "tool-weather",
|
|
1866
|
+
toolCallId: "tc-1",
|
|
1867
|
+
state: "output-available",
|
|
1868
|
+
input: { a: 1, b: 2 },
|
|
1869
|
+
output: { ok: true },
|
|
1870
|
+
},
|
|
1871
|
+
],
|
|
1872
|
+
},
|
|
1873
|
+
] as any;
|
|
1874
|
+
|
|
1875
|
+
stableStringifySpy.mockClear();
|
|
1876
|
+
for (let i = 0; i < 3; i++) {
|
|
1877
|
+
AISDKMessageConverter.toThreadMessages(messages, false, metadata);
|
|
1878
|
+
}
|
|
1879
|
+
expect(stableStringifySpy).toHaveBeenCalledTimes(1);
|
|
1880
|
+
});
|
|
1268
1881
|
});
|