@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
|
@@ -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,968 @@ 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
|
+
|
|
559
|
+
it("persists a message that lands while the thread is idle", async () => {
|
|
560
|
+
const { append, step } = createPersistenceHarness(false);
|
|
561
|
+
const message = createAssistantMessage(
|
|
562
|
+
{ type: "complete", reason: "stop" },
|
|
563
|
+
[{ id: "spoken-1", parts: ["spoken"] }],
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
await step({ messages: [message] });
|
|
567
|
+
|
|
568
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(1));
|
|
569
|
+
expect(append).toHaveBeenCalledWith({
|
|
570
|
+
parentId: null,
|
|
571
|
+
message: { id: "spoken-1", parts: ["spoken"] },
|
|
572
|
+
});
|
|
573
|
+
});
|
|
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
|
+
|
|
1457
|
+
it("schedules one idle persistence attempt per landed message when append fails", async () => {
|
|
1458
|
+
const { append, step, flush } = createPersistenceHarness(false);
|
|
1459
|
+
append.mockRejectedValueOnce(new Error("offline"));
|
|
1460
|
+
const first = createAssistantMessage(
|
|
1461
|
+
{ type: "complete", reason: "stop" },
|
|
1462
|
+
[{ id: "spoken-1", parts: ["first"] }],
|
|
1463
|
+
"assistant-a",
|
|
1464
|
+
);
|
|
1465
|
+
|
|
1466
|
+
await step({ messages: [first] });
|
|
1467
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(1));
|
|
1468
|
+
await step({ messages: [first] });
|
|
1469
|
+
await step({ messages: [first] });
|
|
1470
|
+
await flush();
|
|
1471
|
+
expect(append).toHaveBeenCalledTimes(1);
|
|
1472
|
+
|
|
1473
|
+
const second = createAssistantMessage(
|
|
1474
|
+
{ type: "complete", reason: "stop" },
|
|
1475
|
+
[{ id: "spoken-2", parts: ["second"] }],
|
|
1476
|
+
"assistant-b",
|
|
1477
|
+
);
|
|
1478
|
+
await step({ messages: [first, second] });
|
|
1479
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(3));
|
|
1480
|
+
expect(append.mock.calls.map(([item]) => item)).toEqual([
|
|
1481
|
+
{ parentId: null, message: { id: "spoken-1", parts: ["first"] } },
|
|
1482
|
+
{ parentId: null, message: { id: "spoken-1", parts: ["first"] } },
|
|
1483
|
+
{ parentId: "spoken-1", message: { id: "spoken-2", parts: ["second"] } },
|
|
1484
|
+
]);
|
|
1485
|
+
});
|
|
1486
|
+
|
|
501
1487
|
it("persists a settled turn when the history adapter becomes active after it", async () => {
|
|
502
1488
|
let listener: (() => void) | undefined;
|
|
503
1489
|
let isRunning = false;
|