@ulearning/ai-sdk-adapter 0.0.0-stage → 1.0.0-next.0
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 +21 -0
- package/README.md +29 -2
- package/dist/adapters/aiSDKFormatAdapter.d.ts +6 -0
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
- package/dist/adapters/aiSDKFormatAdapter.js +17 -0
- package/dist/adapters/aiSDKFormatAdapter.js.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts +3 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.js +36 -0
- package/dist/adapters/vercelAttachmentAdapter.js.map +1 -0
- package/dist/aiSDKExtras.d.ts +13 -0
- package/dist/aiSDKExtras.d.ts.map +1 -0
- package/dist/aiSDKExtras.js +6 -0
- package/dist/aiSDKExtras.js.map +1 -0
- package/dist/converters/convertMessage.d.ts +44 -0
- package/dist/converters/convertMessage.d.ts.map +1 -0
- package/dist/converters/convertMessage.js +465 -0
- package/dist/converters/convertMessage.js.map +1 -0
- package/dist/converters/toCreateMessage.d.ts +4 -0
- package/dist/converters/toCreateMessage.d.ts.map +1 -0
- package/dist/converters/toCreateMessage.js +65 -0
- package/dist/converters/toCreateMessage.js.map +1 -0
- package/dist/converters/toolApprovalAnswers.d.ts +3 -0
- package/dist/converters/toolApprovalAnswers.d.ts.map +1 -0
- package/dist/converters/toolApprovalAnswers.js +16 -0
- package/dist/converters/toolApprovalAnswers.js.map +1 -0
- package/dist/hooks.d.ts +17 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +19 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/model-context/injectInteractableContext.d.ts +37 -0
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -0
- package/dist/model-context/injectInteractableContext.js +54 -0
- package/dist/model-context/injectInteractableContext.js.map +1 -0
- package/dist/model-context/injectQuoteContext.d.ts +25 -0
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -0
- package/dist/model-context/injectQuoteContext.js +51 -0
- package/dist/model-context/injectQuoteContext.js.map +1 -0
- package/dist/runtime/AISDKChat.d.ts +13 -0
- package/dist/runtime/AISDKChat.d.ts.map +1 -0
- package/dist/runtime/AISDKChat.js +34 -0
- package/dist/runtime/AISDKChat.js.map +1 -0
- package/dist/runtime/AISDKThreads.d.ts +24 -0
- package/dist/runtime/AISDKThreads.d.ts.map +1 -0
- package/dist/runtime/AISDKThreads.js +92 -0
- package/dist/runtime/AISDKThreads.js.map +1 -0
- package/dist/runtime/DynamicChatTransport.d.ts +50 -0
- package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/DynamicChatTransport.js +153 -0
- package/dist/runtime/DynamicChatTransport.js.map +1 -0
- package/dist/runtime/getResumableAdapter.d.ts +5 -0
- package/dist/runtime/getResumableAdapter.d.ts.map +1 -0
- package/dist/runtime/getResumableAdapter.js +11 -0
- package/dist/runtime/getResumableAdapter.js.map +1 -0
- package/dist/runtime/toolHistoryCodec.d.ts +20 -0
- package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
- package/dist/runtime/toolHistoryCodec.js +122 -0
- package/dist/runtime/toolHistoryCodec.js.map +1 -0
- package/dist/runtime/useAISDKRuntime.d.ts +95 -0
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -0
- package/dist/runtime/useAISDKRuntime.js +754 -0
- package/dist/runtime/useAISDKRuntime.js.map +1 -0
- package/dist/runtime/useChatRuntime.d.ts +17 -0
- package/dist/runtime/useChatRuntime.d.ts.map +1 -0
- package/dist/runtime/useChatRuntime.js +50 -0
- package/dist/runtime/useChatRuntime.js.map +1 -0
- package/dist/runtime/useChatSnapshot.d.ts +6 -0
- package/dist/runtime/useChatSnapshot.d.ts.map +1 -0
- package/dist/runtime/useChatSnapshot.js +62 -0
- package/dist/runtime/useChatSnapshot.js.map +1 -0
- package/dist/runtime/useChatThread.d.ts +121 -0
- package/dist/runtime/useChatThread.d.ts.map +1 -0
- package/dist/runtime/useChatThread.js +255 -0
- package/dist/runtime/useChatThread.js.map +1 -0
- package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
- package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/useDynamicChatTransport.js +26 -0
- package/dist/runtime/useDynamicChatTransport.js.map +1 -0
- package/dist/runtime/useExternalHistory.d.ts +12 -0
- package/dist/runtime/useExternalHistory.d.ts.map +1 -0
- package/dist/runtime/useExternalHistory.js +556 -0
- package/dist/runtime/useExternalHistory.js.map +1 -0
- package/dist/runtime/useMessageQueue.d.ts +14 -0
- package/dist/runtime/useMessageQueue.d.ts.map +1 -0
- package/dist/runtime/useMessageQueue.js +142 -0
- package/dist/runtime/useMessageQueue.js.map +1 -0
- package/dist/runtime/useResourceCleanup.d.ts +2 -0
- package/dist/runtime/useResourceCleanup.d.ts.map +1 -0
- package/dist/runtime/useResourceCleanup.js +47 -0
- package/dist/runtime/useResourceCleanup.js.map +1 -0
- package/dist/runtime/useStreamingTiming.d.ts +12 -0
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -0
- package/dist/runtime/useStreamingTiming.js +36 -0
- package/dist/runtime/useStreamingTiming.js.map +1 -0
- package/dist/tools/generativeTools.d.ts +71 -0
- package/dist/tools/generativeTools.d.ts.map +1 -0
- package/dist/tools/generativeTools.js +258 -0
- package/dist/tools/generativeTools.js.map +1 -0
- package/dist/tools/mcp-stdio.node.d.ts +2 -0
- package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.node.js +1 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts +3 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.unsupported.js +10 -0
- package/dist/tools/mcp-stdio.unsupported.js.map +1 -0
- package/dist/transport/AssistantChatTransport.d.ts +20 -0
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -0
- package/dist/transport/AssistantChatTransport.js +151 -0
- package/dist/transport/AssistantChatTransport.js.map +1 -0
- package/dist/transport/resumable.d.ts +36 -0
- package/dist/transport/resumable.d.ts.map +1 -0
- package/dist/transport/resumable.js +112 -0
- package/dist/transport/resumable.js.map +1 -0
- package/dist/usage.d.ts +24 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +121 -0
- package/dist/usage.js.map +1 -0
- package/dist/utils/getVercelAIMessages.d.ts +4 -0
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -0
- package/dist/utils/getVercelAIMessages.js +8 -0
- package/dist/utils/getVercelAIMessages.js.map +1 -0
- package/dist/utils/sliceMessagesUntil.d.ts +3 -0
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -0
- package/dist/utils/sliceMessagesUntil.js +10 -0
- package/dist/utils/sliceMessagesUntil.js.map +1 -0
- package/package.json +96 -3
- package/src/__tests__/standalone.e2e.ts +118 -0
- package/src/adapters/aiSDKFormatAdapter.test.ts +36 -0
- package/src/adapters/aiSDKFormatAdapter.ts +18 -0
- package/src/adapters/vercelAttachmentAdapter.test.ts +52 -0
- package/src/adapters/vercelAttachmentAdapter.ts +36 -0
- package/src/aiSDKExtras.ts +31 -0
- package/src/converters/convertMessage.metadata-key.test.tsx +182 -0
- package/src/converters/convertMessage.test.ts +2428 -0
- package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
- package/src/converters/convertMessage.ts +919 -0
- package/src/converters/toCreateMessage.test.ts +716 -0
- package/src/converters/toCreateMessage.ts +110 -0
- package/src/converters/toolApprovalAnswers.ts +27 -0
- package/src/hooks.test.ts +168 -0
- package/src/hooks.ts +26 -0
- package/src/index.ts +29 -0
- package/src/model-context/injectInteractableContext.test.ts +167 -0
- package/src/model-context/injectInteractableContext.ts +64 -0
- package/src/model-context/injectQuoteContext.test.ts +33 -0
- package/src/model-context/injectQuoteContext.ts +63 -0
- package/src/runtime/AISDKChat.artifacts.test.ts +48 -0
- package/src/runtime/AISDKChat.integration.test.tsx +148 -0
- package/src/runtime/AISDKChat.test.ts +241 -0
- package/src/runtime/AISDKChat.ts +43 -0
- package/src/runtime/AISDKThreads.test.ts +811 -0
- package/src/runtime/AISDKThreads.ts +194 -0
- package/src/runtime/DynamicChatTransport.test.ts +228 -0
- package/src/runtime/DynamicChatTransport.ts +281 -0
- package/src/runtime/__tests__/controlled-transport.ts +70 -0
- package/src/runtime/__tests__/tap.ts +80 -0
- package/src/runtime/__tests__/vue.ts +46 -0
- package/src/runtime/getResumableAdapter.ts +16 -0
- package/src/runtime/toolHistoryCodec.test.ts +187 -0
- package/src/runtime/toolHistoryCodec.ts +245 -0
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1956 -0
- package/src/runtime/useAISDKRuntime.approval.test.tsx +580 -0
- package/src/runtime/useAISDKRuntime.denied-tool.test.tsx +122 -0
- package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +302 -0
- package/src/runtime/useAISDKRuntime.metadata-cache.test.tsx +157 -0
- package/src/runtime/useAISDKRuntime.queue.test.tsx +707 -0
- package/src/runtime/useAISDKRuntime.reload.test.tsx +217 -0
- package/src/runtime/useAISDKRuntime.test.ts +2345 -0
- package/src/runtime/useAISDKRuntime.tool-output.test.tsx +113 -0
- package/src/runtime/useAISDKRuntime.ts +1503 -0
- package/src/runtime/useAISDKRuntime.voice.test.tsx +319 -0
- package/src/runtime/useChatOnTap.test.ts +60 -0
- package/src/runtime/useChatRuntime.fast-refresh.test.tsx +83 -0
- package/src/runtime/useChatRuntime.integration.test.tsx +661 -0
- package/src/runtime/useChatRuntime.local-storage.test.tsx +114 -0
- package/src/runtime/useChatRuntime.native.test.ts +147 -0
- package/src/runtime/useChatRuntime.run-telemetry.test.tsx +226 -0
- package/src/runtime/useChatRuntime.test.ts +619 -0
- package/src/runtime/useChatRuntime.ts +80 -0
- package/src/runtime/useChatSnapshot.test.ts +60 -0
- package/src/runtime/useChatSnapshot.ts +86 -0
- package/src/runtime/useChatThread.binding.test.tsx +140 -0
- package/src/runtime/useChatThread.test.ts +993 -0
- package/src/runtime/useChatThread.transport.test.tsx +149 -0
- package/src/runtime/useChatThread.ts +533 -0
- package/src/runtime/useDynamicChatTransport.ts +26 -0
- package/src/runtime/useExternalHistory.test.ts +4187 -0
- package/src/runtime/useExternalHistory.ts +902 -0
- package/src/runtime/useMessageQueue.effects.test.tsx +88 -0
- package/src/runtime/useMessageQueue.test.tsx +256 -0
- package/src/runtime/useMessageQueue.ts +189 -0
- package/src/runtime/useResourceCleanup.ts +58 -0
- package/src/runtime/useStreamingTiming.test.tsx +96 -0
- package/src/runtime/useStreamingTiming.ts +69 -0
- package/src/tools/generativeTools.test.ts +1058 -0
- package/src/tools/generativeTools.ts +491 -0
- package/src/tools/mcp-stdio.node.ts +1 -0
- package/src/tools/mcp-stdio.unsupported.ts +12 -0
- package/src/transport/AssistantChatTransport.test.ts +631 -0
- package/src/transport/AssistantChatTransport.ts +236 -0
- package/src/transport/resumable.test.ts +262 -0
- package/src/transport/resumable.ts +162 -0
- package/src/usage.test.ts +204 -0
- package/src/usage.ts +179 -0
- package/src/utils/getVercelAIMessages.ts +11 -0
- package/src/utils/sliceMessagesUntil.test.ts +51 -0
- package/src/utils/sliceMessagesUntil.ts +16 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ChatTransport, UIMessage, UIMessageChunk } from "ai";
|
|
2
|
+
import { useAui, type AssistantClient } from "@ulearning/ai-store";
|
|
3
|
+
import { defineComponent } from "vue";
|
|
4
|
+
import { flushTapSync } from "@ulearning/ai-tap";
|
|
5
|
+
|
|
6
|
+
export const createControlledTransport = () => {
|
|
7
|
+
let controller!: ReadableStreamDefaultController<UIMessageChunk>;
|
|
8
|
+
const transport: ChatTransport<UIMessage> = {
|
|
9
|
+
sendMessages: async () =>
|
|
10
|
+
new ReadableStream<UIMessageChunk>({
|
|
11
|
+
start(c) {
|
|
12
|
+
controller = c;
|
|
13
|
+
},
|
|
14
|
+
}),
|
|
15
|
+
reconnectToStream: async () => null,
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
transport,
|
|
19
|
+
emit: (...chunks: UIMessageChunk[]) => {
|
|
20
|
+
for (const chunk of chunks) controller.enqueue(chunk);
|
|
21
|
+
},
|
|
22
|
+
close: () => controller.close(),
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const createCancellableTransport = () => {
|
|
27
|
+
let cancelCount = 0;
|
|
28
|
+
let controller!: ReadableStreamDefaultController<UIMessageChunk>;
|
|
29
|
+
const transport: ChatTransport<UIMessage> = {
|
|
30
|
+
sendMessages: async () =>
|
|
31
|
+
new ReadableStream<UIMessageChunk>({
|
|
32
|
+
start(c) {
|
|
33
|
+
controller = c;
|
|
34
|
+
},
|
|
35
|
+
cancel() {
|
|
36
|
+
cancelCount++;
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
reconnectToStream: async () => null,
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
transport,
|
|
43
|
+
getCancelCount: () => cancelCount,
|
|
44
|
+
emit: (...chunks: UIMessageChunk[]) => {
|
|
45
|
+
for (const chunk of chunks) controller.enqueue(chunk);
|
|
46
|
+
},
|
|
47
|
+
close: () => controller.close(),
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const nextTask = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
52
|
+
|
|
53
|
+
export const createStreamHarness = () => {
|
|
54
|
+
let aui: AssistantClient | undefined;
|
|
55
|
+
const Probe = defineComponent({
|
|
56
|
+
setup() {
|
|
57
|
+
aui = useAui();
|
|
58
|
+
return () => null;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
Probe,
|
|
63
|
+
send: () => {
|
|
64
|
+
flushTapSync(() => aui!.composer.setText("keep streaming"));
|
|
65
|
+
flushTapSync(() => aui!.composer.send());
|
|
66
|
+
},
|
|
67
|
+
isRunning: () => aui?.thread.getState().isRunning === true,
|
|
68
|
+
client: () => aui!,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createTapRoot, flushTapSync, useState } from "@ulearning/ai-tap";
|
|
2
|
+
import { afterEach, vi } from "vitest";
|
|
3
|
+
import { Chat } from "@ai-sdk/vue";
|
|
4
|
+
import type { ChatInit, UIMessage } from "ai";
|
|
5
|
+
import { useChatSnapshot } from "../useChatSnapshot";
|
|
6
|
+
|
|
7
|
+
const roots = new Set<{ unmount(): void }>();
|
|
8
|
+
const refresh = new Set<() => void>();
|
|
9
|
+
export const cleanup = () => {
|
|
10
|
+
for (const root of roots) root.unmount();
|
|
11
|
+
roots.clear();
|
|
12
|
+
refresh.clear();
|
|
13
|
+
};
|
|
14
|
+
afterEach(cleanup);
|
|
15
|
+
export const waitFor = vi.waitFor;
|
|
16
|
+
export function act<T>(action: () => Promise<T>): Promise<T>;
|
|
17
|
+
export function act<T>(action: () => T): T;
|
|
18
|
+
export function act(action: () => unknown): unknown {
|
|
19
|
+
const result = flushTapSync(action);
|
|
20
|
+
if (result instanceof Promise) {
|
|
21
|
+
// An async resource action schedules outside flushTapSync's callback.
|
|
22
|
+
// Render the fixtures' current inputs after settlement before exposing results.
|
|
23
|
+
const commit = () =>
|
|
24
|
+
flushTapSync(() => {
|
|
25
|
+
for (const read of refresh) read();
|
|
26
|
+
});
|
|
27
|
+
return result.then(
|
|
28
|
+
(value) => {
|
|
29
|
+
commit();
|
|
30
|
+
return value;
|
|
31
|
+
},
|
|
32
|
+
(error) => {
|
|
33
|
+
commit();
|
|
34
|
+
throw error;
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Real Vue Chat state retained by a resource fixture, not a framework hook alias. */
|
|
42
|
+
export function useTestChat<M extends UIMessage = UIMessage>(
|
|
43
|
+
options: ChatInit<M> & { chat?: Chat<M>; throttle?: number },
|
|
44
|
+
) {
|
|
45
|
+
const [chat] = useState(() => options.chat ?? new Chat<M>(options));
|
|
46
|
+
return useChatSnapshot(chat, options.throttle);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Executes resource hooks on their actual host, preserving retained cells on updates. */
|
|
50
|
+
export function renderHook<T, P = undefined>(
|
|
51
|
+
read: (props: P) => T,
|
|
52
|
+
options?: { initialProps: P },
|
|
53
|
+
) {
|
|
54
|
+
let update!: (value: { props: P }) => void;
|
|
55
|
+
let props = options?.initialProps as P;
|
|
56
|
+
const root = createTapRoot(function useTestResource() {
|
|
57
|
+
const [input, setInput] = useState({ props });
|
|
58
|
+
update = setInput;
|
|
59
|
+
return read(input.props);
|
|
60
|
+
});
|
|
61
|
+
roots.add(root);
|
|
62
|
+
const commit = () => update({ props });
|
|
63
|
+
refresh.add(commit);
|
|
64
|
+
return {
|
|
65
|
+
result: {
|
|
66
|
+
get current() {
|
|
67
|
+
return root.getValue();
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
rerender(next: P = props) {
|
|
71
|
+
props = next;
|
|
72
|
+
flushTapSync(() => update({ props }));
|
|
73
|
+
},
|
|
74
|
+
unmount() {
|
|
75
|
+
root.unmount();
|
|
76
|
+
roots.delete(root);
|
|
77
|
+
refresh.delete(commit);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createApp,
|
|
3
|
+
defineComponent,
|
|
4
|
+
nextTick,
|
|
5
|
+
shallowRef,
|
|
6
|
+
type App,
|
|
7
|
+
type VNode,
|
|
8
|
+
} from "vue";
|
|
9
|
+
import { afterEach, vi } from "vitest";
|
|
10
|
+
import { flushTapSync } from "@ulearning/ai-tap";
|
|
11
|
+
|
|
12
|
+
const apps = new Map<App, HTMLElement>();
|
|
13
|
+
export function cleanup() {
|
|
14
|
+
for (const [app, container] of apps) {
|
|
15
|
+
app.unmount();
|
|
16
|
+
container.remove();
|
|
17
|
+
}
|
|
18
|
+
apps.clear();
|
|
19
|
+
}
|
|
20
|
+
afterEach(cleanup);
|
|
21
|
+
export const waitFor = vi.waitFor;
|
|
22
|
+
export async function act<T>(action: () => T) {
|
|
23
|
+
const value = await flushTapSync(action);
|
|
24
|
+
await nextTick();
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
export function render(node: VNode) {
|
|
28
|
+
const source = shallowRef(node);
|
|
29
|
+
const container = document.createElement("div");
|
|
30
|
+
document.body.append(container);
|
|
31
|
+
const app = createApp(defineComponent({ setup: () => () => source.value }));
|
|
32
|
+
apps.set(app, container);
|
|
33
|
+
app.mount(container);
|
|
34
|
+
return {
|
|
35
|
+
container,
|
|
36
|
+
async rerender(next: VNode) {
|
|
37
|
+
source.value = next;
|
|
38
|
+
await nextTick();
|
|
39
|
+
},
|
|
40
|
+
unmount() {
|
|
41
|
+
app.unmount();
|
|
42
|
+
apps.delete(app);
|
|
43
|
+
container.remove();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
import type { ChatTransport } from "ai";
|
|
3
|
+
import { AssistantChatTransport } from "../transport/AssistantChatTransport";
|
|
4
|
+
import type { AssistantChatResumableOptions } from "../transport/resumable";
|
|
5
|
+
|
|
6
|
+
export const getResumableAdapter = <UI_MESSAGE extends UIMessage>(
|
|
7
|
+
transport: ChatTransport<UI_MESSAGE>,
|
|
8
|
+
): AssistantChatResumableOptions | undefined => {
|
|
9
|
+
if (transport instanceof AssistantChatTransport) {
|
|
10
|
+
return transport.getResumableAdapter();
|
|
11
|
+
}
|
|
12
|
+
const candidate = (transport as { getResumableAdapter?: () => unknown })
|
|
13
|
+
.getResumableAdapter;
|
|
14
|
+
if (typeof candidate !== "function") return undefined;
|
|
15
|
+
return candidate.call(transport) as AssistantChatResumableOptions | undefined;
|
|
16
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RespondToToolApprovalOptions,
|
|
3
|
+
ThreadMessage,
|
|
4
|
+
Unstable_ToolInteractionLog,
|
|
5
|
+
} from "@ulearning/ai-core";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
addToolData,
|
|
9
|
+
collectToolApprovalResponses,
|
|
10
|
+
collectToolArtifacts,
|
|
11
|
+
collectToolInteractions,
|
|
12
|
+
restoreToolData,
|
|
13
|
+
} from "./toolHistoryCodec";
|
|
14
|
+
|
|
15
|
+
describe("tool history codec", () => {
|
|
16
|
+
it("round trips artifacts, interactions, and approval responses", () => {
|
|
17
|
+
const log: Unstable_ToolInteractionLog = {
|
|
18
|
+
entries: [{ type: "action", occurredAt: 1, payload: { refresh: true } }],
|
|
19
|
+
};
|
|
20
|
+
const response: RespondToToolApprovalOptions = {
|
|
21
|
+
approvalId: "approval-1",
|
|
22
|
+
approved: true,
|
|
23
|
+
answers: { scope: { optionIds: ["tests"] } },
|
|
24
|
+
reason: "Approved",
|
|
25
|
+
};
|
|
26
|
+
const threadMessage: ThreadMessage = {
|
|
27
|
+
id: "assistant-1",
|
|
28
|
+
role: "assistant",
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: "tool-call",
|
|
32
|
+
toolCallId: "call-1",
|
|
33
|
+
toolName: "weather",
|
|
34
|
+
args: {},
|
|
35
|
+
argsText: "{}",
|
|
36
|
+
result: undefined,
|
|
37
|
+
isError: false,
|
|
38
|
+
approval: { id: "approval-1" },
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
createdAt: new Date(),
|
|
42
|
+
status: { type: "complete", reason: "stop" },
|
|
43
|
+
metadata: {
|
|
44
|
+
unstable_state: null,
|
|
45
|
+
unstable_annotations: [],
|
|
46
|
+
unstable_data: [],
|
|
47
|
+
steps: [],
|
|
48
|
+
custom: {},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
const innerMessage = {
|
|
52
|
+
id: "inner-1",
|
|
53
|
+
parts: [{ toolCallId: "call-1", approval: { id: "approval-1" } }],
|
|
54
|
+
metadata: { custom: "kept" },
|
|
55
|
+
};
|
|
56
|
+
const artifacts = new Map<string, unknown>([
|
|
57
|
+
["call-1", { preview: "sunny" }],
|
|
58
|
+
["unrelated", "ignored"],
|
|
59
|
+
]);
|
|
60
|
+
const interactions = new Map<string, Unstable_ToolInteractionLog>([
|
|
61
|
+
["call-1", log],
|
|
62
|
+
["unrelated", log],
|
|
63
|
+
]);
|
|
64
|
+
const responses = new Map<string, RespondToToolApprovalOptions>([
|
|
65
|
+
["approval-1", response],
|
|
66
|
+
["unrelated", response],
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const encoded = addToolData(
|
|
70
|
+
innerMessage,
|
|
71
|
+
collectToolArtifacts(threadMessage, artifacts),
|
|
72
|
+
collectToolInteractions(threadMessage, interactions),
|
|
73
|
+
collectToolApprovalResponses(threadMessage, responses),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(encoded).toEqual({
|
|
77
|
+
...innerMessage,
|
|
78
|
+
metadata: {
|
|
79
|
+
custom: "kept",
|
|
80
|
+
__aui_toolArtifacts: { "call-1": { preview: "sunny" } },
|
|
81
|
+
__aui_toolInteractions: { "call-1": log },
|
|
82
|
+
__aui_toolApprovalResponses: {
|
|
83
|
+
"approval-1": {
|
|
84
|
+
approved: true,
|
|
85
|
+
answers: response.answers,
|
|
86
|
+
reason: "Approved",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
expect(innerMessage.metadata).toEqual({ custom: "kept" });
|
|
92
|
+
|
|
93
|
+
const restoredArtifacts = new Map<string, unknown>();
|
|
94
|
+
const restoredInteractions = new Map<string, Unstable_ToolInteractionLog>();
|
|
95
|
+
const restoredResponses = new Map<string, RespondToToolApprovalOptions>();
|
|
96
|
+
expect(
|
|
97
|
+
restoreToolData(
|
|
98
|
+
encoded,
|
|
99
|
+
restoredArtifacts,
|
|
100
|
+
restoredInteractions,
|
|
101
|
+
restoredResponses,
|
|
102
|
+
),
|
|
103
|
+
).toEqual(innerMessage);
|
|
104
|
+
expect(restoredArtifacts).toEqual(
|
|
105
|
+
new Map([["call-1", { preview: "sunny" }]]),
|
|
106
|
+
);
|
|
107
|
+
expect(restoredInteractions).toEqual(new Map([["call-1", log]]));
|
|
108
|
+
expect(restoredResponses).toEqual(new Map([["approval-1", response]]));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("leaves messages without tool metadata unchanged", () => {
|
|
112
|
+
const message = {
|
|
113
|
+
parts: [{ toolCallId: "call-1" }],
|
|
114
|
+
metadata: { custom: 1 },
|
|
115
|
+
};
|
|
116
|
+
expect(addToolData(message, undefined, undefined, undefined)).toBe(message);
|
|
117
|
+
expect(restoreToolData(message, new Map(), new Map(), new Map())).toBe(
|
|
118
|
+
message,
|
|
119
|
+
);
|
|
120
|
+
expect(
|
|
121
|
+
restoreToolData({ parts: [] }, new Map(), new Map(), new Map()),
|
|
122
|
+
).toEqual({ parts: [] });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("keeps live artifacts for matching calls while restoring other stored artifacts", () => {
|
|
126
|
+
const artifacts = new Map<string, unknown>([
|
|
127
|
+
["call-live", { preview: "new" }],
|
|
128
|
+
]);
|
|
129
|
+
restoreToolData(
|
|
130
|
+
{
|
|
131
|
+
metadata: {
|
|
132
|
+
__aui_toolArtifacts: {
|
|
133
|
+
"call-live": { preview: "stale" },
|
|
134
|
+
"call-stored": { preview: "earlier" },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
artifacts,
|
|
139
|
+
undefined,
|
|
140
|
+
undefined,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(artifacts).toEqual(
|
|
144
|
+
new Map([
|
|
145
|
+
["call-live", { preview: "new" }],
|
|
146
|
+
["call-stored", { preview: "earlier" }],
|
|
147
|
+
]),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("ignores non-object messages and malformed metadata values", () => {
|
|
152
|
+
expect(
|
|
153
|
+
addToolData(null, { "call-1": "artifact" }, undefined, undefined),
|
|
154
|
+
).toBeNull();
|
|
155
|
+
const invalidParts = { parts: "invalid" };
|
|
156
|
+
expect(
|
|
157
|
+
addToolData(invalidParts, { "call-1": "artifact" }, undefined, undefined),
|
|
158
|
+
).toBe(invalidParts);
|
|
159
|
+
expect(restoreToolData(42, new Map(), new Map(), new Map())).toBe(42);
|
|
160
|
+
const invalidMetadata = { parts: [], metadata: "invalid" };
|
|
161
|
+
expect(
|
|
162
|
+
restoreToolData(invalidMetadata, new Map(), new Map(), new Map()),
|
|
163
|
+
).toBe(invalidMetadata);
|
|
164
|
+
|
|
165
|
+
const artifacts = new Map<string, unknown>();
|
|
166
|
+
const interactions = new Map<string, Unstable_ToolInteractionLog>();
|
|
167
|
+
const responses = new Map<string, RespondToToolApprovalOptions>();
|
|
168
|
+
expect(
|
|
169
|
+
restoreToolData(
|
|
170
|
+
{
|
|
171
|
+
metadata: {
|
|
172
|
+
custom: "kept",
|
|
173
|
+
__aui_toolArtifacts: "invalid",
|
|
174
|
+
__aui_toolInteractions: 1,
|
|
175
|
+
__aui_toolApprovalResponses: [],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
artifacts,
|
|
179
|
+
interactions,
|
|
180
|
+
responses,
|
|
181
|
+
),
|
|
182
|
+
).toEqual({ metadata: { custom: "kept" } });
|
|
183
|
+
expect(artifacts.size).toBe(0);
|
|
184
|
+
expect(interactions.size).toBe(0);
|
|
185
|
+
expect(responses.size).toBe(0);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RespondToToolApprovalOptions,
|
|
3
|
+
ThreadMessage,
|
|
4
|
+
Unstable_ToolInteractionLog,
|
|
5
|
+
} from "@ulearning/ai-core";
|
|
6
|
+
import {
|
|
7
|
+
appendToolInteraction,
|
|
8
|
+
isJSONValueEqual,
|
|
9
|
+
isRecord,
|
|
10
|
+
readToolInteractionLog,
|
|
11
|
+
} from "@ulearning/ai-core/internal";
|
|
12
|
+
import { normalizeToolApprovalAnswers } from "../converters/toolApprovalAnswers";
|
|
13
|
+
|
|
14
|
+
const TOOL_ARTIFACTS_METADATA_KEY = "__aui_toolArtifacts";
|
|
15
|
+
const TOOL_INTERACTIONS_METADATA_KEY = "__aui_toolInteractions";
|
|
16
|
+
const TOOL_APPROVAL_RESPONSES_METADATA_KEY = "__aui_toolApprovalResponses";
|
|
17
|
+
|
|
18
|
+
export type StoredToolApprovalResponse = Omit<
|
|
19
|
+
RespondToToolApprovalOptions,
|
|
20
|
+
"approvalId"
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
const mergeInteractionLogs = (
|
|
24
|
+
stored: Unstable_ToolInteractionLog,
|
|
25
|
+
current: Unstable_ToolInteractionLog | undefined,
|
|
26
|
+
): Unstable_ToolInteractionLog => {
|
|
27
|
+
const incoming = readToolInteractionLog(current);
|
|
28
|
+
if (!incoming) return stored;
|
|
29
|
+
const omitted = Math.max(stored.omitted ?? 0, incoming.omitted ?? 0);
|
|
30
|
+
let merged: Unstable_ToolInteractionLog = {
|
|
31
|
+
entries: stored.entries,
|
|
32
|
+
...(omitted ? { omitted } : undefined),
|
|
33
|
+
};
|
|
34
|
+
for (const entry of incoming.entries) {
|
|
35
|
+
if (
|
|
36
|
+
merged.entries.some(
|
|
37
|
+
(existing) =>
|
|
38
|
+
existing.type === entry.type &&
|
|
39
|
+
existing.occurredAt === entry.occurredAt &&
|
|
40
|
+
isJSONValueEqual(existing.payload, entry.payload),
|
|
41
|
+
)
|
|
42
|
+
) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
merged = appendToolInteraction(merged, entry);
|
|
46
|
+
}
|
|
47
|
+
return merged;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const collectToolArtifacts = (
|
|
51
|
+
message: ThreadMessage,
|
|
52
|
+
toolArtifacts: ReadonlyMap<string, unknown> | undefined,
|
|
53
|
+
) => {
|
|
54
|
+
if (!toolArtifacts) return undefined;
|
|
55
|
+
const entries = message.content.flatMap((part) => {
|
|
56
|
+
if (part.type !== "tool-call") return [];
|
|
57
|
+
const artifact = toolArtifacts.get(part.toolCallId);
|
|
58
|
+
return artifact === undefined ? [] : [[part.toolCallId, artifact] as const];
|
|
59
|
+
});
|
|
60
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const collectToolInteractions = (
|
|
64
|
+
message: ThreadMessage,
|
|
65
|
+
toolInteractions:
|
|
66
|
+
| ReadonlyMap<string, Unstable_ToolInteractionLog>
|
|
67
|
+
| undefined,
|
|
68
|
+
) => {
|
|
69
|
+
if (!toolInteractions) return undefined;
|
|
70
|
+
const entries = message.content.flatMap((part) => {
|
|
71
|
+
if (part.type !== "tool-call") return [];
|
|
72
|
+
const interactions = toolInteractions.get(part.toolCallId);
|
|
73
|
+
return interactions === undefined
|
|
74
|
+
? []
|
|
75
|
+
: [[part.toolCallId, interactions] as const];
|
|
76
|
+
});
|
|
77
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const collectToolApprovalResponses = (
|
|
81
|
+
message: ThreadMessage,
|
|
82
|
+
toolApprovalResponses:
|
|
83
|
+
| ReadonlyMap<string, RespondToToolApprovalOptions>
|
|
84
|
+
| undefined,
|
|
85
|
+
) => {
|
|
86
|
+
if (!toolApprovalResponses) return undefined;
|
|
87
|
+
const entries = message.content.flatMap((part) => {
|
|
88
|
+
if (part.type !== "tool-call" || !part.approval) return [];
|
|
89
|
+
const response = toolApprovalResponses.get(part.approval.id);
|
|
90
|
+
if (!response) return [];
|
|
91
|
+
return [
|
|
92
|
+
[
|
|
93
|
+
part.approval.id,
|
|
94
|
+
{
|
|
95
|
+
approved: response.approved,
|
|
96
|
+
...(response.optionId != null && { optionId: response.optionId }),
|
|
97
|
+
...(response.text != null && { text: response.text }),
|
|
98
|
+
...(response.answers != null && { answers: response.answers }),
|
|
99
|
+
...(response.reason != null && { reason: response.reason }),
|
|
100
|
+
},
|
|
101
|
+
] as const,
|
|
102
|
+
];
|
|
103
|
+
});
|
|
104
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const addToolData = <TMessage>(
|
|
108
|
+
message: TMessage,
|
|
109
|
+
toolArtifacts: Record<string, unknown> | undefined,
|
|
110
|
+
toolInteractions: Record<string, Unstable_ToolInteractionLog> | undefined,
|
|
111
|
+
toolApprovalResponses: Record<string, StoredToolApprovalResponse> | undefined,
|
|
112
|
+
): TMessage => {
|
|
113
|
+
if (
|
|
114
|
+
(!toolArtifacts && !toolInteractions && !toolApprovalResponses) ||
|
|
115
|
+
!isRecord(message) ||
|
|
116
|
+
!Array.isArray(message.parts)
|
|
117
|
+
)
|
|
118
|
+
return message;
|
|
119
|
+
const toolCallIds = message.parts.flatMap((part) => {
|
|
120
|
+
if (!isRecord(part) || typeof part.toolCallId !== "string") return [];
|
|
121
|
+
return [part.toolCallId];
|
|
122
|
+
});
|
|
123
|
+
const artifacts = toolArtifacts
|
|
124
|
+
? Object.fromEntries(
|
|
125
|
+
toolCallIds.flatMap((toolCallId) =>
|
|
126
|
+
Object.hasOwn(toolArtifacts, toolCallId)
|
|
127
|
+
? [[toolCallId, toolArtifacts[toolCallId]] as const]
|
|
128
|
+
: [],
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
: undefined;
|
|
132
|
+
const interactions = toolInteractions
|
|
133
|
+
? Object.fromEntries(
|
|
134
|
+
toolCallIds.flatMap((toolCallId) =>
|
|
135
|
+
Object.hasOwn(toolInteractions, toolCallId)
|
|
136
|
+
? [[toolCallId, toolInteractions[toolCallId]] as const]
|
|
137
|
+
: [],
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
: undefined;
|
|
141
|
+
const approvalIds = message.parts.flatMap((part) => {
|
|
142
|
+
if (!isRecord(part) || !isRecord(part.approval)) return [];
|
|
143
|
+
const approvalId = part.approval.id;
|
|
144
|
+
return typeof approvalId === "string" ? [approvalId] : [];
|
|
145
|
+
});
|
|
146
|
+
const approvalResponses = toolApprovalResponses
|
|
147
|
+
? Object.fromEntries(
|
|
148
|
+
approvalIds.flatMap((approvalId) =>
|
|
149
|
+
Object.hasOwn(toolApprovalResponses, approvalId)
|
|
150
|
+
? [[approvalId, toolApprovalResponses[approvalId]] as const]
|
|
151
|
+
: [],
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
: undefined;
|
|
155
|
+
const hasArtifacts = !!artifacts && Object.keys(artifacts).length > 0;
|
|
156
|
+
const hasInteractions =
|
|
157
|
+
!!interactions && Object.keys(interactions).length > 0;
|
|
158
|
+
const hasApprovalResponses =
|
|
159
|
+
!!approvalResponses && Object.keys(approvalResponses).length > 0;
|
|
160
|
+
if (!hasArtifacts && !hasInteractions && !hasApprovalResponses)
|
|
161
|
+
return message;
|
|
162
|
+
const metadata = isRecord(message.metadata) ? message.metadata : {};
|
|
163
|
+
return {
|
|
164
|
+
...message,
|
|
165
|
+
metadata: {
|
|
166
|
+
...metadata,
|
|
167
|
+
...(hasArtifacts && { [TOOL_ARTIFACTS_METADATA_KEY]: artifacts }),
|
|
168
|
+
...(hasInteractions && {
|
|
169
|
+
[TOOL_INTERACTIONS_METADATA_KEY]: interactions,
|
|
170
|
+
}),
|
|
171
|
+
...(hasApprovalResponses && {
|
|
172
|
+
[TOOL_APPROVAL_RESPONSES_METADATA_KEY]: approvalResponses,
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
} as TMessage;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const restoreToolData = <TMessage>(
|
|
179
|
+
message: TMessage,
|
|
180
|
+
toolArtifacts: Map<string, unknown> | undefined,
|
|
181
|
+
toolInteractions: Map<string, Unstable_ToolInteractionLog> | undefined,
|
|
182
|
+
toolApprovalResponses: Map<string, RespondToToolApprovalOptions> | undefined,
|
|
183
|
+
): TMessage => {
|
|
184
|
+
if (!isRecord(message) || !isRecord(message.metadata)) return message;
|
|
185
|
+
const metadata = message.metadata;
|
|
186
|
+
const hasArtifacts = Object.hasOwn(metadata, TOOL_ARTIFACTS_METADATA_KEY);
|
|
187
|
+
const hasInteractions = Object.hasOwn(
|
|
188
|
+
metadata,
|
|
189
|
+
TOOL_INTERACTIONS_METADATA_KEY,
|
|
190
|
+
);
|
|
191
|
+
const hasApprovalResponses = Object.hasOwn(
|
|
192
|
+
metadata,
|
|
193
|
+
TOOL_APPROVAL_RESPONSES_METADATA_KEY,
|
|
194
|
+
);
|
|
195
|
+
if (!hasArtifacts && !hasInteractions && !hasApprovalResponses)
|
|
196
|
+
return message;
|
|
197
|
+
const artifacts = metadata[TOOL_ARTIFACTS_METADATA_KEY];
|
|
198
|
+
if (toolArtifacts && isRecord(artifacts)) {
|
|
199
|
+
for (const [toolCallId, artifact] of Object.entries(artifacts)) {
|
|
200
|
+
if (!toolArtifacts.has(toolCallId))
|
|
201
|
+
toolArtifacts.set(toolCallId, artifact);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const interactions = metadata[TOOL_INTERACTIONS_METADATA_KEY];
|
|
205
|
+
if (toolInteractions && isRecord(interactions)) {
|
|
206
|
+
for (const [toolCallId, value] of Object.entries(interactions)) {
|
|
207
|
+
const log = readToolInteractionLog(value);
|
|
208
|
+
if (log) {
|
|
209
|
+
toolInteractions.set(
|
|
210
|
+
toolCallId,
|
|
211
|
+
mergeInteractionLogs(log, toolInteractions.get(toolCallId)),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const approvalResponses = metadata[TOOL_APPROVAL_RESPONSES_METADATA_KEY];
|
|
217
|
+
if (toolApprovalResponses && isRecord(approvalResponses)) {
|
|
218
|
+
for (const [approvalId, value] of Object.entries(approvalResponses)) {
|
|
219
|
+
if (!isRecord(value) || typeof value.approved !== "boolean") continue;
|
|
220
|
+
const answers = normalizeToolApprovalAnswers(value.answers);
|
|
221
|
+
toolApprovalResponses.set(approvalId, {
|
|
222
|
+
approvalId,
|
|
223
|
+
approved: value.approved,
|
|
224
|
+
...(typeof value.optionId === "string" && {
|
|
225
|
+
optionId: value.optionId,
|
|
226
|
+
}),
|
|
227
|
+
...(typeof value.text === "string" && { text: value.text }),
|
|
228
|
+
...(answers && Object.keys(answers).length > 0 && { answers }),
|
|
229
|
+
...(typeof value.reason === "string" && { reason: value.reason }),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const {
|
|
234
|
+
[TOOL_ARTIFACTS_METADATA_KEY]: _,
|
|
235
|
+
[TOOL_INTERACTIONS_METADATA_KEY]: __,
|
|
236
|
+
[TOOL_APPROVAL_RESPONSES_METADATA_KEY]: ___,
|
|
237
|
+
...restMetadata
|
|
238
|
+
} = metadata;
|
|
239
|
+
const { metadata: _metadata, ...restMessage } = message;
|
|
240
|
+
return (
|
|
241
|
+
Object.keys(restMetadata).length === 0
|
|
242
|
+
? restMessage
|
|
243
|
+
: { ...restMessage, metadata: restMetadata }
|
|
244
|
+
) as TMessage;
|
|
245
|
+
};
|