@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/events.ts
CHANGED
|
@@ -1,7 +1,57 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
aiEventClient,
|
|
3
|
+
createAIDevtoolsEventEnvelope,
|
|
4
|
+
} from '@tanstack/ai-event-client'
|
|
2
5
|
import type { ContentPart } from '@tanstack/ai'
|
|
3
6
|
import type { UIMessage } from './types'
|
|
4
7
|
|
|
8
|
+
export interface ChatClientRunEventContext {
|
|
9
|
+
threadId: string
|
|
10
|
+
runId: string
|
|
11
|
+
toolCallId?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChatClientEventContext {
|
|
15
|
+
threadId?: string
|
|
16
|
+
runId?: string
|
|
17
|
+
toolCallId?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ChatClientToolFixtureAppliedEvent {
|
|
21
|
+
hookId: string
|
|
22
|
+
threadId: string
|
|
23
|
+
toolName: string
|
|
24
|
+
input: unknown
|
|
25
|
+
output: unknown
|
|
26
|
+
execute?: boolean
|
|
27
|
+
message?: {
|
|
28
|
+
id: string
|
|
29
|
+
role: 'system' | 'user' | 'assistant'
|
|
30
|
+
parts: Array<unknown>
|
|
31
|
+
createdAt?: number | string
|
|
32
|
+
}
|
|
33
|
+
messageId: string
|
|
34
|
+
toolCallId: string
|
|
35
|
+
runId?: string
|
|
36
|
+
errorText?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ChatClientStructuredOutputEvent {
|
|
40
|
+
status: 'streaming' | 'complete' | 'error'
|
|
41
|
+
raw?: string
|
|
42
|
+
partial?: unknown
|
|
43
|
+
data?: unknown
|
|
44
|
+
reasoning?: string
|
|
45
|
+
errorMessage?: string
|
|
46
|
+
delta?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ChatClientStructuredOutputEventName =
|
|
50
|
+
| 'structured-output:started'
|
|
51
|
+
| 'structured-output:updated'
|
|
52
|
+
| 'structured-output:completed'
|
|
53
|
+
| 'structured-output:errored'
|
|
54
|
+
|
|
5
55
|
/**
|
|
6
56
|
* Abstract base class for ChatClient event emission
|
|
7
57
|
*/
|
|
@@ -49,11 +99,17 @@ export abstract class ChatClientEventEmitter {
|
|
|
49
99
|
/**
|
|
50
100
|
* Emit text update events (combines processor and client events)
|
|
51
101
|
*/
|
|
52
|
-
textUpdated(
|
|
102
|
+
textUpdated(
|
|
103
|
+
streamId: string,
|
|
104
|
+
messageId: string,
|
|
105
|
+
content: string,
|
|
106
|
+
context?: ChatClientRunEventContext,
|
|
107
|
+
): void {
|
|
53
108
|
this.emitEvent('text:chunk:content', {
|
|
54
109
|
streamId,
|
|
55
110
|
messageId,
|
|
56
111
|
content,
|
|
112
|
+
...context,
|
|
57
113
|
})
|
|
58
114
|
}
|
|
59
115
|
|
|
@@ -67,6 +123,7 @@ export abstract class ChatClientEventEmitter {
|
|
|
67
123
|
toolName: string,
|
|
68
124
|
state: string,
|
|
69
125
|
args: string,
|
|
126
|
+
context?: ChatClientRunEventContext,
|
|
70
127
|
): void {
|
|
71
128
|
this.emitEvent('tools:call:updated', {
|
|
72
129
|
streamId,
|
|
@@ -75,6 +132,7 @@ export abstract class ChatClientEventEmitter {
|
|
|
75
132
|
toolName,
|
|
76
133
|
state,
|
|
77
134
|
arguments: args,
|
|
135
|
+
...context,
|
|
78
136
|
})
|
|
79
137
|
}
|
|
80
138
|
|
|
@@ -89,12 +147,29 @@ export abstract class ChatClientEventEmitter {
|
|
|
89
147
|
messageId: string,
|
|
90
148
|
content: string,
|
|
91
149
|
delta?: string,
|
|
150
|
+
context?: ChatClientRunEventContext,
|
|
92
151
|
): void {
|
|
93
152
|
this.emitEvent('text:chunk:thinking', {
|
|
94
153
|
streamId,
|
|
95
154
|
messageId,
|
|
96
155
|
content,
|
|
97
156
|
delta,
|
|
157
|
+
...context,
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
structuredOutputChanged(
|
|
162
|
+
eventName: ChatClientStructuredOutputEventName,
|
|
163
|
+
streamId: string,
|
|
164
|
+
messageId: string,
|
|
165
|
+
output: ChatClientStructuredOutputEvent,
|
|
166
|
+
context?: ChatClientRunEventContext,
|
|
167
|
+
): void {
|
|
168
|
+
this.emitEvent(eventName, {
|
|
169
|
+
streamId,
|
|
170
|
+
messageId,
|
|
171
|
+
...output,
|
|
172
|
+
...context,
|
|
98
173
|
})
|
|
99
174
|
}
|
|
100
175
|
|
|
@@ -108,6 +183,7 @@ export abstract class ChatClientEventEmitter {
|
|
|
108
183
|
toolName: string,
|
|
109
184
|
input: unknown,
|
|
110
185
|
approvalId: string,
|
|
186
|
+
context?: ChatClientRunEventContext,
|
|
111
187
|
): void {
|
|
112
188
|
this.emitEvent('tools:approval:requested', {
|
|
113
189
|
streamId,
|
|
@@ -116,13 +192,18 @@ export abstract class ChatClientEventEmitter {
|
|
|
116
192
|
toolName,
|
|
117
193
|
input,
|
|
118
194
|
approvalId,
|
|
195
|
+
...context,
|
|
119
196
|
})
|
|
120
197
|
}
|
|
121
198
|
|
|
122
199
|
/**
|
|
123
200
|
* Emit message appended event
|
|
124
201
|
*/
|
|
125
|
-
messageAppended(
|
|
202
|
+
messageAppended(
|
|
203
|
+
uiMessage: UIMessage,
|
|
204
|
+
streamId?: string,
|
|
205
|
+
context?: ChatClientEventContext,
|
|
206
|
+
): void {
|
|
126
207
|
const content = uiMessage.parts
|
|
127
208
|
.filter((part) => part.type === 'text')
|
|
128
209
|
.map((part) => part.content)
|
|
@@ -134,6 +215,7 @@ export abstract class ChatClientEventEmitter {
|
|
|
134
215
|
role: uiMessage.role,
|
|
135
216
|
content,
|
|
136
217
|
parts: uiMessage.parts,
|
|
218
|
+
...context,
|
|
137
219
|
})
|
|
138
220
|
}
|
|
139
221
|
|
|
@@ -201,12 +283,14 @@ export abstract class ChatClientEventEmitter {
|
|
|
201
283
|
toolName: string,
|
|
202
284
|
output: unknown,
|
|
203
285
|
state: string,
|
|
286
|
+
context?: ChatClientEventContext,
|
|
204
287
|
): void {
|
|
205
288
|
this.emitEvent('tools:result:added', {
|
|
206
289
|
toolCallId,
|
|
207
290
|
toolName,
|
|
208
291
|
output,
|
|
209
292
|
state,
|
|
293
|
+
...context,
|
|
210
294
|
})
|
|
211
295
|
}
|
|
212
296
|
|
|
@@ -217,13 +301,22 @@ export abstract class ChatClientEventEmitter {
|
|
|
217
301
|
approvalId: string,
|
|
218
302
|
toolCallId: string,
|
|
219
303
|
approved: boolean,
|
|
304
|
+
context?: ChatClientRunEventContext,
|
|
220
305
|
): void {
|
|
221
306
|
this.emitEvent('tools:approval:responded', {
|
|
222
307
|
approvalId,
|
|
223
308
|
toolCallId,
|
|
224
309
|
approved,
|
|
310
|
+
...context,
|
|
225
311
|
})
|
|
226
312
|
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Emit tool fixture applied event.
|
|
316
|
+
*/
|
|
317
|
+
toolFixtureApplied(fixture: ChatClientToolFixtureAppliedEvent): void {
|
|
318
|
+
this.emitEvent('devtools:tool-fixture:applied', { ...fixture })
|
|
319
|
+
}
|
|
227
320
|
}
|
|
228
321
|
|
|
229
322
|
/**
|
|
@@ -234,23 +327,61 @@ export class DefaultChatClientEventEmitter extends ChatClientEventEmitter {
|
|
|
234
327
|
* Emit an event with automatic clientId and timestamp for client/tool events
|
|
235
328
|
*/
|
|
236
329
|
protected emitEvent(eventName: string, data?: Record<string, any>): void {
|
|
237
|
-
|
|
238
|
-
|
|
330
|
+
const timestamp = Date.now()
|
|
331
|
+
const isUserVisibleEvent =
|
|
332
|
+
eventName.startsWith('text:') ||
|
|
333
|
+
eventName.startsWith('tools:') ||
|
|
334
|
+
eventName.startsWith('structured-output:') ||
|
|
335
|
+
eventName === 'devtools:tool-fixture:applied'
|
|
336
|
+
const includesClientContext =
|
|
239
337
|
eventName.startsWith('client:') ||
|
|
240
338
|
eventName.startsWith('tools:') ||
|
|
241
|
-
eventName.startsWith('text:')
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
339
|
+
eventName.startsWith('text:') ||
|
|
340
|
+
eventName.startsWith('structured-output:') ||
|
|
341
|
+
eventName === 'devtools:tool-fixture:applied'
|
|
342
|
+
const visibility = isUserVisibleEvent ? 'user-visible' : 'client-state'
|
|
343
|
+
const envelopeContext = {
|
|
344
|
+
hookId: this.clientId,
|
|
345
|
+
...(typeof data?.threadId === 'string'
|
|
346
|
+
? { threadId: data.threadId }
|
|
347
|
+
: {}),
|
|
348
|
+
...(typeof data?.runId === 'string' ? { runId: data.runId } : {}),
|
|
349
|
+
...(typeof data?.streamId === 'string'
|
|
350
|
+
? { streamId: data.streamId }
|
|
351
|
+
: {}),
|
|
352
|
+
...(typeof data?.messageId === 'string'
|
|
353
|
+
? { messageId: data.messageId }
|
|
354
|
+
: {}),
|
|
355
|
+
...(typeof data?.toolCallId === 'string'
|
|
356
|
+
? { toolCallId: data.toolCallId }
|
|
357
|
+
: {}),
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// For client:* and tool:* events, automatically add clientId and timestamp
|
|
361
|
+
if (includesClientContext) {
|
|
362
|
+
const envelope = createAIDevtoolsEventEnvelope({
|
|
363
|
+
eventType: eventName,
|
|
245
364
|
clientId: this.clientId,
|
|
365
|
+
...envelopeContext,
|
|
246
366
|
source: 'client',
|
|
247
|
-
|
|
367
|
+
visibility,
|
|
368
|
+
timestamp,
|
|
369
|
+
})
|
|
370
|
+
aiEventClient.emit(eventName as any, {
|
|
371
|
+
...data,
|
|
372
|
+
...envelope,
|
|
248
373
|
})
|
|
249
374
|
} else {
|
|
375
|
+
const envelope = createAIDevtoolsEventEnvelope({
|
|
376
|
+
eventType: eventName,
|
|
377
|
+
source: 'client',
|
|
378
|
+
visibility: 'client-state',
|
|
379
|
+
timestamp,
|
|
380
|
+
})
|
|
250
381
|
// For other events, just add timestamp
|
|
251
382
|
aiEventClient.emit(eventName as any, {
|
|
252
383
|
...data,
|
|
253
|
-
|
|
384
|
+
...envelope,
|
|
254
385
|
})
|
|
255
386
|
}
|
|
256
387
|
}
|
package/src/generation-client.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { GENERATION_EVENTS } from './generation-types'
|
|
2
|
+
import { createNoOpGenerationDevtoolsBridge } from './devtools-noop'
|
|
2
3
|
import { parseSSEResponse } from './sse-parser'
|
|
3
4
|
import type { StreamChunk } from '@tanstack/ai'
|
|
4
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
ConnectConnectionAdapter,
|
|
7
|
+
RunAgentInputContext,
|
|
8
|
+
} from './connection-adapters'
|
|
9
|
+
import type {
|
|
10
|
+
AIDevtoolsClientMetadata,
|
|
11
|
+
AIDevtoolsGenerationProgress,
|
|
12
|
+
GenerationDevtoolsBridge,
|
|
13
|
+
GenerationDevtoolsBridgeOptions,
|
|
14
|
+
} from './devtools'
|
|
5
15
|
import type {
|
|
6
16
|
GenerationClientOptions,
|
|
7
17
|
GenerationClientState,
|
|
@@ -67,13 +77,20 @@ export class GenerationClient<
|
|
|
67
77
|
> {
|
|
68
78
|
private readonly connection: ConnectConnectionAdapter | undefined
|
|
69
79
|
private readonly fetcher: GenerationFetcher<TInput, TResult> | undefined
|
|
80
|
+
private readonly uniqueId: string
|
|
81
|
+
private readonly devtoolsMetadata: AIDevtoolsClientMetadata
|
|
82
|
+
private readonly devtoolsBridge: GenerationDevtoolsBridge<TOutput>
|
|
83
|
+
private readonly threadId: string
|
|
70
84
|
private body: Record<string, any>
|
|
71
85
|
private result: TOutput | null = null
|
|
86
|
+
private input: TInput | null = null
|
|
87
|
+
private progress: AIDevtoolsGenerationProgress | null = null
|
|
72
88
|
private isLoading = false
|
|
73
89
|
private error: Error | undefined = undefined
|
|
74
90
|
private status: GenerationClientState = 'idle'
|
|
75
91
|
private abortController: AbortController | null = null
|
|
76
92
|
private readonly callbacksRef: GenerationCallbacks<TResult, TOutput>
|
|
93
|
+
private devtoolsMounted = false
|
|
77
94
|
|
|
78
95
|
constructor(
|
|
79
96
|
options: GenerationClientOptions<TInput, TResult, TOutput> &
|
|
@@ -85,6 +102,8 @@ export class GenerationClient<
|
|
|
85
102
|
}
|
|
86
103
|
),
|
|
87
104
|
) {
|
|
105
|
+
this.uniqueId = options.id ?? this.generateUniqueId('generation')
|
|
106
|
+
this.threadId = this.uniqueId
|
|
88
107
|
this.connection = options.connection
|
|
89
108
|
this.fetcher = options.fetcher
|
|
90
109
|
this.body = options.body ?? {}
|
|
@@ -99,6 +118,38 @@ export class GenerationClient<
|
|
|
99
118
|
onErrorChange: options.onErrorChange,
|
|
100
119
|
onStatusChange: options.onStatusChange,
|
|
101
120
|
}
|
|
121
|
+
|
|
122
|
+
this.devtoolsMetadata = this.createDevtoolsMetadata(options.devtools)
|
|
123
|
+
this.devtoolsBridge = (
|
|
124
|
+
options.devtoolsBridgeFactory ?? createNoOpGenerationDevtoolsBridge
|
|
125
|
+
)<TOutput>(this.buildDevtoolsBridgeOptions())
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private buildDevtoolsBridgeOptions(): GenerationDevtoolsBridgeOptions<TOutput> {
|
|
129
|
+
return {
|
|
130
|
+
hookId: this.uniqueId,
|
|
131
|
+
clientId: this.uniqueId,
|
|
132
|
+
threadId: this.threadId,
|
|
133
|
+
metadata: this.devtoolsMetadata,
|
|
134
|
+
getCoreState: () => ({
|
|
135
|
+
input: this.input,
|
|
136
|
+
result: this.result,
|
|
137
|
+
progress: this.progress,
|
|
138
|
+
status: this.status,
|
|
139
|
+
isLoading: this.isLoading,
|
|
140
|
+
...(this.error ? { error: this.error.message } : {}),
|
|
141
|
+
}),
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
mountDevtools(): void {
|
|
146
|
+
if (this.devtoolsMounted) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.devtoolsMounted = true
|
|
151
|
+
this.devtoolsBridge.emitRegistered()
|
|
152
|
+
this.devtoolsBridge.emitSnapshot()
|
|
102
153
|
}
|
|
103
154
|
|
|
104
155
|
/**
|
|
@@ -107,8 +158,12 @@ export class GenerationClient<
|
|
|
107
158
|
* while already generating will be a no-op.
|
|
108
159
|
*/
|
|
109
160
|
async generate(input: TInput): Promise<void> {
|
|
161
|
+
this.mountDevtools()
|
|
110
162
|
if (this.isLoading) return
|
|
111
163
|
|
|
164
|
+
this.input = input
|
|
165
|
+
this.progress = null
|
|
166
|
+
const runId = this.devtoolsBridge.beginRun(input)
|
|
112
167
|
this.setIsLoading(true)
|
|
113
168
|
this.setStatus('generating')
|
|
114
169
|
this.setError(undefined)
|
|
@@ -124,26 +179,50 @@ export class GenerationClient<
|
|
|
124
179
|
if (signal.aborted) return
|
|
125
180
|
if (result instanceof Response) {
|
|
126
181
|
// Server function returned SSE Response — parse stream
|
|
127
|
-
await this.processStream(parseSSEResponse(result, signal))
|
|
182
|
+
await this.processStream(parseSSEResponse(result, signal), runId)
|
|
128
183
|
} else {
|
|
184
|
+
this.devtoolsBridge.ensureRunStarted(runId)
|
|
129
185
|
this.setResult(result)
|
|
130
186
|
this.setStatus('success')
|
|
131
187
|
}
|
|
132
188
|
} else if (this.connection) {
|
|
133
189
|
// Streaming adapter path
|
|
134
190
|
const mergedData = { ...this.body, ...input }
|
|
135
|
-
const stream = this.connection.connect(
|
|
136
|
-
|
|
191
|
+
const stream = this.connection.connect(
|
|
192
|
+
[],
|
|
193
|
+
mergedData,
|
|
194
|
+
signal,
|
|
195
|
+
this.createRunContext(runId),
|
|
196
|
+
)
|
|
197
|
+
await this.processStream(stream, runId)
|
|
137
198
|
} else {
|
|
138
199
|
throw new Error(
|
|
139
200
|
'GenerationClient requires either a connection or fetcher option',
|
|
140
201
|
)
|
|
141
202
|
}
|
|
142
|
-
|
|
203
|
+
if (!signal.aborted && this.status === 'success') {
|
|
204
|
+
// Bump progress to 100 on successful completion so devtools
|
|
205
|
+
// snapshots reflect the final state. The bridge mirrors this in
|
|
206
|
+
// the run's recorded progress, but the snapshot reads `progress`
|
|
207
|
+
// from the client's core state.
|
|
208
|
+
this.progress = completeProgressValue(this.progress)
|
|
209
|
+
this.devtoolsBridge.finishRun(
|
|
210
|
+
this.devtoolsBridge.getActiveRunId() ?? runId,
|
|
211
|
+
'run:completed',
|
|
212
|
+
'completed',
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
} catch (err: unknown) {
|
|
143
216
|
if (signal.aborted) return
|
|
144
217
|
const error = err instanceof Error ? err : new Error(String(err))
|
|
145
218
|
this.setError(error)
|
|
146
219
|
this.setStatus('error')
|
|
220
|
+
this.devtoolsBridge.finishRun(
|
|
221
|
+
this.devtoolsBridge.getActiveRunId() ?? runId,
|
|
222
|
+
'run:errored',
|
|
223
|
+
'errored',
|
|
224
|
+
error.message,
|
|
225
|
+
)
|
|
147
226
|
this.callbacksRef.onError?.(error)
|
|
148
227
|
} finally {
|
|
149
228
|
this.abortController = null
|
|
@@ -156,15 +235,28 @@ export class GenerationClient<
|
|
|
156
235
|
*/
|
|
157
236
|
private async processStream(
|
|
158
237
|
source: AsyncIterable<StreamChunk>,
|
|
238
|
+
fallbackRunId: string,
|
|
159
239
|
): Promise<void> {
|
|
240
|
+
let streamRunId: string | undefined
|
|
241
|
+
|
|
160
242
|
for await (const chunk of source) {
|
|
161
243
|
if (this.abortController?.signal.aborted) break
|
|
162
244
|
|
|
163
245
|
this.callbacksRef.onChunk?.(chunk)
|
|
246
|
+
const chunkRunId =
|
|
247
|
+
'runId' in chunk && typeof chunk.runId === 'string'
|
|
248
|
+
? chunk.runId
|
|
249
|
+
: undefined
|
|
164
250
|
|
|
165
251
|
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType has ~22 variants; this consumer only handles the subset relevant to generation lifecycle.
|
|
166
252
|
switch (chunk.type) {
|
|
253
|
+
case 'RUN_STARTED': {
|
|
254
|
+
streamRunId = chunk.runId
|
|
255
|
+
this.devtoolsBridge.ensureRunStarted(chunk.runId)
|
|
256
|
+
break
|
|
257
|
+
}
|
|
167
258
|
case 'CUSTOM': {
|
|
259
|
+
this.devtoolsBridge.ensureRunStarted(streamRunId ?? fallbackRunId)
|
|
168
260
|
if (chunk.name === GENERATION_EVENTS.RESULT) {
|
|
169
261
|
this.setResult(chunk.value as TResult)
|
|
170
262
|
} else if (chunk.name === GENERATION_EVENTS.PROGRESS) {
|
|
@@ -172,15 +264,20 @@ export class GenerationClient<
|
|
|
172
264
|
progress: number
|
|
173
265
|
message?: string
|
|
174
266
|
}
|
|
175
|
-
this.
|
|
267
|
+
this.setProgress(progress, message)
|
|
176
268
|
}
|
|
177
269
|
break
|
|
178
270
|
}
|
|
179
271
|
case 'RUN_FINISHED': {
|
|
272
|
+
streamRunId = chunk.runId
|
|
273
|
+
this.devtoolsBridge.ensureRunStarted(chunk.runId)
|
|
180
274
|
this.setStatus('success')
|
|
181
275
|
break
|
|
182
276
|
}
|
|
183
277
|
case 'RUN_ERROR': {
|
|
278
|
+
this.devtoolsBridge.ensureRunStarted(
|
|
279
|
+
chunkRunId ?? streamRunId ?? fallbackRunId,
|
|
280
|
+
)
|
|
184
281
|
// Prefer spec `message`; fall back to deprecated `error.message`
|
|
185
282
|
const msg =
|
|
186
283
|
(chunk.message as string | undefined) ||
|
|
@@ -198,6 +295,7 @@ export class GenerationClient<
|
|
|
198
295
|
* Abort any in-flight generation request.
|
|
199
296
|
*/
|
|
200
297
|
stop(): void {
|
|
298
|
+
const runId = this.devtoolsBridge.getActiveRunId()
|
|
201
299
|
if (this.abortController) {
|
|
202
300
|
this.abortController.abort()
|
|
203
301
|
this.abortController = null
|
|
@@ -205,6 +303,9 @@ export class GenerationClient<
|
|
|
205
303
|
this.setIsLoading(false)
|
|
206
304
|
if (this.status === 'generating') {
|
|
207
305
|
this.setStatus('idle')
|
|
306
|
+
if (runId) {
|
|
307
|
+
this.devtoolsBridge.finishRun(runId, 'run:cancelled', 'cancelled')
|
|
308
|
+
}
|
|
208
309
|
}
|
|
209
310
|
}
|
|
210
311
|
|
|
@@ -214,8 +315,12 @@ export class GenerationClient<
|
|
|
214
315
|
reset(): void {
|
|
215
316
|
this.stop()
|
|
216
317
|
this.setResult(null)
|
|
318
|
+
this.input = null
|
|
319
|
+
this.progress = null
|
|
320
|
+
this.devtoolsBridge.resetRuns()
|
|
217
321
|
this.setError(undefined)
|
|
218
322
|
this.setStatus('idle')
|
|
323
|
+
this.devtoolsBridge.emitState()
|
|
219
324
|
}
|
|
220
325
|
|
|
221
326
|
/**
|
|
@@ -246,6 +351,12 @@ export class GenerationClient<
|
|
|
246
351
|
}
|
|
247
352
|
}
|
|
248
353
|
|
|
354
|
+
dispose(): void {
|
|
355
|
+
this.stop()
|
|
356
|
+
this.devtoolsBridge.dispose()
|
|
357
|
+
this.devtoolsMounted = false
|
|
358
|
+
}
|
|
359
|
+
|
|
249
360
|
// ===========================
|
|
250
361
|
// Getters
|
|
251
362
|
// ===========================
|
|
@@ -274,40 +385,96 @@ export class GenerationClient<
|
|
|
274
385
|
if (rawResult === null) {
|
|
275
386
|
this.result = null
|
|
276
387
|
this.callbacksRef.onResultChange?.(null)
|
|
388
|
+
this.devtoolsBridge.recordResultChange()
|
|
277
389
|
return
|
|
278
390
|
}
|
|
279
391
|
|
|
280
392
|
if (this.callbacksRef.onResult) {
|
|
281
393
|
const transformed = this.callbacksRef.onResult(rawResult)
|
|
282
394
|
if (transformed === null) {
|
|
283
|
-
// null return → keep previous result unchanged
|
|
395
|
+
// null return → keep previous result unchanged, just re-emit
|
|
396
|
+
this.devtoolsBridge.emitState()
|
|
284
397
|
return
|
|
285
398
|
}
|
|
286
399
|
if (transformed !== undefined) {
|
|
287
400
|
// Non-null, non-undefined → use transformed value
|
|
288
401
|
this.result = transformed
|
|
289
402
|
this.callbacksRef.onResultChange?.(this.result)
|
|
403
|
+
this.devtoolsBridge.recordResultChange()
|
|
290
404
|
return
|
|
291
405
|
}
|
|
292
406
|
}
|
|
293
407
|
|
|
294
|
-
// No onResult callback, or callback returned void → use raw value
|
|
295
|
-
|
|
408
|
+
// No onResult callback, or callback returned void → use raw value as
|
|
409
|
+
// TOutput. When the caller did not supply an onResult transform,
|
|
410
|
+
// `TOutput` defaults to `TResult`, so the runtime cast is sound.
|
|
411
|
+
// eslint-disable-next-line no-restricted-syntax -- TOutput defaults to TResult when no onResult transform is supplied
|
|
412
|
+
this.result = rawResult as unknown as TOutput
|
|
296
413
|
this.callbacksRef.onResultChange?.(this.result)
|
|
414
|
+
this.devtoolsBridge.recordResultChange()
|
|
297
415
|
}
|
|
298
416
|
|
|
299
417
|
private setIsLoading(isLoading: boolean): void {
|
|
300
418
|
this.isLoading = isLoading
|
|
301
419
|
this.callbacksRef.onLoadingChange?.(isLoading)
|
|
420
|
+
this.devtoolsBridge.recordLoadingChange()
|
|
302
421
|
}
|
|
303
422
|
|
|
304
423
|
private setError(error: Error | undefined): void {
|
|
305
424
|
this.error = error
|
|
306
425
|
this.callbacksRef.onErrorChange?.(error)
|
|
426
|
+
this.devtoolsBridge.recordErrorChange(error)
|
|
307
427
|
}
|
|
308
428
|
|
|
309
429
|
private setStatus(status: GenerationClientState): void {
|
|
310
430
|
this.status = status
|
|
311
431
|
this.callbacksRef.onStatusChange?.(status)
|
|
432
|
+
this.devtoolsBridge.recordStatusChange(status)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private setProgress(value: number, message?: string): void {
|
|
436
|
+
this.progress = {
|
|
437
|
+
value,
|
|
438
|
+
...(message ? { message } : {}),
|
|
439
|
+
}
|
|
440
|
+
if (message === undefined) {
|
|
441
|
+
this.callbacksRef.onProgress?.(value)
|
|
442
|
+
} else {
|
|
443
|
+
this.callbacksRef.onProgress?.(value, message)
|
|
444
|
+
}
|
|
445
|
+
this.devtoolsBridge.recordProgressChange()
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private createDevtoolsMetadata(
|
|
449
|
+
metadata?: Partial<AIDevtoolsClientMetadata>,
|
|
450
|
+
): AIDevtoolsClientMetadata {
|
|
451
|
+
return {
|
|
452
|
+
hookName: metadata?.hookName ?? 'useGeneration',
|
|
453
|
+
...(metadata?.framework ? { framework: metadata.framework } : {}),
|
|
454
|
+
...(metadata?.outputKind ? { outputKind: metadata.outputKind } : {}),
|
|
455
|
+
...(metadata?.name ? { name: metadata.name } : {}),
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private generateUniqueId(prefix: string): string {
|
|
460
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private createRunContext(runId: string): RunAgentInputContext {
|
|
464
|
+
return {
|
|
465
|
+
threadId: this.threadId,
|
|
466
|
+
runId,
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function completeProgressValue(
|
|
472
|
+
progress: AIDevtoolsGenerationProgress | null,
|
|
473
|
+
): AIDevtoolsGenerationProgress | null {
|
|
474
|
+
if (!progress) return null
|
|
475
|
+
const message = progress.message
|
|
476
|
+
return {
|
|
477
|
+
value: 100,
|
|
478
|
+
...(message ? { message } : {}),
|
|
312
479
|
}
|
|
313
480
|
}
|
package/src/generation-types.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { StreamChunk } from '@tanstack/ai'
|
|
2
2
|
import type { ConnectConnectionAdapter } from './connection-adapters'
|
|
3
|
+
import type { AIDevtoolsClientMetadata } from './devtools'
|
|
4
|
+
import type {
|
|
5
|
+
GenerationDevtoolsBridgeFactory,
|
|
6
|
+
VideoDevtoolsBridgeFactory,
|
|
7
|
+
} from './devtools-noop'
|
|
3
8
|
|
|
4
9
|
// ===========================
|
|
5
10
|
// Inference Utilities
|
|
@@ -106,6 +111,15 @@ export interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {
|
|
|
106
111
|
/** Additional body parameters to send with connect-based adapter requests */
|
|
107
112
|
body?: Record<string, any>
|
|
108
113
|
|
|
114
|
+
/** Metadata used to register this generation hook with TanStack AI Devtools */
|
|
115
|
+
devtools?: Partial<AIDevtoolsClientMetadata>
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Factory that constructs the devtools bridge. Default is a no-op
|
|
119
|
+
* factory; the real implementation lives in `@tanstack/ai-client/devtools`.
|
|
120
|
+
*/
|
|
121
|
+
devtoolsBridgeFactory?: GenerationDevtoolsBridgeFactory
|
|
122
|
+
|
|
109
123
|
/**
|
|
110
124
|
* Callback when a result is received. Can optionally return a transformed value
|
|
111
125
|
* that replaces the stored result.
|
|
@@ -172,11 +186,16 @@ export interface VideoGenerateResult {
|
|
|
172
186
|
*/
|
|
173
187
|
export interface VideoGenerationClientOptions<
|
|
174
188
|
TOutput = VideoGenerateResult,
|
|
175
|
-
> extends
|
|
176
|
-
VideoGenerateInput,
|
|
177
|
-
|
|
178
|
-
TOutput
|
|
189
|
+
> extends Omit<
|
|
190
|
+
GenerationClientOptions<VideoGenerateInput, VideoGenerateResult, TOutput>,
|
|
191
|
+
'devtoolsBridgeFactory'
|
|
179
192
|
> {
|
|
193
|
+
/**
|
|
194
|
+
* Factory that constructs the video devtools bridge. Default is a no-op
|
|
195
|
+
* factory; the real implementation lives in `@tanstack/ai-client/devtools`.
|
|
196
|
+
*/
|
|
197
|
+
devtoolsBridgeFactory?: VideoDevtoolsBridgeFactory
|
|
198
|
+
|
|
180
199
|
/** Callback when a video job is created */
|
|
181
200
|
onJobCreated?: (jobId: string) => void
|
|
182
201
|
/** Callback on each status update */
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,11 @@ export type {
|
|
|
17
17
|
InferChatMessages,
|
|
18
18
|
ChatClientState,
|
|
19
19
|
ConnectionStatus,
|
|
20
|
-
|
|
20
|
+
ChatFetcher,
|
|
21
|
+
ChatFetcherInput,
|
|
22
|
+
ChatFetcherOptions,
|
|
23
|
+
ChatTransport,
|
|
24
|
+
DistributedOmit,
|
|
21
25
|
MultimodalContent,
|
|
22
26
|
} from './types'
|
|
23
27
|
// Generation client types
|
|
@@ -40,6 +44,15 @@ export type {
|
|
|
40
44
|
} from './generation-types'
|
|
41
45
|
export { GENERATION_EVENTS } from './generation-types'
|
|
42
46
|
export { clientTools, createChatClientOptions } from './types'
|
|
47
|
+
export {
|
|
48
|
+
createAIDevtoolsGenerationPreview,
|
|
49
|
+
type AIDevtoolsClientMetadata,
|
|
50
|
+
type AIDevtoolsDisplayOptions,
|
|
51
|
+
type AIDevtoolsGenerationMediaItem,
|
|
52
|
+
type AIDevtoolsGenerationPreview,
|
|
53
|
+
type AIDevtoolsGenerationProgress,
|
|
54
|
+
type AIDevtoolsGenerationVideoJob,
|
|
55
|
+
} from './devtools'
|
|
43
56
|
export type {
|
|
44
57
|
ExtractToolNames,
|
|
45
58
|
ExtractToolInput,
|
|
@@ -58,6 +71,7 @@ export {
|
|
|
58
71
|
fetchHttpStream,
|
|
59
72
|
stream,
|
|
60
73
|
rpcStream,
|
|
74
|
+
StreamTruncatedError,
|
|
61
75
|
type ConnectConnectionAdapter,
|
|
62
76
|
type ConnectionAdapter,
|
|
63
77
|
type FetchConnectionOptions,
|