@assistant-ui/react 0.10.44 → 0.10.46
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/dist/model-context/frame/AssistantFrameHost.d.ts +37 -0
- package/dist/model-context/frame/AssistantFrameHost.d.ts.map +1 -0
- package/dist/model-context/frame/AssistantFrameHost.js +151 -0
- package/dist/model-context/frame/AssistantFrameHost.js.map +1 -0
- package/dist/model-context/frame/AssistantFrameProvider.d.ts +41 -0
- package/dist/model-context/frame/AssistantFrameProvider.d.ts.map +1 -0
- package/dist/model-context/frame/AssistantFrameProvider.js +142 -0
- package/dist/model-context/frame/AssistantFrameProvider.js.map +1 -0
- package/dist/model-context/frame/AssistantFrameTypes.d.ts +29 -0
- package/dist/model-context/frame/AssistantFrameTypes.d.ts.map +1 -0
- package/dist/model-context/frame/AssistantFrameTypes.js +6 -0
- package/dist/model-context/frame/AssistantFrameTypes.js.map +1 -0
- package/dist/model-context/frame/index.d.ts +5 -0
- package/dist/model-context/frame/index.d.ts.map +1 -0
- package/dist/model-context/frame/index.js +6 -0
- package/dist/model-context/frame/index.js.map +1 -0
- package/dist/model-context/frame/useAssistantFrameHost.d.ts +28 -0
- package/dist/model-context/frame/useAssistantFrameHost.d.ts.map +1 -0
- package/dist/model-context/frame/useAssistantFrameHost.js +25 -0
- package/dist/model-context/frame/useAssistantFrameHost.js.map +1 -0
- package/dist/model-context/index.d.ts +2 -0
- package/dist/model-context/index.d.ts.map +1 -1
- package/dist/model-context/index.js +2 -0
- package/dist/model-context/index.js.map +1 -1
- package/dist/model-context/registry/ModelContextRegistry.d.ts +19 -0
- package/dist/model-context/registry/ModelContextRegistry.d.ts.map +1 -0
- package/dist/model-context/registry/ModelContextRegistry.js +117 -0
- package/dist/model-context/registry/ModelContextRegistry.js.map +1 -0
- package/dist/model-context/registry/ModelContextRegistryHandles.d.ts +14 -0
- package/dist/model-context/registry/ModelContextRegistryHandles.d.ts.map +1 -0
- package/dist/model-context/registry/ModelContextRegistryHandles.js +1 -0
- package/dist/model-context/registry/ModelContextRegistryHandles.js.map +1 -0
- package/dist/model-context/registry/index.d.ts +3 -0
- package/dist/model-context/registry/index.d.ts.map +1 -0
- package/dist/model-context/registry/index.js +4 -0
- package/dist/model-context/registry/index.js.map +1 -0
- package/dist/model-context/useAssistantInstructions.d.ts +1 -2
- package/dist/model-context/useAssistantInstructions.d.ts.map +1 -1
- package/dist/model-context/useAssistantInstructions.js.map +1 -1
- package/dist/runtimes/utils/MessageRepository.d.ts.map +1 -1
- package/dist/runtimes/utils/MessageRepository.js +15 -2
- package/dist/runtimes/utils/MessageRepository.js.map +1 -1
- package/package.json +3 -3
- package/src/model-context/frame/AssistantFrame.test.ts +353 -0
- package/src/model-context/frame/AssistantFrameHost.ts +218 -0
- package/src/model-context/frame/AssistantFrameProvider.ts +225 -0
- package/src/model-context/frame/AssistantFrameTypes.ts +40 -0
- package/src/model-context/frame/SPEC_AssistantFrame.md +104 -0
- package/src/model-context/frame/index.ts +4 -0
- package/src/model-context/frame/useAssistantFrameHost.ts +48 -0
- package/src/model-context/index.ts +3 -0
- package/src/model-context/registry/ModelContextRegistry.ts +165 -0
- package/src/model-context/registry/ModelContextRegistryHandles.ts +19 -0
- package/src/model-context/registry/SPEC_ModelContextRegistry.md +40 -0
- package/src/model-context/registry/index.ts +2 -0
- package/src/model-context/useAssistantInstructions.tsx +1 -1
- package/src/runtimes/utils/MessageRepository.tsx +17 -2
- package/src/tests/MessageRepository.test.ts +45 -0
@@ -0,0 +1,353 @@
|
|
1
|
+
/**
|
2
|
+
* @vitest-environment jsdom
|
3
|
+
*/
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
5
|
+
import { AssistantFrameProvider } from "./AssistantFrameProvider";
|
6
|
+
import { AssistantFrameHost } from "./AssistantFrameHost";
|
7
|
+
import { ModelContextRegistry } from "../registry/ModelContextRegistry";
|
8
|
+
import z from "zod";
|
9
|
+
|
10
|
+
describe("AssistantFrame Integration", () => {
|
11
|
+
let messageHandlers: Map<string, (event: MessageEvent) => void>;
|
12
|
+
let iframeWindow: Window;
|
13
|
+
let parentWindow: any;
|
14
|
+
|
15
|
+
beforeEach(() => {
|
16
|
+
messageHandlers = new Map();
|
17
|
+
|
18
|
+
// Create a mock parent window that the iframe can post back to
|
19
|
+
parentWindow = {
|
20
|
+
postMessage: vi.fn((data: any) => {
|
21
|
+
// When iframe posts to parent, deliver to parent handler
|
22
|
+
const parentHandler = messageHandlers.get("parent");
|
23
|
+
if (parentHandler) {
|
24
|
+
Promise.resolve().then(() => {
|
25
|
+
parentHandler({
|
26
|
+
data,
|
27
|
+
source: iframeWindow,
|
28
|
+
origin: "*",
|
29
|
+
} as MessageEvent);
|
30
|
+
});
|
31
|
+
}
|
32
|
+
}),
|
33
|
+
};
|
34
|
+
|
35
|
+
// Create mock iframe window with proper message routing
|
36
|
+
iframeWindow = {
|
37
|
+
postMessage: vi.fn((data: any) => {
|
38
|
+
// Route message to iframe handler (provider)
|
39
|
+
const iframeHandler = messageHandlers.get("iframe");
|
40
|
+
if (iframeHandler) {
|
41
|
+
Promise.resolve().then(() => {
|
42
|
+
iframeHandler({
|
43
|
+
data,
|
44
|
+
source: parentWindow, // parent window is the source for subscription
|
45
|
+
origin: "*",
|
46
|
+
} as MessageEvent);
|
47
|
+
});
|
48
|
+
}
|
49
|
+
}),
|
50
|
+
} as any;
|
51
|
+
|
52
|
+
// Mock window.parent for iframe to broadcast to
|
53
|
+
Object.defineProperty(window, "parent", {
|
54
|
+
value: parentWindow,
|
55
|
+
writable: true,
|
56
|
+
configurable: true,
|
57
|
+
});
|
58
|
+
|
59
|
+
// Mock window methods for message passing
|
60
|
+
vi.spyOn(window, "addEventListener").mockImplementation(
|
61
|
+
(event: string, handler: any) => {
|
62
|
+
if (event === "message") {
|
63
|
+
// Store both handlers - we'll determine which is which based on usage
|
64
|
+
if (!messageHandlers.has("iframe")) {
|
65
|
+
messageHandlers.set("iframe", handler); // First registration is provider
|
66
|
+
} else {
|
67
|
+
messageHandlers.set("parent", handler); // Second is host
|
68
|
+
}
|
69
|
+
}
|
70
|
+
},
|
71
|
+
);
|
72
|
+
|
73
|
+
vi.spyOn(window, "removeEventListener").mockImplementation(() => {});
|
74
|
+
|
75
|
+
vi.spyOn(window, "postMessage").mockImplementation(() => {
|
76
|
+
// This shouldn't be called in our test setup
|
77
|
+
});
|
78
|
+
});
|
79
|
+
|
80
|
+
afterEach(() => {
|
81
|
+
// Clean up
|
82
|
+
vi.restoreAllMocks();
|
83
|
+
AssistantFrameProvider.dispose();
|
84
|
+
messageHandlers.clear();
|
85
|
+
});
|
86
|
+
|
87
|
+
it("should establish connection between host and provider", async () => {
|
88
|
+
// Setup provider in iframe
|
89
|
+
const registry = new ModelContextRegistry();
|
90
|
+
const unsubscribe =
|
91
|
+
AssistantFrameProvider.addModelContextProvider(registry);
|
92
|
+
|
93
|
+
// Setup host in parent
|
94
|
+
const host = new AssistantFrameHost(iframeWindow);
|
95
|
+
|
96
|
+
// Wait for connection
|
97
|
+
await vi.waitFor(() => {
|
98
|
+
expect(iframeWindow.postMessage).toHaveBeenCalledWith(
|
99
|
+
expect.objectContaining({
|
100
|
+
channel: "assistant-ui-frame",
|
101
|
+
message: expect.objectContaining({
|
102
|
+
type: "model-context-request",
|
103
|
+
}),
|
104
|
+
}),
|
105
|
+
"*",
|
106
|
+
);
|
107
|
+
});
|
108
|
+
|
109
|
+
// Clean up
|
110
|
+
host.dispose();
|
111
|
+
unsubscribe();
|
112
|
+
});
|
113
|
+
|
114
|
+
it("should sync tools from provider to host", async () => {
|
115
|
+
// Setup provider with tools
|
116
|
+
const registry = new ModelContextRegistry();
|
117
|
+
|
118
|
+
const toolExecute = vi.fn().mockResolvedValue({ result: "search results" });
|
119
|
+
registry.addTool({
|
120
|
+
toolName: "search",
|
121
|
+
description: "Search the web",
|
122
|
+
parameters: z.object({ query: z.string() }),
|
123
|
+
execute: toolExecute,
|
124
|
+
});
|
125
|
+
|
126
|
+
const unsubscribe =
|
127
|
+
AssistantFrameProvider.addModelContextProvider(registry);
|
128
|
+
|
129
|
+
// Setup host
|
130
|
+
const host = new AssistantFrameHost(iframeWindow);
|
131
|
+
|
132
|
+
// Wait for connection and initial sync
|
133
|
+
await vi.waitFor(() => {
|
134
|
+
const context = host.getModelContext();
|
135
|
+
expect(context.tools).toBeDefined();
|
136
|
+
expect(context.tools?.search).toBeDefined();
|
137
|
+
expect(context.tools?.search.description).toBe("Search the web");
|
138
|
+
});
|
139
|
+
|
140
|
+
// Clean up
|
141
|
+
host.dispose();
|
142
|
+
unsubscribe();
|
143
|
+
});
|
144
|
+
|
145
|
+
it("should execute tools through the frame boundary", async () => {
|
146
|
+
// Setup provider with executable tool
|
147
|
+
const registry = new ModelContextRegistry();
|
148
|
+
|
149
|
+
const toolExecute = vi
|
150
|
+
.fn()
|
151
|
+
.mockResolvedValue({ results: ["result1", "result2"] });
|
152
|
+
registry.addTool({
|
153
|
+
toolName: "search",
|
154
|
+
description: "Search the web",
|
155
|
+
parameters: z.object({ query: z.string() }),
|
156
|
+
execute: toolExecute,
|
157
|
+
});
|
158
|
+
|
159
|
+
const unsubscribe =
|
160
|
+
AssistantFrameProvider.addModelContextProvider(registry);
|
161
|
+
|
162
|
+
// Setup host
|
163
|
+
const host = new AssistantFrameHost(iframeWindow);
|
164
|
+
|
165
|
+
// Wait for tools to be available
|
166
|
+
await vi.waitFor(() => {
|
167
|
+
const context = host.getModelContext();
|
168
|
+
expect(context.tools?.search).toBeDefined();
|
169
|
+
});
|
170
|
+
|
171
|
+
// Execute tool through host
|
172
|
+
const context = host.getModelContext();
|
173
|
+
const searchTool = context.tools?.search;
|
174
|
+
|
175
|
+
const resultPromise = searchTool!.execute!(
|
176
|
+
{ query: "test query" },
|
177
|
+
{} as any,
|
178
|
+
);
|
179
|
+
|
180
|
+
// Wait for tool execution
|
181
|
+
await vi.waitFor(() => {
|
182
|
+
expect(toolExecute).toHaveBeenCalledWith(
|
183
|
+
{ query: "test query" },
|
184
|
+
expect.objectContaining({
|
185
|
+
toolCallId: expect.any(String),
|
186
|
+
abortSignal: expect.any(AbortSignal),
|
187
|
+
}),
|
188
|
+
);
|
189
|
+
});
|
190
|
+
|
191
|
+
const result = await resultPromise;
|
192
|
+
expect(result).toEqual({ results: ["result1", "result2"] });
|
193
|
+
|
194
|
+
// Clean up
|
195
|
+
host.dispose();
|
196
|
+
unsubscribe();
|
197
|
+
});
|
198
|
+
|
199
|
+
it("should handle tool execution errors", async () => {
|
200
|
+
// Setup provider with failing tool
|
201
|
+
const registry = new ModelContextRegistry();
|
202
|
+
|
203
|
+
const toolExecute = vi
|
204
|
+
.fn()
|
205
|
+
.mockRejectedValue(new Error("Tool execution failed"));
|
206
|
+
registry.addTool({
|
207
|
+
toolName: "failingTool",
|
208
|
+
description: "A tool that fails",
|
209
|
+
parameters: z.object({ input: z.string() }),
|
210
|
+
execute: toolExecute,
|
211
|
+
});
|
212
|
+
|
213
|
+
const unsubscribe =
|
214
|
+
AssistantFrameProvider.addModelContextProvider(registry);
|
215
|
+
|
216
|
+
// Setup host
|
217
|
+
const host = new AssistantFrameHost(iframeWindow);
|
218
|
+
|
219
|
+
// Wait for tools to be available
|
220
|
+
await vi.waitFor(() => {
|
221
|
+
const context = host.getModelContext();
|
222
|
+
expect(context.tools?.failingTool).toBeDefined();
|
223
|
+
});
|
224
|
+
|
225
|
+
// Execute tool and expect error
|
226
|
+
const context = host.getModelContext();
|
227
|
+
const failingTool = context.tools?.failingTool;
|
228
|
+
|
229
|
+
await expect(
|
230
|
+
failingTool!.execute!({ input: "test" }, {} as any),
|
231
|
+
).rejects.toThrow("Tool execution failed");
|
232
|
+
|
233
|
+
// Clean up
|
234
|
+
host.dispose();
|
235
|
+
unsubscribe();
|
236
|
+
});
|
237
|
+
|
238
|
+
it("should handle multiple providers", async () => {
|
239
|
+
// Setup multiple providers
|
240
|
+
const registry1 = new ModelContextRegistry();
|
241
|
+
const registry2 = new ModelContextRegistry();
|
242
|
+
|
243
|
+
registry1.addTool({
|
244
|
+
toolName: "tool1",
|
245
|
+
description: "First tool",
|
246
|
+
parameters: z.object({ input: z.string() }),
|
247
|
+
execute: async () => ({ from: "tool1" }),
|
248
|
+
});
|
249
|
+
|
250
|
+
registry2.addTool({
|
251
|
+
toolName: "tool2",
|
252
|
+
description: "Second tool",
|
253
|
+
parameters: z.object({ input: z.string() }),
|
254
|
+
execute: async () => ({ from: "tool2" }),
|
255
|
+
});
|
256
|
+
|
257
|
+
const unsub1 = AssistantFrameProvider.addModelContextProvider(registry1);
|
258
|
+
const unsub2 = AssistantFrameProvider.addModelContextProvider(registry2);
|
259
|
+
|
260
|
+
// Setup host
|
261
|
+
const host = new AssistantFrameHost(iframeWindow);
|
262
|
+
|
263
|
+
// Wait for both tools to be available
|
264
|
+
await vi.waitFor(() => {
|
265
|
+
const context = host.getModelContext();
|
266
|
+
expect(context.tools?.tool1).toBeDefined();
|
267
|
+
expect(context.tools?.tool2).toBeDefined();
|
268
|
+
});
|
269
|
+
|
270
|
+
// Clean up
|
271
|
+
host.dispose();
|
272
|
+
unsub1();
|
273
|
+
unsub2();
|
274
|
+
});
|
275
|
+
|
276
|
+
it("should merge system instructions from multiple providers", async () => {
|
277
|
+
// Setup providers with system instructions
|
278
|
+
const registry1 = new ModelContextRegistry();
|
279
|
+
const registry2 = new ModelContextRegistry();
|
280
|
+
|
281
|
+
registry1.addInstruction("You are a helpful assistant.");
|
282
|
+
registry2.addInstruction("Always be concise.");
|
283
|
+
|
284
|
+
const unsub1 = AssistantFrameProvider.addModelContextProvider(registry1);
|
285
|
+
const unsub2 = AssistantFrameProvider.addModelContextProvider(registry2);
|
286
|
+
|
287
|
+
// Setup host
|
288
|
+
const host = new AssistantFrameHost(iframeWindow);
|
289
|
+
|
290
|
+
// Wait for instructions to be synced
|
291
|
+
await vi.waitFor(() => {
|
292
|
+
const context = host.getModelContext();
|
293
|
+
expect(context.system).toBeDefined();
|
294
|
+
expect(context.system).toContain("You are a helpful assistant.");
|
295
|
+
expect(context.system).toContain("Always be concise.");
|
296
|
+
});
|
297
|
+
|
298
|
+
// Clean up
|
299
|
+
host.dispose();
|
300
|
+
unsub1();
|
301
|
+
unsub2();
|
302
|
+
});
|
303
|
+
|
304
|
+
it("should act as empty ModelContextProvider when iframe has no providers", async () => {
|
305
|
+
// Don't register any providers in the iframe
|
306
|
+
// This simulates an iframe that doesn't respond to model-context requests
|
307
|
+
|
308
|
+
// Setup host
|
309
|
+
const host = new AssistantFrameHost(iframeWindow);
|
310
|
+
|
311
|
+
// Host should immediately return empty context
|
312
|
+
const context = host.getModelContext();
|
313
|
+
expect(context).toEqual({});
|
314
|
+
|
315
|
+
// Wait a bit to ensure no errors occur
|
316
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
317
|
+
|
318
|
+
// Context should still be empty
|
319
|
+
expect(host.getModelContext()).toEqual({});
|
320
|
+
|
321
|
+
// Clean up
|
322
|
+
host.dispose();
|
323
|
+
});
|
324
|
+
|
325
|
+
it("should clean up properly on dispose", async () => {
|
326
|
+
// Setup provider
|
327
|
+
const registry = new ModelContextRegistry();
|
328
|
+
|
329
|
+
const unsubscribe =
|
330
|
+
AssistantFrameProvider.addModelContextProvider(registry);
|
331
|
+
|
332
|
+
// Setup host
|
333
|
+
const host = new AssistantFrameHost(iframeWindow);
|
334
|
+
|
335
|
+
// Wait for connection
|
336
|
+
await vi.waitFor(() => {
|
337
|
+
expect(iframeWindow.postMessage).toHaveBeenCalled();
|
338
|
+
});
|
339
|
+
|
340
|
+
// Dispose host
|
341
|
+
host.dispose();
|
342
|
+
|
343
|
+
// Verify event listener was removed (no unsubscribe message in new design)
|
344
|
+
expect(window.removeEventListener).toHaveBeenCalledWith(
|
345
|
+
"message",
|
346
|
+
expect.any(Function),
|
347
|
+
);
|
348
|
+
|
349
|
+
// Clean up provider
|
350
|
+
unsubscribe();
|
351
|
+
AssistantFrameProvider.dispose();
|
352
|
+
});
|
353
|
+
});
|
@@ -0,0 +1,218 @@
|
|
1
|
+
import {
|
2
|
+
ModelContextProvider,
|
3
|
+
ModelContext,
|
4
|
+
} from "../../model-context/ModelContextTypes";
|
5
|
+
import { Unsubscribe } from "../../types/Unsubscribe";
|
6
|
+
import { Tool } from "assistant-stream";
|
7
|
+
import {
|
8
|
+
FrameMessage,
|
9
|
+
FRAME_MESSAGE_CHANNEL,
|
10
|
+
SerializedModelContext,
|
11
|
+
SerializedTool,
|
12
|
+
} from "./AssistantFrameTypes";
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Deserializes tools from JSON Schema format back to Tool objects
|
16
|
+
*/
|
17
|
+
const deserializeTool = (serializedTool: SerializedTool): Tool<any, any> =>
|
18
|
+
({
|
19
|
+
parameters: serializedTool.parameters,
|
20
|
+
...(serializedTool.description && {
|
21
|
+
description: serializedTool.description,
|
22
|
+
}),
|
23
|
+
...(serializedTool.disabled !== undefined && {
|
24
|
+
disabled: serializedTool.disabled,
|
25
|
+
}),
|
26
|
+
...(serializedTool.type && { type: serializedTool.type }),
|
27
|
+
}) as Tool<any, any>;
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Deserializes a ModelContext from transmission format
|
31
|
+
*/
|
32
|
+
const deserializeModelContext = (
|
33
|
+
serialized: SerializedModelContext,
|
34
|
+
): ModelContext => ({
|
35
|
+
...(serialized.system !== undefined && { system: serialized.system }),
|
36
|
+
...(serialized.tools && {
|
37
|
+
tools: Object.fromEntries(
|
38
|
+
Object.entries(serialized.tools).map(([name, tool]) => [
|
39
|
+
name,
|
40
|
+
deserializeTool(tool),
|
41
|
+
]),
|
42
|
+
),
|
43
|
+
}),
|
44
|
+
});
|
45
|
+
|
46
|
+
/**
|
47
|
+
* AssistantFrameHost - Runs in the parent window and acts as a ModelContextProvider
|
48
|
+
* that receives context from an iframe's AssistantFrameProvider.
|
49
|
+
*
|
50
|
+
* Usage example:
|
51
|
+
* ```typescript
|
52
|
+
* // In parent window
|
53
|
+
* const frameHost = new AssistantFrameHost(iframeWindow);
|
54
|
+
*
|
55
|
+
* // Register with assistant runtime
|
56
|
+
* const runtime = useAssistantRuntime();
|
57
|
+
* runtime.registerModelContextProvider(frameHost);
|
58
|
+
*
|
59
|
+
* // The assistant now has access to tools from the iframe
|
60
|
+
* ```
|
61
|
+
*/
|
62
|
+
export class AssistantFrameHost implements ModelContextProvider {
|
63
|
+
private _context: ModelContext = {};
|
64
|
+
private _subscribers = new Set<() => void>();
|
65
|
+
private _pendingRequests = new Map<
|
66
|
+
string,
|
67
|
+
{
|
68
|
+
resolve: (value: any) => void;
|
69
|
+
reject: (error: any) => void;
|
70
|
+
}
|
71
|
+
>();
|
72
|
+
private _requestCounter = 0;
|
73
|
+
private _iframeWindow: Window;
|
74
|
+
private _targetOrigin: string;
|
75
|
+
|
76
|
+
constructor(iframeWindow: Window, targetOrigin: string = "*") {
|
77
|
+
this._iframeWindow = iframeWindow;
|
78
|
+
this._targetOrigin = targetOrigin;
|
79
|
+
|
80
|
+
this.handleMessage = this.handleMessage.bind(this);
|
81
|
+
window.addEventListener("message", this.handleMessage);
|
82
|
+
|
83
|
+
// Request initial context
|
84
|
+
this.requestContext();
|
85
|
+
}
|
86
|
+
|
87
|
+
private handleMessage(event: MessageEvent) {
|
88
|
+
// Security: Validate origin and source
|
89
|
+
if (this._targetOrigin !== "*" && event.origin !== this._targetOrigin)
|
90
|
+
return;
|
91
|
+
if (event.source !== this._iframeWindow) return;
|
92
|
+
if (event.data?.channel !== FRAME_MESSAGE_CHANNEL) return;
|
93
|
+
|
94
|
+
const message = event.data.message as FrameMessage;
|
95
|
+
|
96
|
+
switch (message.type) {
|
97
|
+
case "model-context-update": {
|
98
|
+
this.updateContext(message.context);
|
99
|
+
break;
|
100
|
+
}
|
101
|
+
|
102
|
+
case "tool-result": {
|
103
|
+
const pending = this._pendingRequests.get(message.id);
|
104
|
+
if (pending) {
|
105
|
+
if (message.error) {
|
106
|
+
pending.reject(new Error(message.error));
|
107
|
+
} else {
|
108
|
+
pending.resolve(message.result);
|
109
|
+
}
|
110
|
+
this._pendingRequests.delete(message.id);
|
111
|
+
}
|
112
|
+
break;
|
113
|
+
}
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
117
|
+
private updateContext(serializedContext: SerializedModelContext) {
|
118
|
+
const context = deserializeModelContext(serializedContext);
|
119
|
+
this._context = {
|
120
|
+
...context,
|
121
|
+
tools:
|
122
|
+
context.tools &&
|
123
|
+
Object.fromEntries(
|
124
|
+
Object.entries(context.tools).map(([name, tool]) => [
|
125
|
+
name,
|
126
|
+
{
|
127
|
+
...tool,
|
128
|
+
execute: (args: any) => this.callTool(name, args),
|
129
|
+
} as Tool<any, any>,
|
130
|
+
]),
|
131
|
+
),
|
132
|
+
};
|
133
|
+
this.notifySubscribers();
|
134
|
+
}
|
135
|
+
|
136
|
+
private callTool(toolName: string, args: any): Promise<any> {
|
137
|
+
return this.sendRequest(
|
138
|
+
{
|
139
|
+
type: "tool-call",
|
140
|
+
id: `tool-${this._requestCounter++}`,
|
141
|
+
toolName,
|
142
|
+
args,
|
143
|
+
},
|
144
|
+
30000,
|
145
|
+
`Tool call "${toolName}" timed out`,
|
146
|
+
);
|
147
|
+
}
|
148
|
+
|
149
|
+
private sendRequest<T extends FrameMessage & { id: string }>(
|
150
|
+
message: T,
|
151
|
+
timeout = 30000,
|
152
|
+
timeoutMessage = "Request timed out",
|
153
|
+
): Promise<any> {
|
154
|
+
return new Promise((resolve, reject) => {
|
155
|
+
this._pendingRequests.set(message.id, { resolve, reject });
|
156
|
+
|
157
|
+
this._iframeWindow.postMessage(
|
158
|
+
{ channel: FRAME_MESSAGE_CHANNEL, message },
|
159
|
+
this._targetOrigin,
|
160
|
+
);
|
161
|
+
|
162
|
+
const timeoutId = setTimeout(() => {
|
163
|
+
const pending = this._pendingRequests.get(message.id);
|
164
|
+
if (pending) {
|
165
|
+
pending.reject(new Error(timeoutMessage));
|
166
|
+
this._pendingRequests.delete(message.id);
|
167
|
+
}
|
168
|
+
}, timeout);
|
169
|
+
|
170
|
+
// Store original resolve/reject with timeout cleanup
|
171
|
+
const originalResolve = this._pendingRequests.get(message.id)!.resolve;
|
172
|
+
const originalReject = this._pendingRequests.get(message.id)!.reject;
|
173
|
+
|
174
|
+
this._pendingRequests.set(message.id, {
|
175
|
+
resolve: (value: any) => {
|
176
|
+
clearTimeout(timeoutId);
|
177
|
+
originalResolve(value);
|
178
|
+
},
|
179
|
+
reject: (error: any) => {
|
180
|
+
clearTimeout(timeoutId);
|
181
|
+
originalReject(error);
|
182
|
+
},
|
183
|
+
});
|
184
|
+
});
|
185
|
+
}
|
186
|
+
|
187
|
+
private requestContext() {
|
188
|
+
// Request current context from iframe
|
189
|
+
this._iframeWindow.postMessage(
|
190
|
+
{
|
191
|
+
channel: FRAME_MESSAGE_CHANNEL,
|
192
|
+
message: {
|
193
|
+
type: "model-context-request",
|
194
|
+
} as FrameMessage,
|
195
|
+
},
|
196
|
+
this._targetOrigin,
|
197
|
+
);
|
198
|
+
}
|
199
|
+
|
200
|
+
private notifySubscribers() {
|
201
|
+
this._subscribers.forEach((callback) => callback());
|
202
|
+
}
|
203
|
+
|
204
|
+
getModelContext(): ModelContext {
|
205
|
+
return this._context;
|
206
|
+
}
|
207
|
+
|
208
|
+
subscribe(callback: () => void): Unsubscribe {
|
209
|
+
this._subscribers.add(callback);
|
210
|
+
return () => this._subscribers.delete(callback);
|
211
|
+
}
|
212
|
+
|
213
|
+
dispose() {
|
214
|
+
window.removeEventListener("message", this.handleMessage);
|
215
|
+
this._subscribers.clear();
|
216
|
+
this._pendingRequests.clear();
|
217
|
+
}
|
218
|
+
}
|