@assistant-ui/react 0.10.44 → 0.10.45
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/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
@@ -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
|
+
}
|
@@ -0,0 +1,225 @@
|
|
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 { z } from "zod";
|
8
|
+
import {
|
9
|
+
FrameMessage,
|
10
|
+
FRAME_MESSAGE_CHANNEL,
|
11
|
+
SerializedModelContext,
|
12
|
+
SerializedTool,
|
13
|
+
} from "./AssistantFrameTypes";
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Converts tools to JSON Schema format for serialization
|
17
|
+
*/
|
18
|
+
const serializeTool = (tool: Tool<any, any>): SerializedTool => ({
|
19
|
+
...(tool.description && { description: tool.description }),
|
20
|
+
parameters:
|
21
|
+
tool.parameters instanceof z.ZodType
|
22
|
+
? ((z as any).toJSONSchema?.(tool.parameters) ?? tool.parameters)
|
23
|
+
: tool.parameters,
|
24
|
+
...(tool.disabled !== undefined && { disabled: tool.disabled }),
|
25
|
+
...(tool.type && { type: tool.type }),
|
26
|
+
});
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Serializes a ModelContext for transmission across iframe boundary
|
30
|
+
*/
|
31
|
+
const serializeModelContext = (
|
32
|
+
context: ModelContext,
|
33
|
+
): SerializedModelContext => ({
|
34
|
+
...(context.system !== undefined && { system: context.system }),
|
35
|
+
...(context.tools && {
|
36
|
+
tools: Object.fromEntries(
|
37
|
+
Object.entries(context.tools).map(([name, tool]) => [
|
38
|
+
name,
|
39
|
+
serializeTool(tool),
|
40
|
+
]),
|
41
|
+
),
|
42
|
+
}),
|
43
|
+
});
|
44
|
+
|
45
|
+
/**
|
46
|
+
* AssistantFrameProvider - Runs inside an iframe and provides ModelContextProviders
|
47
|
+
* to the parent window's AssistantFrameHost.
|
48
|
+
*
|
49
|
+
* Usage example:
|
50
|
+
* ```typescript
|
51
|
+
* // Inside the iframe
|
52
|
+
* // Add model context providers
|
53
|
+
* const registry = new ModelContextRegistry();
|
54
|
+
* AssistantFrameProvider.addModelContextProvider(registry);
|
55
|
+
*
|
56
|
+
* // Add tools to registry
|
57
|
+
* registry.addTool({
|
58
|
+
* toolName: "search",
|
59
|
+
* description: "Search the web",
|
60
|
+
* parameters: z.object({ query: z.string() }),
|
61
|
+
* execute: async (args) => {
|
62
|
+
* // Tool implementation runs in iframe
|
63
|
+
* return { results: ["..."] };
|
64
|
+
* }
|
65
|
+
* });
|
66
|
+
* ```
|
67
|
+
*/
|
68
|
+
export class AssistantFrameProvider {
|
69
|
+
private static _instance: AssistantFrameProvider | null = null;
|
70
|
+
|
71
|
+
private _providers = new Set<ModelContextProvider>();
|
72
|
+
private _providerUnsubscribes = new Map<
|
73
|
+
ModelContextProvider,
|
74
|
+
Unsubscribe | undefined
|
75
|
+
>();
|
76
|
+
private _targetOrigin: string;
|
77
|
+
|
78
|
+
private constructor(targetOrigin: string = "*") {
|
79
|
+
this._targetOrigin = targetOrigin;
|
80
|
+
this.handleMessage = this.handleMessage.bind(this);
|
81
|
+
window.addEventListener("message", this.handleMessage);
|
82
|
+
|
83
|
+
// Send initial update on initialization
|
84
|
+
setTimeout(() => this.broadcastUpdate(), 0);
|
85
|
+
}
|
86
|
+
|
87
|
+
private static getInstance(targetOrigin?: string): AssistantFrameProvider {
|
88
|
+
if (!AssistantFrameProvider._instance) {
|
89
|
+
AssistantFrameProvider._instance = new AssistantFrameProvider(
|
90
|
+
targetOrigin,
|
91
|
+
);
|
92
|
+
}
|
93
|
+
return AssistantFrameProvider._instance;
|
94
|
+
}
|
95
|
+
|
96
|
+
private handleMessage(event: MessageEvent) {
|
97
|
+
// Security: Validate origin if specified
|
98
|
+
if (this._targetOrigin !== "*" && event.origin !== this._targetOrigin)
|
99
|
+
return;
|
100
|
+
if (event.data?.channel !== FRAME_MESSAGE_CHANNEL) return;
|
101
|
+
|
102
|
+
const message = event.data.message as FrameMessage;
|
103
|
+
|
104
|
+
switch (message.type) {
|
105
|
+
case "model-context-request":
|
106
|
+
// Respond with current context
|
107
|
+
this.sendMessage(event, {
|
108
|
+
type: "model-context-update",
|
109
|
+
context: serializeModelContext(this.getModelContext()),
|
110
|
+
});
|
111
|
+
break;
|
112
|
+
|
113
|
+
case "tool-call":
|
114
|
+
this.handleToolCall(message, event);
|
115
|
+
break;
|
116
|
+
}
|
117
|
+
}
|
118
|
+
|
119
|
+
private async handleToolCall(
|
120
|
+
message: Extract<FrameMessage, { type: "tool-call" }>,
|
121
|
+
event: MessageEvent,
|
122
|
+
) {
|
123
|
+
const tool = this.getModelContext().tools?.[message.toolName];
|
124
|
+
|
125
|
+
let result: any;
|
126
|
+
let error: string | undefined;
|
127
|
+
|
128
|
+
if (!tool) {
|
129
|
+
error = `Tool "${message.toolName}" not found`;
|
130
|
+
} else {
|
131
|
+
try {
|
132
|
+
result = tool.execute
|
133
|
+
? await tool.execute(message.args, {
|
134
|
+
toolCallId: message.id,
|
135
|
+
abortSignal: new AbortController().signal,
|
136
|
+
})
|
137
|
+
: undefined;
|
138
|
+
} catch (e) {
|
139
|
+
error = e instanceof Error ? e.message : String(e);
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
this.sendMessage(event, {
|
144
|
+
type: "tool-result",
|
145
|
+
id: message.id,
|
146
|
+
...(error ? { error } : { result }),
|
147
|
+
});
|
148
|
+
}
|
149
|
+
|
150
|
+
private sendMessage(event: MessageEvent, message: FrameMessage) {
|
151
|
+
event.source?.postMessage(
|
152
|
+
{ channel: FRAME_MESSAGE_CHANNEL, message },
|
153
|
+
{ targetOrigin: event.origin },
|
154
|
+
);
|
155
|
+
}
|
156
|
+
|
157
|
+
private getModelContext(): ModelContext {
|
158
|
+
const contexts = Array.from(this._providers).map((p) =>
|
159
|
+
p.getModelContext(),
|
160
|
+
);
|
161
|
+
|
162
|
+
return contexts.reduce(
|
163
|
+
(merged, context) => ({
|
164
|
+
system: context.system
|
165
|
+
? merged.system
|
166
|
+
? `${merged.system}\n\n${context.system}`
|
167
|
+
: context.system
|
168
|
+
: merged.system,
|
169
|
+
tools: { ...(merged.tools || {}), ...(context.tools || {}) },
|
170
|
+
}),
|
171
|
+
{} as ModelContext,
|
172
|
+
);
|
173
|
+
}
|
174
|
+
|
175
|
+
private broadcastUpdate() {
|
176
|
+
// Always broadcast to parent window
|
177
|
+
if (window.parent && window.parent !== window) {
|
178
|
+
const updateMessage: FrameMessage = {
|
179
|
+
type: "model-context-update",
|
180
|
+
context: serializeModelContext(this.getModelContext()),
|
181
|
+
};
|
182
|
+
|
183
|
+
window.parent.postMessage(
|
184
|
+
{ channel: FRAME_MESSAGE_CHANNEL, message: updateMessage },
|
185
|
+
this._targetOrigin,
|
186
|
+
);
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
190
|
+
static addModelContextProvider(
|
191
|
+
provider: ModelContextProvider,
|
192
|
+
targetOrigin?: string,
|
193
|
+
): Unsubscribe {
|
194
|
+
const instance = AssistantFrameProvider.getInstance(targetOrigin);
|
195
|
+
instance._providers.add(provider);
|
196
|
+
|
197
|
+
const unsubscribe = provider.subscribe?.(() => instance.broadcastUpdate());
|
198
|
+
if (unsubscribe) {
|
199
|
+
instance._providerUnsubscribes.set(provider, unsubscribe);
|
200
|
+
}
|
201
|
+
|
202
|
+
instance.broadcastUpdate();
|
203
|
+
|
204
|
+
return () => {
|
205
|
+
instance._providers.delete(provider);
|
206
|
+
instance._providerUnsubscribes.get(provider)?.();
|
207
|
+
instance._providerUnsubscribes.delete(provider);
|
208
|
+
instance.broadcastUpdate();
|
209
|
+
};
|
210
|
+
}
|
211
|
+
|
212
|
+
static dispose() {
|
213
|
+
if (AssistantFrameProvider._instance) {
|
214
|
+
const instance = AssistantFrameProvider._instance;
|
215
|
+
window.removeEventListener("message", instance.handleMessage);
|
216
|
+
|
217
|
+
// Unsubscribe from all providers
|
218
|
+
instance._providerUnsubscribes.forEach((unsubscribe) => unsubscribe?.());
|
219
|
+
instance._providerUnsubscribes.clear();
|
220
|
+
instance._providers.clear();
|
221
|
+
|
222
|
+
AssistantFrameProvider._instance = null;
|
223
|
+
}
|
224
|
+
}
|
225
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
export type SerializedTool = {
|
2
|
+
description?: string;
|
3
|
+
parameters: any; // JSON Schema
|
4
|
+
disabled?: boolean;
|
5
|
+
type?: string;
|
6
|
+
};
|
7
|
+
|
8
|
+
export type SerializedModelContext = {
|
9
|
+
system?: string;
|
10
|
+
tools?: Record<string, SerializedTool>;
|
11
|
+
};
|
12
|
+
|
13
|
+
export type FrameMessageType =
|
14
|
+
| "model-context-request"
|
15
|
+
| "model-context-update"
|
16
|
+
| "tool-call"
|
17
|
+
| "tool-result";
|
18
|
+
|
19
|
+
export type FrameMessage =
|
20
|
+
| {
|
21
|
+
type: "model-context-request";
|
22
|
+
}
|
23
|
+
| {
|
24
|
+
type: "model-context-update";
|
25
|
+
context: SerializedModelContext;
|
26
|
+
}
|
27
|
+
| {
|
28
|
+
type: "tool-call";
|
29
|
+
id: string;
|
30
|
+
toolName: string;
|
31
|
+
args: unknown;
|
32
|
+
}
|
33
|
+
| {
|
34
|
+
type: "tool-result";
|
35
|
+
id: string;
|
36
|
+
result?: unknown;
|
37
|
+
error?: string;
|
38
|
+
};
|
39
|
+
|
40
|
+
export const FRAME_MESSAGE_CHANNEL = "assistant-ui-frame";
|
@@ -0,0 +1,104 @@
|
|
1
|
+
## Assistant Frames
|
2
|
+
|
3
|
+
Assistant frames allow an iframe to provide model context (tools, instructions) to a parent window's assistant.
|
4
|
+
|
5
|
+
### Scope
|
6
|
+
|
7
|
+
Supported features are:
|
8
|
+
|
9
|
+
- ModelContextProvider API
|
10
|
+
- support for tools (defining tool name, description, parameters, execute)
|
11
|
+
- support for instructions (system instructions)
|
12
|
+
|
13
|
+
Out of scope for now:
|
14
|
+
|
15
|
+
- model configuration (temprature, etc.)
|
16
|
+
- ToolCallReader API (incremental reading support)
|
17
|
+
|
18
|
+
### API design
|
19
|
+
|
20
|
+
[SPEC_ModelContextRegistry](../registry/SPEC_ModelContextRegistry.md)
|
21
|
+
|
22
|
+
### Inside the iframe (provides context)
|
23
|
+
|
24
|
+
```typescript
|
25
|
+
// Add model context providers
|
26
|
+
const registry = new ModelContextRegistry();
|
27
|
+
AssistantFrameProvider.addModelContextProvider(registry);
|
28
|
+
|
29
|
+
// Add tools/instructions to registry
|
30
|
+
registry.addTool({
|
31
|
+
toolName: "search",
|
32
|
+
description: "Search the web",
|
33
|
+
parameters: z.object({ query: z.string() }),
|
34
|
+
execute: async (args) => {
|
35
|
+
// Tool implementation runs in iframe
|
36
|
+
return { results: ["..."] };
|
37
|
+
},
|
38
|
+
});
|
39
|
+
```
|
40
|
+
|
41
|
+
### In the parent window (consumes context)
|
42
|
+
|
43
|
+
```typescript
|
44
|
+
// The parent window hosts the assistant that needs the context
|
45
|
+
const frameHost = new AssistantFrameHost(iframeWindow);
|
46
|
+
|
47
|
+
// Register with assistant runtime
|
48
|
+
const runtime = useAssistantRuntime();
|
49
|
+
runtime.registerModelContextProvider(frameHost);
|
50
|
+
|
51
|
+
// The assistant now has access to tools from the iframe
|
52
|
+
```
|
53
|
+
|
54
|
+
### Communication Channel Design
|
55
|
+
|
56
|
+
The communication between `AssistantFrameProvider` (iframe) and `AssistantFrameHost` (parent window) uses the `window.postMessage` API with a structured protocol. The iframe provides model context to the parent window's assistant.
|
57
|
+
|
58
|
+
#### ModelContextProvider API
|
59
|
+
|
60
|
+
AssistantFrameHost implements the ModelContextProvider API. It immediately subscribes to the iframe for updates. This is necssary because ModelContextProvider.getModelContext() is synchronous.
|
61
|
+
|
62
|
+
#### Message Channel
|
63
|
+
|
64
|
+
All messages are wrapped with a channel identifier to avoid conflicts with other postMessage usage:
|
65
|
+
|
66
|
+
```typescript
|
67
|
+
{
|
68
|
+
channel: "assistant-ui-frame",
|
69
|
+
message: FrameMessage
|
70
|
+
}
|
71
|
+
```
|
72
|
+
|
73
|
+
#### Message Types
|
74
|
+
|
75
|
+
1. **Context Discovery**
|
76
|
+
- `model-context-request`: Parent (Host) requests current context from iframe (Provider)
|
77
|
+
- `model-context-update`: Iframe pushes context changes to parent
|
78
|
+
2. **Tool Execution**
|
79
|
+
- `tool-call`: Parent requests tool execution in iframe (where tools are defined)
|
80
|
+
- `tool-result`: Iframe returns execution result or error to parent
|
81
|
+
|
82
|
+
#### Serialization
|
83
|
+
|
84
|
+
- **Tools**: Zod schemas are converted to JSON Schema format using `z.toJSONSchema()`
|
85
|
+
- **Parameters**: Tool parameters are serialized as JSON
|
86
|
+
- **System messages**: Passed as strings
|
87
|
+
- **Unsupported features**: Model config, call settings, and priority are not transmitted
|
88
|
+
|
89
|
+
#### Security Considerations
|
90
|
+
|
91
|
+
1. **Origin Validation**: Both sides can specify `targetOrigin` to restrict message sources
|
92
|
+
2. **Window Reference**: Host (parent) only accepts messages from the specific iframe window it's connected to
|
93
|
+
3. **Message Channel**: Using a unique channel identifier prevents cross-talk with other postMessage users
|
94
|
+
|
95
|
+
#### Connection Lifecycle
|
96
|
+
|
97
|
+
1. **Initialization**: Parent (Host) sends `model-context-request` to iframe on creation
|
98
|
+
2. **Updates**: Iframe (Provider) notifies parent whenever any registered ModelContextProvider changes
|
99
|
+
|
100
|
+
#### Error Handling
|
101
|
+
|
102
|
+
- Tool execution errors are serialized and sent back as error messages
|
103
|
+
- Connection failures (timeout, no response) are silently handled - the Host continues to work as an empty ModelContextProvider
|
104
|
+
- If the iframe doesn't register any providers, the AssistantFrameHost acts as a no-op empty ModelContextProvider returning `{}` from `getModelContext()`
|
@@ -0,0 +1,48 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import { useEffect, RefObject } from "react";
|
4
|
+
import { AssistantFrameHost } from "./AssistantFrameHost";
|
5
|
+
import { Unsubscribe } from "../../types";
|
6
|
+
|
7
|
+
type UseAssistantFrameHostOptions = {
|
8
|
+
iframeRef: Readonly<RefObject<HTMLIFrameElement | null | undefined>>;
|
9
|
+
targetOrigin?: string;
|
10
|
+
register: (frameHost: AssistantFrameHost) => Unsubscribe;
|
11
|
+
};
|
12
|
+
|
13
|
+
/**
|
14
|
+
* React hook that manages the lifecycle of an AssistantFrameHost and its binding to the current AssistantRuntime.
|
15
|
+
*
|
16
|
+
* Usage example:
|
17
|
+
* ```typescript
|
18
|
+
* function MyComponent() {
|
19
|
+
* const iframeRef = useRef<HTMLIFrameElement>(null);
|
20
|
+
*
|
21
|
+
* useAssistantFrameHost({
|
22
|
+
* iframeRef,
|
23
|
+
* targetOrigin: "https://trusted-domain.com", // optional
|
24
|
+
* });
|
25
|
+
*
|
26
|
+
* return <iframe ref={iframeRef} src="..." />;
|
27
|
+
* }
|
28
|
+
* ```
|
29
|
+
*/
|
30
|
+
export const useAssistantFrameHost = ({
|
31
|
+
iframeRef,
|
32
|
+
targetOrigin = "*",
|
33
|
+
register,
|
34
|
+
}: UseAssistantFrameHostOptions): void => {
|
35
|
+
useEffect(() => {
|
36
|
+
const iframeWindow = iframeRef.current?.contentWindow;
|
37
|
+
if (!iframeWindow) return;
|
38
|
+
|
39
|
+
const frameHost = new AssistantFrameHost(iframeWindow, targetOrigin);
|
40
|
+
|
41
|
+
const unsubscribe = register(frameHost);
|
42
|
+
|
43
|
+
return () => {
|
44
|
+
frameHost.dispose();
|
45
|
+
unsubscribe();
|
46
|
+
};
|
47
|
+
}, [iframeRef, targetOrigin, register]);
|
48
|
+
};
|