@tanstack/ai-client 0.33.2 → 0.34.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.
@@ -1,4 +1,5 @@
1
1
  import {
2
+ EventType,
2
3
  StreamProcessor,
3
4
  convertSchemaToJsonSchema,
4
5
  generateMessageId,
@@ -72,6 +73,7 @@ import type {
72
73
  QueueStrategy,
73
74
  QueuedMessage,
74
75
  SendMessageOptions,
76
+ SubagentHandle,
75
77
  ToolCallPart,
76
78
  UIMessage,
77
79
  WhenBusy,
@@ -330,11 +332,54 @@ const REJOIN_REBUILD_TRIGGERS = new Set<string>([
330
332
  'TEXT_MESSAGE_CONTENT',
331
333
  'TOOL_CALL_START',
332
334
  'MESSAGES_SNAPSHOT',
335
+ // Drop the hydrated card before this chunk creates it again. A subagent
336
+ // turn may have no parent text, so the text triggers arrive too late.
337
+ 'SUBAGENT_STARTED',
333
338
  ])
334
339
 
340
+ type SubagentCard = Extract<UIMessage['parts'][number], { type: 'subagent' }>
341
+
342
+ /** Every subagent card in the messages, nested cards included. */
343
+ function collectSubagentParts(
344
+ messages: ReadonlyArray<UIMessage>,
345
+ ): Array<SubagentCard> {
346
+ const cards: Array<SubagentCard> = []
347
+ for (const message of messages) {
348
+ for (const part of message.parts) {
349
+ if (part.type !== 'subagent') continue
350
+ cards.push(part)
351
+ cards.push(...collectSubagentParts(part.subagent.messages))
352
+ }
353
+ }
354
+ return cards
355
+ }
356
+
357
+ function readSubagentRunId(chunk: StreamChunk) {
358
+ if ('subagentRunId' in chunk && typeof chunk.subagentRunId === 'string') {
359
+ return chunk.subagentRunId
360
+ }
361
+ return undefined
362
+ }
363
+
335
364
  // Cap parent-chain walks so a cyclic replayed parentRunId cannot loop.
336
365
  const MAX_RUN_LINEAGE_DEPTH = 64
337
366
 
367
+ /**
368
+ * A deep copy of plain data. Dates stay Dates. Function-valued properties are
369
+ * dropped.
370
+ */
371
+ function copyForDevtools<T>(value: T): T
372
+ function copyForDevtools(value: unknown): unknown {
373
+ if (Array.isArray(value)) return value.map(copyForDevtools)
374
+ if (value instanceof Date) return new Date(value.getTime())
375
+ if (value === null || typeof value !== 'object') return value
376
+ const out: Record<string, unknown> = {}
377
+ for (const [key, item] of Object.entries(value)) {
378
+ if (typeof item !== 'function') out[key] = copyForDevtools(item)
379
+ }
380
+ return out
381
+ }
382
+
338
383
  export class ChatClient<
339
384
  TTools extends ReadonlyArray<AnyClientTool> = any,
340
385
  TContext = unknown,
@@ -342,6 +387,8 @@ export class ChatClient<
342
387
  any,
343
388
  > {
344
389
  private readonly processor: StreamProcessor
390
+ private readonly subagentHandles = new Map<string, SubagentHandle>()
391
+ private readonly stoppedSubagentIds = new Set<string>()
345
392
  private connection: SubscribeConnectionAdapter
346
393
  private uniqueId: string
347
394
  private threadId: string
@@ -669,9 +716,16 @@ export class ChatClient<
669
716
  : {}),
670
717
  ...(initialMessages ? { initialMessages } : {}),
671
718
  events: {
672
- onMessagesChange: (messages: Array<UIMessage>) => {
719
+ onMessagesChange: (messages) => {
720
+ // Restored or replaced messages bring their own cards. Give each one
721
+ // its live handle before anyone reads the messages.
722
+ this.syncSubagentHandles()
673
723
  this.persistor?.notifyMessagesChanged(messages)
674
724
  this.callbacksRef.current.onMessagesChange(messages)
725
+ // The bridge only snapshots on status changes, and the panel draws a
726
+ // child card from the snapshot, so without this the card would update
727
+ // only when the parent run ends.
728
+ this.queueDevtoolsSnapshot()
675
729
  },
676
730
  onStreamStart: () => {
677
731
  this.setStatus('streaming')
@@ -887,6 +941,8 @@ export class ChatClient<
887
941
  },
888
942
  },
889
943
  })
944
+ // `initialMessages` do not fire a change event. Give their cards handles.
945
+ this.syncSubagentHandles()
890
946
 
891
947
  this.persistor?.hydrateAsync(persistedState)
892
948
 
@@ -1693,7 +1749,9 @@ export class ChatClient<
1693
1749
 
1694
1750
  private getDevtoolsSnapshot(): AIDevtoolsChatSnapshot {
1695
1751
  return {
1696
- messages: this.processor.getMessages(),
1752
+ // The devtools store keeps the objects it gets, and a subagent handle
1753
+ // changes in place. A copy lets the panel see each new status and part.
1754
+ messages: copyForDevtools(this.processor.getMessages()),
1697
1755
  status: this.status,
1698
1756
  isLoading: this.isLoading,
1699
1757
  isSubscribed: this.isSubscribed,
@@ -1970,7 +2028,18 @@ export class ChatClient<
1970
2028
  }
1971
2029
  this.callbacksRef.current.onChunk(chunk)
1972
2030
  this.devtoolsBridge.observeChunk(chunk)
2031
+ const attributedId = readSubagentRunId(chunk)
2032
+ // A resumed child starts again under the same id, so its start lifts the stop.
2033
+ if (attributedId && chunk.type === EventType.SUBAGENT_STARTED) {
2034
+ this.stoppedSubagentIds.delete(attributedId)
2035
+ }
2036
+ if (attributedId && this.stoppedSubagentIds.has(attributedId)) {
2037
+ this.updateRunLifecycle(chunk)
2038
+ this.resolveJoinedRun(chunk)
2039
+ return
2040
+ }
1973
2041
  this.processor.processChunk(chunk)
2042
+ this.syncSubagentHandles()
1974
2043
  this.updateRunLifecycle(chunk)
1975
2044
  this.observeInterruptState(chunk)
1976
2045
  // Live path: yield a macrotask so the UI can paint. Skip when the page is
@@ -2678,6 +2747,7 @@ export class ChatClient<
2678
2747
  */
2679
2748
  stop(): void {
2680
2749
  // Invalidate deferred work from the stopped continuation.
2750
+ // This aborts the local request. A durable server run keeps going.
2681
2751
  this.continuationGeneration++
2682
2752
  const hadLocalStream = this.abortController !== null
2683
2753
  this.cancelInFlightStream({ setReadyStatus: true })
@@ -2715,6 +2785,7 @@ export class ChatClient<
2715
2785
  this.resetHistoryPaging()
2716
2786
  this.discardPendingSends()
2717
2787
  this.persistor?.remove()
2788
+ this.stoppedSubagentIds.clear()
2718
2789
  this.lastResume = null
2719
2790
  this.interruptManager.reset()
2720
2791
  this.pendingResumeThreadId = null
@@ -2974,6 +3045,72 @@ export class ChatClient<
2974
3045
  return this.processor.getMessages() as Array<UIMessage<TTools>>
2975
3046
  }
2976
3047
 
3048
+ getSubagents() {
3049
+ return [...this.subagentHandles.values()]
3050
+ }
3051
+
3052
+ private devtoolsSnapshotQueued = false
3053
+
3054
+ /** One devtools snapshot per microtask, however many chunks arrive. */
3055
+ private queueDevtoolsSnapshot(): void {
3056
+ if (this.devtoolsSnapshotQueued) return
3057
+ this.devtoolsSnapshotQueued = true
3058
+ queueMicrotask(() => {
3059
+ this.devtoolsSnapshotQueued = false
3060
+ this.devtoolsBridge.emitSnapshot()
3061
+ })
3062
+ }
3063
+
3064
+ private syncSubagentHandles(): void {
3065
+ // Every card, nested ones included, gets a handle.
3066
+ const cards = collectSubagentParts(this.processor.getMessages())
3067
+ const present = new Set(cards.map((part) => part.subagent.id))
3068
+ // A card that left the messages (clear, reload) loses its handle.
3069
+ for (const id of this.subagentHandles.keys()) {
3070
+ if (!present.has(id)) this.subagentHandles.delete(id)
3071
+ }
3072
+ for (const part of cards) {
3073
+ const id = part.subagent.id
3074
+ const existing = this.subagentHandles.get(id)
3075
+ if (existing) {
3076
+ if (part.subagent === existing) continue
3077
+ // Keep the same live object, with exactly the card's fields. The
3078
+ // wire reads this object, so a field the card dropped goes too.
3079
+ const { stop } = existing
3080
+ for (const key of Object.keys(existing)) {
3081
+ Reflect.deleteProperty(existing, key)
3082
+ }
3083
+ Object.assign(existing, part.subagent, { stop })
3084
+ part.subagent = existing
3085
+ continue
3086
+ }
3087
+ const handle: SubagentHandle = {
3088
+ ...part.subagent,
3089
+ stop: () => {
3090
+ this.stopSubagent(id)
3091
+ },
3092
+ }
3093
+ this.subagentHandles.set(id, handle)
3094
+ part.subagent = handle
3095
+ }
3096
+ }
3097
+
3098
+ private stopSubagent(id: string): void {
3099
+ const handle = this.subagentHandles.get(id)
3100
+ if (!handle || handle.status !== 'running') return
3101
+ this.stoppedSubagentIds.add(id)
3102
+ this.processor.processChunk({
3103
+ type: EventType.SUBAGENT_ERROR,
3104
+ subagentRunId: id,
3105
+ message: 'Stopped',
3106
+ timestamp: Date.now(),
3107
+ })
3108
+ this.syncSubagentHandles()
3109
+ handle.status = 'error'
3110
+ handle.error = { message: 'Stopped' }
3111
+ this.abortController?.abort()
3112
+ }
3113
+
2977
3114
  /**
2978
3115
  * True when the last hydrate or older-page response said more messages exist.
2979
3116
  */
package/src/index.ts CHANGED
@@ -33,6 +33,13 @@ export type {
33
33
  ToolResultPart,
34
34
  ThinkingPart,
35
35
  StructuredOutputPart,
36
+ SubagentPart,
37
+ SubagentPartOf,
38
+ SubagentHandle,
39
+ SubagentHandleOf,
40
+ SubagentHandles,
41
+ SubagentClientAgent,
42
+ SubagentStatus,
36
43
  // Client configuration types
37
44
  ChatClientPersistence,
38
45
  ChatPersistedState,
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,