@tanstack/ai-client 0.12.0 → 0.14.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.
Files changed (59) hide show
  1. package/dist/esm/chat-client.d.ts +16 -2
  2. package/dist/esm/chat-client.js +197 -31
  3. package/dist/esm/chat-client.js.map +1 -1
  4. package/dist/esm/connection-adapters.d.ts +21 -1
  5. package/dist/esm/connection-adapters.js +262 -68
  6. package/dist/esm/connection-adapters.js.map +1 -1
  7. package/dist/esm/devtools-noop.d.ts +60 -0
  8. package/dist/esm/devtools-noop.js +135 -0
  9. package/dist/esm/devtools-noop.js.map +1 -0
  10. package/dist/esm/devtools.d.ts +276 -0
  11. package/dist/esm/devtools.js +1178 -0
  12. package/dist/esm/devtools.js.map +1 -0
  13. package/dist/esm/events.d.ts +51 -8
  14. package/dist/esm/events.js +65 -20
  15. package/dist/esm/events.js.map +1 -1
  16. package/dist/esm/generation-client.d.ts +14 -0
  17. package/dist/esm/generation-client.js +135 -5
  18. package/dist/esm/generation-client.js.map +1 -1
  19. package/dist/esm/generation-types.d.ts +16 -2
  20. package/dist/esm/generation-types.js.map +1 -1
  21. package/dist/esm/index.d.ts +6 -4
  22. package/dist/esm/index.js +9 -3
  23. package/dist/esm/index.js.map +1 -1
  24. package/dist/esm/realtime-client.d.ts +1 -1
  25. package/dist/esm/realtime-client.js +1 -1
  26. package/dist/esm/realtime-client.js.map +1 -1
  27. package/dist/esm/realtime-types.d.ts +1 -1
  28. package/dist/esm/response-stream.d.ts +7 -0
  29. package/dist/esm/response-stream.js +32 -0
  30. package/dist/esm/response-stream.js.map +1 -0
  31. package/dist/esm/sse-parser.d.ts +1 -1
  32. package/dist/esm/sse-parser.js +5 -6
  33. package/dist/esm/sse-parser.js.map +1 -1
  34. package/dist/esm/sse-utils.d.ts +1 -0
  35. package/dist/esm/sse-utils.js +11 -0
  36. package/dist/esm/sse-utils.js.map +1 -0
  37. package/dist/esm/tool-types.d.ts +1 -1
  38. package/dist/esm/types.d.ts +23 -5
  39. package/dist/esm/types.js.map +1 -1
  40. package/dist/esm/video-generation-client.d.ts +15 -0
  41. package/dist/esm/video-generation-client.js +147 -8
  42. package/dist/esm/video-generation-client.js.map +1 -1
  43. package/package.json +7 -3
  44. package/src/chat-client.ts +250 -33
  45. package/src/connection-adapters.ts +360 -68
  46. package/src/devtools-noop.ts +205 -0
  47. package/src/devtools.ts +1866 -0
  48. package/src/events.ts +143 -12
  49. package/src/generation-client.ts +177 -10
  50. package/src/generation-types.ts +24 -5
  51. package/src/index.ts +16 -3
  52. package/src/realtime-client.ts +2 -2
  53. package/src/realtime-types.ts +1 -1
  54. package/src/response-stream.ts +42 -0
  55. package/src/sse-parser.ts +9 -7
  56. package/src/sse-utils.ts +8 -0
  57. package/src/tool-types.ts +1 -1
  58. package/src/types.ts +31 -4
  59. package/src/video-generation-client.ts +187 -12
@@ -1,11 +1,16 @@
1
- import { EventType, uiMessagesToWire } from '@tanstack/ai'
1
+ import { EventType, uiMessagesToWire } from '@tanstack/ai/client'
2
+ import {
3
+ createResponseStreamTextDecoder,
4
+ getResponseStreamReader,
5
+ } from './response-stream'
6
+ import { parseSseDataLine } from './sse-utils'
2
7
  import type {
3
8
  ModelMessage,
4
9
  RunErrorEvent,
5
10
  RunFinishedEvent,
6
11
  StreamChunk,
7
12
  UIMessage,
8
- } from '@tanstack/ai'
13
+ } from '@tanstack/ai/client'
9
14
  import type { ChatFetcher } from './types'
10
15
 
11
16
  /**
@@ -71,7 +76,7 @@ async function* readStreamLines(
71
76
  abortSignal?: AbortSignal,
72
77
  ): AsyncGenerator<string> {
73
78
  try {
74
- const decoder = new TextDecoder()
79
+ const decoder = createResponseStreamTextDecoder()
75
80
  let buffer = ''
76
81
 
77
82
  while (!abortSignal?.aborted) {
@@ -125,10 +130,7 @@ async function* responseToSSEChunks(
125
130
  `HTTP error! status: ${response.status} ${response.statusText}`,
126
131
  )
127
132
  }
128
- const reader = response.body?.getReader()
129
- if (!reader) {
130
- throw new Error('Response body is not readable')
131
- }
133
+ const reader = getResponseStreamReader(response)
132
134
  let lastThreadId: string | undefined
133
135
  let lastRunId: string | undefined
134
136
  let lastModel: string | undefined
@@ -141,7 +143,7 @@ async function* responseToSSEChunks(
141
143
  ) {
142
144
  continue
143
145
  }
144
- const data = line.startsWith('data: ') ? line.slice(6) : line
146
+ const data = parseSseDataLine(line)
145
147
  if (data === '[DONE]') {
146
148
  const synthetic: RunFinishedEvent = {
147
149
  type: EventType.RUN_FINISHED,
@@ -381,6 +383,53 @@ export interface FetchConnectionOptions {
381
383
  fetchClient?: typeof globalThis.fetch
382
384
  }
383
385
 
386
+ /**
387
+ * Options for XHR-based connection adapters.
388
+ */
389
+ export interface XhrConnectionOptions {
390
+ headers?: Record<string, string> | Headers
391
+ withCredentials?: boolean
392
+ signal?: AbortSignal
393
+ body?: Record<string, any>
394
+ xhrFactory?: () => XMLHttpRequest
395
+ }
396
+
397
+ type ResolvedConnectionOptions = Pick<
398
+ FetchConnectionOptions,
399
+ 'body' | 'headers'
400
+ >
401
+
402
+ function buildRunAgentInputBody(
403
+ messages: Array<UIMessage> | Array<ModelMessage>,
404
+ data: Record<string, any> | undefined,
405
+ runContext: RunAgentInputContext | undefined,
406
+ options: ResolvedConnectionOptions,
407
+ ): Record<string, unknown> {
408
+ // Precedence (later spreads win): static adapter `body` is the base,
409
+ // overridden by `runContext.forwardedProps`, overridden by per-message `data`.
410
+ const wireMessages = uiMessagesToWire(messages as Array<UIMessage>)
411
+ const forwardedProps = {
412
+ ...options.body,
413
+ ...(runContext?.forwardedProps ?? {}),
414
+ ...data,
415
+ }
416
+
417
+ return {
418
+ threadId: runContext?.threadId ?? generateRunId('thread'),
419
+ runId: runContext?.runId ?? generateRunId('run'),
420
+ ...(runContext?.parentRunId !== undefined && {
421
+ parentRunId: runContext.parentRunId,
422
+ }),
423
+ state: {},
424
+ messages: wireMessages,
425
+ tools: runContext?.clientTools ?? [],
426
+ context: [],
427
+ forwardedProps,
428
+ // Backward-compat mirror of `forwardedProps` under the legacy field name.
429
+ data: { ...forwardedProps },
430
+ }
431
+ }
432
+
384
433
  /**
385
434
  * Create a Server-Sent Events connection adapter
386
435
  *
@@ -440,34 +489,12 @@ export function fetchServerSentEvents(
440
489
  // forwardedProps options), overridden by per-message `data` passed
441
490
  // to `connection.send`. Runtime values win over static config —
442
491
  // this matches the documented "forwardedProps wins" semantic.
443
- const wireMessages = uiMessagesToWire(messages as Array<UIMessage>)
444
- const forwardedProps = {
445
- ...resolvedOptions.body,
446
- ...(runContext?.forwardedProps ?? {}),
447
- ...data,
448
- }
449
- const requestBody = {
450
- threadId: runContext?.threadId ?? generateRunId('thread'),
451
- runId: runContext?.runId ?? generateRunId('run'),
452
- ...(runContext?.parentRunId !== undefined && {
453
- parentRunId: runContext.parentRunId,
454
- }),
455
- state: {},
456
- messages: wireMessages,
457
- tools: runContext?.clientTools ?? [],
458
- context: [],
459
- forwardedProps,
460
- // Backward-compat mirror of `forwardedProps` under the legacy
461
- // field name `data`. Server endpoints that have not migrated
462
- // off the pre-AG-UI shape (`{ messages, data }`) keep working.
463
- // AG-UI strict consumers strip this via `RunAgentInputSchema`
464
- // (see `chatParamsFromRequestBody`). Will be removed when the
465
- // legacy `body` client option is dropped.
466
- // Shallow-cloned so that downstream mutation of `data` (e.g.
467
- // by a logging interceptor or fetch wrapper) cannot corrupt
468
- // `forwardedProps` and vice versa.
469
- data: { ...forwardedProps },
470
- }
492
+ const requestBody = buildRunAgentInputBody(
493
+ messages,
494
+ data,
495
+ runContext,
496
+ resolvedOptions,
497
+ )
471
498
 
472
499
  const fetchClient = resolvedOptions.fetchClient ?? fetch
473
500
  // `RequestInit.signal` is typed `AbortSignal | null` (no `undefined`
@@ -546,34 +573,12 @@ export function fetchHttpStream(
546
573
  // forwardedProps options), overridden by per-message `data` passed
547
574
  // to `connection.send`. Runtime values win over static config —
548
575
  // this matches the documented "forwardedProps wins" semantic.
549
- const wireMessages = uiMessagesToWire(messages as Array<UIMessage>)
550
- const forwardedProps = {
551
- ...resolvedOptions.body,
552
- ...(runContext?.forwardedProps ?? {}),
553
- ...data,
554
- }
555
- const requestBody = {
556
- threadId: runContext?.threadId ?? generateRunId('thread'),
557
- runId: runContext?.runId ?? generateRunId('run'),
558
- ...(runContext?.parentRunId !== undefined && {
559
- parentRunId: runContext.parentRunId,
560
- }),
561
- state: {},
562
- messages: wireMessages,
563
- tools: runContext?.clientTools ?? [],
564
- context: [],
565
- forwardedProps,
566
- // Backward-compat mirror of `forwardedProps` under the legacy
567
- // field name `data`. Server endpoints that have not migrated
568
- // off the pre-AG-UI shape (`{ messages, data }`) keep working.
569
- // AG-UI strict consumers strip this via `RunAgentInputSchema`
570
- // (see `chatParamsFromRequestBody`). Will be removed when the
571
- // legacy `body` client option is dropped.
572
- // Shallow-cloned so that downstream mutation of `data` (e.g.
573
- // by a logging interceptor or fetch wrapper) cannot corrupt
574
- // `forwardedProps` and vice versa.
575
- data: { ...forwardedProps },
576
- }
576
+ const requestBody = buildRunAgentInputBody(
577
+ messages,
578
+ data,
579
+ runContext,
580
+ resolvedOptions,
581
+ )
577
582
 
578
583
  const fetchClient = resolvedOptions.fetchClient ?? fetch
579
584
  // `RequestInit.signal` is typed `AbortSignal | null` (no `undefined`
@@ -595,10 +600,7 @@ export function fetchHttpStream(
595
600
  }
596
601
 
597
602
  // Parse raw HTTP stream (newline-delimited JSON)
598
- const reader = response.body?.getReader()
599
- if (!reader) {
600
- throw new Error('Response body is not readable')
601
- }
603
+ const reader = getResponseStreamReader(response)
602
604
 
603
605
  for await (const line of readStreamLines(reader, abortSignal)) {
604
606
  yield JSON.parse(line) as StreamChunk
@@ -607,6 +609,296 @@ export function fetchHttpStream(
607
609
  }
608
610
  }
609
611
 
612
+ type XhrConnectionOptionsResolver =
613
+ | XhrConnectionOptions
614
+ | (() => XhrConnectionOptions | Promise<XhrConnectionOptions>)
615
+
616
+ function createDefaultXMLHttpRequest(): XMLHttpRequest {
617
+ if (typeof globalThis.XMLHttpRequest !== 'function') {
618
+ throw new Error('XMLHttpRequest is not available in this runtime')
619
+ }
620
+
621
+ return new globalThis.XMLHttpRequest()
622
+ }
623
+
624
+ function cleanupXhr(
625
+ xhr: XMLHttpRequest,
626
+ abortSignal: AbortSignal | undefined,
627
+ onAbort: (() => void) | undefined,
628
+ ): void {
629
+ xhr.onprogress = null
630
+ xhr.onload = null
631
+ xhr.onerror = null
632
+ xhr.onabort = null
633
+ xhr.onloadend = null
634
+
635
+ if (abortSignal && onAbort) {
636
+ abortSignal.removeEventListener('abort', onAbort)
637
+ }
638
+ }
639
+
640
+ function readXhrLines(
641
+ xhr: XMLHttpRequest,
642
+ abortSignal?: AbortSignal,
643
+ ): AsyncGenerator<string> {
644
+ let offset = 0
645
+ let buffer = ''
646
+ const lines: Array<string> = []
647
+ const waiters: Array<() => void> = []
648
+ let done = false
649
+ let aborted = false
650
+ let error: unknown
651
+ let onAbort: (() => void) | undefined
652
+
653
+ const wake = () => {
654
+ const waiter = waiters.shift()
655
+ waiter?.()
656
+ }
657
+
658
+ const enqueueDelta = () => {
659
+ if (xhr.status !== 0 && (xhr.status < 200 || xhr.status >= 300)) {
660
+ error = new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`)
661
+ done = true
662
+ return
663
+ }
664
+
665
+ const responseText = xhr.responseText
666
+ if (responseText.length <= offset) {
667
+ return
668
+ }
669
+
670
+ buffer += responseText.slice(offset)
671
+ offset = responseText.length
672
+ const splitLines = buffer.split('\n')
673
+ buffer = splitLines.pop() ?? ''
674
+
675
+ for (const line of splitLines) {
676
+ const normalized = line.endsWith('\r') ? line.slice(0, -1) : line
677
+ if (normalized.trim()) {
678
+ lines.push(normalized)
679
+ }
680
+ }
681
+ }
682
+
683
+ const finish = () => {
684
+ enqueueDelta()
685
+ if (xhr.status < 200 || xhr.status >= 300) {
686
+ error = new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`)
687
+ } else if (buffer.trim() && !aborted) {
688
+ error = new StreamTruncatedError()
689
+ }
690
+ done = true
691
+ wake()
692
+ }
693
+
694
+ xhr.onprogress = () => {
695
+ enqueueDelta()
696
+ wake()
697
+ }
698
+ xhr.onload = finish
699
+ xhr.onerror = () => {
700
+ error = new Error('XHR request failed')
701
+ done = true
702
+ wake()
703
+ }
704
+ xhr.onabort = () => {
705
+ aborted = true
706
+ done = true
707
+ wake()
708
+ }
709
+ xhr.onloadend = () => {
710
+ if (!done) {
711
+ finish()
712
+ }
713
+ }
714
+
715
+ if (abortSignal) {
716
+ onAbort = () => {
717
+ aborted = true
718
+ xhr.abort()
719
+ }
720
+ if (abortSignal.aborted) {
721
+ onAbort()
722
+ } else {
723
+ abortSignal.addEventListener('abort', onAbort, { once: true })
724
+ }
725
+ }
726
+
727
+ return (async function* () {
728
+ try {
729
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
730
+ while (true) {
731
+ const line = lines.shift()
732
+ if (line !== undefined) {
733
+ yield line
734
+ continue
735
+ }
736
+
737
+ if (error) {
738
+ throw error
739
+ }
740
+
741
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
742
+ if (done || abortSignal?.aborted) {
743
+ return
744
+ }
745
+
746
+ await new Promise<void>((resolve) => {
747
+ waiters.push(resolve)
748
+ })
749
+ }
750
+ } finally {
751
+ cleanupXhr(xhr, abortSignal, onAbort)
752
+ }
753
+ })()
754
+ }
755
+
756
+ interface ConfiguredXhrRequest {
757
+ xhr: XMLHttpRequest
758
+ body: string
759
+ }
760
+
761
+ function createConfiguredXhrRequest(
762
+ url: string,
763
+ options: XhrConnectionOptions,
764
+ messages: Array<UIMessage> | Array<ModelMessage>,
765
+ data: Record<string, any> | undefined,
766
+ runContext: RunAgentInputContext | undefined,
767
+ ): ConfiguredXhrRequest {
768
+ const xhr = options.xhrFactory?.() ?? createDefaultXMLHttpRequest()
769
+ xhr.open('POST', url)
770
+ if (options.withCredentials !== undefined) {
771
+ xhr.withCredentials = options.withCredentials
772
+ }
773
+
774
+ const requestHeaders: Record<string, string> = {
775
+ 'Content-Type': 'application/json',
776
+ ...mergeHeaders(options.headers),
777
+ }
778
+
779
+ for (const [name, value] of Object.entries(requestHeaders)) {
780
+ xhr.setRequestHeader(name, value)
781
+ }
782
+
783
+ const requestBody = buildRunAgentInputBody(
784
+ messages,
785
+ data,
786
+ runContext,
787
+ options,
788
+ )
789
+
790
+ return { xhr, body: JSON.stringify(requestBody) }
791
+ }
792
+
793
+ async function resolveXhrConnectionOptions(
794
+ options: XhrConnectionOptionsResolver,
795
+ ): Promise<XhrConnectionOptions> {
796
+ return typeof options === 'function' ? await options() : options
797
+ }
798
+
799
+ /**
800
+ * Create an XMLHttpRequest-backed Server-Sent Events connection adapter.
801
+ */
802
+ export function xhrServerSentEvents(
803
+ url: string | (() => string),
804
+ options: XhrConnectionOptionsResolver = {},
805
+ ): ConnectConnectionAdapter {
806
+ return {
807
+ async *connect(messages, data, abortSignal, runContext) {
808
+ const resolvedUrl = typeof url === 'function' ? url() : url
809
+ const resolvedOptions = await resolveXhrConnectionOptions(options)
810
+ const signal = abortSignal || resolvedOptions.signal
811
+ const request = createConfiguredXhrRequest(
812
+ resolvedUrl,
813
+ resolvedOptions,
814
+ messages,
815
+ data,
816
+ runContext,
817
+ )
818
+ const lines = readXhrLines(request.xhr, signal)
819
+ if (signal?.aborted) {
820
+ await lines.next()
821
+ return
822
+ }
823
+ request.xhr.send(request.body)
824
+ let lastThreadId: string | undefined
825
+ let lastRunId: string | undefined
826
+ let lastModel: string | undefined
827
+
828
+ for await (const line of lines) {
829
+ if (
830
+ line.startsWith(':') ||
831
+ line.startsWith('event:') ||
832
+ line.startsWith('id:') ||
833
+ line.startsWith('retry:')
834
+ ) {
835
+ continue
836
+ }
837
+
838
+ const chunkData = parseSseDataLine(line)
839
+ if (chunkData === '[DONE]') {
840
+ const synthetic: RunFinishedEvent = {
841
+ type: EventType.RUN_FINISHED,
842
+ threadId: lastThreadId ?? runContext?.threadId ?? '',
843
+ runId: lastRunId ?? runContext?.runId ?? '',
844
+ model: lastModel ?? '',
845
+ timestamp: Date.now(),
846
+ finishReason: 'stop',
847
+ }
848
+ request.xhr.abort()
849
+ yield synthetic
850
+ return
851
+ }
852
+
853
+ const chunk = JSON.parse(chunkData) as StreamChunk
854
+ if ('threadId' in chunk && typeof chunk.threadId === 'string') {
855
+ lastThreadId = chunk.threadId
856
+ }
857
+ if ('runId' in chunk && typeof chunk.runId === 'string') {
858
+ lastRunId = chunk.runId
859
+ }
860
+ if ('model' in chunk && typeof chunk.model === 'string') {
861
+ lastModel = chunk.model
862
+ }
863
+ yield chunk
864
+ }
865
+ },
866
+ }
867
+ }
868
+
869
+ /**
870
+ * Create an XMLHttpRequest-backed newline-delimited JSON stream adapter.
871
+ */
872
+ export function xhrHttpStream(
873
+ url: string | (() => string),
874
+ options: XhrConnectionOptionsResolver = {},
875
+ ): ConnectConnectionAdapter {
876
+ return {
877
+ async *connect(messages, data, abortSignal, runContext) {
878
+ const resolvedUrl = typeof url === 'function' ? url() : url
879
+ const resolvedOptions = await resolveXhrConnectionOptions(options)
880
+ const signal = abortSignal || resolvedOptions.signal
881
+ const request = createConfiguredXhrRequest(
882
+ resolvedUrl,
883
+ resolvedOptions,
884
+ messages,
885
+ data,
886
+ runContext,
887
+ )
888
+ const lines = readXhrLines(request.xhr, signal)
889
+ if (signal?.aborted) {
890
+ await lines.next()
891
+ return
892
+ }
893
+ request.xhr.send(request.body)
894
+
895
+ for await (const line of lines) {
896
+ yield JSON.parse(line) as StreamChunk
897
+ }
898
+ },
899
+ }
900
+ }
901
+
610
902
  /**
611
903
  * Create a direct stream connection adapter (for server functions or direct streams)
612
904
  *
@@ -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/client'
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>