@tanstack/ai-client 0.33.2 → 0.35.1

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/src/types.ts CHANGED
@@ -22,6 +22,8 @@ import type {
22
22
  SchemaInput,
23
23
  StreamChunk,
24
24
  StructuredOutputPart,
25
+ SubagentHandleData,
26
+ SubagentStatus,
25
27
  UIResourcePart,
26
28
  VideoPart,
27
29
  } from '@tanstack/ai/client'
@@ -514,6 +516,20 @@ export interface TextPart {
514
516
  content: string
515
517
  }
516
518
 
519
+ /**
520
+ * Tool-call part for a bare `{ name }` tool, such as a server tool
521
+ * definition a subagent carries. No `approval` field.
522
+ */
523
+ type ToolCallPartForNamedTool<T extends { name: string }> = {
524
+ type: 'tool-call'
525
+ id: string
526
+ name: T['name']
527
+ arguments: string
528
+ input?: InferToolInput<T>
529
+ state: ToolCallState
530
+ output?: InferToolOutput<T>
531
+ }
532
+
517
533
  /**
518
534
  * Helper type that creates a tool-call part for a specific tool.
519
535
  * This is a conditional type to enable proper distribution over union types,
@@ -549,7 +565,9 @@ type ToolCallPartForTool<T> = T extends AnyClientTool
549
565
  : // Tools without `needsApproval: true` never carry an approval field.
550
566
  // `& unknown` is a no-op intersection (adds nothing).
551
567
  unknown)
552
- : never
568
+ : T extends { name: string }
569
+ ? ToolCallPartForNamedTool<T>
570
+ : never
553
571
 
554
572
  /**
555
573
  * Fallback tool-call part type when tools are not typed
@@ -581,14 +599,14 @@ type UntypedToolCallPart = {
581
599
  * }
582
600
  * ```
583
601
  */
584
- export type ToolCallPart<TTools extends ReadonlyArray<AnyClientTool> = any> =
602
+ export type ToolCallPart<TTools extends ReadonlyArray<{ name: string }> = any> =
585
603
  // Check if we have a concrete tools array (not 'any' or 'never')
586
604
  [TTools] extends [never]
587
605
  ? UntypedToolCallPart
588
606
  : unknown extends TTools
589
607
  ? UntypedToolCallPart
590
608
  : TTools extends ReadonlyArray<infer Tool>
591
- ? Tool extends AnyClientTool
609
+ ? Tool extends { name: string }
592
610
  ? ToolCallPartForTool<Tool>
593
611
  : UntypedToolCallPart
594
612
  : UntypedToolCallPart
@@ -610,9 +628,97 @@ export interface ThinkingPart {
610
628
  content: string
611
629
  }
612
630
 
631
+ export type { SubagentStatus }
632
+
633
+ export interface SubagentHandle extends SubagentHandleData {
634
+ /**
635
+ * Set by ChatClient on every card it holds, streamed, restored, or initial,
636
+ * nested cards included. Calling it marks the card `error` (`Stopped`),
637
+ * ignores that child's later chunks until the same child starts again, and
638
+ * aborts the local request, parent stream included. A durable server run
639
+ * keeps going.
640
+ */
641
+ stop?: () => void
642
+ }
643
+
644
+ export interface SubagentPart {
645
+ type: 'subagent'
646
+ subagent: SubagentHandle
647
+ }
648
+
649
+ /**
650
+ * The slice of a server `defineAgent` result that the client reads for types.
651
+ * The client does not call `run`. Put tool definitions (from
652
+ * `toolDefinition`) in `tools` to type the child's tool calls and approvals.
653
+ */
654
+ export type SubagentClientAgent = {
655
+ name: string
656
+ description?: string
657
+ tools?: ReadonlyArray<{ name: string }>
658
+ interrupts?: ReadonlyArray<InterruptDefinition<any, any, any, any>>
659
+ outputSchema?: SchemaInput
660
+ }
661
+
662
+ type AgentToolList<TAgent> = TAgent extends { tools?: infer TTools }
663
+ ? [undefined] extends [TTools]
664
+ ? [TTools] extends [undefined]
665
+ ? any
666
+ : Exclude<TTools, undefined> extends ReadonlyArray<infer TTool>
667
+ ? [TTool] extends [never]
668
+ ? any
669
+ : Exclude<TTools, undefined>
670
+ : any
671
+ : TTools extends ReadonlyArray<infer TTool>
672
+ ? [TTool] extends [never]
673
+ ? any
674
+ : TTools
675
+ : any
676
+ : any
677
+
678
+ type AgentOutputData<TAgent> = TAgent extends { outputSchema?: infer TSchema }
679
+ ? Exclude<TSchema, undefined> extends SchemaInput
680
+ ? [Exclude<TSchema, undefined>] extends [never]
681
+ ? unknown
682
+ : InferSchemaType<Exclude<TSchema, undefined>>
683
+ : unknown
684
+ : unknown
685
+
686
+ /** One child handle. `name` is the discriminant. */
687
+ export type SubagentHandleOf<TAgent extends SubagentClientAgent> = Omit<
688
+ SubagentHandle,
689
+ 'name' | 'messages'
690
+ > & {
691
+ name: TAgent['name']
692
+ messages: Array<UIMessage<AgentToolList<TAgent>, AgentOutputData<TAgent>>>
693
+ }
694
+
695
+ export type SubagentHandles<
696
+ TAgents extends ReadonlyArray<SubagentClientAgent> | undefined,
697
+ > = [TAgents] extends [undefined]
698
+ ? SubagentHandle
699
+ : unknown extends TAgents
700
+ ? SubagentHandle
701
+ : TAgents extends ReadonlyArray<infer TAgent>
702
+ ? TAgent extends SubagentClientAgent
703
+ ? SubagentHandleOf<TAgent>
704
+ : SubagentHandle
705
+ : SubagentHandle
706
+
707
+ export type SubagentPartOf<
708
+ TAgents extends ReadonlyArray<SubagentClientAgent> | undefined,
709
+ > = [TAgents] extends [undefined]
710
+ ? SubagentPart
711
+ : unknown extends TAgents
712
+ ? SubagentPart
713
+ : {
714
+ type: 'subagent'
715
+ subagent: SubagentHandles<TAgents>
716
+ }
717
+
613
718
  export type MessagePart<
614
- TTools extends ReadonlyArray<AnyClientTool> = any,
719
+ TTools extends ReadonlyArray<{ name: string }> = any,
615
720
  TData = unknown,
721
+ TSubagents extends ReadonlyArray<SubagentClientAgent> | undefined = undefined,
616
722
  > =
617
723
  | TextPart
618
724
  | ImagePart
@@ -624,6 +730,7 @@ export type MessagePart<
624
730
  | ThinkingPart
625
731
  | StructuredOutputPart<TData>
626
732
  | UIResourcePart
733
+ | SubagentPartOf<TSubagents>
627
734
 
628
735
  /**
629
736
  * UIMessage - Domain-specific message format optimized for building chat UIs
@@ -638,13 +745,14 @@ export type MessagePart<
638
745
  * is typed without manual casts.
639
746
  */
640
747
  export interface UIMessage<
641
- TTools extends ReadonlyArray<AnyClientTool> = any,
748
+ TTools extends ReadonlyArray<{ name: string }> = any,
642
749
  TData = unknown,
750
+ TSubagents extends ReadonlyArray<SubagentClientAgent> | undefined = undefined,
643
751
  > {
644
752
  id: string
645
753
  role: 'system' | 'user' | 'assistant'
646
754
  name?: string
647
- parts: Array<MessagePart<TTools, TData>>
755
+ parts: Array<MessagePart<TTools, TData, TSubagents>>
648
756
  createdAt?: Date
649
757
  /**
650
758
  * Optional AG-UI metadata bag. TanStack writes the `tanstack` key.
@@ -895,6 +1003,14 @@ export interface ChatClientBaseOptions<
895
1003
  */
896
1004
  byok?: ByokClient
897
1005
 
1006
+ /**
1007
+ * The agents you pass to `chat({ subagents: { agents } })`, for types only.
1008
+ * The client does not call `run`. `createChatHook` from
1009
+ * `@tanstack/ai-react/ui` requires a `subagentsComponents` entry for every
1010
+ * agent name.
1011
+ */
1012
+ subagents?: ReadonlyArray<SubagentClientAgent>
1013
+
898
1014
  /**
899
1015
  * Optional provider id for this chat. If it returns a provider slug,
900
1016
  * only that key is prepared and sent. Otherwise the merged `provider`
@@ -25,6 +25,7 @@ const PART_KEY_BY_TYPE: Record<string, ChatUIPartKey> = {
25
25
  'tool-result': 'toolResult',
26
26
  'structured-output': 'structuredOutput',
27
27
  'ui-resource': 'uiResource',
28
+ subagent: 'subagent',
28
29
  }
29
30
 
30
31
  export function partTypeToKey(type: string): ChatUIPartKey | string {
package/src/ui/types.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  ChatInterrupt,
11
11
  MessagePart,
12
12
  RegisteredGenericInterrupt,
13
+ SubagentClientAgent,
13
14
  StructuredOutputPart,
14
15
  ToolApprovalInterrupt,
15
16
  ToolCallPart,
@@ -28,6 +29,7 @@ export type ChatUIPartKey =
28
29
  | 'toolResult'
29
30
  | 'structuredOutput'
30
31
  | 'uiResource'
32
+ | 'subagent'
31
33
 
32
34
  export type ChatUIPartTypeByKey = {
33
35
  text: 'text'
@@ -40,6 +42,7 @@ export type ChatUIPartTypeByKey = {
40
42
  toolResult: 'tool-result'
41
43
  structuredOutput: 'structured-output'
42
44
  uiResource: 'ui-resource'
45
+ subagent: 'subagent'
43
46
  }
44
47
 
45
48
  export type ChatUIPartOf<
@@ -60,7 +63,7 @@ export type ChatUIToolsOf<TOptions> = TOptions extends {
60
63
  : any
61
64
  : any
62
65
 
63
- export type ChatUIInterruptsOf<TOptions> = TOptions extends {
66
+ type RootInterruptsOf<TOptions> = TOptions extends {
64
67
  interrupts: infer TInterrupts
65
68
  }
66
69
  ? TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>
@@ -68,6 +71,23 @@ export type ChatUIInterruptsOf<TOptions> = TOptions extends {
68
71
  : readonly []
69
72
  : readonly []
70
73
 
74
+ type AgentInterrupts<TOptions> = ConcreteItems<
75
+ AgentField<ChatUISubagentsOf<TOptions>[number], 'interrupts', readonly []>
76
+ >
77
+
78
+ /** The root `interrupts` plus every child agent's `interrupts`. */
79
+ export type ChatUIInterruptsOf<TOptions> = [AgentInterrupts<TOptions>] extends [
80
+ never,
81
+ ]
82
+ ? RootInterruptsOf<TOptions>
83
+ : ReadonlyArray<
84
+ | RootInterruptsOf<TOptions>[number]
85
+ | Extract<
86
+ AgentInterrupts<TOptions>,
87
+ InterruptDefinition<any, any, any, any>
88
+ >
89
+ >
90
+
71
91
  export type ChatUISchemaOf<TOptions> = TOptions extends {
72
92
  outputSchema: infer TSchema
73
93
  }
@@ -114,14 +134,105 @@ export type ChatUIHasNamedInterrupts<TOptions> = [
114
134
  ? false
115
135
  : true
116
136
 
137
+ /** The agents in `options.subagents`, or an empty list. */
138
+ export type ChatUISubagentsOf<TOptions> = TOptions extends {
139
+ subagents?: infer TAgents
140
+ }
141
+ ? Exclude<TAgents, undefined> extends ReadonlyArray<SubagentClientAgent>
142
+ ? Exclude<TAgents, undefined>
143
+ : readonly []
144
+ : readonly []
145
+
146
+ export type ChatUISubagentName<TOptions> =
147
+ ChatUISubagentsOf<TOptions>[number]['name']
148
+
149
+ /** The agent in `options.subagents` with this name. */
150
+ export type ChatUISubagentOf<TOptions, TName> = Extract<
151
+ ChatUISubagentsOf<TOptions>[number],
152
+ { name: TName }
153
+ >
154
+
155
+ type AgentField<TAgent, TKey extends string, TFallback> = TAgent extends {
156
+ [K in TKey]?: infer TValue
157
+ }
158
+ ? [Exclude<TValue, undefined>] extends [never]
159
+ ? TFallback
160
+ : Exclude<TValue, undefined>
161
+ : TFallback
162
+
163
+ /**
164
+ * One child agent seen as chat options, so the part, tool, and interrupt
165
+ * types read that agent's `tools`, `interrupts`, and `outputSchema`. An
166
+ * unknown name gives untyped options.
167
+ */
168
+ export type ChatUISubagentOptions<TOptions, TName> = [
169
+ ChatUISubagentOf<TOptions, TName>,
170
+ ] extends [never]
171
+ ? {}
172
+ : ChatUISubagentOf<TOptions, TName> extends infer TAgent
173
+ ? {
174
+ tools: AgentField<TAgent, 'tools', readonly []>
175
+ interrupts: AgentField<TAgent, 'interrupts', readonly []>
176
+ outputSchema: AgentField<TAgent, 'outputSchema', undefined>
177
+ }
178
+ : {}
179
+
180
+ type ConcreteItems<TList> = unknown extends TList
181
+ ? never
182
+ : TList extends ReadonlyArray<infer TItem>
183
+ ? TItem
184
+ : never
185
+
186
+ type AgentTools<TOptions> = ConcreteItems<
187
+ AgentField<ChatUISubagentsOf<TOptions>[number], 'tools', readonly []>
188
+ >
189
+
190
+ /**
191
+ * The tools whose approvals can reach this chat: the root `tools` plus every
192
+ * child agent's `tools`.
193
+ */
194
+ export type ChatUIApprovalToolsOf<TOptions> = [
195
+ ConcreteItems<ChatUIToolsOf<TOptions>> | AgentTools<TOptions>,
196
+ ] extends [never]
197
+ ? ChatUIToolsOf<TOptions>
198
+ : ReadonlyArray<
199
+ Extract<
200
+ ConcreteItems<ChatUIToolsOf<TOptions>> | AgentTools<TOptions>,
201
+ AnyClientTool
202
+ >
203
+ >
204
+
205
+ export type ChatUIApprovalToolName<TOptions> =
206
+ ChatUIApprovalToolsOf<TOptions>[number] extends infer TTool
207
+ ? TTool extends AnyClientTool
208
+ ? TTool['name']
209
+ : string
210
+ : string
211
+
212
+ export type ChatUIHasNamedSubagents<TOptions> = [
213
+ ChatUISubagentName<TOptions>,
214
+ ] extends [never]
215
+ ? false
216
+ : [string] extends [ChatUISubagentName<TOptions>]
217
+ ? false
218
+ : true
219
+
117
220
  export type ChatUINamedInterruptId<TOptions> = Exclude<
118
221
  ChatUIRegisteredInterruptId<TOptions>,
119
222
  'fallback'
120
223
  >
121
224
 
225
+ type ChatUIHasNamedApprovalTools<TOptions> = [
226
+ ChatUIApprovalToolName<TOptions>,
227
+ ] extends [never]
228
+ ? false
229
+ : [string] extends [ChatUIApprovalToolName<TOptions>]
230
+ ? false
231
+ : true
232
+
122
233
  export type ChatUIInterruptName<TOptions> =
123
- | (ChatUIHasNamedTools<TOptions> extends true
124
- ? ChatUIToolName<TOptions>
234
+ | (ChatUIHasNamedApprovalTools<TOptions> extends true
235
+ ? ChatUIApprovalToolName<TOptions>
125
236
  : never)
126
237
  | (ChatUIHasNamedInterrupts<TOptions> extends true
127
238
  ? ChatUINamedInterruptId<TOptions>
@@ -136,9 +247,9 @@ export type ChatUIInterrupt = ChatInterrupt | ToolApprovalInterrupt
136
247
 
137
248
  export type ChatUIToolApproval<
138
249
  TOptions,
139
- TName extends ChatUIToolName<TOptions> = ChatUIToolName<TOptions>,
250
+ TName extends string = ChatUIApprovalToolName<TOptions>,
140
251
  > = Extract<
141
- ChatInterrupt<ChatUIToolsOf<TOptions>, ChatUIInterruptsOf<TOptions>>,
252
+ ChatInterrupt<ChatUIApprovalToolsOf<TOptions>, ChatUIInterruptsOf<TOptions>>,
142
253
  { kind: 'tool-approval'; toolName: TName }
143
254
  >
144
255
 
@@ -149,10 +260,7 @@ export type ChatUIToolPart<
149
260
  key: 'toolCall'
150
261
  part: Extract<ToolCallPart<ChatUIToolsOf<TOptions>>, { name: TName }>
151
262
  result?: ToolResultPart
152
- interrupt?: Extract<
153
- ChatInterrupt<ChatUIToolsOf<TOptions>, ChatUIInterruptsOf<TOptions>>,
154
- { kind: 'tool-approval'; toolName: TName }
155
- >
263
+ interrupt?: ChatUIToolApproval<TOptions, TName>
156
264
  input?: InferToolInput<ToolByName<TOptions, TName>>
157
265
  output?: InferToolOutput<ToolByName<TOptions, TName>>
158
266
  }
@@ -172,8 +280,8 @@ export type ChatUIInterruptOf<
172
280
  > = [TName] extends [never]
173
281
  ? ChatUIInterrupt
174
282
  : TName extends (
175
- ChatUIHasNamedTools<TOptions> extends true
176
- ? ChatUIToolName<TOptions>
283
+ ChatUIHasNamedApprovalTools<TOptions> extends true
284
+ ? ChatUIApprovalToolName<TOptions>
177
285
  : never
178
286
  )
179
287
  ? ChatUIToolApproval<TOptions, TName>
package/src/ui.ts CHANGED
@@ -6,8 +6,11 @@ export {
6
6
  selectMessageUI,
7
7
  } from './ui/selectors'
8
8
  export type {
9
+ ChatUIApprovalToolName,
10
+ ChatUIApprovalToolsOf,
9
11
  ChatUIData,
10
12
  ChatUIHasNamedInterrupts,
13
+ ChatUIHasNamedSubagents,
11
14
  ChatUIHasNamedTools,
12
15
  ChatUIInterrupt,
13
16
  ChatUIInterruptName,
@@ -28,6 +31,10 @@ export type {
28
31
  ChatUISelectedResultPart,
29
32
  ChatUISelectedToolPart,
30
33
  ChatUISelection,
34
+ ChatUISubagentName,
35
+ ChatUISubagentOf,
36
+ ChatUISubagentOptions,
37
+ ChatUISubagentsOf,
31
38
  ChatUIStructuredPart,
32
39
  ChatUIToolApproval,
33
40
  ChatUIToolName,
@@ -209,3 +209,184 @@ export async function registerWebMCPTools<
209
209
  throw error
210
210
  }
211
211
  }
212
+
213
+ /** A tool that a page registered with WebMCP, as `getTools()` returns it. */
214
+ export interface WebMCPPageTool {
215
+ name: string
216
+ title?: string
217
+ description: string
218
+ /** A JSON Schema object for the tool input. */
219
+ inputSchema?: object
220
+ /** The origin of the document that registered the tool. */
221
+ origin: string
222
+ annotations?: WebMCPToolAnnotations
223
+ }
224
+
225
+ interface WebMCPToolReader {
226
+ getTools: () => Promise<Array<WebMCPPageTool>>
227
+ executeTool: (
228
+ tool: WebMCPPageTool,
229
+ input: unknown,
230
+ options: { signal?: AbortSignal },
231
+ ) => Promise<string>
232
+ addEventListener: EventTarget['addEventListener']
233
+ removeEventListener: EventTarget['removeEventListener']
234
+ }
235
+
236
+ /** Options for {@link getWebMCPTools}. */
237
+ export interface GetWebMCPToolsOptions {
238
+ /** Return `false` to skip a tool. */
239
+ filter?: (tool: WebMCPPageTool) => boolean
240
+ }
241
+
242
+ /** Options for {@link subscribeWebMCPTools}. */
243
+ export interface SubscribeWebMCPToolsOptions extends GetWebMCPToolsOptions {
244
+ /** Stops the subscription when it aborts. */
245
+ signal: AbortSignal
246
+ /** Receives a failure from the WebMCP `getTools()` call. */
247
+ onError?: (error: unknown) => void
248
+ }
249
+
250
+ function isWebMCPToolReader(value: unknown): value is WebMCPToolReader {
251
+ return (
252
+ value !== null &&
253
+ typeof value === 'object' &&
254
+ 'getTools' in value &&
255
+ typeof value.getTools === 'function' &&
256
+ 'executeTool' in value &&
257
+ typeof value.executeTool === 'function' &&
258
+ 'addEventListener' in value &&
259
+ typeof value.addEventListener === 'function' &&
260
+ 'removeEventListener' in value &&
261
+ typeof value.removeEventListener === 'function'
262
+ )
263
+ }
264
+
265
+ function getWebMCPToolReader() {
266
+ if (
267
+ typeof document === 'undefined' ||
268
+ (typeof isSecureContext !== 'undefined' && !isSecureContext) ||
269
+ !('modelContext' in document) ||
270
+ !isWebMCPToolReader(document.modelContext)
271
+ ) {
272
+ return undefined
273
+ }
274
+ return document.modelContext
275
+ }
276
+
277
+ function parseToolResult(result: string): unknown {
278
+ try {
279
+ return JSON.parse(result)
280
+ } catch {
281
+ return result
282
+ }
283
+ }
284
+
285
+ async function readWebMCPTools(
286
+ reader: WebMCPToolReader,
287
+ options: GetWebMCPToolsOptions | undefined,
288
+ ): Promise<Array<AnyClientTool>> {
289
+ const pageTools = await reader.getTools()
290
+ const names = new Set<string>()
291
+ return pageTools
292
+ .filter((tool) => options?.filter?.(tool) ?? true)
293
+ .map((tool) => {
294
+ if (names.has(tool.name)) {
295
+ throw new Error(
296
+ `Duplicate WebMCP tool name "${tool.name}". Use a filter or register tools with unique names.`,
297
+ )
298
+ }
299
+ names.add(tool.name)
300
+ return {
301
+ __toolSide: 'client' as const,
302
+ name: tool.name,
303
+ description: tool.description,
304
+ inputSchema: tool.inputSchema ?? { type: 'object' },
305
+ async execute(input: unknown, context?: { abortSignal?: AbortSignal }) {
306
+ const result = await reader.executeTool(
307
+ tool,
308
+ input,
309
+ context?.abortSignal ? { signal: context.abortSignal } : {},
310
+ )
311
+ return parseToolResult(result)
312
+ },
313
+ }
314
+ })
315
+ }
316
+
317
+ /**
318
+ * Reads the WebMCP tools on the page and returns them as client tools.
319
+ *
320
+ * Pass the result to a chat as `tools`. Each tool runs through the WebMCP
321
+ * `executeTool()` call. Unsupported browsers and server environments return
322
+ * an empty array.
323
+ *
324
+ * @param options - A filter that skips tools.
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * const tools = await getWebMCPTools({
329
+ * filter: (tool) => tool.origin === location.origin,
330
+ * })
331
+ * ```
332
+ */
333
+ export async function getWebMCPTools(
334
+ options?: GetWebMCPToolsOptions,
335
+ ): Promise<Array<AnyClientTool>> {
336
+ const reader = getWebMCPToolReader()
337
+ return reader ? readWebMCPTools(reader, options) : []
338
+ }
339
+
340
+ /**
341
+ * Calls `listener` with the page WebMCP tools now and after each
342
+ * `toolchange` event, until `options.signal` aborts.
343
+ *
344
+ * Unsupported browsers and server environments call `listener` once with an
345
+ * empty array. When a read fails, `options.onError` gets the error and the
346
+ * listener keeps the last list.
347
+ *
348
+ * @param listener - Receives the current client tools.
349
+ * @param options - The subscription signal, a filter, and an error callback.
350
+ *
351
+ * @example
352
+ * ```ts
353
+ * const controller = new AbortController()
354
+ * subscribeWebMCPTools((tools) => client.updateOptions({ tools }), {
355
+ * signal: controller.signal,
356
+ * })
357
+ * ```
358
+ */
359
+ export function subscribeWebMCPTools(
360
+ listener: (tools: Array<AnyClientTool>) => void,
361
+ options: SubscribeWebMCPToolsOptions,
362
+ ): void {
363
+ if (options.signal.aborted) return
364
+ const reader = getWebMCPToolReader()
365
+ if (!reader) {
366
+ listener([])
367
+ return
368
+ }
369
+
370
+ let latestRead = 0
371
+ const refresh = () => {
372
+ const read = ++latestRead
373
+ const isCurrent = () => read === latestRead && !options.signal.aborted
374
+ readWebMCPTools(reader, options).then(
375
+ (tools) => {
376
+ if (isCurrent()) listener(tools)
377
+ },
378
+ (error: unknown) => {
379
+ if (isCurrent()) options.onError?.(error)
380
+ },
381
+ )
382
+ }
383
+
384
+ // Remove the listener by hand: Zone.js breaks the `signal` listener option.
385
+ reader.addEventListener('toolchange', refresh)
386
+ options.signal.addEventListener(
387
+ 'abort',
388
+ () => reader.removeEventListener('toolchange', refresh),
389
+ { once: true },
390
+ )
391
+ refresh()
392
+ }