@assistant-ui/react 0.10.43 → 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.
Files changed (65) hide show
  1. package/dist/model-context/frame/AssistantFrameHost.d.ts +37 -0
  2. package/dist/model-context/frame/AssistantFrameHost.d.ts.map +1 -0
  3. package/dist/model-context/frame/AssistantFrameHost.js +151 -0
  4. package/dist/model-context/frame/AssistantFrameHost.js.map +1 -0
  5. package/dist/model-context/frame/AssistantFrameProvider.d.ts +41 -0
  6. package/dist/model-context/frame/AssistantFrameProvider.d.ts.map +1 -0
  7. package/dist/model-context/frame/AssistantFrameProvider.js +142 -0
  8. package/dist/model-context/frame/AssistantFrameProvider.js.map +1 -0
  9. package/dist/model-context/frame/AssistantFrameTypes.d.ts +29 -0
  10. package/dist/model-context/frame/AssistantFrameTypes.d.ts.map +1 -0
  11. package/dist/model-context/frame/AssistantFrameTypes.js +6 -0
  12. package/dist/model-context/frame/AssistantFrameTypes.js.map +1 -0
  13. package/dist/model-context/frame/index.d.ts +5 -0
  14. package/dist/model-context/frame/index.d.ts.map +1 -0
  15. package/dist/model-context/frame/index.js +6 -0
  16. package/dist/model-context/frame/index.js.map +1 -0
  17. package/dist/model-context/frame/useAssistantFrameHost.d.ts +28 -0
  18. package/dist/model-context/frame/useAssistantFrameHost.d.ts.map +1 -0
  19. package/dist/model-context/frame/useAssistantFrameHost.js +25 -0
  20. package/dist/model-context/frame/useAssistantFrameHost.js.map +1 -0
  21. package/dist/model-context/index.d.ts +2 -0
  22. package/dist/model-context/index.d.ts.map +1 -1
  23. package/dist/model-context/index.js +2 -0
  24. package/dist/model-context/index.js.map +1 -1
  25. package/dist/model-context/registry/ModelContextRegistry.d.ts +19 -0
  26. package/dist/model-context/registry/ModelContextRegistry.d.ts.map +1 -0
  27. package/dist/model-context/registry/ModelContextRegistry.js +117 -0
  28. package/dist/model-context/registry/ModelContextRegistry.js.map +1 -0
  29. package/dist/model-context/registry/ModelContextRegistryHandles.d.ts +14 -0
  30. package/dist/model-context/registry/ModelContextRegistryHandles.d.ts.map +1 -0
  31. package/dist/model-context/registry/ModelContextRegistryHandles.js +1 -0
  32. package/dist/model-context/registry/ModelContextRegistryHandles.js.map +1 -0
  33. package/dist/model-context/registry/index.d.ts +3 -0
  34. package/dist/model-context/registry/index.d.ts.map +1 -0
  35. package/dist/model-context/registry/index.js +4 -0
  36. package/dist/model-context/registry/index.js.map +1 -0
  37. package/dist/model-context/useAssistantInstructions.d.ts +1 -2
  38. package/dist/model-context/useAssistantInstructions.d.ts.map +1 -1
  39. package/dist/model-context/useAssistantInstructions.js.map +1 -1
  40. package/dist/runtimes/composer/BaseComposerRuntimeCore.d.ts.map +1 -1
  41. package/dist/runtimes/composer/BaseComposerRuntimeCore.js +5 -4
  42. package/dist/runtimes/composer/BaseComposerRuntimeCore.js.map +1 -1
  43. package/dist/runtimes/external-store/ExternalStoreThreadRuntimeCore.d.ts +4 -1
  44. package/dist/runtimes/external-store/ExternalStoreThreadRuntimeCore.d.ts.map +1 -1
  45. package/dist/runtimes/external-store/ExternalStoreThreadRuntimeCore.js +25 -10
  46. package/dist/runtimes/external-store/ExternalStoreThreadRuntimeCore.js.map +1 -1
  47. package/dist/types/MessagePartTypes.d.ts +2 -0
  48. package/dist/types/MessagePartTypes.d.ts.map +1 -1
  49. package/package.json +3 -3
  50. package/src/model-context/frame/AssistantFrame.test.ts +353 -0
  51. package/src/model-context/frame/AssistantFrameHost.ts +218 -0
  52. package/src/model-context/frame/AssistantFrameProvider.ts +225 -0
  53. package/src/model-context/frame/AssistantFrameTypes.ts +40 -0
  54. package/src/model-context/frame/SPEC_AssistantFrame.md +104 -0
  55. package/src/model-context/frame/index.ts +4 -0
  56. package/src/model-context/frame/useAssistantFrameHost.ts +48 -0
  57. package/src/model-context/index.ts +3 -0
  58. package/src/model-context/registry/ModelContextRegistry.ts +165 -0
  59. package/src/model-context/registry/ModelContextRegistryHandles.ts +19 -0
  60. package/src/model-context/registry/SPEC_ModelContextRegistry.md +40 -0
  61. package/src/model-context/registry/index.ts +2 -0
  62. package/src/model-context/useAssistantInstructions.tsx +1 -1
  63. package/src/runtimes/composer/BaseComposerRuntimeCore.tsx +5 -4
  64. package/src/runtimes/external-store/ExternalStoreThreadRuntimeCore.tsx +29 -11
  65. package/src/types/MessagePartTypes.ts +2 -0
@@ -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,4 @@
1
+ export * from "./AssistantFrameHost";
2
+ export * from "./AssistantFrameProvider";
3
+ export * from "./AssistantFrameTypes";
4
+ export * from "./useAssistantFrameHost";
@@ -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
+ };
@@ -33,3 +33,6 @@ export { tool } from "./tool";
33
33
  */
34
34
  export { makeAssistantVisible as makeAssistantReadable } from "./makeAssistantVisible";
35
35
  export { makeAssistantVisible } from "./makeAssistantVisible";
36
+
37
+ export * from "./registry";
38
+ export * from "./frame";
@@ -0,0 +1,165 @@
1
+ import { Tool } from "assistant-stream";
2
+ import {
3
+ ModelContext,
4
+ ModelContextProvider,
5
+ mergeModelContexts,
6
+ } from "../../model-context/ModelContextTypes";
7
+ import { Unsubscribe } from "../../types/Unsubscribe";
8
+ import {
9
+ ModelContextRegistryToolHandle,
10
+ ModelContextRegistryInstructionHandle,
11
+ ModelContextRegistryProviderHandle,
12
+ } from "./ModelContextRegistryHandles";
13
+ import type { AssistantToolProps } from "../../model-context/useAssistantTool";
14
+ import type { AssistantInstructionsConfig } from "../../model-context/useAssistantInstructions";
15
+
16
+ export class ModelContextRegistry implements ModelContextProvider {
17
+ private _tools = new Map<symbol, AssistantToolProps<any, any>>();
18
+ private _instructions = new Map<symbol, string>();
19
+ private _providers = new Map<symbol, ModelContextProvider>();
20
+ private _subscribers = new Set<() => void>();
21
+ private _providerUnsubscribes = new Map<symbol, Unsubscribe | undefined>();
22
+
23
+ getModelContext(): ModelContext {
24
+ // Merge instructions
25
+ const instructions = Array.from(this._instructions.values()).filter(
26
+ Boolean,
27
+ );
28
+
29
+ const system =
30
+ instructions.length > 0 ? instructions.join("\n\n") : undefined;
31
+
32
+ // Collect tools
33
+ const tools: Record<string, Tool<any, any>> = {};
34
+ for (const toolProps of this._tools.values()) {
35
+ const { toolName, render, ...tool } = toolProps;
36
+ tools[toolName] = tool;
37
+ }
38
+
39
+ // Merge provider contexts
40
+ const providerContexts = mergeModelContexts(
41
+ new Set(this._providers.values()),
42
+ );
43
+
44
+ // Combine everything
45
+ const context: ModelContext = {
46
+ system,
47
+ tools: Object.keys(tools).length > 0 ? tools : undefined,
48
+ };
49
+
50
+ // Merge with provider contexts
51
+ if (providerContexts.system) {
52
+ context.system = context.system
53
+ ? `${context.system}\n\n${providerContexts.system}`
54
+ : providerContexts.system;
55
+ }
56
+
57
+ if (providerContexts.tools) {
58
+ context.tools = { ...(context.tools || {}), ...providerContexts.tools };
59
+ }
60
+
61
+ if (providerContexts.callSettings) {
62
+ context.callSettings = providerContexts.callSettings;
63
+ }
64
+
65
+ if (providerContexts.config) {
66
+ context.config = providerContexts.config;
67
+ }
68
+
69
+ return context;
70
+ }
71
+
72
+ subscribe(callback: () => void): Unsubscribe {
73
+ this._subscribers.add(callback);
74
+ return () => this._subscribers.delete(callback);
75
+ }
76
+
77
+ private notifySubscribers(): void {
78
+ for (const callback of this._subscribers) {
79
+ callback();
80
+ }
81
+ }
82
+
83
+ addTool<TArgs extends Record<string, unknown>, TResult>(
84
+ tool: AssistantToolProps<TArgs, TResult>,
85
+ ): ModelContextRegistryToolHandle<TArgs, TResult> {
86
+ const id = Symbol();
87
+
88
+ this._tools.set(id, tool);
89
+ this.notifySubscribers();
90
+
91
+ return {
92
+ update: (newTool: AssistantToolProps<TArgs, TResult>) => {
93
+ if (this._tools.has(id)) {
94
+ this._tools.set(id, newTool);
95
+ this.notifySubscribers();
96
+ }
97
+ },
98
+ remove: () => {
99
+ this._tools.delete(id);
100
+ this.notifySubscribers();
101
+ },
102
+ };
103
+ }
104
+
105
+ addInstruction(
106
+ config: string | AssistantInstructionsConfig,
107
+ ): ModelContextRegistryInstructionHandle {
108
+ const id = Symbol();
109
+
110
+ const instruction =
111
+ typeof config === "string" ? config : config.instruction;
112
+ const disabled = typeof config === "object" ? config.disabled : false;
113
+
114
+ if (!disabled) {
115
+ this._instructions.set(id, instruction);
116
+ this.notifySubscribers();
117
+ }
118
+
119
+ return {
120
+ update: (newConfig: string | AssistantInstructionsConfig) => {
121
+ const newInstruction =
122
+ typeof newConfig === "string" ? newConfig : newConfig.instruction;
123
+ const newDisabled =
124
+ typeof newConfig === "object" ? newConfig.disabled : false;
125
+
126
+ if (newDisabled) {
127
+ this._instructions.delete(id);
128
+ } else {
129
+ this._instructions.set(id, newInstruction);
130
+ }
131
+ this.notifySubscribers();
132
+ },
133
+ remove: () => {
134
+ this._instructions.delete(id);
135
+ this.notifySubscribers();
136
+ },
137
+ };
138
+ }
139
+
140
+ addProvider(
141
+ provider: ModelContextProvider,
142
+ ): ModelContextRegistryProviderHandle {
143
+ const id = Symbol();
144
+
145
+ this._providers.set(id, provider);
146
+
147
+ // Subscribe to provider changes
148
+ const unsubscribe = provider.subscribe?.(() => {
149
+ this.notifySubscribers();
150
+ });
151
+ this._providerUnsubscribes.set(id, unsubscribe);
152
+
153
+ this.notifySubscribers();
154
+
155
+ return {
156
+ remove: () => {
157
+ this._providers.delete(id);
158
+ const unsubscribe = this._providerUnsubscribes.get(id);
159
+ unsubscribe?.();
160
+ this._providerUnsubscribes.delete(id);
161
+ this.notifySubscribers();
162
+ },
163
+ };
164
+ }
165
+ }
@@ -0,0 +1,19 @@
1
+ import type { AssistantToolProps } from "../../model-context/useAssistantTool";
2
+ import type { AssistantInstructionsConfig } from "../../model-context/useAssistantInstructions";
3
+
4
+ export interface ModelContextRegistryToolHandle<
5
+ TArgs extends Record<string, unknown> = any,
6
+ TResult = any,
7
+ > {
8
+ update(tool: AssistantToolProps<TArgs, TResult>): void;
9
+ remove(): void;
10
+ }
11
+
12
+ export interface ModelContextRegistryInstructionHandle {
13
+ update(config: string | AssistantInstructionsConfig): void;
14
+ remove(): void;
15
+ }
16
+
17
+ export interface ModelContextRegistryProviderHandle {
18
+ remove(): void;
19
+ }
@@ -0,0 +1,40 @@
1
+ # ModelContextRegistry
2
+
3
+ An imperative API for registering tools and instructions to a model context provider.
4
+
5
+ ```typescript
6
+ const registry = new ModelContextRegistry();
7
+
8
+ const handle = registry.addTool({
9
+ toolName: "search",
10
+ description: "Search the web",
11
+ parameters: z.object({ query: z.string() }),
12
+ execute: async (args) => {
13
+ return { results: ["..."] };
14
+ },
15
+ });
16
+ ```
17
+
18
+ ## API
19
+
20
+ - addTool(tool: Tool & { toolName: string }): ModelContextRegistryToolHandle
21
+ - addInstruction(instruction: string): ModelContextRegistryInstructionHandle
22
+ - addProvider(provider: ModelContextProvider): ModelContextRegistryProviderHandle
23
+
24
+ ## ModelContextRegistryToolHandle
25
+
26
+ - update(tool: Tool & { toolName: string }): void;
27
+ - remove(): void;
28
+
29
+ ## ModelContextRegistryInstructionHandle
30
+
31
+ - update(instruction: string): void;
32
+ - remove(): void;
33
+
34
+ ## ModelContextRegistryProviderHandle
35
+
36
+ - remove(): void;
37
+
38
+ ## ModelContextProvider
39
+
40
+ The registry is a ModelContextProvider.
@@ -0,0 +1,2 @@
1
+ export * from "./ModelContextRegistry";
2
+ export * from "./ModelContextRegistryHandles";
@@ -3,7 +3,7 @@
3
3
  import { useEffect } from "react";
4
4
  import { useAssistantRuntime } from "../context";
5
5
 
6
- type AssistantInstructionsConfig = {
6
+ export type AssistantInstructionsConfig = {
7
7
  disabled?: boolean | undefined;
8
8
  instruction: string;
9
9
  };
@@ -124,7 +124,7 @@ export abstract class BaseComposerRuntimeCore
124
124
  const adapter = this.getAttachmentAdapter();
125
125
  const attachments =
126
126
  adapter && this.attachments.length > 0
127
- ? await Promise.all(
127
+ ? Promise.all(
128
128
  this.attachments.map(async (a) => {
129
129
  if (isAttachmentComplete(a)) return a;
130
130
  const result = await adapter.send(a);
@@ -133,15 +133,16 @@ export abstract class BaseComposerRuntimeCore
133
133
  )
134
134
  : [];
135
135
 
136
+ const text = this.text;
137
+ this._emptyTextAndAttachments();
136
138
  const message: Omit<AppendMessage, "parentId" | "sourceId"> = {
137
139
  createdAt: new Date(),
138
140
  role: this.role,
139
- content: this.text ? [{ type: "text", text: this.text }] : [],
140
- attachments,
141
+ content: text ? [{ type: "text", text }] : [],
142
+ attachments: await attachments,
141
143
  runConfig: this.runConfig,
142
144
  metadata: { custom: {} },
143
145
  };
144
- this._emptyTextAndAttachments();
145
146
 
146
147
  this.handleSend(message);
147
148
  this._notifyEventSubscribers("send");