@assistant-ui/core 0.2.8 → 0.2.10

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 (123) hide show
  1. package/dist/model-context/tool.d.ts +1 -1
  2. package/dist/model-context/tool.js +1 -1
  3. package/dist/model-context/tool.js.map +1 -1
  4. package/dist/model-context/types.js +17 -2
  5. package/dist/model-context/types.js.map +1 -1
  6. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  7. package/dist/react/adapters/LocalStorageThreadListAdapter.js +12 -2
  8. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  9. package/dist/react/client/Tools.d.ts.map +1 -1
  10. package/dist/react/client/Tools.js +9 -3
  11. package/dist/react/client/Tools.js.map +1 -1
  12. package/dist/react/index.d.ts +8 -2
  13. package/dist/react/index.js +7 -1
  14. package/dist/react/model-context/define-mcp-toolkit.d.ts +12 -0
  15. package/dist/react/model-context/define-mcp-toolkit.d.ts.map +1 -0
  16. package/dist/react/model-context/define-mcp-toolkit.js +14 -0
  17. package/dist/react/model-context/define-mcp-toolkit.js.map +1 -0
  18. package/dist/react/model-context/define-toolkit.d.ts +21 -0
  19. package/dist/react/model-context/define-toolkit.d.ts.map +1 -0
  20. package/dist/react/model-context/define-toolkit.js +8 -0
  21. package/dist/react/model-context/define-toolkit.js.map +1 -0
  22. package/dist/react/model-context/hitl.d.ts +23 -0
  23. package/dist/react/model-context/hitl.d.ts.map +1 -0
  24. package/dist/react/model-context/hitl.js +26 -0
  25. package/dist/react/model-context/hitl.js.map +1 -0
  26. package/dist/react/model-context/makeAssistantTool.d.ts +8 -0
  27. package/dist/react/model-context/makeAssistantTool.d.ts.map +1 -1
  28. package/dist/react/model-context/makeAssistantTool.js +4 -0
  29. package/dist/react/model-context/makeAssistantTool.js.map +1 -1
  30. package/dist/react/model-context/makeAssistantToolUI.d.ts +8 -0
  31. package/dist/react/model-context/makeAssistantToolUI.d.ts.map +1 -1
  32. package/dist/react/model-context/makeAssistantToolUI.js +4 -0
  33. package/dist/react/model-context/makeAssistantToolUI.js.map +1 -1
  34. package/dist/react/model-context/provider-tool.d.ts +15 -0
  35. package/dist/react/model-context/provider-tool.d.ts.map +1 -0
  36. package/dist/react/model-context/provider-tool.js +12 -0
  37. package/dist/react/model-context/provider-tool.js.map +1 -0
  38. package/dist/react/model-context/stub-tool.d.ts +12 -0
  39. package/dist/react/model-context/stub-tool.d.ts.map +1 -0
  40. package/dist/react/model-context/stub-tool.js +15 -0
  41. package/dist/react/model-context/stub-tool.js.map +1 -0
  42. package/dist/react/model-context/toolbox.d.ts +62 -15
  43. package/dist/react/model-context/toolbox.d.ts.map +1 -1
  44. package/dist/react/model-context/toolbox.js +19 -1
  45. package/dist/react/model-context/toolbox.js.map +1 -1
  46. package/dist/react/model-context/useAssistantTool.d.ts +11 -1
  47. package/dist/react/model-context/useAssistantTool.d.ts.map +1 -1
  48. package/dist/react/model-context/useAssistantTool.js +12 -6
  49. package/dist/react/model-context/useAssistantTool.js.map +1 -1
  50. package/dist/react/model-context/useAssistantToolUI.d.ts +13 -4
  51. package/dist/react/model-context/useAssistantToolUI.d.ts.map +1 -1
  52. package/dist/react/model-context/useAssistantToolUI.js +6 -3
  53. package/dist/react/model-context/useAssistantToolUI.js.map +1 -1
  54. package/dist/react/model-context/useAuiToolOverrides.d.ts +22 -0
  55. package/dist/react/model-context/useAuiToolOverrides.d.ts.map +1 -0
  56. package/dist/react/model-context/useAuiToolOverrides.js +31 -0
  57. package/dist/react/model-context/useAuiToolOverrides.js.map +1 -0
  58. package/dist/react/primitives/part/PartMessages.d.ts +13 -11
  59. package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
  60. package/dist/react/primitives/part/PartMessages.js +13 -11
  61. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  62. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +1 -0
  63. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  64. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +28 -0
  65. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  66. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  67. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +9 -2
  68. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  69. package/dist/runtime/api/thread-list-item-runtime.d.ts +2 -0
  70. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  71. package/dist/runtime/api/thread-list-item-runtime.js +6 -0
  72. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  73. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +1 -0
  74. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  75. package/dist/runtimes/external-store/external-store-adapter.d.ts +2 -0
  76. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  77. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -0
  78. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  79. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +5 -0
  80. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  81. package/dist/runtimes/remote-thread-list/adapter/in-memory.d.ts +1 -0
  82. package/dist/runtimes/remote-thread-list/adapter/in-memory.d.ts.map +1 -1
  83. package/dist/runtimes/remote-thread-list/adapter/in-memory.js +3 -0
  84. package/dist/runtimes/remote-thread-list/adapter/in-memory.js.map +1 -1
  85. package/dist/runtimes/remote-thread-list/types.d.ts +1 -0
  86. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  87. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -0
  88. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  89. package/dist/store/scopes/thread-list-item.d.ts +1 -0
  90. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  91. package/package.json +5 -5
  92. package/src/model-context/tool.ts +1 -1
  93. package/src/model-context/types.ts +21 -3
  94. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +15 -2
  95. package/src/react/client/Tools.ts +22 -7
  96. package/src/react/index.ts +15 -2
  97. package/src/react/model-context/define-mcp-toolkit.ts +16 -0
  98. package/src/react/model-context/define-toolkit.test.ts +101 -0
  99. package/src/react/model-context/define-toolkit.ts +41 -0
  100. package/src/react/model-context/hitl.ts +27 -0
  101. package/src/react/model-context/makeAssistantTool.ts +8 -0
  102. package/src/react/model-context/makeAssistantToolUI.ts +8 -0
  103. package/src/react/model-context/provider-tool.ts +30 -0
  104. package/src/react/model-context/stub-tool.ts +14 -0
  105. package/src/react/model-context/toolbox.test.ts +182 -0
  106. package/src/react/model-context/toolbox.ts +189 -21
  107. package/src/react/model-context/useAssistantTool.ts +28 -8
  108. package/src/react/model-context/useAssistantToolUI.ts +13 -4
  109. package/src/react/model-context/useAuiToolOverrides.ts +38 -0
  110. package/src/react/primitives/part/PartMessages.tsx +13 -11
  111. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +43 -0
  112. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +9 -0
  113. package/src/runtime/api/thread-list-item-runtime.ts +15 -0
  114. package/src/runtime/interfaces/thread-list-runtime-core.ts +4 -0
  115. package/src/runtimes/external-store/external-store-adapter.ts +7 -0
  116. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +13 -0
  117. package/src/runtimes/remote-thread-list/adapter/in-memory.ts +4 -0
  118. package/src/runtimes/remote-thread-list/types.ts +4 -0
  119. package/src/store/clients/model-context-client.test.ts +87 -2
  120. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +1 -0
  121. package/src/store/scopes/thread-list-item.ts +1 -0
  122. package/src/tests/RemoteThreadListThreadListRuntimeCore-custom-metadata.test.ts +69 -1
  123. package/src/tests/thread-list-runtime-getLoadThreadsPromise.test.ts +1 -0
@@ -1,5 +1,14 @@
1
- import type { Tool, ToolDeclaration } from "assistant-stream";
2
- import type { ToolCallMessagePartComponent } from "../types/MessagePartComponentTypes";
1
+ import type {
2
+ Tool,
3
+ ToolCallReader,
4
+ ToolDeclaration,
5
+ ToolModelOutputFunction,
6
+ } from "assistant-stream";
7
+ import type { ReactNode } from "react";
8
+ import type {
9
+ ToolCallMessagePartComponent,
10
+ ToolCallMessagePartProps,
11
+ } from "../types/MessagePartComponentTypes";
3
12
 
4
13
  /**
5
14
  * Resolves whether a tool's UI should be presented standalone (outside the
@@ -20,23 +29,96 @@ export const isStandaloneToolDisplay = (
20
29
  type WithRender<T, TArgs extends Record<string, unknown>, TResult> = T extends {
21
30
  type: "frontend" | "human";
22
31
  }
23
- ? T & { render: ToolCallMessagePartComponent<TArgs, TResult> }
32
+ ? T &
33
+ (T extends { type: "frontend" }
34
+ ?
35
+ | { render: ToolCallMessagePartComponent<TArgs, TResult> }
36
+ | {
37
+ render?: ToolCallMessagePartComponent<TArgs, TResult>;
38
+ renderText: ToolCallText<TArgs, TResult>;
39
+ }
40
+ : { render: ToolCallMessagePartComponent<TArgs, TResult> })
24
41
  : T & {
25
42
  render?: ToolCallMessagePartComponent<TArgs, TResult> | undefined;
43
+ renderText?: ToolCallText<TArgs, TResult> | undefined;
26
44
  };
27
45
 
46
+ type ToolParameters<TArgs extends Record<string, unknown>> =
47
+ ToolDeclaration<TArgs>["parameters"];
48
+
49
+ // ToolExecutionContext is not re-exported from assistant-stream's public entry.
50
+ type ToolExecuteContext = Parameters<
51
+ NonNullable<ToolDeclaration["execute"]>
52
+ >[1];
53
+
54
+ type ToolExecute<TArgs extends Record<string, unknown>, TResult> = (
55
+ args: TArgs,
56
+ context: ToolExecuteContext,
57
+ ) => TResult | Promise<TResult>;
58
+
59
+ type ToolStreamCall<TArgs extends Record<string, unknown>, TResult> = (
60
+ reader: ToolCallReader<TArgs, TResult>,
61
+ context: ToolExecuteContext,
62
+ ) => void;
63
+
64
+ type ToolCallRunningText<TArgs extends Record<string, unknown>> =
65
+ | ReactNode
66
+ | ((options: { args: TArgs }) => ReactNode);
67
+
68
+ type ToolCallCompleteText<TArgs extends Record<string, unknown>, TResult> =
69
+ | ReactNode
70
+ | ((options: { args: TArgs; result: TResult | undefined }) => ReactNode);
71
+
72
+ export type ToolCallText<TArgs extends Record<string, unknown>, TResult> =
73
+ | {
74
+ running: ToolCallRunningText<TArgs>;
75
+ complete?: ToolCallCompleteText<TArgs, TResult> | undefined;
76
+ }
77
+ | {
78
+ running?: ToolCallRunningText<TArgs> | undefined;
79
+ complete: ToolCallCompleteText<TArgs, TResult>;
80
+ };
81
+
82
+ const resolveToolCallText = <TArgs extends Record<string, unknown>, TResult>(
83
+ text: ToolCallText<TArgs, TResult>,
84
+ part: ToolCallMessagePartProps<TArgs, TResult>,
85
+ ): ReactNode => {
86
+ const isRunning =
87
+ part.status?.type === "running" || part.status?.type === "requires-action";
88
+
89
+ if (!isRunning) {
90
+ const value = text.complete;
91
+ if (typeof value !== "function") return value ?? null;
92
+ return value({ args: part.args, result: part.result });
93
+ }
94
+
95
+ const value = text.running;
96
+ if (typeof value !== "function") return value ?? null;
97
+ return value({ args: part.args });
98
+ };
99
+
100
+ export const makeToolCallTextComponent = <
101
+ TArgs extends Record<string, unknown>,
102
+ TResult,
103
+ >(
104
+ text: ToolCallText<TArgs, TResult>,
105
+ ): ToolCallMessagePartComponent<TArgs, TResult> => {
106
+ return function ToolCallTextComponent(part) {
107
+ return resolveToolCallText(text, part);
108
+ };
109
+ };
110
+
28
111
  /**
29
112
  * Tool definition accepted by the React tool registry.
30
113
  *
31
- * Extends the core tool contract with a render component. Human tools rely on
32
- * the renderer to collect input from the user. Frontend tools execute in the
33
- * browser and require a UI surface for their progress and result. Backend
34
- * tools execute server-side and may omit a renderer. The `render` component is
35
- * required for frontend and human tools and optional for backend tools.
114
+ * Extends the core tool contract with tool-call display options. Human tools
115
+ * rely on `render` to collect input from the user. Frontend tools execute in
116
+ * the browser and require either `render` or `renderText` for their progress
117
+ * and result. Backend tools execute server-side and may omit a renderer.
36
118
  */
37
119
  export type ToolDefinition<
38
- TArgs extends Record<string, unknown>,
39
- TResult,
120
+ TArgs extends Record<string, unknown> = Record<string, unknown>,
121
+ TResult = unknown,
40
122
  > = WithRender<Tool<TArgs, TResult>, TArgs, TResult>;
41
123
 
42
124
  /**
@@ -63,19 +145,96 @@ export type Toolkit = Record<string, ToolDefinition<any, any>>;
63
145
  * A tool as authored, before the build splits it: like {@link ToolDefinition}
64
146
  * but it may declare `description`, `parameters`, and a server-side `execute`
65
147
  * alongside its `render`. The `type` field is **not** authored — the
66
- * `"use generative"` compiler infers it (`execute: hitl()` → human; `execute`
67
- * with a `"use client"` directive frontend; otherwise backend) and writes it
68
- * back so declaring it here is a type error.
148
+ * `"use generative"` compiler infers it (`execute: hitlTool()` → human;
149
+ * `execute: providerTool(...)` → provider; `execute` with a `"use client"`
150
+ * directive frontend; otherwise backend) and writes it back so declaring it
151
+ * here is a type error.
69
152
  */
70
- export type ToolkitDeclarationDefinition<
153
+ type OverrideOptionalField<
154
+ T,
155
+ TKey extends keyof T,
156
+ TValue,
157
+ > = undefined extends T[TKey]
158
+ ? // Preserve `?: undefined` fields (for variants that explicitly disallow a
159
+ // callback) instead of widening them to accept the override value.
160
+ Exclude<T[TKey], undefined> extends never
161
+ ? { [K in TKey]?: undefined }
162
+ : { [K in TKey]?: TValue | undefined }
163
+ : { [K in TKey]: TValue };
164
+
165
+ type OverrideToolDeclarationCallbacks<
166
+ T extends { streamCall?: unknown },
167
+ TArgs extends Record<string, unknown>,
168
+ TResult,
169
+ > = Omit<
170
+ T,
171
+ | "type"
172
+ | "execute"
173
+ | "toModelOutput"
174
+ | "experimental_onSchemaValidationError"
175
+ | "streamCall"
176
+ > & {
177
+ type?: never;
178
+ } & ("execute" extends keyof T
179
+ ? OverrideOptionalField<T, "execute", ToolExecute<NoInfer<TArgs>, TResult>>
180
+ : {}) &
181
+ ("toModelOutput" extends keyof T
182
+ ? OverrideOptionalField<
183
+ T,
184
+ "toModelOutput",
185
+ ToolModelOutputFunction<NoInfer<TArgs>, NoInfer<TResult>>
186
+ >
187
+ : {}) &
188
+ ("experimental_onSchemaValidationError" extends keyof T
189
+ ? OverrideOptionalField<
190
+ T,
191
+ "experimental_onSchemaValidationError",
192
+ (
193
+ args: unknown,
194
+ context: ToolExecuteContext,
195
+ ) => NoInfer<TResult> | Promise<NoInfer<TResult>>
196
+ >
197
+ : {}) &
198
+ OverrideOptionalField<
199
+ T,
200
+ "streamCall",
201
+ ToolStreamCall<TArgs, NoInfer<TResult>>
202
+ >;
203
+
204
+ // Keep the authored shape tied to ToolDeclaration's union variants while
205
+ // overriding callback fields to avoid inference pollution.
206
+ type ToolkitDefinitionInput<
71
207
  TArgs extends Record<string, unknown>,
72
208
  TResult,
73
209
  > = WithRender<
74
- Omit<ToolDeclaration<TArgs, TResult>, "type">,
210
+ ToolDeclaration<TArgs, TResult> extends infer T
211
+ ? T extends { streamCall?: unknown }
212
+ ? OverrideToolDeclarationCallbacks<T, TArgs, TResult>
213
+ : never
214
+ : never,
75
215
  TArgs,
76
216
  TResult
77
- > & {
78
- type?: never;
217
+ >;
218
+
219
+ /**
220
+ * A single entry in a {@link ToolkitDefinition}.
221
+ *
222
+ * Either authored inline (whose `type` the compiler infers) or an already-formed
223
+ * {@link ToolDefinition} produced by a factory whose own build splits it across
224
+ * targets — e.g. `new JSONGenerativeUI({ library }).present()`. The factory case
225
+ * carries a `type`, so it can only match the {@link ToolDefinition} arm of this
226
+ * union.
227
+ */
228
+ export type ToolkitDefinitionEntry<
229
+ TArgs extends Record<string, unknown> = Record<string, unknown>,
230
+ TResult = unknown,
231
+ > = ToolkitDefinitionInput<TArgs, TResult> | ToolDefinition<any, any>;
232
+
233
+ export type ToolkitDefinitionEntryWithParameters<
234
+ TArgs extends Record<string, unknown> = Record<string, unknown>,
235
+ TResult = unknown,
236
+ > = ToolkitDefinitionInput<TArgs, TResult> & {
237
+ parameters: NonNullable<ToolParameters<TArgs>>;
79
238
  };
80
239
 
81
240
  /**
@@ -83,10 +242,19 @@ export type ToolkitDeclarationDefinition<
83
242
  * {@link defineToolkit}. Backend entries may carry their server `execute` here;
84
243
  * the canonical {@link Toolkit} keeps those fields `undefined`.
85
244
  */
86
- export type ToolkitDeclaration = Record<
87
- string,
88
- ToolkitDeclarationDefinition<any, any>
89
- >;
245
+ export type ToolkitDefinition<
246
+ TArgsByName extends {
247
+ [K in keyof TArgsByName]: Record<string, unknown>;
248
+ } = Record<string, any>,
249
+ TResultByName extends { [K in keyof TArgsByName]: unknown } = {
250
+ [K in keyof TArgsByName]: any;
251
+ },
252
+ > = {
253
+ [K in keyof TArgsByName]: ToolkitDefinitionEntry<
254
+ TArgsByName[K],
255
+ TResultByName[K]
256
+ >;
257
+ };
90
258
 
91
259
  /** Configuration for the {@link Tools} resource. */
92
260
  export type ToolsConfig = {
@@ -1,11 +1,19 @@
1
- import { useEffect } from "react";
1
+ import { useEffect, useMemo } from "react";
2
2
  import { useAui } from "@assistant-ui/store";
3
3
  import type { ToolCallMessagePartComponent } from "../types/MessagePartComponentTypes";
4
4
  import type { AssistantToolProps as CoreAssistantToolProps } from "../..";
5
- import { isStandaloneToolDisplay } from "./toolbox";
5
+ import {
6
+ isStandaloneToolDisplay,
7
+ makeToolCallTextComponent,
8
+ type ToolCallText,
9
+ } from "./toolbox";
6
10
 
7
11
  /**
8
12
  * Props used to register a tool from React.
13
+ *
14
+ * @deprecated Use a toolkit with `Tools({ toolkit })` and register it via
15
+ * `useAui({ tools: Tools({ toolkit }) })` instead. See
16
+ * https://assistant-ui.com/docs/migrations/toolkit-tools.
9
17
  */
10
18
  export type AssistantToolProps<
11
19
  TArgs extends Record<string, unknown>,
@@ -13,6 +21,8 @@ export type AssistantToolProps<
13
21
  > = CoreAssistantToolProps<TArgs, TResult> & {
14
22
  /** Component used to render calls to this tool in assistant messages. */
15
23
  render?: ToolCallMessagePartComponent<TArgs, TResult> | undefined;
24
+ /** Lightweight text rendered while a tool call is running or complete. */
25
+ renderText?: ToolCallText<TArgs, TResult> | undefined;
16
26
  };
17
27
 
18
28
  /**
@@ -28,6 +38,10 @@ export type AssistantToolProps<
28
38
  *
29
39
  * @param tool - Tool definition and name to register.
30
40
  *
41
+ * @deprecated Use a toolkit with `Tools({ toolkit })` and register it via
42
+ * `useAui({ tools: Tools({ toolkit }) })` instead. See
43
+ * https://assistant-ui.com/docs/migrations/toolkit-tools.
44
+ *
31
45
  * @example
32
46
  * ```tsx
33
47
  * const weatherTool = {
@@ -54,16 +68,22 @@ export const useAssistantTool = <
54
68
  const aui = useAui();
55
69
 
56
70
  const standalone = isStandaloneToolDisplay(tool);
71
+ const renderTextComponent = useMemo(
72
+ () =>
73
+ tool.renderText ? makeToolCallTextComponent(tool.renderText) : undefined,
74
+ [tool.renderText],
75
+ );
76
+ const render = tool.render ?? renderTextComponent;
57
77
 
58
78
  useEffect(() => {
59
- if (!tool.render) return undefined;
60
- return aui.tools().setToolUI(tool.toolName, tool.render, { standalone });
61
- }, [aui, tool.toolName, tool.render, standalone]);
79
+ if (!render) return undefined;
80
+ return aui.tools().setToolUI(tool.toolName, render, { standalone });
81
+ }, [aui, tool.toolName, render, standalone]);
62
82
 
63
83
  useEffect(() => {
64
- // `render` and `display` are client-only presentation concerns and never
65
- // reach the model.
66
- const { toolName, render, display, ...rest } = tool;
84
+ // `render`, `renderText`, and `display` are client-only presentation
85
+ // concerns and never reach the model.
86
+ const { toolName, render, renderText, display, ...rest } = tool;
67
87
  const context = {
68
88
  tools: {
69
89
  [toolName]: rest,
@@ -2,7 +2,13 @@ import { useEffect } from "react";
2
2
  import { useAui } from "@assistant-ui/store";
3
3
  import type { ToolCallMessagePartComponent } from "../types/MessagePartComponentTypes";
4
4
 
5
- /** Props used to register a renderer for tool-call message parts. */
5
+ /**
6
+ * Props used to register a renderer for tool-call message parts.
7
+ *
8
+ * @deprecated Put `render`/`renderText` on the matching toolkit entry, or use
9
+ * `MessagePrimitive.Parts` inline tool render overrides for per-message UI.
10
+ * See https://assistant-ui.com/docs/migrations/toolkit-tools.
11
+ */
6
12
  export type AssistantToolUIProps<TArgs, TResult> = {
7
13
  /** Name of the tool whose calls should use this renderer. */
8
14
  toolName: string;
@@ -19,11 +25,14 @@ export type AssistantToolUIProps<TArgs, TResult> = {
19
25
  /**
20
26
  * Registers a tool-call renderer while the component is mounted.
21
27
  *
22
- * This only affects rendering. Pair it with {@link useAssistantTool},
23
- * {@link Tools}, or a backend tool registry to expose the actual tool
24
- * definition to the model.
28
+ * This only affects rendering. Pair it with {@link Tools} or a backend tool
29
+ * registry to expose the actual tool definition to the model.
25
30
  *
26
31
  * @param tool - Tool renderer registration, or `null` to skip registration.
32
+ *
33
+ * @deprecated Put `render`/`renderText` on the matching toolkit entry, or use
34
+ * `MessagePrimitive.Parts` inline tool render overrides for per-message UI.
35
+ * See https://assistant-ui.com/docs/migrations/toolkit-tools.
27
36
  */
28
37
  export const useAssistantToolUI = (
29
38
  tool: AssistantToolUIProps<any, any> | null,
@@ -0,0 +1,38 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useAui } from "@assistant-ui/store";
3
+ import type { Tool } from "assistant-stream";
4
+
5
+ type AuiToolOverride<
6
+ TArgs extends Record<string, unknown> = Record<string, unknown>,
7
+ TResult = unknown,
8
+ > = Partial<Tool<TArgs, TResult>>;
9
+
10
+ type AuiToolOverrides = Record<string, AuiToolOverride<any, any>>;
11
+
12
+ /**
13
+ * Overrides toolkit entries for the current assistant scope.
14
+ *
15
+ * This is intended for dynamic local-state tools whose model-facing contract is
16
+ * declared in a `"use generative"` toolkit file with `execute: stubTool()`, but
17
+ * whose actual executor must close over React state in the mounted component.
18
+ * Keep the override keys stable after mount; dynamic key addition/removal is not
19
+ * currently observed.
20
+ * Overrides are registered at priority 1000, above toolkit defaults. Only one
21
+ * mounted override provider may define a given tool name at a time.
22
+ *
23
+ * @deprecated Experimental, API may change.
24
+ */
25
+ export function useAuiToolOverrides(overrides: AuiToolOverrides): void {
26
+ const aui = useAui();
27
+ const overridesRef = useRef(overrides);
28
+ overridesRef.current = overrides;
29
+
30
+ useEffect(() => {
31
+ return aui.modelContext().register({
32
+ getModelContext: () => ({
33
+ priority: 1000,
34
+ tools: overridesRef.current as Record<string, Tool<any, any>>,
35
+ }),
36
+ });
37
+ }, [aui]);
38
+ }
@@ -34,17 +34,19 @@ const usePartMessages = (): readonly ThreadMessage[] | undefined => {
34
34
  *
35
35
  * @example
36
36
  * ```tsx
37
- * const SubAgentToolUI = makeAssistantToolUI({
38
- * toolName: "invoke_sub_agent",
39
- * render: () => (
40
- * <PartPrimitive.Messages>
41
- * {({ message }) => {
42
- * if (message.role === "user") return <MyUserMessage />;
43
- * return <MyAssistantMessage />;
44
- * }}
45
- * </PartPrimitive.Messages>
46
- * ),
47
- * });
37
+ * const toolkit = {
38
+ * invoke_sub_agent: {
39
+ * type: "backend",
40
+ * render: () => (
41
+ * <PartPrimitive.Messages>
42
+ * {({ message }) => {
43
+ * if (message.role === "user") return <MyUserMessage />;
44
+ * return <MyAssistantMessage />;
45
+ * }}
46
+ * </PartPrimitive.Messages>
47
+ * ),
48
+ * },
49
+ * } satisfies Toolkit;
48
50
  * ```
49
51
  */
50
52
  export const PartPrimitiveMessagesImpl: FC<PartPrimitiveMessages.Props> = ({
@@ -508,6 +508,49 @@ export class RemoteThreadListThreadListRuntimeCore
508
508
  });
509
509
  }
510
510
 
511
+ public updateCustom(
512
+ threadIdOrRemoteId: string,
513
+ custom: Record<string, unknown> | undefined,
514
+ ): Promise<void> {
515
+ const data = this.getItemById(threadIdOrRemoteId);
516
+ if (!data) throw new Error("Thread not found");
517
+ if (data.status === "new") throw new Error("Thread is not yet initialized");
518
+
519
+ if (!this._options.adapter.updateCustom) {
520
+ throw new Error(
521
+ "Remote thread list adapter does not support updating custom metadata",
522
+ );
523
+ }
524
+
525
+ return this._state.optimisticUpdate({
526
+ execute: async () => {
527
+ const { remoteId } = await data.initializeTask;
528
+ const adapter = this._options.adapter;
529
+ if (!adapter.updateCustom) {
530
+ throw new Error(
531
+ "Remote thread list adapter does not support updating custom metadata",
532
+ );
533
+ }
534
+ return adapter.updateCustom(remoteId, custom);
535
+ },
536
+ optimistic: (state) => {
537
+ const data = getThreadData(state, threadIdOrRemoteId);
538
+ if (!data) return state;
539
+
540
+ return {
541
+ ...state,
542
+ threadData: {
543
+ ...state.threadData,
544
+ [data.id]: {
545
+ ...data,
546
+ custom,
547
+ },
548
+ },
549
+ };
550
+ },
551
+ });
552
+ }
553
+
511
554
  private async _ensureThreadIsNotMain(threadId: string) {
512
555
  if (threadId === this.newThreadId)
513
556
  throw new Error("Cannot ensure new thread is not main");
@@ -14,6 +14,7 @@ import { InMemoryThreadListAdapter } from "../../../runtimes/remote-thread-list/
14
14
  import { useAssistantCloudThreadHistoryAdapter } from "./AssistantCloudThreadHistoryAdapter";
15
15
  import { RuntimeAdapterProvider } from "../RuntimeAdapterProvider";
16
16
  import { CloudFileAttachmentAdapter } from "./CloudFileAttachmentAdapter";
17
+ import { isRecord } from "../../../utils/json/is-json";
17
18
 
18
19
  type ThreadData = {
19
20
  externalId: string | undefined;
@@ -26,6 +27,9 @@ type CloudThreadListAdapterOptions = {
26
27
  delete?: ((threadId: string) => Promise<void>) | undefined;
27
28
  };
28
29
 
30
+ const toCustom = (value: unknown): Record<string, unknown> | undefined =>
31
+ isRecord(value) ? value : undefined;
32
+
29
33
  const baseUrl =
30
34
  typeof process !== "undefined" &&
31
35
  process?.env?.NEXT_PUBLIC_ASSISTANT_BASE_URL;
@@ -91,6 +95,7 @@ export const useCloudThreadListAdapter = (
91
95
  remoteId: t.id,
92
96
  title: t.title,
93
97
  externalId: t.external_id ?? undefined,
98
+ custom: toCustom(t.metadata),
94
99
  })),
95
100
  };
96
101
  },
@@ -110,6 +115,9 @@ export const useCloudThreadListAdapter = (
110
115
  rename: async (threadId, newTitle) => {
111
116
  return cloud.threads.update(threadId, { title: newTitle });
112
117
  },
118
+ updateCustom: async (threadId, custom) => {
119
+ return cloud.threads.update(threadId, { metadata: custom ?? null });
120
+ },
113
121
  archive: async (threadId) => {
114
122
  return cloud.threads.update(threadId, { is_archived: true });
115
123
  },
@@ -146,6 +154,7 @@ export const useCloudThreadListAdapter = (
146
154
  remoteId: thread.id,
147
155
  title: thread.title,
148
156
  externalId: thread.external_id ?? undefined,
157
+ custom: toCustom(thread.metadata),
149
158
  };
150
159
  },
151
160
 
@@ -38,6 +38,7 @@ export type ThreadListItemRuntime = {
38
38
 
39
39
  switchTo(options?: { unarchive?: boolean }): Promise<void>;
40
40
  rename(newTitle: string): Promise<void>;
41
+ updateCustom(custom: Record<string, unknown> | undefined): Promise<void>;
41
42
  archive(): Promise<void>;
42
43
  unarchive(): Promise<void>;
43
44
  delete(): Promise<void>;
@@ -74,6 +75,7 @@ export class ThreadListItemRuntimeImpl implements ThreadListItemRuntime {
74
75
  protected __internal_bindMethods() {
75
76
  this.switchTo = this.switchTo.bind(this);
76
77
  this.rename = this.rename.bind(this);
78
+ this.updateCustom = this.updateCustom.bind(this);
77
79
  this.archive = this.archive.bind(this);
78
80
  this.unarchive = this.unarchive.bind(this);
79
81
  this.delete = this.delete.bind(this);
@@ -100,6 +102,19 @@ export class ThreadListItemRuntimeImpl implements ThreadListItemRuntime {
100
102
  return this._threadListBinding.rename(state.id, newTitle);
101
103
  }
102
104
 
105
+ public updateCustom(
106
+ custom: Record<string, unknown> | undefined,
107
+ ): Promise<void> {
108
+ const state = this._core.getState();
109
+ if (!this._threadListBinding.updateCustom) {
110
+ throw new Error(
111
+ "Thread list runtime does not support updating custom metadata",
112
+ );
113
+ }
114
+
115
+ return this._threadListBinding.updateCustom(state.id, custom);
116
+ }
117
+
103
118
  public archive(): Promise<void> {
104
119
  const state = this._core.getState();
105
120
 
@@ -44,6 +44,10 @@ export type ThreadListRuntimeCore = {
44
44
 
45
45
  detach(threadId: string): Promise<void>;
46
46
  rename(threadId: string, newTitle: string): Promise<void>;
47
+ updateCustom?(
48
+ threadId: string,
49
+ custom: Record<string, unknown> | undefined,
50
+ ): Promise<void>;
47
51
  archive(threadId: string): Promise<void>;
48
52
  unarchive(threadId: string): Promise<void>;
49
53
  delete(threadId: string): Promise<void>;
@@ -24,6 +24,7 @@ export type ExternalStoreThreadData<TState extends "regular" | "archived"> = {
24
24
  remoteId?: string | undefined;
25
25
  externalId?: string | undefined;
26
26
  title?: string | undefined;
27
+ custom?: Record<string, unknown> | undefined;
27
28
  };
28
29
 
29
30
  export type ExternalStoreThreadListAdapter = {
@@ -46,6 +47,12 @@ export type ExternalStoreThreadListAdapter = {
46
47
  threadId: string,
47
48
  newTitle: string,
48
49
  ) => (Promise<void> | void) | undefined;
50
+ onUpdateCustom?:
51
+ | ((
52
+ threadId: string,
53
+ custom: Record<string, unknown> | undefined,
54
+ ) => Promise<void> | void)
55
+ | undefined;
49
56
  onArchive?: ((threadId: string) => Promise<void> | void) | undefined;
50
57
  onUnarchive?: ((threadId: string) => Promise<void> | void) | undefined;
51
58
  onDelete?: ((threadId: string) => Promise<void> | void) | undefined;
@@ -197,6 +197,19 @@ export class ExternalStoreThreadListRuntimeCore implements ThreadListRuntimeCore
197
197
  await onRename(threadId, newTitle);
198
198
  }
199
199
 
200
+ public async updateCustom(
201
+ threadId: string,
202
+ custom: Record<string, unknown> | undefined,
203
+ ): Promise<void> {
204
+ const onUpdateCustom = this.adapter.onUpdateCustom;
205
+ if (!onUpdateCustom)
206
+ throw new Error(
207
+ "External store adapter does not support updating custom metadata",
208
+ );
209
+
210
+ await onUpdateCustom(threadId, custom);
211
+ }
212
+
200
213
  public async detach(): Promise<void> {
201
214
  // no-op
202
215
  }
@@ -17,6 +17,10 @@ export class InMemoryThreadListAdapter implements RemoteThreadListAdapter {
17
17
  return Promise.resolve();
18
18
  }
19
19
 
20
+ updateCustom(): Promise<void> {
21
+ return Promise.resolve();
22
+ }
23
+
20
24
  archive(): Promise<void> {
21
25
  return Promise.resolve();
22
26
  }
@@ -29,6 +29,10 @@ export type RemoteThreadListAdapter = {
29
29
  list(params?: RemoteThreadListPageOptions): Promise<RemoteThreadListResponse>;
30
30
 
31
31
  rename(remoteId: string, newTitle: string): Promise<void>;
32
+ updateCustom?(
33
+ remoteId: string,
34
+ custom: Record<string, unknown> | undefined,
35
+ ): Promise<void>;
32
36
  archive(remoteId: string): Promise<void>;
33
37
  unarchive(remoteId: string): Promise<void>;
34
38
  delete(remoteId: string): Promise<void>;