@artooi/ag-ui-web-component 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +515 -1
  2. package/README.md +232 -56
  3. package/dist/ag-ui-web-component.bundle.js +370 -139
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +17 -14
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/activity_registry.d.ts +46 -0
  8. package/dist/core/activity_registry.d.ts.map +1 -0
  9. package/dist/core/ag_ui_chat.d.ts +23 -10
  10. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  11. package/dist/core/agui_client.d.ts +28 -0
  12. package/dist/core/agui_client.d.ts.map +1 -1
  13. package/dist/core/client_seed.d.ts +27 -0
  14. package/dist/core/client_seed.d.ts.map +1 -0
  15. package/dist/core/read_max_tool_rounds.d.ts +9 -0
  16. package/dist/core/read_max_tool_rounds.d.ts.map +1 -0
  17. package/dist/core/run_handlers.d.ts +80 -0
  18. package/dist/core/run_handlers.d.ts.map +1 -0
  19. package/dist/core/storage_scope.d.ts +110 -0
  20. package/dist/core/storage_scope.d.ts.map +1 -0
  21. package/dist/core/tool_outcome.d.ts +2 -3
  22. package/dist/core/tool_outcome.d.ts.map +1 -1
  23. package/dist/core/utils.d.ts +46 -0
  24. package/dist/core/utils.d.ts.map +1 -1
  25. package/dist/index.js +16268 -14747
  26. package/dist/index.js.map +4 -4
  27. package/dist/skills/skill_catalog.d.ts +67 -0
  28. package/dist/skills/skill_catalog.d.ts.map +1 -0
  29. package/dist/skills/skill_name_from.d.ts +12 -0
  30. package/dist/skills/skill_name_from.d.ts.map +1 -0
  31. package/dist/tools/tool_catalog.d.ts +79 -0
  32. package/dist/tools/tool_catalog.d.ts.map +1 -0
  33. package/dist/tools/tool_dispatch.d.ts +104 -0
  34. package/dist/tools/tool_dispatch.d.ts.map +1 -0
  35. package/dist/ui/composer/attachment_tray.d.ts.map +1 -1
  36. package/dist/ui/composer/auto_grow.d.ts +10 -0
  37. package/dist/ui/composer/auto_grow.d.ts.map +1 -0
  38. package/dist/ui/composer/composer_attachments.d.ts +57 -0
  39. package/dist/ui/composer/composer_attachments.d.ts.map +1 -0
  40. package/dist/ui/composer/composer_voice.d.ts +48 -0
  41. package/dist/ui/composer/composer_voice.d.ts.map +1 -0
  42. package/dist/ui/composer/voice_input.d.ts.map +1 -1
  43. package/dist/ui/excerpts/transcript_quote_offer.d.ts +57 -0
  44. package/dist/ui/excerpts/transcript_quote_offer.d.ts.map +1 -0
  45. package/dist/ui/fill_ui_string.d.ts +32 -0
  46. package/dist/ui/fill_ui_string.d.ts.map +1 -0
  47. package/dist/ui/history/conversation_history.d.ts +203 -0
  48. package/dist/ui/history/conversation_history.d.ts.map +1 -0
  49. package/dist/ui/history/relative_time.d.ts.map +1 -1
  50. package/dist/ui/history/thread_drawer.d.ts +0 -14
  51. package/dist/ui/history/thread_drawer.d.ts.map +1 -1
  52. package/dist/ui/interrupts/confirmation_card.d.ts.map +1 -1
  53. package/dist/ui/interrupts/pending_decision.d.ts +23 -0
  54. package/dist/ui/interrupts/pending_decision.d.ts.map +1 -0
  55. package/dist/ui/placement/is_collapsible_placement.d.ts +18 -0
  56. package/dist/ui/placement/is_collapsible_placement.d.ts.map +1 -0
  57. package/dist/ui/placement/is_draggable_placement.d.ts +9 -0
  58. package/dist/ui/placement/is_draggable_placement.d.ts.map +1 -0
  59. package/dist/ui/placement/launcher_drag.d.ts +6 -0
  60. package/dist/ui/placement/launcher_drag.d.ts.map +1 -1
  61. package/dist/ui/placement/panel_placement.d.ts +119 -0
  62. package/dist/ui/placement/panel_placement.d.ts.map +1 -0
  63. package/dist/ui/progress/run_announcer.d.ts +41 -0
  64. package/dist/ui/progress/run_announcer.d.ts.map +1 -0
  65. package/dist/ui/progress/subagent_progress.d.ts +58 -0
  66. package/dist/ui/progress/subagent_progress.d.ts.map +1 -0
  67. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  68. package/dist/ui/shell/adopt_styles.d.ts +23 -0
  69. package/dist/ui/shell/adopt_styles.d.ts.map +1 -0
  70. package/dist/ui/shell/glyph_slot.d.ts +7 -0
  71. package/dist/ui/shell/glyph_slot.d.ts.map +1 -0
  72. package/dist/ui/shell/header_button.d.ts +10 -0
  73. package/dist/ui/shell/header_button.d.ts.map +1 -0
  74. package/dist/ui/shell/icon_element.d.ts +12 -0
  75. package/dist/ui/shell/icon_element.d.ts.map +1 -0
  76. package/dist/ui/shell/is_unread_badge_enabled.d.ts +10 -0
  77. package/dist/ui/shell/is_unread_badge_enabled.d.ts.map +1 -0
  78. package/dist/ui/shell/read_launcher_icon_url.d.ts +8 -0
  79. package/dist/ui/shell/read_launcher_icon_url.d.ts.map +1 -0
  80. package/dist/ui/styles.d.ts +1 -1
  81. package/dist/ui/styles.d.ts.map +1 -1
  82. package/dist/ui/transcript/answer_actions.d.ts +39 -0
  83. package/dist/ui/transcript/answer_actions.d.ts.map +1 -0
  84. package/dist/ui/transcript/answer_stream.d.ts +56 -0
  85. package/dist/ui/transcript/answer_stream.d.ts.map +1 -0
  86. package/dist/ui/transcript/render_or_warn.d.ts +16 -0
  87. package/dist/ui/transcript/render_or_warn.d.ts.map +1 -0
  88. package/dist/ui/transcript/starter_chips.d.ts +21 -0
  89. package/dist/ui/transcript/starter_chips.d.ts.map +1 -0
  90. package/dist/ui/transcript/transcript.d.ts +171 -0
  91. package/dist/ui/transcript/transcript.d.ts.map +1 -0
  92. package/dist/ui/ui_strings.d.ts +29 -1
  93. package/dist/ui/ui_strings.d.ts.map +1 -1
  94. package/package.json +1 -1
  95. package/src/constants.ts +17 -15
  96. package/src/core/activity_registry.ts +133 -0
  97. package/src/core/ag_ui_chat.ts +945 -4419
  98. package/src/core/agui_client.ts +172 -16
  99. package/src/core/client_seed.ts +27 -0
  100. package/src/core/read_max_tool_rounds.ts +13 -0
  101. package/src/core/remote_conversation_store.ts +1 -1
  102. package/src/core/run_handlers.ts +501 -0
  103. package/src/core/storage_scope.ts +279 -0
  104. package/src/core/tool_outcome.ts +5 -3
  105. package/src/core/utils.ts +109 -0
  106. package/src/skills/skill_catalog.ts +166 -0
  107. package/src/skills/skill_name_from.ts +19 -0
  108. package/src/tools/tool_catalog.ts +354 -0
  109. package/src/tools/tool_dispatch.ts +521 -0
  110. package/src/ui/composer/attachment_tray.ts +6 -5
  111. package/src/ui/composer/auto_grow.ts +12 -0
  112. package/src/ui/composer/composer_attachments.ts +324 -0
  113. package/src/ui/composer/composer_voice.ts +93 -0
  114. package/src/ui/composer/voice_input.ts +4 -4
  115. package/src/ui/excerpts/transcript_quote_offer.ts +198 -0
  116. package/src/ui/fill_ui_string.ts +41 -0
  117. package/src/ui/history/conversation_history.ts +699 -0
  118. package/src/ui/history/relative_time.ts +5 -4
  119. package/src/ui/history/thread_drawer.ts +3 -50
  120. package/src/ui/interrupts/confirmation_card.ts +4 -2
  121. package/src/ui/interrupts/pending_decision.ts +34 -0
  122. package/src/ui/placement/is_collapsible_placement.ts +19 -0
  123. package/src/ui/placement/is_draggable_placement.ts +23 -0
  124. package/src/ui/placement/launcher_drag.ts +104 -89
  125. package/src/ui/placement/panel_placement.ts +1130 -0
  126. package/src/ui/progress/run_announcer.ts +67 -0
  127. package/src/ui/progress/subagent_progress.ts +201 -0
  128. package/src/ui/progress/tool_call_card.ts +2 -0
  129. package/src/ui/shell/adopt_styles.ts +28 -0
  130. package/src/ui/shell/glyph_slot.ts +12 -0
  131. package/src/ui/shell/header_button.ts +21 -0
  132. package/src/ui/shell/icon_element.ts +33 -0
  133. package/src/ui/shell/is_unread_badge_enabled.ts +11 -0
  134. package/src/ui/shell/read_launcher_icon_url.ts +9 -0
  135. package/src/ui/styles.ts +301 -70
  136. package/src/ui/transcript/answer_actions.ts +139 -0
  137. package/src/ui/transcript/answer_stream.ts +123 -0
  138. package/src/ui/transcript/render_or_warn.ts +22 -0
  139. package/src/ui/transcript/starter_chips.ts +42 -0
  140. package/src/ui/transcript/transcript.ts +429 -0
  141. package/src/ui/ui_strings.ts +38 -1
  142. package/src/version.ts +1 -1
@@ -0,0 +1,354 @@
1
+ import type { Tool } from "@ag-ui/core";
2
+ import { READ_PAGE_TOOL, X_SUMMARY_KEY } from "../constants.js";
3
+ import { commaTokens } from "../core/utils.js";
4
+ import type { PendingDecision } from "../ui/interrupts/pending_decision.js";
5
+ import {
6
+ type QuestionRenderer,
7
+ type QuestionRequest,
8
+ requestQuestion,
9
+ } from "../ui/interrupts/question_card.js";
10
+ import type { UiStrings } from "../ui/ui_strings.js";
11
+ import { type ChatSurface, createChatSurfaceTools } from "./chat_surface_tools.js";
12
+ import { type ClientTool, ClientToolRegistry } from "./client_tool_registry.js";
13
+ import { createPageActionTools, PAGE_ACTIONS } from "./page_action_tools.js";
14
+ import type { PageMap } from "./page_map.js";
15
+ import { createPageStateTools, type PageState } from "./page_state.js";
16
+ import { parseToolCatalog, type ToolCatalogEntry } from "./parse_tool_catalog.js";
17
+ import { createRouteTools, type RouteMap } from "./route_map.js";
18
+
19
+ /** What the tool catalog needs from the element that owns it. */
20
+ export interface ToolCatalogHost {
21
+ /** The custom element: its tool attributes, and the surface the chat tools drive. */
22
+ readonly element: HTMLElement & ChatSurface;
23
+ /** The element's `routeMap`, read per call. */
24
+ readonly routeMap: () => RouteMap;
25
+ /** The element's `navigate`, read per call. */
26
+ readonly navigate: () => ((path: string) => void) | null;
27
+ /** The element's `getPageMap`, read per call. */
28
+ readonly getPageMap: () => (() => PageMap) | null;
29
+ /** The element's `resolvePageTarget`, called per target. */
30
+ readonly resolvePageTarget: (target: string) => HTMLElement | null;
31
+ /** The element's `getTools`, which a host may have replaced. */
32
+ readonly getTools: () => Tool[];
33
+ /** The element's `askUser` flag. */
34
+ readonly askUser: () => boolean;
35
+ /** The element's `askUserRenderer`. */
36
+ readonly askUserRenderer: () => QuestionRenderer | null;
37
+ /** The decision a run is suspended on, which a Stop abandons. */
38
+ readonly decision: PendingDecision;
39
+ /** The current answer group, opening one if none is open. */
40
+ readonly ensureGroup: () => HTMLDivElement;
41
+ /** The resolved string table. */
42
+ readonly strings: () => UiStrings;
43
+ /** Remove the pending indicator if shown. */
44
+ readonly hidePending: () => void;
45
+ /** Update the empty state after the transcript changed. */
46
+ readonly updateEmptyState: () => void;
47
+ /** Keep the transcript at its foot, if the reader is there. */
48
+ readonly follow: () => void;
49
+ /** The fetch options every request the element makes carries. */
50
+ readonly fetchInit: (url: string) => RequestInit | undefined;
51
+ }
52
+
53
+ /**
54
+ * The frontend tools the agent is offered: the host's registered tools, the
55
+ * built-in ones the element's configuration switches on, what the current round
56
+ * advertised, and the server's catalog of labels for the tools it runs itself.
57
+ *
58
+ * Owned one-to-one by an `<ag-ui-chat>`, and holding no state outside the
59
+ * instance.
60
+ */
61
+ export class ToolCatalog {
62
+ readonly #host: ToolCatalogHost;
63
+ /**
64
+ * The server tool catalog fetched from `data-tools-url`, keyed by tool
65
+ * name. Cards label themselves from each entry's `summary`, the base
66
+ * layer behind `toolSummaries`: an explicit entry in `toolSummaries`
67
+ * wins, this fills the rest. Held as whole entries rather than labels so a
68
+ * field the server sent is not lost on the way in. Populated once on connect.
69
+ */
70
+ #toolCatalog: Record<string, ToolCatalogEntry> = {};
71
+ /**
72
+ * The tool names the current round handed the agent, captured as the catalog
73
+ * went out.
74
+ *
75
+ * The registry is mount-wide but `getTools` is per-run, so a host is
76
+ * free to scope what a given page offers — and a call naming a tool this run
77
+ * withheld must not reach the handler that is merely still registered.
78
+ * Snapshotted rather than re-asked at dispatch: a provider is a function, and
79
+ * calling it again asks a question the run already answered, which is exactly
80
+ * the window a scoped catalog exists to close.
81
+ *
82
+ * Empty until the first round advertises, which cannot precede a call: the
83
+ * client builds `RunAgentInput.tools` at the top of every round, before the
84
+ * calls that round produces are executed.
85
+ */
86
+ #advertisedTools: ReadonlySet<string> = new Set();
87
+ readonly #toolRegistry = new ClientToolRegistry();
88
+
89
+ constructor(host: ToolCatalogHost) {
90
+ this.#host = host;
91
+ }
92
+
93
+ /** The registration behind `AgUiChat.registerTool`, whose doc is the contract. */
94
+ register(tool: ClientTool): void {
95
+ this.#toolRegistry.register(tool);
96
+ }
97
+
98
+ /** The binding behind `AgUiChat.registerPageState`, whose doc is the contract. */
99
+ registerPageState(binding: PageState): void {
100
+ for (const tool of createPageStateTools(binding)) {
101
+ this.#toolRegistry.register(tool);
102
+ }
103
+ }
104
+
105
+ /** Whether a tool of this name is registered, built-ins aside. */
106
+ has(name: string): boolean {
107
+ return this.#toolRegistry.has(name);
108
+ }
109
+
110
+ /** The catalog behind the default `AgUiChat.getTools`, whose doc is the contract. */
111
+ defaultTools(): Tool[] {
112
+ return [
113
+ ...this.#builtinTools().map((t) => ({
114
+ name: t.name,
115
+ description: t.description,
116
+ parameters: t.parameters,
117
+ })),
118
+ ...this.#toolRegistry.tools(),
119
+ ];
120
+ }
121
+
122
+ /**
123
+ * The catalog for the round about to start, remembering what it offered.
124
+ *
125
+ * Every path to a frontend tool goes through here first — the client asks
126
+ * for `RunAgentInput.tools` at the top of each round — so this is the one
127
+ * place that can know what the agent was actually told about.
128
+ */
129
+ advertise(): Tool[] {
130
+ const tools = this.#host.getTools();
131
+ this.#advertisedTools = new Set(tools.map((tool) => tool.name));
132
+ return tools;
133
+ }
134
+
135
+ /** Whether the current round offered a tool of this name. */
136
+ wasAdvertised(name: string): boolean {
137
+ return this.#advertisedTools.has(name);
138
+ }
139
+
140
+ /** Resolve a tool by name: built-in tools first, then the registry. */
141
+ resolve(name: string): ClientTool | null {
142
+ const builtin = this.#builtinTools().find((t) => t.name === name);
143
+ if (builtin !== undefined) {
144
+ return builtin;
145
+ }
146
+ return this.#toolRegistry.has(name) ? this.#toolRegistry.get(name) : null;
147
+ }
148
+
149
+ /** The server catalog's label for a tool, if it sent one. */
150
+ summary(name: string): string | undefined {
151
+ return this.#toolCatalog[name]?.summary;
152
+ }
153
+
154
+ /** Fetch the server tool-label catalog from `data-tools-url`, if set. */
155
+ async fetchCatalog(): Promise<void> {
156
+ const url = this.#host.element.getAttribute("data-tools-url");
157
+ if (url === null) {
158
+ return;
159
+ }
160
+ try {
161
+ const response = await fetch(url, this.#host.fetchInit(url));
162
+ this.#toolCatalog = parseToolCatalog(await response.json());
163
+ } catch {
164
+ // Network/parse failure: cards fall back to toolSummaries / raw names.
165
+ }
166
+ }
167
+
168
+ /** The built-in `route.*` tools, present only when a route map is set. */
169
+ #routeTools(): ClientTool[] {
170
+ if (this.#host.routeMap().length === 0) {
171
+ return [];
172
+ }
173
+ return createRouteTools(
174
+ () => this.#host.routeMap(),
175
+ () => this.#host.navigate(),
176
+ );
177
+ }
178
+
179
+ /**
180
+ * The built-in `read_page` tool, present only when a `getPageMap`
181
+ * provider is set. A *pull* the agent can call mid-turn to see the page after
182
+ * it has acted (the auto-injected context is a send-time snapshot).
183
+ */
184
+ #pageTools(): ClientTool[] {
185
+ const getPageMap = this.#host.getPageMap();
186
+ if (getPageMap === null) {
187
+ return [];
188
+ }
189
+ return [
190
+ {
191
+ name: READ_PAGE_TOOL,
192
+ description:
193
+ "Read the current page's structure (fields, buttons, route). Call after " +
194
+ "acting to observe the result within the same turn.",
195
+ parameters: {
196
+ type: "object",
197
+ properties: {},
198
+ required: [],
199
+ [X_SUMMARY_KEY]: "Read the page",
200
+ },
201
+ handler: () => getPageMap(),
202
+ },
203
+ ];
204
+ }
205
+
206
+ /**
207
+ * Opt-in page-action tools (`scroll_to` / `drag_and_drop`), enabled per token
208
+ * via the `data-page-actions` attribute (e.g. `"scroll,drag"`). Targets resolve
209
+ * through `resolvePageTarget` so a host controls the agent's interaction
210
+ * surface; absent attribute ⇒ no tools registered.
211
+ */
212
+ #pageActionTools(): ClientTool[] {
213
+ const attr = this.#host.element.getAttribute("data-page-actions");
214
+ if (attr === null) {
215
+ return [];
216
+ }
217
+ const enabled = new Set(commaTokens(attr));
218
+ return [
219
+ ...createPageActionTools(enabled, (target) => this.#host.resolvePageTarget(target)),
220
+ ...(enabled.has(PAGE_ACTIONS.CHAT) ? createChatSurfaceTools(this.#host.element) : []),
221
+ ];
222
+ }
223
+
224
+ /** All built-in (route + page + page-action + ask_user) frontend tools. */
225
+ #builtinTools(): ClientTool[] {
226
+ return [
227
+ ...this.#routeTools(),
228
+ ...this.#pageTools(),
229
+ ...this.#pageActionTools(),
230
+ ...this.#askUserTool(),
231
+ ];
232
+ }
233
+
234
+ /**
235
+ * The built-in `ask_user` frontend tool, or `[]` when `askUser` is off.
236
+ *
237
+ * The agent calls it, the client executes it locally through the normal
238
+ * frontend-tool path by rendering a {@link requestQuestion} card, and the
239
+ * answer flows back as the tool result. No new protocol.
240
+ */
241
+ #askUserTool(): ClientTool[] {
242
+ if (!this.#host.askUser()) {
243
+ return [];
244
+ }
245
+ return [
246
+ {
247
+ name: "ask_user",
248
+ description:
249
+ "Ask the user a question and wait for their answer. Provide `options` for a " +
250
+ "multiple-choice prompt; set `allow_custom` to also accept a free-text answer.",
251
+ parameters: {
252
+ type: "object",
253
+ properties: {
254
+ question: { type: "string", description: "The question to ask the user." },
255
+ options: {
256
+ type: "array",
257
+ items: { type: "string" },
258
+ description: "Preset choices offered as radio buttons.",
259
+ },
260
+ allow_custom: {
261
+ type: "boolean",
262
+ description: "Allow a free-text answer in addition to any options.",
263
+ },
264
+ },
265
+ required: ["question"],
266
+ },
267
+ handler: (args, callId) => this.#askUser(args, callId),
268
+ },
269
+ ];
270
+ }
271
+
272
+ /** Render the `ask_user` question card and resolve with the user's answer. */
273
+ async #askUser(args: Record<string, unknown>, callId: string | undefined): Promise<string> {
274
+ const question = typeof args["question"] === "string" ? args["question"] : "";
275
+ const request: QuestionRequest = { question };
276
+ const rawOptions = args["options"];
277
+ if (Array.isArray(rawOptions)) {
278
+ request.options = rawOptions.filter((option): option is string => typeof option === "string");
279
+ }
280
+ if (args["allow_custom"] === true) {
281
+ request.allowCustom = true;
282
+ }
283
+ // The run is suspended on the card; a Stop aborts the controller, resolving
284
+ // it with an empty answer (the run is then cancelled).
285
+ const signal = this.#host.decision.open();
286
+ this.#host.hidePending();
287
+ // The built-in inline card renders into the current answer group.
288
+ const builtIn = (): Promise<string> =>
289
+ requestQuestion(this.#host.ensureGroup(), request, {
290
+ signal,
291
+ strings: this.#host.strings(),
292
+ });
293
+ // A host-supplied renderer takes full control of the UI.
294
+ const renderer = this.#host.askUserRenderer();
295
+ let answer: string;
296
+ if (renderer === null) {
297
+ answer = await builtIn();
298
+ } else {
299
+ // Awaited here rather than inside a helper, so an answering renderer
300
+ // takes exactly as many turns to be heard as it always did.
301
+ try {
302
+ // Called on the element, as `this.askUserRenderer(...)` always was.
303
+ answer = await renderer.call(this.#host.element, request, { signal });
304
+ } catch (error) {
305
+ answer = await this.#afterRendererFailed(error, signal, callId, builtIn);
306
+ }
307
+ }
308
+ this.#host.decision.close();
309
+ this.#host.updateEmptyState();
310
+ this.#host.follow();
311
+ return answer;
312
+ }
313
+
314
+ /**
315
+ * Answer an `ask_user` call whose host renderer threw or rejected instead of
316
+ * answering: put the question to the built-in card.
317
+ *
318
+ * The same answer `approvalRenderer` gets for the same failure, and for the
319
+ * same reason: a renderer is presentation, not a policy. It decides how the
320
+ * question looks, never whether the agent's question is put to the user.
321
+ * Uncaught, the throw escaped the handler, so the call's card settled as an
322
+ * error quoting the host's message, that message went on to the agent as the
323
+ * tool result -- a detail of the host's page, never written for the model --
324
+ * and the pending decision was never closed, so the next Stop, in whatever
325
+ * round, aborted the signal of a wait that had already ended rather than
326
+ * finding nothing open. The built-in card still asks,
327
+ * and the run carries on as if no renderer had been set. Reported the way a
328
+ * failed `render` is, and for the same reason: survived is not the same as
329
+ * findable.
330
+ *
331
+ * Except when the wait was already abandoned. A renderer honouring its signal
332
+ * rejects once a Stop fires it, which is the signal working rather than the
333
+ * renderer failing, and a card drawn then would ask about a run the user just
334
+ * ended. So it resolves with the empty answer the built-in card resolves with
335
+ * on the same abort, and says nothing.
336
+ */
337
+ #afterRendererFailed(
338
+ error: unknown,
339
+ signal: AbortSignal,
340
+ callId: string | undefined,
341
+ builtIn: () => Promise<string>,
342
+ ): Promise<string> {
343
+ if (signal.aborted) {
344
+ return Promise.resolve("");
345
+ }
346
+ // Named by call rather than by question: the model can ask the same thing
347
+ // twice in one turn, and the id is the one thing that tells them apart.
348
+ console.warn(
349
+ `ag-ui-chat: askUserRenderer failed for tool call ${callId}, so the built-in question card asks instead`,
350
+ error,
351
+ );
352
+ return builtIn();
353
+ }
354
+ }