@assistant-ui/ai-sdk 0.0.7 → 0.0.9
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/LICENSE +1 -1
- 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 +1 -3
- 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 +4 -6
- package/dist/aiSDKExtras.d.ts.map +1 -1
- package/dist/converters/convertMessage.d.ts +34 -24
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +56 -12
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/converters/modelContentEnvelope.d.ts +6 -7
- package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.d.ts +2 -4
- package/dist/converters/toCreateMessage.d.ts.map +1 -1
- package/dist/converters/toolOutputConversion.d.ts +20 -22
- package/dist/converters/toolOutputConversion.d.ts.map +1 -1
- package/dist/hooks.d.ts +2 -4
- 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 +2 -4
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +1 -3
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.d.ts +2 -4
- package/dist/runtime/AISDKChat.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.js +1 -1
- package/dist/runtime/AISDKChat.js.map +1 -1
- package/dist/runtime/AISDKThreads.d.ts +29 -31
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +22 -23
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.d.ts +1 -3
- package/dist/runtime/sdkIdentity.d.ts.map +1 -1
- package/dist/runtime/sdkIdentity.js +1 -1
- package/dist/runtime/useAISDKRuntime.d.ts +76 -70
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +311 -77
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts +6 -8
- 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 +94 -89
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +44 -15
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts +7 -7
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +231 -60
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.d.ts +1 -3
- 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 +3 -5
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
- package/dist/runtime/useStreamingTiming.js +1 -1
- package/dist/tools/frontendTools.d.ts +21 -23
- package/dist/tools/frontendTools.d.ts.map +1 -1
- package/dist/tools/generativeTools.d.ts +24 -26
- 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 -4
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.d.ts +14 -16
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.js +9 -2
- package/dist/transport/AssistantChatTransport.js.map +1 -1
- package/dist/transport/resumable.d.ts +25 -27
- package/dist/transport/resumable.d.ts.map +1 -1
- package/dist/usage.d.ts +8 -10
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js.map +1 -1
- package/dist/utils/getVercelAIMessages.d.ts +2 -4
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
- package/dist/utils/sliceMessagesUntil.d.ts +1 -3
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
- package/package.json +13 -9
- package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
- package/src/adapters/vercelAttachmentAdapter.ts +2 -2
- package/src/converters/convertMessage.test.ts +541 -3
- package/src/converters/convertMessage.ts +123 -15
- package/src/index.native.ts +1 -1
- package/src/index.ts +1 -1
- package/src/runtime/AISDKChat.ts +0 -4
- package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
- package/src/runtime/AISDKThreads.test.ts +105 -46
- package/src/runtime/AISDKThreads.ts +1 -1
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1639 -19
- package/src/runtime/useAISDKRuntime.approval.test.tsx +252 -9
- package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +182 -0
- package/src/runtime/useAISDKRuntime.test.ts +275 -5
- package/src/runtime/useAISDKRuntime.ts +533 -60
- package/src/runtime/useAISDKRuntime.voice.test.tsx +50 -1
- package/src/runtime/useChatRuntime.fast-refresh.test.tsx +94 -0
- package/src/runtime/useChatRuntime.integration.test.tsx +143 -65
- package/src/runtime/useChatRuntime.test.ts +11 -7
- package/src/runtime/useChatRuntime.ts +6 -2
- package/src/runtime/useChatThread.test.ts +166 -2
- package/src/runtime/useChatThread.transport.test.tsx +152 -0
- package/src/runtime/useChatThread.ts +89 -22
- package/src/runtime/useExternalHistory.test.ts +943 -3
- package/src/runtime/useExternalHistory.ts +378 -15
- package/src/runtime/useResourceCleanup.ts +41 -15
- package/src/transport/AssistantChatTransport.test.ts +164 -0
- package/src/transport/AssistantChatTransport.ts +22 -2
- package/src/usage.ts +1 -1
|
@@ -10,6 +10,8 @@ import type {
|
|
|
10
10
|
ThreadAssistantMessage,
|
|
11
11
|
ThreadHistoryAdapter,
|
|
12
12
|
ThreadMessage,
|
|
13
|
+
RespondToToolApprovalOptions,
|
|
14
|
+
Unstable_ToolInteractionLog,
|
|
13
15
|
} from "@assistant-ui/core";
|
|
14
16
|
|
|
15
17
|
const mocks = vi.hoisted(() => ({
|
|
@@ -349,7 +351,12 @@ describe("useExternalHistory persistence", () => {
|
|
|
349
351
|
mocks.listeners.clear();
|
|
350
352
|
});
|
|
351
353
|
|
|
352
|
-
type InnerMessage = {
|
|
354
|
+
type InnerMessage = {
|
|
355
|
+
id: string;
|
|
356
|
+
parts: unknown[];
|
|
357
|
+
role?: string;
|
|
358
|
+
metadata?: Record<string, unknown>;
|
|
359
|
+
};
|
|
353
360
|
|
|
354
361
|
const persistenceStorageFormat: MessageFormatAdapter<
|
|
355
362
|
InnerMessage,
|
|
@@ -393,6 +400,13 @@ describe("useExternalHistory persistence", () => {
|
|
|
393
400
|
loadMessages?: MessageFormatRepository<InnerMessage>;
|
|
394
401
|
toThreadMessages?: (messages: InnerMessage[]) => ThreadMessage[];
|
|
395
402
|
initialIsRunning?: boolean;
|
|
403
|
+
onSetMessages?: (messages: InnerMessage[]) => void;
|
|
404
|
+
toolArtifacts?: Map<string, unknown>;
|
|
405
|
+
onToolArtifactsRestored?: () => void;
|
|
406
|
+
toolInteractions?: Map<string, Unstable_ToolInteractionLog>;
|
|
407
|
+
onToolInteractionsRestored?: () => void;
|
|
408
|
+
toolApprovalResponses?: Map<string, RespondToToolApprovalOptions>;
|
|
409
|
+
onToolApprovalResponsesRestored?: () => void;
|
|
396
410
|
},
|
|
397
411
|
) => {
|
|
398
412
|
const append = vi.fn(
|
|
@@ -430,13 +444,14 @@ describe("useExternalHistory persistence", () => {
|
|
|
430
444
|
let isRunning = options?.initialIsRunning ?? false;
|
|
431
445
|
let messages: ThreadMessage[] = [];
|
|
432
446
|
const getState = vi.fn(() => ({ isRunning, messages }));
|
|
447
|
+
const importMessages = vi.fn();
|
|
433
448
|
const thread = {
|
|
434
449
|
subscribe: (nextListener: () => void) => {
|
|
435
450
|
listener = nextListener;
|
|
436
451
|
return () => {};
|
|
437
452
|
},
|
|
438
453
|
getState,
|
|
439
|
-
import:
|
|
454
|
+
import: importMessages,
|
|
440
455
|
export: vi.fn(() => ({ headId: null, messages: [] })),
|
|
441
456
|
} as unknown as AssistantRuntime["thread"];
|
|
442
457
|
const persistenceRuntimeRef = {
|
|
@@ -452,7 +467,13 @@ describe("useExternalHistory persistence", () => {
|
|
|
452
467
|
historyAdapter,
|
|
453
468
|
options?.toThreadMessages ?? (() => []),
|
|
454
469
|
persistenceStorageFormat,
|
|
455
|
-
() => {},
|
|
470
|
+
options?.onSetMessages ?? (() => {}),
|
|
471
|
+
options?.toolArtifacts,
|
|
472
|
+
options?.onToolArtifactsRestored,
|
|
473
|
+
options?.toolInteractions,
|
|
474
|
+
options?.onToolInteractionsRestored,
|
|
475
|
+
options?.toolApprovalResponses,
|
|
476
|
+
options?.onToolApprovalResponsesRestored,
|
|
456
477
|
),
|
|
457
478
|
);
|
|
458
479
|
|
|
@@ -488,9 +509,12 @@ describe("useExternalHistory persistence", () => {
|
|
|
488
509
|
update,
|
|
489
510
|
deleteItems,
|
|
490
511
|
formattedAdapter,
|
|
512
|
+
importMessages,
|
|
491
513
|
deleteMessage: result.current.deleteMessage,
|
|
492
514
|
reportTelemetry,
|
|
493
515
|
load,
|
|
516
|
+
persistToolInteractions: result.current.persistToolInteractions,
|
|
517
|
+
persistToolApprovalResponses: result.current.persistToolApprovalResponses,
|
|
494
518
|
runCycle,
|
|
495
519
|
flush,
|
|
496
520
|
step,
|
|
@@ -498,6 +522,40 @@ describe("useExternalHistory persistence", () => {
|
|
|
498
522
|
};
|
|
499
523
|
};
|
|
500
524
|
|
|
525
|
+
it("clears tool data and invokes restore callbacks when loaded history is empty", async () => {
|
|
526
|
+
const toolArtifacts = new Map<string, unknown>([
|
|
527
|
+
["call-1", { preview: "72°F and sunny" }],
|
|
528
|
+
]);
|
|
529
|
+
const toolInteractions = new Map<string, Unstable_ToolInteractionLog>([
|
|
530
|
+
["call-1", { entries: [{ type: "action", occurredAt: 1, payload: {} }] }],
|
|
531
|
+
]);
|
|
532
|
+
const toolApprovalResponses = new Map<string, RespondToToolApprovalOptions>(
|
|
533
|
+
[["approval-1", { approvalId: "approval-1", approved: true }]],
|
|
534
|
+
);
|
|
535
|
+
const onToolArtifactsRestored = vi.fn();
|
|
536
|
+
const onToolInteractionsRestored = vi.fn();
|
|
537
|
+
const onToolApprovalResponsesRestored = vi.fn();
|
|
538
|
+
const { importMessages, load } = createPersistenceHarness(false, {
|
|
539
|
+
loadMessages: { messages: [] },
|
|
540
|
+
toolArtifacts,
|
|
541
|
+
onToolArtifactsRestored,
|
|
542
|
+
toolInteractions,
|
|
543
|
+
onToolInteractionsRestored,
|
|
544
|
+
toolApprovalResponses,
|
|
545
|
+
onToolApprovalResponsesRestored,
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
549
|
+
|
|
550
|
+
expect(toolArtifacts).toEqual(new Map());
|
|
551
|
+
expect(toolInteractions).toEqual(new Map());
|
|
552
|
+
expect(toolApprovalResponses).toEqual(new Map());
|
|
553
|
+
expect(onToolArtifactsRestored).toHaveBeenCalledOnce();
|
|
554
|
+
expect(onToolInteractionsRestored).toHaveBeenCalledOnce();
|
|
555
|
+
expect(onToolApprovalResponsesRestored).toHaveBeenCalledOnce();
|
|
556
|
+
expect(importMessages).not.toHaveBeenCalled();
|
|
557
|
+
});
|
|
558
|
+
|
|
501
559
|
it("persists a message that lands while the thread is idle", async () => {
|
|
502
560
|
const { append, step } = createPersistenceHarness(false);
|
|
503
561
|
const message = createAssistantMessage(
|
|
@@ -514,6 +572,888 @@ describe("useExternalHistory persistence", () => {
|
|
|
514
572
|
});
|
|
515
573
|
});
|
|
516
574
|
|
|
575
|
+
it("updates stored tool artifacts without mutating chat messages", async () => {
|
|
576
|
+
const toolArtifacts = new Map<string, unknown>();
|
|
577
|
+
const { append, update, runCycle } = createPersistenceHarness(true, {
|
|
578
|
+
toolArtifacts,
|
|
579
|
+
});
|
|
580
|
+
const innerMessage: InnerMessage = {
|
|
581
|
+
id: "inner-a",
|
|
582
|
+
role: "assistant",
|
|
583
|
+
parts: [
|
|
584
|
+
{
|
|
585
|
+
type: "tool-weather",
|
|
586
|
+
toolCallId: "call-1",
|
|
587
|
+
state: "output-available",
|
|
588
|
+
input: { city: "New York" },
|
|
589
|
+
output: { temperature: 72 },
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
};
|
|
593
|
+
const content: ThreadAssistantMessage["content"] = [
|
|
594
|
+
{
|
|
595
|
+
type: "tool-call",
|
|
596
|
+
toolCallId: "call-1",
|
|
597
|
+
toolName: "weather",
|
|
598
|
+
args: { city: "New York" },
|
|
599
|
+
argsText: '{"city":"New York"}',
|
|
600
|
+
result: { temperature: 72 },
|
|
601
|
+
isError: false,
|
|
602
|
+
},
|
|
603
|
+
];
|
|
604
|
+
const message = Object.assign(
|
|
605
|
+
createAssistantMessage({ type: "complete", reason: "stop" }, [
|
|
606
|
+
innerMessage,
|
|
607
|
+
]),
|
|
608
|
+
{ content },
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
await runCycle([message]);
|
|
612
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(1));
|
|
613
|
+
expect(append).toHaveBeenCalledWith({
|
|
614
|
+
parentId: null,
|
|
615
|
+
message: innerMessage,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
toolArtifacts.set("call-1", { preview: "72°F and sunny" });
|
|
619
|
+
await runCycle([message]);
|
|
620
|
+
|
|
621
|
+
await waitFor(() =>
|
|
622
|
+
expect(update).toHaveBeenCalledWith(
|
|
623
|
+
{
|
|
624
|
+
parentId: null,
|
|
625
|
+
message: {
|
|
626
|
+
...innerMessage,
|
|
627
|
+
metadata: {
|
|
628
|
+
__aui_toolArtifacts: {
|
|
629
|
+
"call-1": { preview: "72°F and sunny" },
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
"inner-a",
|
|
635
|
+
),
|
|
636
|
+
);
|
|
637
|
+
expect(innerMessage.metadata).toBeUndefined();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("keeps tool artifacts out of run telemetry", async () => {
|
|
641
|
+
const toolArtifacts = new Map<string, unknown>([
|
|
642
|
+
["call-1", { preview: "72°F and sunny" }],
|
|
643
|
+
]);
|
|
644
|
+
const { append, load, reportTelemetry, runCycle } =
|
|
645
|
+
createPersistenceHarness(false, { toolArtifacts });
|
|
646
|
+
const innerMessage: InnerMessage = {
|
|
647
|
+
id: "inner-a",
|
|
648
|
+
role: "assistant",
|
|
649
|
+
parts: [
|
|
650
|
+
{
|
|
651
|
+
type: "tool-weather",
|
|
652
|
+
toolCallId: "call-1",
|
|
653
|
+
state: "output-available",
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
};
|
|
657
|
+
const message = Object.assign(
|
|
658
|
+
createAssistantMessage({ type: "complete", reason: "stop" }, [
|
|
659
|
+
innerMessage,
|
|
660
|
+
]),
|
|
661
|
+
{
|
|
662
|
+
content: [
|
|
663
|
+
{
|
|
664
|
+
type: "tool-call" as const,
|
|
665
|
+
toolCallId: "call-1",
|
|
666
|
+
toolName: "weather",
|
|
667
|
+
args: {},
|
|
668
|
+
argsText: "{}",
|
|
669
|
+
result: { temperature: 72 },
|
|
670
|
+
isError: false,
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
},
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
677
|
+
toolArtifacts.set("call-1", { preview: "72°F and sunny" });
|
|
678
|
+
|
|
679
|
+
await runCycle([message]);
|
|
680
|
+
|
|
681
|
+
await waitFor(() => expect(reportTelemetry).toHaveBeenCalledTimes(1));
|
|
682
|
+
expect(append.mock.calls[0]?.[0].message).toHaveProperty(
|
|
683
|
+
"metadata.__aui_toolArtifacts",
|
|
684
|
+
);
|
|
685
|
+
expect(reportTelemetry.mock.calls[0]?.[0]).toEqual([
|
|
686
|
+
{ parentId: null, message: innerMessage },
|
|
687
|
+
]);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("keeps tool interactions and approval responses out of run telemetry", async () => {
|
|
691
|
+
const toolInteractions = new Map<string, Unstable_ToolInteractionLog>([
|
|
692
|
+
[
|
|
693
|
+
"call-1",
|
|
694
|
+
{
|
|
695
|
+
entries: [
|
|
696
|
+
{
|
|
697
|
+
type: "action",
|
|
698
|
+
occurredAt: 1,
|
|
699
|
+
payload: { refresh: true },
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
},
|
|
703
|
+
],
|
|
704
|
+
]);
|
|
705
|
+
const toolApprovalResponses = new Map<string, RespondToToolApprovalOptions>(
|
|
706
|
+
[
|
|
707
|
+
[
|
|
708
|
+
"approval-1",
|
|
709
|
+
{ approvalId: "approval-1", approved: true, reason: "Approved" },
|
|
710
|
+
],
|
|
711
|
+
],
|
|
712
|
+
);
|
|
713
|
+
const { append, load, reportTelemetry, runCycle } =
|
|
714
|
+
createPersistenceHarness(false, {
|
|
715
|
+
toolInteractions,
|
|
716
|
+
toolApprovalResponses,
|
|
717
|
+
});
|
|
718
|
+
const innerMessage: InnerMessage = {
|
|
719
|
+
id: "inner-a",
|
|
720
|
+
role: "assistant",
|
|
721
|
+
parts: [
|
|
722
|
+
{
|
|
723
|
+
type: "tool-weather",
|
|
724
|
+
toolCallId: "call-1",
|
|
725
|
+
state: "output-available",
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
type: "tool-deploy",
|
|
729
|
+
toolCallId: "call-2",
|
|
730
|
+
state: "approval-requested",
|
|
731
|
+
approval: { id: "approval-1" },
|
|
732
|
+
},
|
|
733
|
+
],
|
|
734
|
+
};
|
|
735
|
+
const message = Object.assign(
|
|
736
|
+
createAssistantMessage({ type: "complete", reason: "stop" }, [
|
|
737
|
+
innerMessage,
|
|
738
|
+
]),
|
|
739
|
+
{
|
|
740
|
+
content: [
|
|
741
|
+
{
|
|
742
|
+
type: "tool-call" as const,
|
|
743
|
+
toolCallId: "call-1",
|
|
744
|
+
toolName: "weather",
|
|
745
|
+
args: {},
|
|
746
|
+
argsText: "{}",
|
|
747
|
+
result: { temperature: 72 },
|
|
748
|
+
isError: false,
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
type: "tool-call" as const,
|
|
752
|
+
toolCallId: "call-2",
|
|
753
|
+
toolName: "deploy",
|
|
754
|
+
args: {},
|
|
755
|
+
argsText: "{}",
|
|
756
|
+
result: undefined,
|
|
757
|
+
isError: false,
|
|
758
|
+
approval: { id: "approval-1" },
|
|
759
|
+
},
|
|
760
|
+
],
|
|
761
|
+
},
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
765
|
+
toolInteractions.set("call-1", {
|
|
766
|
+
entries: [
|
|
767
|
+
{
|
|
768
|
+
type: "action",
|
|
769
|
+
occurredAt: 1,
|
|
770
|
+
payload: { refresh: true },
|
|
771
|
+
},
|
|
772
|
+
],
|
|
773
|
+
});
|
|
774
|
+
toolApprovalResponses.set("approval-1", {
|
|
775
|
+
approvalId: "approval-1",
|
|
776
|
+
approved: true,
|
|
777
|
+
reason: "Approved",
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
await runCycle([message]);
|
|
781
|
+
|
|
782
|
+
await waitFor(() => expect(reportTelemetry).toHaveBeenCalledTimes(1));
|
|
783
|
+
expect(append.mock.calls[0]?.[0].message).toMatchObject({
|
|
784
|
+
metadata: {
|
|
785
|
+
__aui_toolInteractions: { "call-1": toolInteractions.get("call-1") },
|
|
786
|
+
__aui_toolApprovalResponses: {
|
|
787
|
+
"approval-1": { approved: true, reason: "Approved" },
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
expect(reportTelemetry.mock.calls[0]?.[0]).toEqual([
|
|
792
|
+
{ parentId: null, message: innerMessage },
|
|
793
|
+
]);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it("stores host approval responses with the first message write", async () => {
|
|
797
|
+
const toolApprovalResponses = new Map<string, RespondToToolApprovalOptions>(
|
|
798
|
+
[
|
|
799
|
+
[
|
|
800
|
+
"approval-1",
|
|
801
|
+
{
|
|
802
|
+
approvalId: "approval-1",
|
|
803
|
+
approved: true,
|
|
804
|
+
optionId: "allow-once",
|
|
805
|
+
text: "Staging only",
|
|
806
|
+
reason: "Approved by operator",
|
|
807
|
+
},
|
|
808
|
+
],
|
|
809
|
+
],
|
|
810
|
+
);
|
|
811
|
+
const { append, load, runCycle } = createPersistenceHarness(false, {
|
|
812
|
+
toolApprovalResponses,
|
|
813
|
+
});
|
|
814
|
+
const innerMessage: InnerMessage = {
|
|
815
|
+
id: "inner-a",
|
|
816
|
+
role: "assistant",
|
|
817
|
+
parts: [
|
|
818
|
+
{
|
|
819
|
+
type: "tool-deploy",
|
|
820
|
+
toolCallId: "call-1",
|
|
821
|
+
state: "approval-requested",
|
|
822
|
+
approval: { id: "approval-1" },
|
|
823
|
+
},
|
|
824
|
+
],
|
|
825
|
+
};
|
|
826
|
+
const message = Object.assign(
|
|
827
|
+
createAssistantMessage({ type: "complete", reason: "stop" }, [
|
|
828
|
+
innerMessage,
|
|
829
|
+
]),
|
|
830
|
+
{
|
|
831
|
+
content: [
|
|
832
|
+
{
|
|
833
|
+
type: "tool-call" as const,
|
|
834
|
+
toolCallId: "call-1",
|
|
835
|
+
toolName: "deploy",
|
|
836
|
+
args: {},
|
|
837
|
+
argsText: "{}",
|
|
838
|
+
result: undefined,
|
|
839
|
+
isError: false,
|
|
840
|
+
approval: { id: "approval-1" },
|
|
841
|
+
},
|
|
842
|
+
],
|
|
843
|
+
},
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
847
|
+
toolApprovalResponses.set("approval-1", {
|
|
848
|
+
approvalId: "approval-1",
|
|
849
|
+
approved: true,
|
|
850
|
+
optionId: "allow-once",
|
|
851
|
+
text: "Staging only",
|
|
852
|
+
reason: "Approved by operator",
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
await runCycle([message]);
|
|
856
|
+
|
|
857
|
+
await waitFor(() =>
|
|
858
|
+
expect(append).toHaveBeenCalledWith({
|
|
859
|
+
parentId: null,
|
|
860
|
+
message: {
|
|
861
|
+
...innerMessage,
|
|
862
|
+
metadata: {
|
|
863
|
+
__aui_toolApprovalResponses: {
|
|
864
|
+
"approval-1": {
|
|
865
|
+
approved: true,
|
|
866
|
+
optionId: "allow-once",
|
|
867
|
+
text: "Staging only",
|
|
868
|
+
reason: "Approved by operator",
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
}),
|
|
874
|
+
);
|
|
875
|
+
expect(innerMessage.metadata).toBeUndefined();
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
it("logs and retries a failed host approval response update after a settled run", async () => {
|
|
879
|
+
const toolApprovalResponses = new Map<
|
|
880
|
+
string,
|
|
881
|
+
RespondToToolApprovalOptions
|
|
882
|
+
>();
|
|
883
|
+
const { append, update, persistToolApprovalResponses, runCycle } =
|
|
884
|
+
createPersistenceHarness(true, { toolApprovalResponses });
|
|
885
|
+
const innerMessage: InnerMessage = {
|
|
886
|
+
id: "inner-a",
|
|
887
|
+
role: "assistant",
|
|
888
|
+
parts: [
|
|
889
|
+
{
|
|
890
|
+
type: "tool-deploy",
|
|
891
|
+
toolCallId: "call-1",
|
|
892
|
+
state: "approval-requested",
|
|
893
|
+
approval: { id: "approval-1" },
|
|
894
|
+
},
|
|
895
|
+
],
|
|
896
|
+
};
|
|
897
|
+
const message = Object.assign(
|
|
898
|
+
createAssistantMessage({ type: "complete", reason: "stop" }, [
|
|
899
|
+
innerMessage,
|
|
900
|
+
]),
|
|
901
|
+
{
|
|
902
|
+
content: [
|
|
903
|
+
{
|
|
904
|
+
type: "tool-call" as const,
|
|
905
|
+
toolCallId: "call-1",
|
|
906
|
+
toolName: "deploy",
|
|
907
|
+
args: {},
|
|
908
|
+
argsText: "{}",
|
|
909
|
+
result: undefined,
|
|
910
|
+
isError: false,
|
|
911
|
+
approval: { id: "approval-1" },
|
|
912
|
+
},
|
|
913
|
+
],
|
|
914
|
+
},
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
await runCycle([message]);
|
|
918
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(1));
|
|
919
|
+
|
|
920
|
+
toolApprovalResponses.set("approval-1", {
|
|
921
|
+
approvalId: "approval-1",
|
|
922
|
+
approved: false,
|
|
923
|
+
reason: "Not now",
|
|
924
|
+
});
|
|
925
|
+
const failure = new Error("storage unavailable");
|
|
926
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
927
|
+
update.mockRejectedValueOnce(failure);
|
|
928
|
+
|
|
929
|
+
try {
|
|
930
|
+
await act(async () => {
|
|
931
|
+
await persistToolApprovalResponses("inner-a");
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
935
|
+
"Failed to persist tool data:",
|
|
936
|
+
failure,
|
|
937
|
+
);
|
|
938
|
+
await act(async () => {
|
|
939
|
+
await persistToolApprovalResponses("inner-a");
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
expect(update).toHaveBeenCalledTimes(2);
|
|
943
|
+
expect(update).toHaveBeenLastCalledWith(
|
|
944
|
+
{
|
|
945
|
+
parentId: null,
|
|
946
|
+
message: {
|
|
947
|
+
...innerMessage,
|
|
948
|
+
metadata: {
|
|
949
|
+
__aui_toolApprovalResponses: {
|
|
950
|
+
"approval-1": { approved: false, reason: "Not now" },
|
|
951
|
+
},
|
|
952
|
+
},
|
|
953
|
+
},
|
|
954
|
+
},
|
|
955
|
+
"inner-a",
|
|
956
|
+
);
|
|
957
|
+
expect(append).toHaveBeenCalledTimes(1);
|
|
958
|
+
expect(innerMessage.metadata).toBeUndefined();
|
|
959
|
+
} finally {
|
|
960
|
+
errorSpy.mockRestore();
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it("stores tool interactions with the first message write", async () => {
|
|
965
|
+
const toolInteractions = new Map<string, Unstable_ToolInteractionLog>([
|
|
966
|
+
[
|
|
967
|
+
"call-1",
|
|
968
|
+
{
|
|
969
|
+
entries: [
|
|
970
|
+
{
|
|
971
|
+
type: "action",
|
|
972
|
+
occurredAt: 1,
|
|
973
|
+
payload: { refresh: true },
|
|
974
|
+
},
|
|
975
|
+
],
|
|
976
|
+
},
|
|
977
|
+
],
|
|
978
|
+
]);
|
|
979
|
+
const { append, load, runCycle } = createPersistenceHarness(false, {
|
|
980
|
+
toolArtifacts: new Map(),
|
|
981
|
+
toolInteractions,
|
|
982
|
+
});
|
|
983
|
+
const innerMessage: InnerMessage = {
|
|
984
|
+
id: "inner-a",
|
|
985
|
+
role: "assistant",
|
|
986
|
+
parts: [
|
|
987
|
+
{
|
|
988
|
+
type: "tool-weather",
|
|
989
|
+
toolCallId: "call-1",
|
|
990
|
+
state: "output-available",
|
|
991
|
+
},
|
|
992
|
+
],
|
|
993
|
+
};
|
|
994
|
+
const message = Object.assign(
|
|
995
|
+
createAssistantMessage({ type: "complete", reason: "stop" }, [
|
|
996
|
+
innerMessage,
|
|
997
|
+
]),
|
|
998
|
+
{
|
|
999
|
+
content: [
|
|
1000
|
+
{
|
|
1001
|
+
type: "tool-call" as const,
|
|
1002
|
+
toolCallId: "call-1",
|
|
1003
|
+
toolName: "weather",
|
|
1004
|
+
args: {},
|
|
1005
|
+
argsText: "{}",
|
|
1006
|
+
result: { temperature: 72 },
|
|
1007
|
+
isError: false,
|
|
1008
|
+
},
|
|
1009
|
+
],
|
|
1010
|
+
},
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
1014
|
+
toolInteractions.set("call-1", {
|
|
1015
|
+
entries: [
|
|
1016
|
+
{
|
|
1017
|
+
type: "action",
|
|
1018
|
+
occurredAt: 1,
|
|
1019
|
+
payload: { refresh: true },
|
|
1020
|
+
},
|
|
1021
|
+
],
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
await runCycle([message]);
|
|
1025
|
+
|
|
1026
|
+
await waitFor(() =>
|
|
1027
|
+
expect(append).toHaveBeenCalledWith({
|
|
1028
|
+
parentId: null,
|
|
1029
|
+
message: {
|
|
1030
|
+
...innerMessage,
|
|
1031
|
+
metadata: {
|
|
1032
|
+
__aui_toolInteractions: {
|
|
1033
|
+
"call-1": toolInteractions.get("call-1"),
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
}),
|
|
1038
|
+
);
|
|
1039
|
+
expect(innerMessage.metadata).toBeUndefined();
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it("leaves stored rows unchanged without tool artifacts", async () => {
|
|
1043
|
+
const toolArtifacts = new Map<string, unknown>();
|
|
1044
|
+
const { append, runCycle } = createPersistenceHarness(false, {
|
|
1045
|
+
toolArtifacts,
|
|
1046
|
+
});
|
|
1047
|
+
const innerMessage = { id: "inner-a", parts: ["answer"] };
|
|
1048
|
+
const message = createAssistantMessage(
|
|
1049
|
+
{ type: "complete", reason: "stop" },
|
|
1050
|
+
[innerMessage],
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
await runCycle([message]);
|
|
1054
|
+
|
|
1055
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(1));
|
|
1056
|
+
expect(append).toHaveBeenCalledWith({
|
|
1057
|
+
parentId: null,
|
|
1058
|
+
message: innerMessage,
|
|
1059
|
+
});
|
|
1060
|
+
expect(append.mock.calls[0]?.[0].message).toBe(innerMessage);
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
it("logs and retries a failed tool interaction update after a settled run", async () => {
|
|
1064
|
+
const toolInteractions = new Map<string, Unstable_ToolInteractionLog>();
|
|
1065
|
+
const { append, update, persistToolInteractions, runCycle } =
|
|
1066
|
+
createPersistenceHarness(true, { toolInteractions });
|
|
1067
|
+
const initialInnerMessage: InnerMessage = {
|
|
1068
|
+
id: "inner-a",
|
|
1069
|
+
role: "assistant",
|
|
1070
|
+
parts: [
|
|
1071
|
+
{
|
|
1072
|
+
type: "tool-weather",
|
|
1073
|
+
toolCallId: "call-1",
|
|
1074
|
+
state: "output-available",
|
|
1075
|
+
},
|
|
1076
|
+
],
|
|
1077
|
+
};
|
|
1078
|
+
const createMessage = (innerMessage: InnerMessage) =>
|
|
1079
|
+
Object.assign(
|
|
1080
|
+
createAssistantMessage({ type: "complete", reason: "stop" }, [
|
|
1081
|
+
innerMessage,
|
|
1082
|
+
]),
|
|
1083
|
+
{
|
|
1084
|
+
content: [
|
|
1085
|
+
{
|
|
1086
|
+
type: "tool-call" as const,
|
|
1087
|
+
toolCallId: "call-1",
|
|
1088
|
+
toolName: "weather",
|
|
1089
|
+
args: {},
|
|
1090
|
+
argsText: "{}",
|
|
1091
|
+
result: undefined,
|
|
1092
|
+
isError: false,
|
|
1093
|
+
},
|
|
1094
|
+
],
|
|
1095
|
+
},
|
|
1096
|
+
);
|
|
1097
|
+
|
|
1098
|
+
await runCycle([createMessage(initialInnerMessage)]);
|
|
1099
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(1));
|
|
1100
|
+
|
|
1101
|
+
const log: Unstable_ToolInteractionLog = {
|
|
1102
|
+
entries: [
|
|
1103
|
+
{
|
|
1104
|
+
type: "human-response",
|
|
1105
|
+
occurredAt: 2,
|
|
1106
|
+
payload: "yes",
|
|
1107
|
+
},
|
|
1108
|
+
],
|
|
1109
|
+
};
|
|
1110
|
+
toolInteractions.set("call-1", log);
|
|
1111
|
+
const failure = new Error("storage unavailable");
|
|
1112
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
1113
|
+
update.mockRejectedValueOnce(failure);
|
|
1114
|
+
|
|
1115
|
+
try {
|
|
1116
|
+
await act(async () => {
|
|
1117
|
+
await persistToolInteractions("assistant-a");
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
1121
|
+
"Failed to persist tool data:",
|
|
1122
|
+
failure,
|
|
1123
|
+
);
|
|
1124
|
+
await act(async () => {
|
|
1125
|
+
await persistToolInteractions("assistant-a");
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
expect(update).toHaveBeenCalledTimes(2);
|
|
1129
|
+
expect(update).toHaveBeenLastCalledWith(
|
|
1130
|
+
{
|
|
1131
|
+
parentId: null,
|
|
1132
|
+
message: {
|
|
1133
|
+
...initialInnerMessage,
|
|
1134
|
+
metadata: {
|
|
1135
|
+
__aui_toolInteractions: { "call-1": log },
|
|
1136
|
+
},
|
|
1137
|
+
},
|
|
1138
|
+
},
|
|
1139
|
+
"inner-a",
|
|
1140
|
+
);
|
|
1141
|
+
expect(append).toHaveBeenCalledTimes(1);
|
|
1142
|
+
|
|
1143
|
+
const rewrittenInnerMessage = {
|
|
1144
|
+
...initialInnerMessage,
|
|
1145
|
+
parts: [...initialInnerMessage.parts, { type: "text", text: "done" }],
|
|
1146
|
+
};
|
|
1147
|
+
await runCycle([createMessage(rewrittenInnerMessage)]);
|
|
1148
|
+
|
|
1149
|
+
await waitFor(() =>
|
|
1150
|
+
expect(update).toHaveBeenLastCalledWith(
|
|
1151
|
+
{
|
|
1152
|
+
parentId: null,
|
|
1153
|
+
message: {
|
|
1154
|
+
...rewrittenInnerMessage,
|
|
1155
|
+
metadata: {
|
|
1156
|
+
__aui_toolInteractions: { "call-1": log },
|
|
1157
|
+
},
|
|
1158
|
+
},
|
|
1159
|
+
},
|
|
1160
|
+
"inner-a",
|
|
1161
|
+
),
|
|
1162
|
+
);
|
|
1163
|
+
} finally {
|
|
1164
|
+
errorSpy.mockRestore();
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it("leaves stored rows unchanged without tool artifacts or interactions", async () => {
|
|
1169
|
+
const toolArtifacts = new Map<string, unknown>();
|
|
1170
|
+
const toolInteractions = new Map<string, Unstable_ToolInteractionLog>();
|
|
1171
|
+
const { append, runCycle } = createPersistenceHarness(false, {
|
|
1172
|
+
toolArtifacts,
|
|
1173
|
+
toolInteractions,
|
|
1174
|
+
});
|
|
1175
|
+
const innerMessage = {
|
|
1176
|
+
id: "inner-a",
|
|
1177
|
+
parts: [
|
|
1178
|
+
{
|
|
1179
|
+
type: "tool-weather",
|
|
1180
|
+
toolCallId: "call-1",
|
|
1181
|
+
state: "output-available",
|
|
1182
|
+
},
|
|
1183
|
+
],
|
|
1184
|
+
};
|
|
1185
|
+
const message = Object.assign(
|
|
1186
|
+
createAssistantMessage({ type: "complete", reason: "stop" }, [
|
|
1187
|
+
innerMessage,
|
|
1188
|
+
]),
|
|
1189
|
+
{
|
|
1190
|
+
content: [
|
|
1191
|
+
{
|
|
1192
|
+
type: "tool-call" as const,
|
|
1193
|
+
toolCallId: "call-1",
|
|
1194
|
+
toolName: "weather",
|
|
1195
|
+
args: {},
|
|
1196
|
+
argsText: "{}",
|
|
1197
|
+
result: undefined,
|
|
1198
|
+
isError: false,
|
|
1199
|
+
},
|
|
1200
|
+
],
|
|
1201
|
+
},
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
await runCycle([message]);
|
|
1205
|
+
|
|
1206
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(1));
|
|
1207
|
+
expect(append).toHaveBeenCalledWith({
|
|
1208
|
+
parentId: null,
|
|
1209
|
+
message: innerMessage,
|
|
1210
|
+
});
|
|
1211
|
+
expect(append.mock.calls[0]?.[0].message).toBe(innerMessage);
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
it("restores stored tool interactions without returning metadata to the chat", async () => {
|
|
1215
|
+
const toolInteractions = new Map<string, Unstable_ToolInteractionLog>();
|
|
1216
|
+
const onSetMessages = vi.fn();
|
|
1217
|
+
const log: Unstable_ToolInteractionLog = {
|
|
1218
|
+
entries: [
|
|
1219
|
+
{
|
|
1220
|
+
type: "human-response",
|
|
1221
|
+
occurredAt: 3,
|
|
1222
|
+
payload: "yes",
|
|
1223
|
+
},
|
|
1224
|
+
],
|
|
1225
|
+
};
|
|
1226
|
+
const storedMessage: InnerMessage = {
|
|
1227
|
+
id: "inner-a",
|
|
1228
|
+
role: "assistant",
|
|
1229
|
+
parts: [
|
|
1230
|
+
{
|
|
1231
|
+
type: "tool-weather",
|
|
1232
|
+
toolCallId: "call-1",
|
|
1233
|
+
state: "output-available",
|
|
1234
|
+
},
|
|
1235
|
+
],
|
|
1236
|
+
metadata: {
|
|
1237
|
+
__aui_toolInteractions: { "call-1": log },
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1240
|
+
const { importMessages, load } = createPersistenceHarness(true, {
|
|
1241
|
+
loadMessages: {
|
|
1242
|
+
messages: [{ parentId: null, message: storedMessage }],
|
|
1243
|
+
},
|
|
1244
|
+
toThreadMessages: (messages) =>
|
|
1245
|
+
messages.map((message) => {
|
|
1246
|
+
const tool = message.parts[0] as { toolCallId: string };
|
|
1247
|
+
const converted = createAssistantMessage(
|
|
1248
|
+
{ type: "complete", reason: "stop" },
|
|
1249
|
+
[message],
|
|
1250
|
+
message.id,
|
|
1251
|
+
) as ThreadAssistantMessage;
|
|
1252
|
+
return Object.assign(converted, {
|
|
1253
|
+
content: [
|
|
1254
|
+
{
|
|
1255
|
+
type: "tool-call" as const,
|
|
1256
|
+
toolCallId: tool.toolCallId,
|
|
1257
|
+
toolName: "weather",
|
|
1258
|
+
args: {},
|
|
1259
|
+
argsText: "{}",
|
|
1260
|
+
result: undefined,
|
|
1261
|
+
isError: false,
|
|
1262
|
+
unstable_interactions: toolInteractions.get(tool.toolCallId),
|
|
1263
|
+
},
|
|
1264
|
+
],
|
|
1265
|
+
});
|
|
1266
|
+
}),
|
|
1267
|
+
onSetMessages,
|
|
1268
|
+
toolInteractions,
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
1272
|
+
await waitFor(() => expect(importMessages).toHaveBeenCalledTimes(1));
|
|
1273
|
+
|
|
1274
|
+
const imported = importMessages.mock.calls[0]?.[0] as {
|
|
1275
|
+
messages: { message: ThreadMessage }[];
|
|
1276
|
+
};
|
|
1277
|
+
expect(imported.messages[0]?.message.content[0]).toMatchObject({
|
|
1278
|
+
type: "tool-call",
|
|
1279
|
+
toolCallId: "call-1",
|
|
1280
|
+
unstable_interactions: log,
|
|
1281
|
+
});
|
|
1282
|
+
expect(toolInteractions.get("call-1")).toEqual(log);
|
|
1283
|
+
expect(onSetMessages.mock.calls[0]?.[0][0]).not.toHaveProperty(
|
|
1284
|
+
"metadata.__aui_toolInteractions",
|
|
1285
|
+
);
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
it("restores host approval responses without returning metadata to the chat", async () => {
|
|
1289
|
+
const toolApprovalResponses = new Map<
|
|
1290
|
+
string,
|
|
1291
|
+
RespondToToolApprovalOptions
|
|
1292
|
+
>();
|
|
1293
|
+
const onSetMessages = vi.fn();
|
|
1294
|
+
const storedMessage: InnerMessage = {
|
|
1295
|
+
id: "inner-a",
|
|
1296
|
+
role: "assistant",
|
|
1297
|
+
parts: [
|
|
1298
|
+
{
|
|
1299
|
+
type: "tool-deploy",
|
|
1300
|
+
toolCallId: "call-1",
|
|
1301
|
+
state: "approval-requested",
|
|
1302
|
+
approval: { id: "approval-1" },
|
|
1303
|
+
},
|
|
1304
|
+
],
|
|
1305
|
+
metadata: {
|
|
1306
|
+
__aui_toolApprovalResponses: {
|
|
1307
|
+
"approval-1": {
|
|
1308
|
+
approved: true,
|
|
1309
|
+
optionId: "allow-once",
|
|
1310
|
+
text: "Staging only",
|
|
1311
|
+
reason: "Approved by operator",
|
|
1312
|
+
},
|
|
1313
|
+
},
|
|
1314
|
+
},
|
|
1315
|
+
};
|
|
1316
|
+
const { importMessages, load } = createPersistenceHarness(true, {
|
|
1317
|
+
loadMessages: {
|
|
1318
|
+
messages: [{ parentId: null, message: storedMessage }],
|
|
1319
|
+
},
|
|
1320
|
+
toThreadMessages: (messages) =>
|
|
1321
|
+
messages.map((message) => {
|
|
1322
|
+
const tool = message.parts[0] as {
|
|
1323
|
+
toolCallId: string;
|
|
1324
|
+
approval: { id: string };
|
|
1325
|
+
};
|
|
1326
|
+
const response = toolApprovalResponses.get(tool.approval.id);
|
|
1327
|
+
const converted = createAssistantMessage(
|
|
1328
|
+
{ type: "complete", reason: "stop" },
|
|
1329
|
+
[message],
|
|
1330
|
+
message.id,
|
|
1331
|
+
) as ThreadAssistantMessage;
|
|
1332
|
+
return Object.assign(converted, {
|
|
1333
|
+
content: [
|
|
1334
|
+
{
|
|
1335
|
+
type: "tool-call" as const,
|
|
1336
|
+
toolCallId: tool.toolCallId,
|
|
1337
|
+
toolName: "deploy",
|
|
1338
|
+
args: {},
|
|
1339
|
+
argsText: "{}",
|
|
1340
|
+
result: undefined,
|
|
1341
|
+
isError: false,
|
|
1342
|
+
approval: {
|
|
1343
|
+
id: tool.approval.id,
|
|
1344
|
+
...(response && { approved: response.approved }),
|
|
1345
|
+
...(response?.optionId && { optionId: response.optionId }),
|
|
1346
|
+
...(response?.text && { text: response.text }),
|
|
1347
|
+
...(response?.reason && { reason: response.reason }),
|
|
1348
|
+
},
|
|
1349
|
+
},
|
|
1350
|
+
],
|
|
1351
|
+
});
|
|
1352
|
+
}),
|
|
1353
|
+
onSetMessages,
|
|
1354
|
+
toolApprovalResponses,
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
1358
|
+
await waitFor(() => expect(importMessages).toHaveBeenCalledTimes(1));
|
|
1359
|
+
|
|
1360
|
+
const imported = importMessages.mock.calls[0]?.[0] as {
|
|
1361
|
+
messages: { message: ThreadMessage }[];
|
|
1362
|
+
};
|
|
1363
|
+
expect(imported.messages[0]?.message.content[0]).toMatchObject({
|
|
1364
|
+
type: "tool-call",
|
|
1365
|
+
toolCallId: "call-1",
|
|
1366
|
+
approval: {
|
|
1367
|
+
id: "approval-1",
|
|
1368
|
+
approved: true,
|
|
1369
|
+
optionId: "allow-once",
|
|
1370
|
+
text: "Staging only",
|
|
1371
|
+
reason: "Approved by operator",
|
|
1372
|
+
},
|
|
1373
|
+
});
|
|
1374
|
+
expect(toolApprovalResponses.get("approval-1")).toEqual({
|
|
1375
|
+
approvalId: "approval-1",
|
|
1376
|
+
approved: true,
|
|
1377
|
+
optionId: "allow-once",
|
|
1378
|
+
text: "Staging only",
|
|
1379
|
+
reason: "Approved by operator",
|
|
1380
|
+
});
|
|
1381
|
+
expect(onSetMessages.mock.calls[0]?.[0][0]).not.toHaveProperty(
|
|
1382
|
+
"metadata.__aui_toolApprovalResponses",
|
|
1383
|
+
);
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
it("restores stored tool artifacts without returning metadata to the chat", async () => {
|
|
1387
|
+
const toolArtifacts = new Map<string, unknown>();
|
|
1388
|
+
const onSetMessages = vi.fn();
|
|
1389
|
+
const storedMessage: InnerMessage = {
|
|
1390
|
+
id: "inner-a",
|
|
1391
|
+
role: "assistant",
|
|
1392
|
+
parts: [
|
|
1393
|
+
{
|
|
1394
|
+
type: "tool-weather",
|
|
1395
|
+
toolCallId: "call-1",
|
|
1396
|
+
state: "output-available",
|
|
1397
|
+
input: { city: "New York" },
|
|
1398
|
+
output: { temperature: 72 },
|
|
1399
|
+
},
|
|
1400
|
+
],
|
|
1401
|
+
metadata: {
|
|
1402
|
+
__aui_toolArtifacts: {
|
|
1403
|
+
"call-1": { preview: "72°F and sunny" },
|
|
1404
|
+
},
|
|
1405
|
+
},
|
|
1406
|
+
};
|
|
1407
|
+
const { importMessages, load } = createPersistenceHarness(true, {
|
|
1408
|
+
loadMessages: {
|
|
1409
|
+
messages: [{ parentId: null, message: storedMessage }],
|
|
1410
|
+
},
|
|
1411
|
+
toThreadMessages: (messages) =>
|
|
1412
|
+
messages.map((message) => {
|
|
1413
|
+
const tool = message.parts[0] as { toolCallId: string };
|
|
1414
|
+
const converted = createAssistantMessage(
|
|
1415
|
+
{ type: "complete", reason: "stop" },
|
|
1416
|
+
[message],
|
|
1417
|
+
message.id,
|
|
1418
|
+
) as ThreadAssistantMessage;
|
|
1419
|
+
return Object.assign(converted, {
|
|
1420
|
+
content: [
|
|
1421
|
+
{
|
|
1422
|
+
type: "tool-call" as const,
|
|
1423
|
+
toolCallId: tool.toolCallId,
|
|
1424
|
+
toolName: "weather",
|
|
1425
|
+
args: { city: "New York" },
|
|
1426
|
+
argsText: '{"city":"New York"}',
|
|
1427
|
+
result: { temperature: 72 },
|
|
1428
|
+
isError: false,
|
|
1429
|
+
artifact: toolArtifacts.get(tool.toolCallId),
|
|
1430
|
+
},
|
|
1431
|
+
],
|
|
1432
|
+
});
|
|
1433
|
+
}),
|
|
1434
|
+
onSetMessages,
|
|
1435
|
+
toolArtifacts,
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
1439
|
+
await waitFor(() => expect(importMessages).toHaveBeenCalledTimes(1));
|
|
1440
|
+
|
|
1441
|
+
const imported = importMessages.mock.calls[0]?.[0] as {
|
|
1442
|
+
messages: { message: ThreadMessage }[];
|
|
1443
|
+
};
|
|
1444
|
+
expect(imported.messages[0]?.message.content[0]).toMatchObject({
|
|
1445
|
+
type: "tool-call",
|
|
1446
|
+
toolCallId: "call-1",
|
|
1447
|
+
artifact: { preview: "72°F and sunny" },
|
|
1448
|
+
});
|
|
1449
|
+
expect(toolArtifacts.get("call-1")).toEqual({
|
|
1450
|
+
preview: "72°F and sunny",
|
|
1451
|
+
});
|
|
1452
|
+
expect(onSetMessages.mock.calls[0]?.[0][0]).not.toHaveProperty(
|
|
1453
|
+
"metadata.__aui_toolArtifacts",
|
|
1454
|
+
);
|
|
1455
|
+
});
|
|
1456
|
+
|
|
517
1457
|
it("schedules one idle persistence attempt per landed message when append fails", async () => {
|
|
518
1458
|
const { append, step, flush } = createPersistenceHarness(false);
|
|
519
1459
|
append.mockRejectedValueOnce(new Error("offline"));
|