@assistant-ui/react-ai-sdk 1.3.23 → 1.3.26
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 +14 -40
- package/dist/frontendTools.d.ts +2 -6
- package/dist/frontendTools.d.ts.map +1 -1
- package/dist/frontendTools.js +35 -3
- package/dist/frontendTools.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/modelContentEnvelope.d.ts +14 -0
- package/dist/modelContentEnvelope.d.ts.map +1 -0
- package/dist/modelContentEnvelope.js +20 -0
- package/dist/modelContentEnvelope.js.map +1 -0
- package/dist/ui/resumable.d.ts +20 -0
- package/dist/ui/resumable.d.ts.map +1 -0
- package/dist/ui/resumable.js +25 -0
- package/dist/ui/resumable.js.map +1 -0
- package/dist/ui/use-chat/AssistantChatTransport.d.ts +7 -1
- package/dist/ui/use-chat/AssistantChatTransport.d.ts.map +1 -1
- package/dist/ui/use-chat/AssistantChatTransport.js +73 -2
- package/dist/ui/use-chat/AssistantChatTransport.js.map +1 -1
- package/dist/ui/use-chat/useAISDKRuntime.d.ts +17 -2
- package/dist/ui/use-chat/useAISDKRuntime.d.ts.map +1 -1
- package/dist/ui/use-chat/useAISDKRuntime.js +12 -2
- package/dist/ui/use-chat/useAISDKRuntime.js.map +1 -1
- package/dist/ui/use-chat/useChatRuntime.d.ts +2 -0
- package/dist/ui/use-chat/useChatRuntime.d.ts.map +1 -1
- package/dist/ui/use-chat/useChatRuntime.js +31 -1
- package/dist/ui/use-chat/useChatRuntime.js.map +1 -1
- package/dist/ui/utils/convertMessage.d.ts +4 -0
- package/dist/ui/utils/convertMessage.d.ts.map +1 -1
- package/dist/ui/utils/convertMessage.js +100 -5
- package/dist/ui/utils/convertMessage.js.map +1 -1
- package/package.json +4 -4
- package/src/frontendTools.test.ts +128 -0
- package/src/frontendTools.ts +41 -6
- package/src/index.ts +8 -0
- package/src/modelContentEnvelope.ts +39 -0
- package/src/ui/resumable.ts +42 -0
- package/src/ui/use-chat/AssistantChatTransport.ts +104 -3
- package/src/ui/use-chat/useAISDKRuntime.test.ts +50 -0
- package/src/ui/use-chat/useAISDKRuntime.ts +34 -1
- package/src/ui/use-chat/useChatRuntime.ts +38 -0
- package/src/ui/utils/convertMessage.test.ts +359 -0
- package/src/ui/utils/convertMessage.ts +125 -16
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
2
3
|
import { AISDKMessageConverter } from "./convertMessage";
|
|
3
4
|
|
|
4
5
|
describe("AISDKMessageConverter", () => {
|
|
@@ -37,6 +38,94 @@ describe("AISDKMessageConverter", () => {
|
|
|
37
38
|
expect(converted[0]?.attachments?.[1]?.type).toBe("file");
|
|
38
39
|
});
|
|
39
40
|
|
|
41
|
+
it("converts source-document parts into document sources", () => {
|
|
42
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
43
|
+
{
|
|
44
|
+
id: "a1",
|
|
45
|
+
role: "assistant",
|
|
46
|
+
parts: [
|
|
47
|
+
{
|
|
48
|
+
type: "source-document",
|
|
49
|
+
sourceId: "doc_123",
|
|
50
|
+
title: "proposal.pdf",
|
|
51
|
+
mediaType: "application/pdf",
|
|
52
|
+
filename: "proposal.pdf",
|
|
53
|
+
providerMetadata: {
|
|
54
|
+
openai: {
|
|
55
|
+
type: "file_citation",
|
|
56
|
+
fileId: "file_123",
|
|
57
|
+
index: 0,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
} as any,
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
expect(converted).toHaveLength(1);
|
|
66
|
+
expect(converted[0]?.role).toBe("assistant");
|
|
67
|
+
|
|
68
|
+
const sourcePart = converted[0]?.content.find(
|
|
69
|
+
(part): part is any => part.type === "source",
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(sourcePart).toMatchObject({
|
|
73
|
+
type: "source",
|
|
74
|
+
sourceType: "document",
|
|
75
|
+
id: "doc_123",
|
|
76
|
+
title: "proposal.pdf",
|
|
77
|
+
mediaType: "application/pdf",
|
|
78
|
+
filename: "proposal.pdf",
|
|
79
|
+
providerMetadata: {
|
|
80
|
+
openai: {
|
|
81
|
+
type: "file_citation",
|
|
82
|
+
fileId: "file_123",
|
|
83
|
+
index: 0,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("converts source-url parts without synthesizing missing optional fields", () => {
|
|
90
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
91
|
+
{
|
|
92
|
+
id: "a1",
|
|
93
|
+
role: "assistant",
|
|
94
|
+
parts: [
|
|
95
|
+
{
|
|
96
|
+
type: "source-url",
|
|
97
|
+
sourceId: "url_123",
|
|
98
|
+
url: "https://example.com/report",
|
|
99
|
+
providerMetadata: {
|
|
100
|
+
openai: {
|
|
101
|
+
type: "url_citation",
|
|
102
|
+
index: 1,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
} as any,
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
const sourcePart = converted[0]?.content.find(
|
|
111
|
+
(part): part is any => part.type === "source",
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(sourcePart).toMatchObject({
|
|
115
|
+
type: "source",
|
|
116
|
+
sourceType: "url",
|
|
117
|
+
id: "url_123",
|
|
118
|
+
url: "https://example.com/report",
|
|
119
|
+
providerMetadata: {
|
|
120
|
+
openai: {
|
|
121
|
+
type: "url_citation",
|
|
122
|
+
index: 1,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
expect(sourcePart).not.toHaveProperty("title");
|
|
127
|
+
});
|
|
128
|
+
|
|
40
129
|
it("converts assistant image file parts into file content", () => {
|
|
41
130
|
const converted = AISDKMessageConverter.toThreadMessages([
|
|
42
131
|
{
|
|
@@ -307,4 +396,274 @@ describe("AISDKMessageConverter", () => {
|
|
|
307
396
|
limit: 5,
|
|
308
397
|
});
|
|
309
398
|
});
|
|
399
|
+
|
|
400
|
+
it("preserves last good input when AI SDK briefly emits null input", () => {
|
|
401
|
+
const metadata = {
|
|
402
|
+
toolArgsKeyOrderCache: new Map<string, Map<string, string[]>>(),
|
|
403
|
+
toolLastInputCache: new Map<string, ReadonlyJSONObject>(),
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const convertWithInput = (input: unknown) =>
|
|
407
|
+
AISDKMessageConverter.toThreadMessages(
|
|
408
|
+
[
|
|
409
|
+
{
|
|
410
|
+
id: "a1",
|
|
411
|
+
role: "assistant",
|
|
412
|
+
parts: [
|
|
413
|
+
{
|
|
414
|
+
type: "tool-weather",
|
|
415
|
+
toolCallId: "tc-1",
|
|
416
|
+
state: "input-streaming",
|
|
417
|
+
input,
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
} as any,
|
|
421
|
+
],
|
|
422
|
+
false,
|
|
423
|
+
metadata,
|
|
424
|
+
)[0]?.content.find((part): part is any => part.type === "tool-call");
|
|
425
|
+
|
|
426
|
+
const first = convertWithInput({ city: "NYC" });
|
|
427
|
+
expect(first?.argsText).toBe('{"city":"NYC');
|
|
428
|
+
expect(first?.args).toEqual({ city: "NYC" });
|
|
429
|
+
|
|
430
|
+
const dropped = convertWithInput(null);
|
|
431
|
+
expect(dropped?.argsText).toBe('{"city":"NYC');
|
|
432
|
+
expect(dropped?.args).toEqual({ city: "NYC" });
|
|
433
|
+
|
|
434
|
+
const undef = convertWithInput(undefined);
|
|
435
|
+
expect(undef?.argsText).toBe('{"city":"NYC');
|
|
436
|
+
expect(undef?.args).toEqual({ city: "NYC" });
|
|
437
|
+
|
|
438
|
+
const grown = convertWithInput({ city: "NYC", units: "F" });
|
|
439
|
+
expect(grown?.argsText).toBe('{"city":"NYC","units":"F');
|
|
440
|
+
expect(grown?.args).toEqual({ city: "NYC", units: "F" });
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("preserves last good input across terminal state transitions", () => {
|
|
444
|
+
const metadata = {
|
|
445
|
+
toolArgsKeyOrderCache: new Map<string, Map<string, string[]>>(),
|
|
446
|
+
toolLastInputCache: new Map<string, ReadonlyJSONObject>(),
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
AISDKMessageConverter.toThreadMessages(
|
|
450
|
+
[
|
|
451
|
+
{
|
|
452
|
+
id: "a1",
|
|
453
|
+
role: "assistant",
|
|
454
|
+
parts: [
|
|
455
|
+
{
|
|
456
|
+
type: "tool-weather",
|
|
457
|
+
toolCallId: "tc-1",
|
|
458
|
+
state: "input-available",
|
|
459
|
+
input: { city: "NYC" },
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
} as any,
|
|
463
|
+
],
|
|
464
|
+
false,
|
|
465
|
+
metadata,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
const terminal = AISDKMessageConverter.toThreadMessages(
|
|
469
|
+
[
|
|
470
|
+
{
|
|
471
|
+
id: "a1",
|
|
472
|
+
role: "assistant",
|
|
473
|
+
parts: [
|
|
474
|
+
{
|
|
475
|
+
type: "tool-weather",
|
|
476
|
+
toolCallId: "tc-1",
|
|
477
|
+
state: "output-available",
|
|
478
|
+
input: null,
|
|
479
|
+
output: { temp: 70 },
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
} as any,
|
|
483
|
+
],
|
|
484
|
+
false,
|
|
485
|
+
metadata,
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
const call = terminal[0]?.content.find(
|
|
489
|
+
(part): part is any => part.type === "tool-call",
|
|
490
|
+
);
|
|
491
|
+
expect(call?.args).toEqual({ city: "NYC" });
|
|
492
|
+
expect(call?.result).toEqual({ temp: 70 });
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("unwraps the modelContent envelope produced by frontend tool execution", () => {
|
|
496
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
497
|
+
{
|
|
498
|
+
id: "a1",
|
|
499
|
+
role: "assistant",
|
|
500
|
+
parts: [
|
|
501
|
+
{
|
|
502
|
+
type: "tool-readPdf",
|
|
503
|
+
toolCallId: "tc-pdf",
|
|
504
|
+
state: "output-available",
|
|
505
|
+
input: {},
|
|
506
|
+
output: {
|
|
507
|
+
__aui_modelContent: [
|
|
508
|
+
{ type: "text", text: "PDF contents:" },
|
|
509
|
+
{
|
|
510
|
+
type: "file",
|
|
511
|
+
data: "JVBERi0xLjQK",
|
|
512
|
+
mediaType: "application/pdf",
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
value: { mediaType: "application/pdf", base64: "JVBERi0xLjQK" },
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
} as any,
|
|
520
|
+
]);
|
|
521
|
+
|
|
522
|
+
const call = converted[0]?.content.find(
|
|
523
|
+
(part): part is any => part.type === "tool-call",
|
|
524
|
+
);
|
|
525
|
+
expect(call?.result).toEqual({
|
|
526
|
+
mediaType: "application/pdf",
|
|
527
|
+
base64: "JVBERi0xLjQK",
|
|
528
|
+
});
|
|
529
|
+
expect(call?.modelContent).toEqual([
|
|
530
|
+
{ type: "text", text: "PDF contents:" },
|
|
531
|
+
{
|
|
532
|
+
type: "file",
|
|
533
|
+
data: "JVBERi0xLjQK",
|
|
534
|
+
mediaType: "application/pdf",
|
|
535
|
+
},
|
|
536
|
+
]);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("leaves a plain output untouched when no envelope is present", () => {
|
|
540
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
541
|
+
{
|
|
542
|
+
id: "a1",
|
|
543
|
+
role: "assistant",
|
|
544
|
+
parts: [
|
|
545
|
+
{
|
|
546
|
+
type: "tool-weather",
|
|
547
|
+
toolCallId: "tc-1",
|
|
548
|
+
state: "output-available",
|
|
549
|
+
input: { city: "NYC" },
|
|
550
|
+
output: { temp: 72 },
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
} as any,
|
|
554
|
+
]);
|
|
555
|
+
|
|
556
|
+
const call = converted[0]?.content.find(
|
|
557
|
+
(part): part is any => part.type === "tool-call",
|
|
558
|
+
);
|
|
559
|
+
expect(call?.result).toEqual({ temp: 72 });
|
|
560
|
+
expect(call?.modelContent).toBeUndefined();
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("forwards callProviderMetadata.mcp.app onto ToolCallMessagePart.mcp.app", () => {
|
|
564
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
565
|
+
{
|
|
566
|
+
id: "a1",
|
|
567
|
+
role: "assistant",
|
|
568
|
+
parts: [
|
|
569
|
+
{
|
|
570
|
+
type: "tool-search",
|
|
571
|
+
toolCallId: "tc-1",
|
|
572
|
+
state: "output-available",
|
|
573
|
+
input: { query: "hi" },
|
|
574
|
+
output: { results: [] },
|
|
575
|
+
callProviderMetadata: {
|
|
576
|
+
mcp: {
|
|
577
|
+
app: {
|
|
578
|
+
resourceUri: "ui://example/search",
|
|
579
|
+
mimeType: "text/html;profile=mcp-app",
|
|
580
|
+
visibility: ["app", "model", "bogus"],
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
} as any,
|
|
587
|
+
]);
|
|
588
|
+
|
|
589
|
+
const call = converted[0]?.content.find(
|
|
590
|
+
(part): part is any => part.type === "tool-call",
|
|
591
|
+
);
|
|
592
|
+
expect(call?.mcp?.app).toEqual({
|
|
593
|
+
resourceUri: "ui://example/search",
|
|
594
|
+
mimeType: "text/html;profile=mcp-app",
|
|
595
|
+
visibility: ["app", "model"],
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it("extracts MCP app metadata from output._meta['ui/resourceUri']", () => {
|
|
600
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
601
|
+
{
|
|
602
|
+
id: "a1",
|
|
603
|
+
role: "assistant",
|
|
604
|
+
parts: [
|
|
605
|
+
{
|
|
606
|
+
type: "tool-hello_ui",
|
|
607
|
+
toolCallId: "tc-1",
|
|
608
|
+
state: "output-available",
|
|
609
|
+
input: {},
|
|
610
|
+
output: {
|
|
611
|
+
_meta: { "ui/resourceUri": "ui://app/hello_ui.html" },
|
|
612
|
+
content: [{ type: "text", text: "" }],
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
} as any,
|
|
617
|
+
]);
|
|
618
|
+
|
|
619
|
+
const call = converted[0]?.content.find(
|
|
620
|
+
(part): part is any => part.type === "tool-call",
|
|
621
|
+
);
|
|
622
|
+
expect(call?.mcp?.app).toEqual({
|
|
623
|
+
resourceUri: "ui://app/hello_ui.html",
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("memoizes MCP app metadata across conversions by resourceUri", () => {
|
|
628
|
+
const metadata = {
|
|
629
|
+
mcpAppMetadataCache: new Map(),
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const buildMessage = (id: string) => ({
|
|
633
|
+
id,
|
|
634
|
+
role: "assistant" as const,
|
|
635
|
+
parts: [
|
|
636
|
+
{
|
|
637
|
+
type: "tool-search",
|
|
638
|
+
toolCallId: `${id}-call`,
|
|
639
|
+
state: "output-available",
|
|
640
|
+
input: { q: "hi" },
|
|
641
|
+
output: {},
|
|
642
|
+
callProviderMetadata: {
|
|
643
|
+
mcp: { app: { resourceUri: "ui://example/search" } },
|
|
644
|
+
},
|
|
645
|
+
} as any,
|
|
646
|
+
],
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
const first = AISDKMessageConverter.toThreadMessages(
|
|
650
|
+
[buildMessage("a1")],
|
|
651
|
+
false,
|
|
652
|
+
metadata,
|
|
653
|
+
);
|
|
654
|
+
const second = AISDKMessageConverter.toThreadMessages(
|
|
655
|
+
[buildMessage("a2")],
|
|
656
|
+
false,
|
|
657
|
+
metadata,
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
const firstApp = first[0]?.content.find(
|
|
661
|
+
(p): p is any => p.type === "tool-call",
|
|
662
|
+
)?.mcp?.app;
|
|
663
|
+
const secondApp = second[0]?.content.find(
|
|
664
|
+
(p): p is any => p.type === "tool-call",
|
|
665
|
+
)?.mcp?.app;
|
|
666
|
+
expect(firstApp).toBeDefined();
|
|
667
|
+
expect(firstApp).toBe(secondApp);
|
|
668
|
+
});
|
|
310
669
|
});
|
|
@@ -3,27 +3,93 @@ import {
|
|
|
3
3
|
createMessageConverter as unstable_createMessageConverter,
|
|
4
4
|
type useExternalMessageConverter,
|
|
5
5
|
} from "@assistant-ui/core/react";
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
import {
|
|
7
|
+
isMcpAppUri,
|
|
8
|
+
type ReasoningMessagePart,
|
|
9
|
+
type ToolCallMessagePart,
|
|
10
|
+
type TextMessagePart,
|
|
11
|
+
type DataMessagePart,
|
|
12
|
+
type SourceMessagePart,
|
|
13
|
+
type SourceProviderMetadata,
|
|
14
|
+
type FileMessagePart,
|
|
15
|
+
type ThreadMessageLike,
|
|
16
|
+
type McpAppMetadata,
|
|
14
17
|
} from "@assistant-ui/core";
|
|
15
18
|
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
19
|
+
import { unwrapModelContentEnvelope } from "../../modelContentEnvelope";
|
|
16
20
|
|
|
17
21
|
type MessageMetadata = ThreadMessageLike["metadata"];
|
|
18
22
|
export type AISDKMessageConverterMetadata =
|
|
19
23
|
useExternalMessageConverter.Metadata & {
|
|
20
24
|
toolArgsKeyOrderCache?: Map<string, Map<string, string[]>>;
|
|
25
|
+
toolLastInputCache?: Map<string, ReadonlyJSONObject>;
|
|
26
|
+
mcpAppMetadataCache?: Map<string, McpAppMetadata>;
|
|
21
27
|
};
|
|
22
28
|
|
|
23
29
|
function stripClosingDelimiters(json: string): string {
|
|
24
30
|
return json.replace(/[}\]"]+$/, "");
|
|
25
31
|
}
|
|
26
32
|
|
|
33
|
+
const MCP_APP_METADATA_CACHE_MAX = 100;
|
|
34
|
+
|
|
35
|
+
function extractMcpAppMetadata(
|
|
36
|
+
part: unknown,
|
|
37
|
+
cache: Map<string, McpAppMetadata> | undefined,
|
|
38
|
+
): McpAppMetadata | undefined {
|
|
39
|
+
if (!part || typeof part !== "object") return undefined;
|
|
40
|
+
const meta = (part as { callProviderMetadata?: unknown })
|
|
41
|
+
.callProviderMetadata;
|
|
42
|
+
const mcp =
|
|
43
|
+
meta && typeof meta === "object"
|
|
44
|
+
? (meta as { mcp?: unknown }).mcp
|
|
45
|
+
: undefined;
|
|
46
|
+
const app =
|
|
47
|
+
mcp && typeof mcp === "object" ? (mcp as { app?: unknown }).app : undefined;
|
|
48
|
+
let a: Record<string, unknown>;
|
|
49
|
+
if (app && typeof app === "object") {
|
|
50
|
+
a = app as Record<string, unknown>;
|
|
51
|
+
} else {
|
|
52
|
+
// MCP-UI tools (e.g. xmcp) surface the UI pointer as
|
|
53
|
+
// result._meta["ui/resourceUri"] rather than in callProviderMetadata.
|
|
54
|
+
const output = (part as { output?: unknown }).output;
|
|
55
|
+
const outMeta =
|
|
56
|
+
output && typeof output === "object"
|
|
57
|
+
? (output as { _meta?: unknown })._meta
|
|
58
|
+
: undefined;
|
|
59
|
+
const uiResourceUri =
|
|
60
|
+
outMeta && typeof outMeta === "object"
|
|
61
|
+
? (outMeta as Record<string, unknown>)["ui/resourceUri"]
|
|
62
|
+
: undefined;
|
|
63
|
+
if (typeof uiResourceUri !== "string") return undefined;
|
|
64
|
+
a = { resourceUri: uiResourceUri };
|
|
65
|
+
}
|
|
66
|
+
if (typeof a["resourceUri"] !== "string") return undefined;
|
|
67
|
+
if (!isMcpAppUri(a["resourceUri"])) return undefined;
|
|
68
|
+
const cached = cache?.get(a["resourceUri"]);
|
|
69
|
+
if (cached) {
|
|
70
|
+
cache!.delete(a["resourceUri"]);
|
|
71
|
+
cache!.set(a["resourceUri"], cached);
|
|
72
|
+
return cached;
|
|
73
|
+
}
|
|
74
|
+
const out: { -readonly [K in keyof McpAppMetadata]: McpAppMetadata[K] } = {
|
|
75
|
+
resourceUri: a["resourceUri"],
|
|
76
|
+
};
|
|
77
|
+
if (typeof a["mimeType"] === "string") out.mimeType = a["mimeType"];
|
|
78
|
+
if (Array.isArray(a["visibility"])) {
|
|
79
|
+
out.visibility = a["visibility"].filter(
|
|
80
|
+
(v): v is "model" | "app" => v === "model" || v === "app",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (cache) {
|
|
84
|
+
if (cache.size >= MCP_APP_METADATA_CACHE_MAX) {
|
|
85
|
+
const oldest = cache.keys().next().value;
|
|
86
|
+
if (oldest !== undefined) cache.delete(oldest);
|
|
87
|
+
}
|
|
88
|
+
cache.set(a["resourceUri"], out);
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
27
93
|
const hasOwn = (value: object, key: string) => Object.hasOwn(value, key);
|
|
28
94
|
|
|
29
95
|
const stabilizeToolArgsValue = (
|
|
@@ -147,14 +213,28 @@ function convertParts(
|
|
|
147
213
|
const toolName = getToolName(part);
|
|
148
214
|
const toolCallId = part.toolCallId;
|
|
149
215
|
const argsKeyOrderCacheKey = `${message.id}:${toolCallId}`;
|
|
150
|
-
|
|
151
|
-
|
|
216
|
+
|
|
217
|
+
const rawInput = part.input as ReadonlyJSONObject | null | undefined;
|
|
218
|
+
let args: ReadonlyJSONObject;
|
|
219
|
+
if (
|
|
220
|
+
rawInput != null &&
|
|
221
|
+
typeof rawInput === "object" &&
|
|
222
|
+
!Array.isArray(rawInput)
|
|
223
|
+
) {
|
|
224
|
+
args = rawInput;
|
|
225
|
+
metadata.toolLastInputCache?.set(argsKeyOrderCacheKey, args);
|
|
226
|
+
} else {
|
|
227
|
+
args = metadata.toolLastInputCache?.get(argsKeyOrderCacheKey) ?? {};
|
|
228
|
+
}
|
|
152
229
|
|
|
153
230
|
let result: unknown;
|
|
231
|
+
let modelContent: ToolCallMessagePart["modelContent"];
|
|
154
232
|
let isError = false;
|
|
155
233
|
|
|
156
234
|
if (part.state === "output-available") {
|
|
157
|
-
|
|
235
|
+
const unwrapped = unwrapModelContentEnvelope(part.output);
|
|
236
|
+
result = unwrapped.result;
|
|
237
|
+
modelContent = unwrapped.modelContent;
|
|
158
238
|
} else if (part.state === "output-error") {
|
|
159
239
|
isError = true;
|
|
160
240
|
result = { error: part.errorText };
|
|
@@ -177,9 +257,20 @@ function convertParts(
|
|
|
177
257
|
argsText = stripClosingDelimiters(argsText);
|
|
178
258
|
} else {
|
|
179
259
|
metadata.toolArgsKeyOrderCache?.delete(argsKeyOrderCacheKey);
|
|
260
|
+
if (
|
|
261
|
+
part.state === "output-available" ||
|
|
262
|
+
part.state === "output-error" ||
|
|
263
|
+
part.state === "output-denied"
|
|
264
|
+
) {
|
|
265
|
+
metadata.toolLastInputCache?.delete(argsKeyOrderCacheKey);
|
|
266
|
+
}
|
|
180
267
|
}
|
|
181
268
|
|
|
182
269
|
const toolStatus = metadata.toolStatuses?.[toolCallId];
|
|
270
|
+
const mcpApp = extractMcpAppMetadata(
|
|
271
|
+
part,
|
|
272
|
+
metadata.mcpAppMetadataCache,
|
|
273
|
+
);
|
|
183
274
|
return {
|
|
184
275
|
type: "tool-call",
|
|
185
276
|
toolName,
|
|
@@ -188,6 +279,8 @@ function convertParts(
|
|
|
188
279
|
args,
|
|
189
280
|
result,
|
|
190
281
|
isError,
|
|
282
|
+
...(modelContent !== undefined && { modelContent }),
|
|
283
|
+
...(mcpApp && { mcp: { app: mcpApp } }),
|
|
191
284
|
...getToolInterrupt(part, toolStatus),
|
|
192
285
|
} satisfies ToolCallMessagePart;
|
|
193
286
|
}
|
|
@@ -198,7 +291,13 @@ function convertParts(
|
|
|
198
291
|
sourceType: "url",
|
|
199
292
|
id: part.sourceId,
|
|
200
293
|
url: part.url,
|
|
201
|
-
title: part.title
|
|
294
|
+
...(part.title != null ? { title: part.title } : undefined),
|
|
295
|
+
...(part.providerMetadata != null
|
|
296
|
+
? {
|
|
297
|
+
providerMetadata:
|
|
298
|
+
part.providerMetadata as SourceProviderMetadata,
|
|
299
|
+
}
|
|
300
|
+
: undefined),
|
|
202
301
|
} satisfies SourceMessagePart;
|
|
203
302
|
}
|
|
204
303
|
|
|
@@ -212,10 +311,20 @@ function convertParts(
|
|
|
212
311
|
}
|
|
213
312
|
|
|
214
313
|
if (part.type === "source-document") {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
314
|
+
return {
|
|
315
|
+
type: "source",
|
|
316
|
+
sourceType: "document",
|
|
317
|
+
id: part.sourceId,
|
|
318
|
+
title: part.title,
|
|
319
|
+
mediaType: part.mediaType,
|
|
320
|
+
...(part.filename != null ? { filename: part.filename } : undefined),
|
|
321
|
+
...(part.providerMetadata != null
|
|
322
|
+
? {
|
|
323
|
+
providerMetadata:
|
|
324
|
+
part.providerMetadata as SourceProviderMetadata,
|
|
325
|
+
}
|
|
326
|
+
: undefined),
|
|
327
|
+
} satisfies SourceMessagePart;
|
|
219
328
|
}
|
|
220
329
|
|
|
221
330
|
if (part.type.startsWith("data-")) {
|