@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
|
@@ -1,10 +1,49 @@
|
|
|
1
1
|
import { EventType, uiMessagesToWire } from '@tanstack/ai'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ModelMessage,
|
|
4
|
+
RunErrorEvent,
|
|
5
|
+
RunFinishedEvent,
|
|
6
|
+
StreamChunk,
|
|
7
|
+
UIMessage,
|
|
8
|
+
} from '@tanstack/ai'
|
|
9
|
+
import type { ChatFetcher } from './types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Thrown when an SSE/HTTP stream ends with a non-empty unterminated buffer.
|
|
13
|
+
* Indicates the connection was cut mid-line (server crash, dropped TCP, proxy
|
|
14
|
+
* timeout) so the partial content cannot be safely parsed.
|
|
15
|
+
*/
|
|
16
|
+
export class StreamTruncatedError extends Error {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(
|
|
19
|
+
'Stream ended with unterminated trailing data — connection was likely cut short.',
|
|
20
|
+
)
|
|
21
|
+
this.name = 'StreamTruncatedError'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
3
24
|
|
|
4
25
|
function generateRunId(prefix: string): string {
|
|
5
26
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
6
27
|
}
|
|
7
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Asserts an id is present when synthesizing a terminal event. The chat
|
|
31
|
+
* client always supplies `runContext.threadId` / `runContext.runId`, so an
|
|
32
|
+
* absent id at this layer indicates the adapter was wired up by a caller
|
|
33
|
+
* that bypassed that contract — surface it rather than fabricating one.
|
|
34
|
+
*/
|
|
35
|
+
function requireSyntheticId(
|
|
36
|
+
value: string | undefined,
|
|
37
|
+
field: 'threadId' | 'runId',
|
|
38
|
+
): string {
|
|
39
|
+
if (!value) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Cannot synthesize terminal event: ${field} not supplied via runContext and not observed in the upstream stream.`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
|
|
8
47
|
/**
|
|
9
48
|
* Merge custom headers into request headers
|
|
10
49
|
*/
|
|
@@ -52,15 +91,83 @@ async function* readStreamLines(
|
|
|
52
91
|
}
|
|
53
92
|
}
|
|
54
93
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
94
|
+
// A non-empty trailing buffer means the connection was cut mid-line.
|
|
95
|
+
// Surface this as an error so the chat client transitions to 'error'
|
|
96
|
+
// state instead of silently presenting a partial stream as success.
|
|
97
|
+
// Skip when the consumer aborted — a user-initiated stop() interrupting
|
|
98
|
+
// mid-line is expected, not a truncation bug.
|
|
99
|
+
if (buffer.trim() && !abortSignal?.aborted) {
|
|
100
|
+
throw new StreamTruncatedError()
|
|
58
101
|
}
|
|
59
102
|
} finally {
|
|
60
103
|
reader.releaseLock()
|
|
61
104
|
}
|
|
62
105
|
}
|
|
63
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Yield StreamChunks parsed from an SSE Response body.
|
|
109
|
+
*
|
|
110
|
+
* Accepts either `data: {...}` lines or bare JSON lines. Skips comments
|
|
111
|
+
* starting with `:` (proxies and CDNs inject these as keepalives) and the
|
|
112
|
+
* `event:` / `id:` / `retry:` SSE control fields. A `[DONE]` sentinel is
|
|
113
|
+
* treated as a terminal event: a synthesized RUN_FINISHED is yielded using
|
|
114
|
+
* the most recent upstream `threadId` / `runId`, ensuring the consumer sees
|
|
115
|
+
* a clean terminal event with real correlation ids.
|
|
116
|
+
*
|
|
117
|
+
* A JSON parse failure throws — the consumer surfaces it as an error.
|
|
118
|
+
*/
|
|
119
|
+
async function* responseToSSEChunks(
|
|
120
|
+
response: Response,
|
|
121
|
+
abortSignal?: AbortSignal,
|
|
122
|
+
): AsyncGenerator<StreamChunk> {
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`HTTP error! status: ${response.status} ${response.statusText}`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
const reader = response.body?.getReader()
|
|
129
|
+
if (!reader) {
|
|
130
|
+
throw new Error('Response body is not readable')
|
|
131
|
+
}
|
|
132
|
+
let lastThreadId: string | undefined
|
|
133
|
+
let lastRunId: string | undefined
|
|
134
|
+
let lastModel: string | undefined
|
|
135
|
+
for await (const line of readStreamLines(reader, abortSignal)) {
|
|
136
|
+
if (
|
|
137
|
+
line.startsWith(':') ||
|
|
138
|
+
line.startsWith('event:') ||
|
|
139
|
+
line.startsWith('id:') ||
|
|
140
|
+
line.startsWith('retry:')
|
|
141
|
+
) {
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
const data = line.startsWith('data: ') ? line.slice(6) : line
|
|
145
|
+
if (data === '[DONE]') {
|
|
146
|
+
const synthetic: RunFinishedEvent = {
|
|
147
|
+
type: EventType.RUN_FINISHED,
|
|
148
|
+
threadId: lastThreadId ?? '',
|
|
149
|
+
runId: lastRunId ?? '',
|
|
150
|
+
model: lastModel ?? '',
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
finishReason: 'stop',
|
|
153
|
+
}
|
|
154
|
+
yield synthetic
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
const chunk = JSON.parse(data) as StreamChunk
|
|
158
|
+
if ('threadId' in chunk && typeof chunk.threadId === 'string') {
|
|
159
|
+
lastThreadId = chunk.threadId
|
|
160
|
+
}
|
|
161
|
+
if ('runId' in chunk && typeof chunk.runId === 'string') {
|
|
162
|
+
lastRunId = chunk.runId
|
|
163
|
+
}
|
|
164
|
+
if ('model' in chunk && typeof chunk.model === 'string') {
|
|
165
|
+
lastModel = chunk.model
|
|
166
|
+
}
|
|
167
|
+
yield chunk
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
64
171
|
/**
|
|
65
172
|
* Per-send context provided by the chat client to the connection adapter.
|
|
66
173
|
* The adapter combines this with serialized messages to build a full
|
|
@@ -196,6 +303,8 @@ export function normalizeConnectionAdapter(
|
|
|
196
303
|
},
|
|
197
304
|
async send(messages, data, abortSignal, runContext) {
|
|
198
305
|
let hasTerminalEvent = false
|
|
306
|
+
let upstreamThreadId: string | undefined
|
|
307
|
+
let upstreamRunId: string | undefined
|
|
199
308
|
try {
|
|
200
309
|
const stream = connection.connect(
|
|
201
310
|
messages,
|
|
@@ -204,6 +313,12 @@ export function normalizeConnectionAdapter(
|
|
|
204
313
|
runContext,
|
|
205
314
|
)
|
|
206
315
|
for await (const chunk of stream) {
|
|
316
|
+
if ('threadId' in chunk && typeof chunk.threadId === 'string') {
|
|
317
|
+
upstreamThreadId = chunk.threadId
|
|
318
|
+
}
|
|
319
|
+
if ('runId' in chunk && typeof chunk.runId === 'string') {
|
|
320
|
+
upstreamRunId = chunk.runId
|
|
321
|
+
}
|
|
207
322
|
if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {
|
|
208
323
|
hasTerminalEvent = true
|
|
209
324
|
}
|
|
@@ -214,31 +329,40 @@ export function normalizeConnectionAdapter(
|
|
|
214
329
|
// synthesize RUN_FINISHED so request-scoped consumers can complete.
|
|
215
330
|
// Reuse the caller's threadId/runId so client-side activeRunIds tracking matches.
|
|
216
331
|
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
217
|
-
|
|
332
|
+
const synthetic: RunFinishedEvent = {
|
|
218
333
|
type: EventType.RUN_FINISHED,
|
|
219
|
-
threadId:
|
|
220
|
-
|
|
334
|
+
threadId: requireSyntheticId(
|
|
335
|
+
upstreamThreadId ?? runContext?.threadId,
|
|
336
|
+
'threadId',
|
|
337
|
+
),
|
|
338
|
+
runId: requireSyntheticId(
|
|
339
|
+
upstreamRunId ?? runContext?.runId,
|
|
340
|
+
'runId',
|
|
341
|
+
),
|
|
221
342
|
model: 'connect-wrapper',
|
|
222
343
|
timestamp: Date.now(),
|
|
223
344
|
finishReason: 'stop',
|
|
224
|
-
}
|
|
345
|
+
}
|
|
346
|
+
push(synthetic)
|
|
225
347
|
}
|
|
226
348
|
} catch (err) {
|
|
227
349
|
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
228
|
-
|
|
350
|
+
const message =
|
|
351
|
+
err instanceof Error ? err.message : 'Unknown error in connect()'
|
|
352
|
+
const synthetic: RunErrorEvent = {
|
|
229
353
|
type: EventType.RUN_ERROR,
|
|
230
|
-
threadId:
|
|
231
|
-
|
|
354
|
+
threadId: requireSyntheticId(
|
|
355
|
+
upstreamThreadId ?? runContext?.threadId,
|
|
356
|
+
'threadId',
|
|
357
|
+
),
|
|
358
|
+
runId: requireSyntheticId(
|
|
359
|
+
upstreamRunId ?? runContext?.runId,
|
|
360
|
+
'runId',
|
|
361
|
+
),
|
|
232
362
|
timestamp: Date.now(),
|
|
233
|
-
message
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
message:
|
|
237
|
-
err instanceof Error
|
|
238
|
-
? err.message
|
|
239
|
-
: 'Unknown error in connect()',
|
|
240
|
-
},
|
|
241
|
-
})
|
|
363
|
+
message,
|
|
364
|
+
}
|
|
365
|
+
push(synthetic)
|
|
242
366
|
}
|
|
243
367
|
throw err
|
|
244
368
|
}
|
|
@@ -358,37 +482,7 @@ export function fetchServerSentEvents(
|
|
|
358
482
|
...(signal ? { signal } : {}),
|
|
359
483
|
})
|
|
360
484
|
|
|
361
|
-
|
|
362
|
-
throw new Error(
|
|
363
|
-
`HTTP error! status: ${response.status} ${response.statusText}`,
|
|
364
|
-
)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Parse Server-Sent Events format
|
|
368
|
-
const reader = response.body?.getReader()
|
|
369
|
-
if (!reader) {
|
|
370
|
-
throw new Error('Response body is not readable')
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
for await (const line of readStreamLines(reader, abortSignal)) {
|
|
374
|
-
// Handle Server-Sent Events format
|
|
375
|
-
const data = line.startsWith('data: ') ? line.slice(6) : line
|
|
376
|
-
|
|
377
|
-
if (data === '[DONE]') {
|
|
378
|
-
console.warn(
|
|
379
|
-
'[@tanstack/ai-client] Received [DONE] sentinel. This is deprecated — upgrade your @tanstack/ai server package. RUN_FINISHED is the stream terminator.',
|
|
380
|
-
)
|
|
381
|
-
continue
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
try {
|
|
385
|
-
const parsed: StreamChunk = JSON.parse(data)
|
|
386
|
-
yield parsed
|
|
387
|
-
} catch (parseError) {
|
|
388
|
-
// Skip non-JSON lines or malformed chunks
|
|
389
|
-
console.warn('Failed to parse SSE chunk:', data)
|
|
390
|
-
}
|
|
391
|
-
}
|
|
485
|
+
yield* responseToSSEChunks(response, abortSignal)
|
|
392
486
|
},
|
|
393
487
|
}
|
|
394
488
|
}
|
|
@@ -507,12 +601,7 @@ export function fetchHttpStream(
|
|
|
507
601
|
}
|
|
508
602
|
|
|
509
603
|
for await (const line of readStreamLines(reader, abortSignal)) {
|
|
510
|
-
|
|
511
|
-
const parsed: StreamChunk = JSON.parse(line)
|
|
512
|
-
yield parsed
|
|
513
|
-
} catch (parseError) {
|
|
514
|
-
console.warn('Failed to parse HTTP stream chunk:', line)
|
|
515
|
-
}
|
|
604
|
+
yield JSON.parse(line) as StreamChunk
|
|
516
605
|
}
|
|
517
606
|
},
|
|
518
607
|
}
|
|
@@ -536,17 +625,92 @@ export function stream(
|
|
|
536
625
|
streamFactory: (
|
|
537
626
|
messages: Array<UIMessage> | Array<ModelMessage>,
|
|
538
627
|
data?: Record<string, any>,
|
|
628
|
+
abortSignal?: AbortSignal,
|
|
539
629
|
) => AsyncIterable<StreamChunk>,
|
|
540
630
|
): ConnectConnectionAdapter {
|
|
541
631
|
return {
|
|
542
|
-
async *connect(messages, data,
|
|
632
|
+
async *connect(messages, data, abortSignal) {
|
|
543
633
|
// Pass messages as-is (UIMessages with parts preserved)
|
|
544
634
|
// Server-side chat() handles conversion to ModelMessages
|
|
545
|
-
yield* streamFactory(messages, data)
|
|
635
|
+
yield* streamFactory(messages, data, abortSignal)
|
|
636
|
+
},
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Wrap a `ChatFetcher` as a `ConnectConnectionAdapter` so the chat client can
|
|
642
|
+
* consume it through the same `subscribe`/`send` plumbing used for SSE /
|
|
643
|
+
* HTTP-stream / RPC connections. May return either a `Response` (parsed as
|
|
644
|
+
* SSE) or an `AsyncIterable<StreamChunk>` (yielded directly).
|
|
645
|
+
*
|
|
646
|
+
* @internal
|
|
647
|
+
*/
|
|
648
|
+
export function fetcherToConnectionAdapter(
|
|
649
|
+
fetcher: ChatFetcher,
|
|
650
|
+
): ConnectConnectionAdapter {
|
|
651
|
+
return {
|
|
652
|
+
async *connect(messages, data, abortSignal, runContext) {
|
|
653
|
+
if (!abortSignal) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
'fetcherToConnectionAdapter requires an AbortSignal — the chat client always supplies one.',
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
if (!runContext) {
|
|
659
|
+
throw new Error(
|
|
660
|
+
'fetcherToConnectionAdapter requires a RunAgentInputContext — the chat client always supplies one.',
|
|
661
|
+
)
|
|
662
|
+
}
|
|
663
|
+
const uiMessages = messages as Array<UIMessage>
|
|
664
|
+
const result = await fetcher(
|
|
665
|
+
{
|
|
666
|
+
messages: uiMessages,
|
|
667
|
+
data,
|
|
668
|
+
threadId: runContext.threadId,
|
|
669
|
+
runId: runContext.runId,
|
|
670
|
+
},
|
|
671
|
+
{ signal: abortSignal },
|
|
672
|
+
)
|
|
673
|
+
if (result instanceof Response) {
|
|
674
|
+
yield* responseToSSEChunks(result, abortSignal)
|
|
675
|
+
} else {
|
|
676
|
+
yield* abortableIterable(result, abortSignal)
|
|
677
|
+
}
|
|
546
678
|
},
|
|
547
679
|
}
|
|
548
680
|
}
|
|
549
681
|
|
|
682
|
+
/**
|
|
683
|
+
* Wrap an AsyncIterable so iteration aborts when `signal` fires. Without
|
|
684
|
+
* this, a fetcher that returns a generator ignoring its signal would leave
|
|
685
|
+
* the for-await loop hanging until the iterable naturally ends.
|
|
686
|
+
*/
|
|
687
|
+
async function* abortableIterable<T>(
|
|
688
|
+
iterable: AsyncIterable<T>,
|
|
689
|
+
signal: AbortSignal,
|
|
690
|
+
): AsyncGenerator<T> {
|
|
691
|
+
if (signal.aborted) return
|
|
692
|
+
const iterator = iterable[Symbol.asyncIterator]()
|
|
693
|
+
const abortPromise = new Promise<{ done: true; value: undefined }>(
|
|
694
|
+
(resolve) => {
|
|
695
|
+
signal.addEventListener(
|
|
696
|
+
'abort',
|
|
697
|
+
() => resolve({ done: true, value: undefined }),
|
|
698
|
+
{ once: true },
|
|
699
|
+
)
|
|
700
|
+
},
|
|
701
|
+
)
|
|
702
|
+
try {
|
|
703
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
704
|
+
while (true) {
|
|
705
|
+
const result = await Promise.race([iterator.next(), abortPromise])
|
|
706
|
+
if (result.done) return
|
|
707
|
+
yield result.value
|
|
708
|
+
}
|
|
709
|
+
} finally {
|
|
710
|
+
await iterator.return?.()
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
550
714
|
/**
|
|
551
715
|
* Create an RPC stream connection adapter (for RPC-based streaming like Cap'n Web RPC)
|
|
552
716
|
*
|
|
@@ -567,13 +731,14 @@ export function rpcStream(
|
|
|
567
731
|
rpcCall: (
|
|
568
732
|
messages: Array<UIMessage> | Array<ModelMessage>,
|
|
569
733
|
data?: Record<string, any>,
|
|
734
|
+
abortSignal?: AbortSignal,
|
|
570
735
|
) => AsyncIterable<StreamChunk>,
|
|
571
736
|
): ConnectConnectionAdapter {
|
|
572
737
|
return {
|
|
573
|
-
async *connect(messages, data,
|
|
738
|
+
async *connect(messages, data, abortSignal) {
|
|
574
739
|
// Pass messages as-is (UIMessages with parts preserved)
|
|
575
740
|
// Server-side chat() handles conversion to ModelMessages
|
|
576
|
-
yield* rpcCall(messages, data)
|
|
741
|
+
yield* rpcCall(messages, data, abortSignal)
|
|
577
742
|
},
|
|
578
743
|
}
|
|
579
744
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// No-op devtools bridge implementations + factories. The chat / generation /
|
|
2
|
+
// video clients import the real bridge classes as types only and accept a
|
|
3
|
+
// factory in options; when no factory is supplied they fall back to the
|
|
4
|
+
// no-op factories here, which never touch `aiEventClient` or any of the
|
|
5
|
+
// heavy preview/fixture machinery in `./devtools`. This keeps `./devtools`
|
|
6
|
+
// outside the main-entry import graph — consumers opt into functional
|
|
7
|
+
// devtools via `@tanstack/ai-client/devtools` (see `package.json#exports`).
|
|
8
|
+
import { ChatClientEventEmitter } from './events'
|
|
9
|
+
import type {
|
|
10
|
+
AIDevtoolsToolFixture,
|
|
11
|
+
ChatDevtoolsBridge,
|
|
12
|
+
ChatDevtoolsBridgeOptions,
|
|
13
|
+
GenerationDevtoolsBridge,
|
|
14
|
+
GenerationDevtoolsBridgeOptions,
|
|
15
|
+
VideoDevtoolsBridge,
|
|
16
|
+
VideoDevtoolsBridgeOptions,
|
|
17
|
+
} from './devtools'
|
|
18
|
+
import type { StreamChunk } from '@tanstack/ai'
|
|
19
|
+
import type {
|
|
20
|
+
ChatClientEventContext,
|
|
21
|
+
ChatClientRunEventContext,
|
|
22
|
+
} from './events'
|
|
23
|
+
|
|
24
|
+
export type ChatDevtoolsBridgeFactory = (
|
|
25
|
+
options: ChatDevtoolsBridgeOptions,
|
|
26
|
+
) => ChatDevtoolsBridge
|
|
27
|
+
|
|
28
|
+
export type GenerationDevtoolsBridgeFactory = <TOutput>(
|
|
29
|
+
options: GenerationDevtoolsBridgeOptions<TOutput>,
|
|
30
|
+
) => GenerationDevtoolsBridge<TOutput>
|
|
31
|
+
|
|
32
|
+
export type VideoDevtoolsBridgeFactory = <TOutput>(
|
|
33
|
+
options: VideoDevtoolsBridgeOptions<TOutput>,
|
|
34
|
+
) => VideoDevtoolsBridge<TOutput>
|
|
35
|
+
|
|
36
|
+
// ===========================================================================
|
|
37
|
+
// No-op event emitter — extends the abstract base so it satisfies the type
|
|
38
|
+
// without dragging in any of the event-bus runtime cost.
|
|
39
|
+
// ===========================================================================
|
|
40
|
+
|
|
41
|
+
class NoOpChatClientEventEmitter extends ChatClientEventEmitter {
|
|
42
|
+
protected emitEvent(): void {
|
|
43
|
+
// intentionally empty
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ===========================================================================
|
|
48
|
+
// No-op bridges. Methods exist to satisfy the structural shape of the real
|
|
49
|
+
// classes; every emit/record call short-circuits.
|
|
50
|
+
// ===========================================================================
|
|
51
|
+
|
|
52
|
+
export class NoOpChatDevtoolsBridge {
|
|
53
|
+
readonly events: ChatClientEventEmitter
|
|
54
|
+
|
|
55
|
+
constructor(options: ChatDevtoolsBridgeOptions) {
|
|
56
|
+
this.events = new NoOpChatClientEventEmitter(options.clientId)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// base bridge surface
|
|
60
|
+
emitRegistered(): void {}
|
|
61
|
+
emitUpdated(): void {}
|
|
62
|
+
emitSnapshot(): void {}
|
|
63
|
+
emitToolsRegistered(): void {}
|
|
64
|
+
emitRunLifecycle(
|
|
65
|
+
_eventType: unknown,
|
|
66
|
+
_runId: string,
|
|
67
|
+
_status: unknown,
|
|
68
|
+
_options?: { error?: string },
|
|
69
|
+
): void {}
|
|
70
|
+
deactivate(): void {}
|
|
71
|
+
supersede(): void {}
|
|
72
|
+
dispose(): void {}
|
|
73
|
+
|
|
74
|
+
// chat-specific surface
|
|
75
|
+
setCurrentStreamId(_streamId: string | null): void {}
|
|
76
|
+
getCurrentStreamId(): string | null {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
getLastStreamId(): string | null {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
resolveStreamId(): string {
|
|
83
|
+
return ''
|
|
84
|
+
}
|
|
85
|
+
observeChunk(_chunk: StreamChunk): void {}
|
|
86
|
+
beginRun(_runId: string, _threadId: string): void {}
|
|
87
|
+
getCurrentRunEventContext(): ChatClientRunEventContext | undefined {
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
getCurrentOrLastRunEventContext(): ChatClientRunEventContext | undefined {
|
|
91
|
+
return undefined
|
|
92
|
+
}
|
|
93
|
+
findToolCallContext(toolCallId: string): ChatClientEventContext {
|
|
94
|
+
return { toolCallId }
|
|
95
|
+
}
|
|
96
|
+
async applyFixture(_fixture: AIDevtoolsToolFixture): Promise<void> {
|
|
97
|
+
// intentionally empty
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class NoOpGenerationDevtoolsBridge<TOutput> {
|
|
102
|
+
constructor(_options: GenerationDevtoolsBridgeOptions<TOutput>) {}
|
|
103
|
+
|
|
104
|
+
// base bridge surface
|
|
105
|
+
emitRegistered(): void {}
|
|
106
|
+
emitUpdated(): void {}
|
|
107
|
+
emitSnapshot(): void {}
|
|
108
|
+
emitToolsRegistered(): void {}
|
|
109
|
+
emitRunLifecycle(): void {}
|
|
110
|
+
deactivate(): void {}
|
|
111
|
+
supersede(): void {}
|
|
112
|
+
dispose(): void {}
|
|
113
|
+
|
|
114
|
+
// generation-specific surface
|
|
115
|
+
beginRun(_input: unknown): string {
|
|
116
|
+
// Real factories supply a stable id; the no-op still returns a
|
|
117
|
+
// unique value because the generation client passes this run id to
|
|
118
|
+
// the adapter's RunAgentInputContext.
|
|
119
|
+
return `noop-run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
120
|
+
}
|
|
121
|
+
ensureRunStarted(_runId: string): void {}
|
|
122
|
+
finishRun(
|
|
123
|
+
_runId: string,
|
|
124
|
+
_eventType: 'run:completed' | 'run:errored' | 'run:cancelled',
|
|
125
|
+
_status: 'completed' | 'errored' | 'cancelled',
|
|
126
|
+
_error?: string,
|
|
127
|
+
): void {}
|
|
128
|
+
getActiveRunId(): string | null {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
resetRuns(): void {}
|
|
132
|
+
recordResultChange(): void {}
|
|
133
|
+
recordLoadingChange(): void {}
|
|
134
|
+
recordErrorChange(_error: Error | undefined): void {}
|
|
135
|
+
recordStatusChange(): void {}
|
|
136
|
+
recordProgressChange(): void {}
|
|
137
|
+
emitState(): void {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class NoOpVideoDevtoolsBridge<
|
|
141
|
+
TOutput,
|
|
142
|
+
> extends NoOpGenerationDevtoolsBridge<TOutput> {
|
|
143
|
+
constructor(options: VideoDevtoolsBridgeOptions<TOutput>) {
|
|
144
|
+
super(options)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
recordJobIdChange(): void {}
|
|
148
|
+
recordVideoStatusChange(): void {}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Compile-time parity checks. If a public method is added to the real
|
|
152
|
+
// bridge class without a matching stub on the no-op, the corresponding
|
|
153
|
+
// `Exclude<...>` will resolve to a non-`never` union and the `as never`
|
|
154
|
+
// assignment below will fail to typecheck — surfacing the drift at build
|
|
155
|
+
// time instead of as a runtime TypeError later.
|
|
156
|
+
type _ChatBridgeMissing = Exclude<
|
|
157
|
+
keyof ChatDevtoolsBridge,
|
|
158
|
+
keyof NoOpChatDevtoolsBridge
|
|
159
|
+
>
|
|
160
|
+
type _GenerationBridgeMissing = Exclude<
|
|
161
|
+
keyof GenerationDevtoolsBridge<unknown>,
|
|
162
|
+
keyof NoOpGenerationDevtoolsBridge<unknown>
|
|
163
|
+
>
|
|
164
|
+
type _VideoBridgeMissing = Exclude<
|
|
165
|
+
keyof VideoDevtoolsBridge<unknown>,
|
|
166
|
+
keyof NoOpVideoDevtoolsBridge<unknown>
|
|
167
|
+
>
|
|
168
|
+
const _chatBridgeParity: _ChatBridgeMissing = undefined as never
|
|
169
|
+
const _generationBridgeParity: _GenerationBridgeMissing = undefined as never
|
|
170
|
+
const _videoBridgeParity: _VideoBridgeMissing = undefined as never
|
|
171
|
+
void _chatBridgeParity
|
|
172
|
+
void _generationBridgeParity
|
|
173
|
+
void _videoBridgeParity
|
|
174
|
+
|
|
175
|
+
// ===========================================================================
|
|
176
|
+
// Factories — these are what the clients call when no real factory was
|
|
177
|
+
// supplied in options.
|
|
178
|
+
// ===========================================================================
|
|
179
|
+
|
|
180
|
+
// Casts use `unknown` because the no-op classes don't `extend` the real bridge
|
|
181
|
+
// (that would pull the real implementation into the main-entry import graph).
|
|
182
|
+
// Structural parity is enforced by the `_*BridgeMissing` checks above.
|
|
183
|
+
|
|
184
|
+
export const createNoOpChatDevtoolsBridge: ChatDevtoolsBridgeFactory = (
|
|
185
|
+
options,
|
|
186
|
+
) =>
|
|
187
|
+
// eslint-disable-next-line no-restricted-syntax -- see comment above
|
|
188
|
+
new NoOpChatDevtoolsBridge(options) as unknown as ChatDevtoolsBridge
|
|
189
|
+
|
|
190
|
+
export const createNoOpGenerationDevtoolsBridge: GenerationDevtoolsBridgeFactory =
|
|
191
|
+
<TOutput>(options: GenerationDevtoolsBridgeOptions<TOutput>) =>
|
|
192
|
+
// eslint-disable-next-line no-restricted-syntax -- see comment above
|
|
193
|
+
new NoOpGenerationDevtoolsBridge<TOutput>(
|
|
194
|
+
options,
|
|
195
|
+
) as unknown as GenerationDevtoolsBridge<TOutput>
|
|
196
|
+
|
|
197
|
+
export const createNoOpVideoDevtoolsBridge: VideoDevtoolsBridgeFactory = <
|
|
198
|
+
TOutput,
|
|
199
|
+
>(
|
|
200
|
+
options: VideoDevtoolsBridgeOptions<TOutput>,
|
|
201
|
+
) =>
|
|
202
|
+
// eslint-disable-next-line no-restricted-syntax -- see comment above
|
|
203
|
+
new NoOpVideoDevtoolsBridge<TOutput>(
|
|
204
|
+
options,
|
|
205
|
+
) as unknown as VideoDevtoolsBridge<TOutput>
|