@tanstack/ai-client 0.11.8 → 0.13.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.
- package/dist/esm/chat-client.d.ts +17 -2
- package/dist/esm/chat-client.js +223 -37
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +20 -2
- package/dist/esm/connection-adapters.js +154 -48
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/devtools-noop.d.ts +60 -0
- package/dist/esm/devtools-noop.js +135 -0
- package/dist/esm/devtools-noop.js.map +1 -0
- package/dist/esm/devtools.d.ts +276 -0
- package/dist/esm/devtools.js +1178 -0
- package/dist/esm/devtools.js.map +1 -0
- package/dist/esm/events.d.ts +50 -7
- package/dist/esm/events.js +65 -20
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/generation-client.d.ts +14 -0
- package/dist/esm/generation-client.js +135 -5
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-types.d.ts +15 -1
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types.d.ts +78 -10
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +15 -0
- package/dist/esm/video-generation-client.js +147 -8
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +8 -4
- package/src/chat-client.ts +285 -39
- package/src/connection-adapters.ts +226 -61
- package/src/devtools-noop.ts +205 -0
- package/src/devtools.ts +1866 -0
- package/src/events.ts +142 -11
- package/src/generation-client.ts +176 -9
- package/src/generation-types.ts +23 -4
- package/src/index.ts +15 -1
- package/src/types.ts +98 -12
- package/src/video-generation-client.ts +186 -11
package/src/chat-client.ts
CHANGED
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
generateMessageId,
|
|
5
5
|
normalizeToUIMessage,
|
|
6
6
|
} from '@tanstack/ai'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { createNoOpChatDevtoolsBridge } from './devtools-noop'
|
|
8
|
+
import {
|
|
9
|
+
fetcherToConnectionAdapter,
|
|
10
|
+
normalizeConnectionAdapter,
|
|
11
|
+
} from './connection-adapters'
|
|
9
12
|
import type {
|
|
10
13
|
AnyClientTool,
|
|
11
14
|
ContentPart,
|
|
@@ -16,10 +19,19 @@ import type {
|
|
|
16
19
|
ConnectionAdapter,
|
|
17
20
|
SubscribeConnectionAdapter,
|
|
18
21
|
} from './connection-adapters'
|
|
19
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
ChatClientEventEmitter,
|
|
24
|
+
ChatClientRunEventContext,
|
|
25
|
+
} from './events'
|
|
26
|
+
import type {
|
|
27
|
+
AIDevtoolsChatSnapshot,
|
|
28
|
+
ChatDevtoolsBridge,
|
|
29
|
+
ChatDevtoolsBridgeOptions,
|
|
30
|
+
} from './devtools'
|
|
20
31
|
import type {
|
|
21
32
|
ChatClientOptions,
|
|
22
33
|
ChatClientState,
|
|
34
|
+
ChatFetcher,
|
|
23
35
|
ConnectionStatus,
|
|
24
36
|
MessagePart,
|
|
25
37
|
MultimodalContent,
|
|
@@ -27,6 +39,21 @@ import type {
|
|
|
27
39
|
UIMessage,
|
|
28
40
|
} from './types'
|
|
29
41
|
|
|
42
|
+
function resolveTransport(transport: {
|
|
43
|
+
connection?: ConnectionAdapter
|
|
44
|
+
fetcher?: ChatFetcher
|
|
45
|
+
}): ConnectionAdapter {
|
|
46
|
+
const { connection, fetcher } = transport
|
|
47
|
+
if (connection && fetcher) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'ChatClient: pass either `connection` or `fetcher`, not both.',
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
if (connection) return connection
|
|
53
|
+
if (fetcher) return fetcherToConnectionAdapter(fetcher)
|
|
54
|
+
throw new Error('ChatClient: either `connection` or `fetcher` is required.')
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
export class ChatClient {
|
|
31
58
|
private readonly processor: StreamProcessor
|
|
32
59
|
private connection: SubscribeConnectionAdapter
|
|
@@ -45,8 +72,15 @@ export class ChatClient {
|
|
|
45
72
|
private status: ChatClientState = 'ready'
|
|
46
73
|
private connectionStatus: ConnectionStatus = 'disconnected'
|
|
47
74
|
private abortController: AbortController | null = null
|
|
48
|
-
private readonly events: ChatClientEventEmitter
|
|
49
75
|
private readonly clientToolsRef: { current: Map<string, AnyClientTool> }
|
|
76
|
+
private readonly devtoolsBridge: ChatDevtoolsBridge
|
|
77
|
+
/**
|
|
78
|
+
* Alias for `this.events`. The bridge installs an
|
|
79
|
+
* emitter that auto-attaches run/thread context and auto-emits a
|
|
80
|
+
* snapshot after every event, so chat-client only ever calls
|
|
81
|
+
* `this.events.X(...)` exactly like it did before devtools landed.
|
|
82
|
+
*/
|
|
83
|
+
private readonly events: ChatClientEventEmitter
|
|
50
84
|
private currentStreamId: string | null = null
|
|
51
85
|
private currentMessageId: string | null = null
|
|
52
86
|
private readonly postStreamActions: Array<() => Promise<void>> = []
|
|
@@ -64,6 +98,7 @@ export class ChatClient {
|
|
|
64
98
|
private draining = false
|
|
65
99
|
private sessionGenerating = false
|
|
66
100
|
private readonly activeRunIds = new Set<string>()
|
|
101
|
+
private devtoolsMounted = false
|
|
67
102
|
|
|
68
103
|
private readonly callbacksRef: {
|
|
69
104
|
current: {
|
|
@@ -96,8 +131,7 @@ export class ChatClient {
|
|
|
96
131
|
// winning on key collision.
|
|
97
132
|
this.bodyOption = options.body || {}
|
|
98
133
|
this.forwardedPropsOption = options.forwardedProps || {}
|
|
99
|
-
this.connection = normalizeConnectionAdapter(options
|
|
100
|
-
this.events = new DefaultChatClientEventEmitter(this.uniqueId)
|
|
134
|
+
this.connection = normalizeConnectionAdapter(resolveTransport(options))
|
|
101
135
|
|
|
102
136
|
// Build client tools map
|
|
103
137
|
this.clientToolsRef = { current: new Map() }
|
|
@@ -107,6 +141,11 @@ export class ChatClient {
|
|
|
107
141
|
}
|
|
108
142
|
}
|
|
109
143
|
|
|
144
|
+
this.devtoolsBridge = (
|
|
145
|
+
options.devtoolsBridgeFactory ?? createNoOpChatDevtoolsBridge
|
|
146
|
+
)(this.buildDevtoolsBridgeOptions(options.devtools))
|
|
147
|
+
this.events = this.devtoolsBridge.events
|
|
148
|
+
|
|
110
149
|
this.callbacksRef = {
|
|
111
150
|
current: {
|
|
112
151
|
onResponse: options.onResponse || (() => {}),
|
|
@@ -181,9 +220,41 @@ export class ChatClient {
|
|
|
181
220
|
this.currentStreamId,
|
|
182
221
|
messageId,
|
|
183
222
|
content,
|
|
223
|
+
undefined,
|
|
184
224
|
)
|
|
185
225
|
}
|
|
186
226
|
},
|
|
227
|
+
onStructuredOutputChange: (args) => {
|
|
228
|
+
const streamId = this.devtoolsBridge.resolveStreamId()
|
|
229
|
+
const eventName =
|
|
230
|
+
args.phase === 'start'
|
|
231
|
+
? 'structured-output:started'
|
|
232
|
+
: args.phase === 'complete'
|
|
233
|
+
? 'structured-output:completed'
|
|
234
|
+
: args.phase === 'error'
|
|
235
|
+
? 'structured-output:errored'
|
|
236
|
+
: 'structured-output:updated'
|
|
237
|
+
|
|
238
|
+
this.currentMessageId = args.messageId
|
|
239
|
+
this.events.structuredOutputChanged(
|
|
240
|
+
eventName,
|
|
241
|
+
streamId,
|
|
242
|
+
args.messageId,
|
|
243
|
+
{
|
|
244
|
+
status: args.status,
|
|
245
|
+
raw: args.raw,
|
|
246
|
+
...(args.partial !== undefined ? { partial: args.partial } : {}),
|
|
247
|
+
...(args.data !== undefined ? { data: args.data } : {}),
|
|
248
|
+
...(args.reasoning !== undefined
|
|
249
|
+
? { reasoning: args.reasoning }
|
|
250
|
+
: {}),
|
|
251
|
+
...(args.errorMessage !== undefined
|
|
252
|
+
? { errorMessage: args.errorMessage }
|
|
253
|
+
: {}),
|
|
254
|
+
...(args.delta !== undefined ? { delta: args.delta } : {}),
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
},
|
|
187
258
|
onToolCallStateChange: (
|
|
188
259
|
messageId: string,
|
|
189
260
|
toolCallId: string,
|
|
@@ -220,24 +291,36 @@ export class ChatClient {
|
|
|
220
291
|
const clientTool = this.clientToolsRef.current.get(args.toolName)
|
|
221
292
|
const executeFunc = clientTool?.execute
|
|
222
293
|
if (executeFunc) {
|
|
294
|
+
// Capture the run context at execution-start so a tool whose
|
|
295
|
+
// result lands AFTER the originating run finishes still reports
|
|
296
|
+
// back against the originating run, not whatever run is
|
|
297
|
+
// current when the result emits.
|
|
298
|
+
const runEventContext =
|
|
299
|
+
this.devtoolsBridge.getCurrentRunEventContext()
|
|
223
300
|
// Create and track the execution promise
|
|
224
301
|
const executionPromise = (async () => {
|
|
225
302
|
try {
|
|
226
303
|
const output = await executeFunc(args.input)
|
|
227
|
-
await this.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
304
|
+
await this.addToolResultInternal(
|
|
305
|
+
{
|
|
306
|
+
toolCallId: args.toolCallId,
|
|
307
|
+
tool: args.toolName,
|
|
308
|
+
output,
|
|
309
|
+
state: 'output-available',
|
|
310
|
+
},
|
|
311
|
+
runEventContext,
|
|
312
|
+
)
|
|
233
313
|
} catch (error: any) {
|
|
234
|
-
await this.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
314
|
+
await this.addToolResultInternal(
|
|
315
|
+
{
|
|
316
|
+
toolCallId: args.toolCallId,
|
|
317
|
+
tool: args.toolName,
|
|
318
|
+
output: null,
|
|
319
|
+
state: 'output-error',
|
|
320
|
+
errorText: error.message,
|
|
321
|
+
},
|
|
322
|
+
runEventContext,
|
|
323
|
+
)
|
|
241
324
|
} finally {
|
|
242
325
|
// Remove from pending when complete
|
|
243
326
|
this.pendingToolExecutions.delete(args.toolCallId)
|
|
@@ -254,16 +337,20 @@ export class ChatClient {
|
|
|
254
337
|
input: any
|
|
255
338
|
approvalId: string
|
|
256
339
|
}) => {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
340
|
+
const streamId = this.devtoolsBridge.resolveStreamId()
|
|
341
|
+
const messageIdForApproval =
|
|
342
|
+
this.findMessageIdForToolCall(args.toolCallId) ??
|
|
343
|
+
this.currentMessageId ??
|
|
344
|
+
''
|
|
345
|
+
|
|
346
|
+
this.events.approvalRequested(
|
|
347
|
+
streamId,
|
|
348
|
+
messageIdForApproval,
|
|
349
|
+
args.toolCallId,
|
|
350
|
+
args.toolName,
|
|
351
|
+
args.input,
|
|
352
|
+
args.approvalId,
|
|
353
|
+
)
|
|
267
354
|
},
|
|
268
355
|
onCustomEvent: (
|
|
269
356
|
eventType: string,
|
|
@@ -274,8 +361,15 @@ export class ChatClient {
|
|
|
274
361
|
},
|
|
275
362
|
},
|
|
276
363
|
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
mountDevtools(): void {
|
|
367
|
+
if (this.devtoolsMounted) {
|
|
368
|
+
return
|
|
369
|
+
}
|
|
277
370
|
|
|
278
|
-
this.
|
|
371
|
+
this.devtoolsMounted = true
|
|
372
|
+
this.devtoolsBridge.mountWithTools(this.processor.getMessages().length)
|
|
279
373
|
}
|
|
280
374
|
|
|
281
375
|
private generateUniqueId(prefix: string): string {
|
|
@@ -291,22 +385,26 @@ export class ChatClient {
|
|
|
291
385
|
private setStatus(status: ChatClientState): void {
|
|
292
386
|
this.status = status
|
|
293
387
|
this.callbacksRef.current.onStatusChange(status)
|
|
388
|
+
this.devtoolsBridge.emitSnapshot()
|
|
294
389
|
}
|
|
295
390
|
|
|
296
391
|
private setIsSubscribed(isSubscribed: boolean): void {
|
|
297
392
|
this.isSubscribed = isSubscribed
|
|
298
393
|
this.callbacksRef.current.onSubscriptionChange(isSubscribed)
|
|
394
|
+
this.devtoolsBridge.emitSnapshot()
|
|
299
395
|
}
|
|
300
396
|
|
|
301
397
|
private setConnectionStatus(status: ConnectionStatus): void {
|
|
302
398
|
this.connectionStatus = status
|
|
303
399
|
this.callbacksRef.current.onConnectionStatusChange(status)
|
|
400
|
+
this.devtoolsBridge.emitSnapshot()
|
|
304
401
|
}
|
|
305
402
|
|
|
306
403
|
private setSessionGenerating(isGenerating: boolean): void {
|
|
307
404
|
if (this.sessionGenerating === isGenerating) return
|
|
308
405
|
this.sessionGenerating = isGenerating
|
|
309
406
|
this.callbacksRef.current.onSessionGeneratingChange(isGenerating)
|
|
407
|
+
this.devtoolsBridge.emitSnapshot()
|
|
310
408
|
}
|
|
311
409
|
|
|
312
410
|
private resetSessionGenerating(): void {
|
|
@@ -320,6 +418,57 @@ export class ChatClient {
|
|
|
320
418
|
this.events.errorChanged(error?.message || null)
|
|
321
419
|
}
|
|
322
420
|
|
|
421
|
+
private buildDevtoolsBridgeOptions(
|
|
422
|
+
devtools: ChatClientOptions['devtools'],
|
|
423
|
+
): ChatDevtoolsBridgeOptions {
|
|
424
|
+
return {
|
|
425
|
+
hookId: this.uniqueId,
|
|
426
|
+
clientId: this.uniqueId,
|
|
427
|
+
threadId: this.threadId,
|
|
428
|
+
metadata: {
|
|
429
|
+
hookName: devtools?.hookName ?? 'useChat',
|
|
430
|
+
outputKind: devtools?.outputKind ?? 'chat',
|
|
431
|
+
...(devtools?.framework ? { framework: devtools.framework } : {}),
|
|
432
|
+
...(devtools?.name ? { name: devtools.name } : {}),
|
|
433
|
+
},
|
|
434
|
+
getSnapshot: () => this.getDevtoolsSnapshot(),
|
|
435
|
+
getTools: () => this.clientToolsRef.current.values(),
|
|
436
|
+
getMessages: () => this.processor.getMessages(),
|
|
437
|
+
setMessages: (messages: Array<UIMessage>) => {
|
|
438
|
+
this.processor.setMessages(messages)
|
|
439
|
+
},
|
|
440
|
+
addToolResult: (toolCallId, output, errorText) => {
|
|
441
|
+
this.processor.addToolResult(toolCallId, output, errorText)
|
|
442
|
+
},
|
|
443
|
+
generateId: (prefix) => this.generateUniqueId(prefix),
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private getDevtoolsSnapshot(): AIDevtoolsChatSnapshot {
|
|
448
|
+
return {
|
|
449
|
+
messages: this.processor.getMessages(),
|
|
450
|
+
status: this.status,
|
|
451
|
+
isLoading: this.isLoading,
|
|
452
|
+
isSubscribed: this.isSubscribed,
|
|
453
|
+
connectionStatus: this.connectionStatus,
|
|
454
|
+
sessionGenerating: this.sessionGenerating,
|
|
455
|
+
activeRunIds: Array.from(this.activeRunIds),
|
|
456
|
+
...(this.error ? { error: this.error.message } : {}),
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private findMessageIdForToolCall(toolCallId: string): string | undefined {
|
|
461
|
+
const messages = this.processor.getMessages()
|
|
462
|
+
for (const message of messages) {
|
|
463
|
+
const match = message.parts.find(
|
|
464
|
+
(part: MessagePart): part is ToolCallPart =>
|
|
465
|
+
part.type === 'tool-call' && part.id === toolCallId,
|
|
466
|
+
)
|
|
467
|
+
if (match) return message.id
|
|
468
|
+
}
|
|
469
|
+
return undefined
|
|
470
|
+
}
|
|
471
|
+
|
|
323
472
|
private abortSubscriptionLoop(): void {
|
|
324
473
|
this.subscriptionAbortController?.abort()
|
|
325
474
|
this.subscriptionAbortController = null
|
|
@@ -409,11 +558,12 @@ export class ChatClient {
|
|
|
409
558
|
this.setConnectionStatus('connected')
|
|
410
559
|
}
|
|
411
560
|
this.callbacksRef.current.onChunk(chunk)
|
|
412
|
-
this.processor.processChunk(chunk)
|
|
413
561
|
if (chunk.type === 'RUN_STARTED') {
|
|
414
562
|
this.activeRunIds.add(chunk.runId)
|
|
415
563
|
this.setSessionGenerating(true)
|
|
416
564
|
}
|
|
565
|
+
this.devtoolsBridge.observeChunk(chunk)
|
|
566
|
+
this.processor.processChunk(chunk)
|
|
417
567
|
// RUN_FINISHED / RUN_ERROR signal run completion — resolve processing
|
|
418
568
|
// (redundant if onStreamEnd already resolved it, harmless)
|
|
419
569
|
if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {
|
|
@@ -422,9 +572,9 @@ export class ChatClient {
|
|
|
422
572
|
// both so a RUN_ERROR with a runId only clears that run, not every
|
|
423
573
|
// active run in the session.
|
|
424
574
|
const runId =
|
|
425
|
-
chunk.
|
|
575
|
+
'runId' in chunk && typeof chunk.runId === 'string'
|
|
426
576
|
? chunk.runId
|
|
427
|
-
:
|
|
577
|
+
: undefined
|
|
428
578
|
if (runId) {
|
|
429
579
|
this.activeRunIds.delete(runId)
|
|
430
580
|
} else if (chunk.type === 'RUN_ERROR') {
|
|
@@ -510,6 +660,7 @@ export class ChatClient {
|
|
|
510
660
|
content: string | MultimodalContent,
|
|
511
661
|
body?: Record<string, any>,
|
|
512
662
|
): Promise<void> {
|
|
663
|
+
this.mountDevtools()
|
|
513
664
|
const emptyMessage = typeof content === 'string' && !content.trim()
|
|
514
665
|
if (emptyMessage || this.isLoading) {
|
|
515
666
|
return
|
|
@@ -548,6 +699,7 @@ export class ChatClient {
|
|
|
548
699
|
* Append a message and stream the response
|
|
549
700
|
*/
|
|
550
701
|
async append(message: UIMessage | ModelMessage): Promise<void> {
|
|
702
|
+
this.mountDevtools()
|
|
551
703
|
// Normalize the message to ensure it has id and createdAt
|
|
552
704
|
const normalizedMessage = normalizeToUIMessage(message, generateMessageId)
|
|
553
705
|
|
|
@@ -565,6 +717,7 @@ export class ChatClient {
|
|
|
565
717
|
// Add to messages
|
|
566
718
|
const messages = this.processor.getMessages()
|
|
567
719
|
this.processor.setMessages([...messages, uiMessage])
|
|
720
|
+
this.devtoolsBridge.emitSnapshot()
|
|
568
721
|
|
|
569
722
|
// If stream is in progress, queue the response for after it ends
|
|
570
723
|
if (this.isLoading) {
|
|
@@ -602,6 +755,8 @@ export class ChatClient {
|
|
|
602
755
|
// Reset pending tool executions for the new stream
|
|
603
756
|
this.pendingToolExecutions.clear()
|
|
604
757
|
let streamCompletedSuccessfully = false
|
|
758
|
+
let activeDevtoolsRunId: string | null = null
|
|
759
|
+
let runTerminalEventEmitted = false
|
|
605
760
|
|
|
606
761
|
try {
|
|
607
762
|
// Get UIMessages with parts (preserves approval state and client tool results)
|
|
@@ -639,6 +794,7 @@ export class ChatClient {
|
|
|
639
794
|
|
|
640
795
|
// Generate stream ID — assistant message will be created by stream events
|
|
641
796
|
this.currentStreamId = this.generateUniqueId('stream')
|
|
797
|
+
this.devtoolsBridge.setCurrentStreamId(this.currentStreamId)
|
|
642
798
|
this.currentMessageId = null
|
|
643
799
|
|
|
644
800
|
// Reset processor stream state for new response — prevents stale
|
|
@@ -674,6 +830,19 @@ export class ChatClient {
|
|
|
674
830
|
),
|
|
675
831
|
forwardedProps: { ...mergedBody },
|
|
676
832
|
}
|
|
833
|
+
this.devtoolsBridge.beginRun(runContext.runId, this.threadId)
|
|
834
|
+
activeDevtoolsRunId = runContext.runId
|
|
835
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
836
|
+
'run:created',
|
|
837
|
+
runContext.runId,
|
|
838
|
+
'created',
|
|
839
|
+
)
|
|
840
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
841
|
+
'run:started',
|
|
842
|
+
runContext.runId,
|
|
843
|
+
'started',
|
|
844
|
+
)
|
|
845
|
+
this.devtoolsBridge.emitSnapshot()
|
|
677
846
|
|
|
678
847
|
// Send through normalized connection (pushes chunks to subscription queue)
|
|
679
848
|
await this.connection.send(messages, mergedBody, signal, runContext)
|
|
@@ -690,6 +859,15 @@ export class ChatClient {
|
|
|
690
859
|
// A RUN_ERROR from the stream transitions status to error.
|
|
691
860
|
// Do not treat this stream as a successful completion.
|
|
692
861
|
if (this.status === 'error') {
|
|
862
|
+
if (activeDevtoolsRunId) {
|
|
863
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
864
|
+
'run:errored',
|
|
865
|
+
activeDevtoolsRunId,
|
|
866
|
+
'errored',
|
|
867
|
+
this.error ? { error: this.error.message } : {},
|
|
868
|
+
)
|
|
869
|
+
runTerminalEventEmitted = true
|
|
870
|
+
}
|
|
693
871
|
return false
|
|
694
872
|
}
|
|
695
873
|
|
|
@@ -704,10 +882,27 @@ export class ChatClient {
|
|
|
704
882
|
} catch (err) {
|
|
705
883
|
if (err instanceof Error) {
|
|
706
884
|
if (err.name === 'AbortError') {
|
|
885
|
+
if (activeDevtoolsRunId) {
|
|
886
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
887
|
+
'run:cancelled',
|
|
888
|
+
activeDevtoolsRunId,
|
|
889
|
+
'cancelled',
|
|
890
|
+
)
|
|
891
|
+
runTerminalEventEmitted = true
|
|
892
|
+
}
|
|
707
893
|
return false
|
|
708
894
|
}
|
|
709
895
|
if (generation === this.streamGeneration) {
|
|
710
896
|
this.reportStreamError(err)
|
|
897
|
+
if (activeDevtoolsRunId) {
|
|
898
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
899
|
+
'run:errored',
|
|
900
|
+
activeDevtoolsRunId,
|
|
901
|
+
'errored',
|
|
902
|
+
{ error: err.message },
|
|
903
|
+
)
|
|
904
|
+
runTerminalEventEmitted = true
|
|
905
|
+
}
|
|
711
906
|
}
|
|
712
907
|
}
|
|
713
908
|
} finally {
|
|
@@ -716,11 +911,28 @@ export class ChatClient {
|
|
|
716
911
|
// clobber the new stream's abortController or isLoading state.
|
|
717
912
|
if (generation === this.streamGeneration) {
|
|
718
913
|
this.currentStreamId = null
|
|
914
|
+
this.devtoolsBridge.setCurrentStreamId(null)
|
|
719
915
|
this.currentMessageId = null
|
|
720
916
|
this.abortController = null
|
|
721
917
|
this.setIsLoading(false)
|
|
722
918
|
this.pendingMessageBody = undefined // Ensure it's cleared even on error
|
|
723
919
|
|
|
920
|
+
if (activeDevtoolsRunId && !runTerminalEventEmitted) {
|
|
921
|
+
if (streamCompletedSuccessfully) {
|
|
922
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
923
|
+
'run:completed',
|
|
924
|
+
activeDevtoolsRunId,
|
|
925
|
+
'completed',
|
|
926
|
+
)
|
|
927
|
+
} else if (signal.aborted) {
|
|
928
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
929
|
+
'run:cancelled',
|
|
930
|
+
activeDevtoolsRunId,
|
|
931
|
+
'cancelled',
|
|
932
|
+
)
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
724
936
|
// Drain any actions that were queued while the stream was in progress
|
|
725
937
|
await this.drainPostStreamActions()
|
|
726
938
|
|
|
@@ -792,7 +1004,7 @@ export class ChatClient {
|
|
|
792
1004
|
|
|
793
1005
|
// Find the last user message
|
|
794
1006
|
const lastUserMessageIndex = messages.findLastIndex(
|
|
795
|
-
(m
|
|
1007
|
+
(m) => m.role === 'user',
|
|
796
1008
|
)
|
|
797
1009
|
|
|
798
1010
|
if (lastUserMessageIndex === -1) return
|
|
@@ -806,6 +1018,7 @@ export class ChatClient {
|
|
|
806
1018
|
|
|
807
1019
|
// Remove all messages after the last user message
|
|
808
1020
|
this.processor.removeMessagesAfter(lastUserMessageIndex)
|
|
1021
|
+
this.devtoolsBridge.emitSnapshot()
|
|
809
1022
|
|
|
810
1023
|
// Resend
|
|
811
1024
|
await this.streamResponse()
|
|
@@ -838,11 +1051,25 @@ export class ChatClient {
|
|
|
838
1051
|
state?: 'output-available' | 'output-error'
|
|
839
1052
|
errorText?: string
|
|
840
1053
|
}): Promise<void> {
|
|
1054
|
+
await this.addToolResultInternal(result)
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
private async addToolResultInternal(
|
|
1058
|
+
result: {
|
|
1059
|
+
toolCallId: string
|
|
1060
|
+
tool: string
|
|
1061
|
+
output: any
|
|
1062
|
+
state?: 'output-available' | 'output-error'
|
|
1063
|
+
errorText?: string
|
|
1064
|
+
},
|
|
1065
|
+
context?: ChatClientRunEventContext,
|
|
1066
|
+
): Promise<void> {
|
|
841
1067
|
this.events.toolResultAdded(
|
|
842
1068
|
result.toolCallId,
|
|
843
1069
|
result.tool,
|
|
844
1070
|
result.output,
|
|
845
1071
|
result.state || 'output-available',
|
|
1072
|
+
context,
|
|
846
1073
|
)
|
|
847
1074
|
|
|
848
1075
|
// Add result via processor
|
|
@@ -893,6 +1120,7 @@ export class ChatClient {
|
|
|
893
1120
|
|
|
894
1121
|
// Add response via processor
|
|
895
1122
|
this.processor.addToolApprovalResponse(response.id, response.approved)
|
|
1123
|
+
this.devtoolsBridge.emitSnapshot()
|
|
896
1124
|
|
|
897
1125
|
// If stream is in progress, queue continuation check for after it ends
|
|
898
1126
|
if (this.isLoading) {
|
|
@@ -964,9 +1192,13 @@ export class ChatClient {
|
|
|
964
1192
|
*/
|
|
965
1193
|
private shouldAutoSend(): boolean {
|
|
966
1194
|
const messages = this.processor.getMessages()
|
|
967
|
-
const lastAssistant = messages.findLast(
|
|
1195
|
+
const lastAssistant = messages.findLast(
|
|
1196
|
+
(m: UIMessage) => m.role === 'assistant',
|
|
1197
|
+
)
|
|
968
1198
|
if (!lastAssistant) return false
|
|
969
|
-
const hasToolCalls = lastAssistant.parts.some(
|
|
1199
|
+
const hasToolCalls = lastAssistant.parts.some(
|
|
1200
|
+
(p: MessagePart) => p.type === 'tool-call',
|
|
1201
|
+
)
|
|
970
1202
|
if (!hasToolCalls) return false
|
|
971
1203
|
return this.processor.areAllToolsComplete()
|
|
972
1204
|
}
|
|
@@ -1028,6 +1260,7 @@ export class ChatClient {
|
|
|
1028
1260
|
*/
|
|
1029
1261
|
setMessagesManually(messages: Array<UIMessage>): void {
|
|
1030
1262
|
this.processor.setMessages(messages)
|
|
1263
|
+
this.devtoolsBridge.emitSnapshot()
|
|
1031
1264
|
}
|
|
1032
1265
|
|
|
1033
1266
|
/**
|
|
@@ -1035,6 +1268,7 @@ export class ChatClient {
|
|
|
1035
1268
|
*/
|
|
1036
1269
|
updateOptions(options: {
|
|
1037
1270
|
connection?: ConnectionAdapter
|
|
1271
|
+
fetcher?: ChatFetcher
|
|
1038
1272
|
/** @deprecated Use `forwardedProps` instead. */
|
|
1039
1273
|
body?: Record<string, any>
|
|
1040
1274
|
forwardedProps?: Record<string, any>
|
|
@@ -1052,7 +1286,7 @@ export class ChatClient {
|
|
|
1052
1286
|
context: { toolCallId?: string },
|
|
1053
1287
|
) => void
|
|
1054
1288
|
}): void {
|
|
1055
|
-
if (options.connection !== undefined) {
|
|
1289
|
+
if (options.connection !== undefined || options.fetcher !== undefined) {
|
|
1056
1290
|
const wasSubscribed = this.isSubscribed
|
|
1057
1291
|
|
|
1058
1292
|
if (this.isLoading) {
|
|
@@ -1067,7 +1301,12 @@ export class ChatClient {
|
|
|
1067
1301
|
this.resetSessionGenerating()
|
|
1068
1302
|
this.setIsSubscribed(false)
|
|
1069
1303
|
this.setConnectionStatus('disconnected')
|
|
1070
|
-
this.connection = normalizeConnectionAdapter(
|
|
1304
|
+
this.connection = normalizeConnectionAdapter(
|
|
1305
|
+
resolveTransport({
|
|
1306
|
+
connection: options.connection,
|
|
1307
|
+
fetcher: options.fetcher,
|
|
1308
|
+
}),
|
|
1309
|
+
)
|
|
1071
1310
|
|
|
1072
1311
|
if (wasSubscribed) {
|
|
1073
1312
|
this.subscribe()
|
|
@@ -1075,7 +1314,7 @@ export class ChatClient {
|
|
|
1075
1314
|
}
|
|
1076
1315
|
// Replace each slot independently so callers can update one without
|
|
1077
1316
|
// wiping the other. (Passing `undefined` for either field is a "leave
|
|
1078
|
-
// unchanged" signal
|
|
1317
|
+
// unchanged" signal - to clear a slot, pass an empty object `{}`.)
|
|
1079
1318
|
if (options.body !== undefined) {
|
|
1080
1319
|
this.bodyOption = options.body
|
|
1081
1320
|
}
|
|
@@ -1087,6 +1326,7 @@ export class ChatClient {
|
|
|
1087
1326
|
for (const tool of options.tools) {
|
|
1088
1327
|
this.clientToolsRef.current.set(tool.name, tool)
|
|
1089
1328
|
}
|
|
1329
|
+
this.devtoolsBridge.notifyToolsChanged()
|
|
1090
1330
|
}
|
|
1091
1331
|
if (options.onResponse !== undefined) {
|
|
1092
1332
|
this.callbacksRef.current.onResponse = options.onResponse
|
|
@@ -1116,4 +1356,10 @@ export class ChatClient {
|
|
|
1116
1356
|
this.callbacksRef.current.onCustomEvent = options.onCustomEvent
|
|
1117
1357
|
}
|
|
1118
1358
|
}
|
|
1359
|
+
|
|
1360
|
+
dispose(): void {
|
|
1361
|
+
this.unsubscribe()
|
|
1362
|
+
this.devtoolsBridge.dispose()
|
|
1363
|
+
this.devtoolsMounted = false
|
|
1364
|
+
}
|
|
1119
1365
|
}
|