@tanstack/ai-client 0.12.0 → 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.
@@ -15,8 +15,15 @@ export declare class ChatClient {
15
15
  private status;
16
16
  private connectionStatus;
17
17
  private abortController;
18
- private readonly events;
19
18
  private readonly clientToolsRef;
19
+ private readonly devtoolsBridge;
20
+ /**
21
+ * Alias for `this.events`. The bridge installs an
22
+ * emitter that auto-attaches run/thread context and auto-emits a
23
+ * snapshot after every event, so chat-client only ever calls
24
+ * `this.events.X(...)` exactly like it did before devtools landed.
25
+ */
26
+ private readonly events;
20
27
  private currentStreamId;
21
28
  private currentMessageId;
22
29
  private readonly postStreamActions;
@@ -30,8 +37,10 @@ export declare class ChatClient {
30
37
  private draining;
31
38
  private sessionGenerating;
32
39
  private readonly activeRunIds;
40
+ private devtoolsMounted;
33
41
  private readonly callbacksRef;
34
42
  constructor(options: ChatClientOptions);
43
+ mountDevtools(): void;
35
44
  private generateUniqueId;
36
45
  private setIsLoading;
37
46
  private setStatus;
@@ -40,6 +49,9 @@ export declare class ChatClient {
40
49
  private setSessionGenerating;
41
50
  private resetSessionGenerating;
42
51
  private setError;
52
+ private buildDevtoolsBridgeOptions;
53
+ private getDevtoolsSnapshot;
54
+ private findMessageIdForToolCall;
43
55
  private abortSubscriptionLoop;
44
56
  private resolveProcessing;
45
57
  private cancelInFlightStream;
@@ -149,6 +161,7 @@ export declare class ChatClient {
149
161
  state?: 'output-available' | 'output-error';
150
162
  errorText?: string;
151
163
  }): Promise<void>;
164
+ private addToolResultInternal;
152
165
  /**
153
166
  * Respond to a tool approval request
154
167
  */
@@ -230,4 +243,5 @@ export declare class ChatClient {
230
243
  toolCallId?: string;
231
244
  }) => void;
232
245
  }): void;
246
+ dispose(): void;
233
247
  }
@@ -1,5 +1,5 @@
1
1
  import { StreamProcessor, normalizeToUIMessage, generateMessageId, convertSchemaToJsonSchema } from "@tanstack/ai";
2
- import { DefaultChatClientEventEmitter } from "./events.js";
2
+ import { createNoOpChatDevtoolsBridge } from "./devtools-noop.js";
3
3
  import { normalizeConnectionAdapter, fetcherToConnectionAdapter } from "./connection-adapters.js";
4
4
  function resolveTransport(transport) {
5
5
  const { connection, fetcher } = transport;
@@ -30,8 +30,15 @@ class ChatClient {
30
30
  status = "ready";
31
31
  connectionStatus = "disconnected";
32
32
  abortController = null;
33
- events;
34
33
  clientToolsRef;
34
+ devtoolsBridge;
35
+ /**
36
+ * Alias for `this.events`. The bridge installs an
37
+ * emitter that auto-attaches run/thread context and auto-emits a
38
+ * snapshot after every event, so chat-client only ever calls
39
+ * `this.events.X(...)` exactly like it did before devtools landed.
40
+ */
41
+ events;
35
42
  currentStreamId = null;
36
43
  currentMessageId = null;
37
44
  postStreamActions = [];
@@ -49,6 +56,7 @@ class ChatClient {
49
56
  draining = false;
50
57
  sessionGenerating = false;
51
58
  activeRunIds = /* @__PURE__ */ new Set();
59
+ devtoolsMounted = false;
52
60
  callbacksRef;
53
61
  constructor(options) {
54
62
  this.uniqueId = options.id || this.generateUniqueId("chat");
@@ -56,13 +64,14 @@ class ChatClient {
56
64
  this.bodyOption = options.body || {};
57
65
  this.forwardedPropsOption = options.forwardedProps || {};
58
66
  this.connection = normalizeConnectionAdapter(resolveTransport(options));
59
- this.events = new DefaultChatClientEventEmitter(this.uniqueId);
60
67
  this.clientToolsRef = { current: /* @__PURE__ */ new Map() };
61
68
  if (options.tools) {
62
69
  for (const tool of options.tools) {
63
70
  this.clientToolsRef.current.set(tool.name, tool);
64
71
  }
65
72
  }
73
+ this.devtoolsBridge = (options.devtoolsBridgeFactory ?? createNoOpChatDevtoolsBridge)(this.buildDevtoolsBridgeOptions(options.devtools));
74
+ this.events = this.devtoolsBridge.events;
66
75
  this.callbacksRef = {
67
76
  current: {
68
77
  onResponse: options.onResponse || (() => {
@@ -134,10 +143,30 @@ class ChatClient {
134
143
  this.events.thinkingUpdated(
135
144
  this.currentStreamId,
136
145
  messageId,
137
- content
146
+ content,
147
+ void 0
138
148
  );
139
149
  }
140
150
  },
151
+ onStructuredOutputChange: (args) => {
152
+ const streamId = this.devtoolsBridge.resolveStreamId();
153
+ const eventName = args.phase === "start" ? "structured-output:started" : args.phase === "complete" ? "structured-output:completed" : args.phase === "error" ? "structured-output:errored" : "structured-output:updated";
154
+ this.currentMessageId = args.messageId;
155
+ this.events.structuredOutputChanged(
156
+ eventName,
157
+ streamId,
158
+ args.messageId,
159
+ {
160
+ status: args.status,
161
+ raw: args.raw,
162
+ ...args.partial !== void 0 ? { partial: args.partial } : {},
163
+ ...args.data !== void 0 ? { data: args.data } : {},
164
+ ...args.reasoning !== void 0 ? { reasoning: args.reasoning } : {},
165
+ ...args.errorMessage !== void 0 ? { errorMessage: args.errorMessage } : {},
166
+ ...args.delta !== void 0 ? { delta: args.delta } : {}
167
+ }
168
+ );
169
+ },
141
170
  onToolCallStateChange: (messageId, toolCallId, state, args) => {
142
171
  const messages = this.processor.getMessages();
143
172
  const message = messages.find((m) => m.id === messageId);
@@ -160,23 +189,30 @@ class ChatClient {
160
189
  const clientTool = this.clientToolsRef.current.get(args.toolName);
161
190
  const executeFunc = clientTool?.execute;
162
191
  if (executeFunc) {
192
+ const runEventContext = this.devtoolsBridge.getCurrentRunEventContext();
163
193
  const executionPromise = (async () => {
164
194
  try {
165
195
  const output = await executeFunc(args.input);
166
- await this.addToolResult({
167
- toolCallId: args.toolCallId,
168
- tool: args.toolName,
169
- output,
170
- state: "output-available"
171
- });
196
+ await this.addToolResultInternal(
197
+ {
198
+ toolCallId: args.toolCallId,
199
+ tool: args.toolName,
200
+ output,
201
+ state: "output-available"
202
+ },
203
+ runEventContext
204
+ );
172
205
  } catch (error) {
173
- await this.addToolResult({
174
- toolCallId: args.toolCallId,
175
- tool: args.toolName,
176
- output: null,
177
- state: "output-error",
178
- errorText: error.message
179
- });
206
+ await this.addToolResultInternal(
207
+ {
208
+ toolCallId: args.toolCallId,
209
+ tool: args.toolName,
210
+ output: null,
211
+ state: "output-error",
212
+ errorText: error.message
213
+ },
214
+ runEventContext
215
+ );
180
216
  } finally {
181
217
  this.pendingToolExecutions.delete(args.toolCallId);
182
218
  }
@@ -185,23 +221,29 @@ class ChatClient {
185
221
  }
186
222
  },
187
223
  onApprovalRequest: (args) => {
188
- if (this.currentStreamId) {
189
- this.events.approvalRequested(
190
- this.currentStreamId,
191
- this.currentMessageId || "",
192
- args.toolCallId,
193
- args.toolName,
194
- args.input,
195
- args.approvalId
196
- );
197
- }
224
+ const streamId = this.devtoolsBridge.resolveStreamId();
225
+ const messageIdForApproval = this.findMessageIdForToolCall(args.toolCallId) ?? this.currentMessageId ?? "";
226
+ this.events.approvalRequested(
227
+ streamId,
228
+ messageIdForApproval,
229
+ args.toolCallId,
230
+ args.toolName,
231
+ args.input,
232
+ args.approvalId
233
+ );
198
234
  },
199
235
  onCustomEvent: (eventType, data, context) => {
200
236
  this.callbacksRef.current.onCustomEvent(eventType, data, context);
201
237
  }
202
238
  }
203
239
  });
204
- this.events.clientCreated(this.processor.getMessages().length);
240
+ }
241
+ mountDevtools() {
242
+ if (this.devtoolsMounted) {
243
+ return;
244
+ }
245
+ this.devtoolsMounted = true;
246
+ this.devtoolsBridge.mountWithTools(this.processor.getMessages().length);
205
247
  }
206
248
  generateUniqueId(prefix) {
207
249
  return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
@@ -214,19 +256,23 @@ class ChatClient {
214
256
  setStatus(status) {
215
257
  this.status = status;
216
258
  this.callbacksRef.current.onStatusChange(status);
259
+ this.devtoolsBridge.emitSnapshot();
217
260
  }
218
261
  setIsSubscribed(isSubscribed) {
219
262
  this.isSubscribed = isSubscribed;
220
263
  this.callbacksRef.current.onSubscriptionChange(isSubscribed);
264
+ this.devtoolsBridge.emitSnapshot();
221
265
  }
222
266
  setConnectionStatus(status) {
223
267
  this.connectionStatus = status;
224
268
  this.callbacksRef.current.onConnectionStatusChange(status);
269
+ this.devtoolsBridge.emitSnapshot();
225
270
  }
226
271
  setSessionGenerating(isGenerating) {
227
272
  if (this.sessionGenerating === isGenerating) return;
228
273
  this.sessionGenerating = isGenerating;
229
274
  this.callbacksRef.current.onSessionGeneratingChange(isGenerating);
275
+ this.devtoolsBridge.emitSnapshot();
230
276
  }
231
277
  resetSessionGenerating() {
232
278
  this.activeRunIds.clear();
@@ -237,6 +283,51 @@ class ChatClient {
237
283
  this.callbacksRef.current.onErrorChange(error);
238
284
  this.events.errorChanged(error?.message || null);
239
285
  }
286
+ buildDevtoolsBridgeOptions(devtools) {
287
+ return {
288
+ hookId: this.uniqueId,
289
+ clientId: this.uniqueId,
290
+ threadId: this.threadId,
291
+ metadata: {
292
+ hookName: devtools?.hookName ?? "useChat",
293
+ outputKind: devtools?.outputKind ?? "chat",
294
+ ...devtools?.framework ? { framework: devtools.framework } : {},
295
+ ...devtools?.name ? { name: devtools.name } : {}
296
+ },
297
+ getSnapshot: () => this.getDevtoolsSnapshot(),
298
+ getTools: () => this.clientToolsRef.current.values(),
299
+ getMessages: () => this.processor.getMessages(),
300
+ setMessages: (messages) => {
301
+ this.processor.setMessages(messages);
302
+ },
303
+ addToolResult: (toolCallId, output, errorText) => {
304
+ this.processor.addToolResult(toolCallId, output, errorText);
305
+ },
306
+ generateId: (prefix) => this.generateUniqueId(prefix)
307
+ };
308
+ }
309
+ getDevtoolsSnapshot() {
310
+ return {
311
+ messages: this.processor.getMessages(),
312
+ status: this.status,
313
+ isLoading: this.isLoading,
314
+ isSubscribed: this.isSubscribed,
315
+ connectionStatus: this.connectionStatus,
316
+ sessionGenerating: this.sessionGenerating,
317
+ activeRunIds: Array.from(this.activeRunIds),
318
+ ...this.error ? { error: this.error.message } : {}
319
+ };
320
+ }
321
+ findMessageIdForToolCall(toolCallId) {
322
+ const messages = this.processor.getMessages();
323
+ for (const message of messages) {
324
+ const match = message.parts.find(
325
+ (part) => part.type === "tool-call" && part.id === toolCallId
326
+ );
327
+ if (match) return message.id;
328
+ }
329
+ return void 0;
330
+ }
240
331
  abortSubscriptionLoop() {
241
332
  this.subscriptionAbortController?.abort();
242
333
  this.subscriptionAbortController = null;
@@ -306,11 +397,12 @@ class ChatClient {
306
397
  this.setConnectionStatus("connected");
307
398
  }
308
399
  this.callbacksRef.current.onChunk(chunk);
309
- this.processor.processChunk(chunk);
310
400
  if (chunk.type === "RUN_STARTED") {
311
401
  this.activeRunIds.add(chunk.runId);
312
402
  this.setSessionGenerating(true);
313
403
  }
404
+ this.devtoolsBridge.observeChunk(chunk);
405
+ this.processor.processChunk(chunk);
314
406
  if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") {
315
407
  const runId = "runId" in chunk && typeof chunk.runId === "string" ? chunk.runId : void 0;
316
408
  if (runId) {
@@ -386,6 +478,7 @@ class ChatClient {
386
478
  * ```
387
479
  */
388
480
  async sendMessage(content, body) {
481
+ this.mountDevtools();
389
482
  const emptyMessage = typeof content === "string" && !content.trim();
390
483
  if (emptyMessage || this.isLoading) {
391
484
  return;
@@ -413,6 +506,7 @@ class ChatClient {
413
506
  * Append a message and stream the response
414
507
  */
415
508
  async append(message) {
509
+ this.mountDevtools();
416
510
  const normalizedMessage = normalizeToUIMessage(message, generateMessageId);
417
511
  if (normalizedMessage.role === "system") {
418
512
  return;
@@ -421,6 +515,7 @@ class ChatClient {
421
515
  this.events.messageAppended(uiMessage);
422
516
  const messages = this.processor.getMessages();
423
517
  this.processor.setMessages([...messages, uiMessage]);
518
+ this.devtoolsBridge.emitSnapshot();
424
519
  if (this.isLoading) {
425
520
  this.queuePostStreamAction(async () => {
426
521
  await this.streamResponse();
@@ -446,6 +541,8 @@ class ChatClient {
446
541
  const signal = this.abortController.signal;
447
542
  this.pendingToolExecutions.clear();
448
543
  let streamCompletedSuccessfully = false;
544
+ let activeDevtoolsRunId = null;
545
+ let runTerminalEventEmitted = false;
449
546
  try {
450
547
  const messages = this.processor.getMessages();
451
548
  await this.callbacksRef.current.onResponse();
@@ -459,6 +556,7 @@ class ChatClient {
459
556
  };
460
557
  this.pendingMessageBody = void 0;
461
558
  this.currentStreamId = this.generateUniqueId("stream");
559
+ this.devtoolsBridge.setCurrentStreamId(this.currentStreamId);
462
560
  this.currentMessageId = null;
463
561
  this.processor.prepareAssistantMessage();
464
562
  this.ensureSubscription();
@@ -475,12 +573,34 @@ class ChatClient {
475
573
  ),
476
574
  forwardedProps: { ...mergedBody }
477
575
  };
576
+ this.devtoolsBridge.beginRun(runContext.runId, this.threadId);
577
+ activeDevtoolsRunId = runContext.runId;
578
+ this.devtoolsBridge.emitRunLifecycle(
579
+ "run:created",
580
+ runContext.runId,
581
+ "created"
582
+ );
583
+ this.devtoolsBridge.emitRunLifecycle(
584
+ "run:started",
585
+ runContext.runId,
586
+ "started"
587
+ );
588
+ this.devtoolsBridge.emitSnapshot();
478
589
  await this.connection.send(messages, mergedBody, signal, runContext);
479
590
  await processingComplete;
480
591
  if (generation !== this.streamGeneration) {
481
592
  return false;
482
593
  }
483
594
  if (this.status === "error") {
595
+ if (activeDevtoolsRunId) {
596
+ this.devtoolsBridge.emitRunLifecycle(
597
+ "run:errored",
598
+ activeDevtoolsRunId,
599
+ "errored",
600
+ this.error ? { error: this.error.message } : {}
601
+ );
602
+ runTerminalEventEmitted = true;
603
+ }
484
604
  return false;
485
605
  }
486
606
  if (this.pendingToolExecutions.size > 0) {
@@ -491,19 +611,52 @@ class ChatClient {
491
611
  } catch (err) {
492
612
  if (err instanceof Error) {
493
613
  if (err.name === "AbortError") {
614
+ if (activeDevtoolsRunId) {
615
+ this.devtoolsBridge.emitRunLifecycle(
616
+ "run:cancelled",
617
+ activeDevtoolsRunId,
618
+ "cancelled"
619
+ );
620
+ runTerminalEventEmitted = true;
621
+ }
494
622
  return false;
495
623
  }
496
624
  if (generation === this.streamGeneration) {
497
625
  this.reportStreamError(err);
626
+ if (activeDevtoolsRunId) {
627
+ this.devtoolsBridge.emitRunLifecycle(
628
+ "run:errored",
629
+ activeDevtoolsRunId,
630
+ "errored",
631
+ { error: err.message }
632
+ );
633
+ runTerminalEventEmitted = true;
634
+ }
498
635
  }
499
636
  }
500
637
  } finally {
501
638
  if (generation === this.streamGeneration) {
502
639
  this.currentStreamId = null;
640
+ this.devtoolsBridge.setCurrentStreamId(null);
503
641
  this.currentMessageId = null;
504
642
  this.abortController = null;
505
643
  this.setIsLoading(false);
506
644
  this.pendingMessageBody = void 0;
645
+ if (activeDevtoolsRunId && !runTerminalEventEmitted) {
646
+ if (streamCompletedSuccessfully) {
647
+ this.devtoolsBridge.emitRunLifecycle(
648
+ "run:completed",
649
+ activeDevtoolsRunId,
650
+ "completed"
651
+ );
652
+ } else if (signal.aborted) {
653
+ this.devtoolsBridge.emitRunLifecycle(
654
+ "run:cancelled",
655
+ activeDevtoolsRunId,
656
+ "cancelled"
657
+ );
658
+ }
659
+ }
507
660
  await this.drainPostStreamActions();
508
661
  if (streamCompletedSuccessfully) {
509
662
  const messages = this.processor.getMessages();
@@ -565,6 +718,7 @@ class ChatClient {
565
718
  }
566
719
  this.events.reloaded(lastUserMessageIndex);
567
720
  this.processor.removeMessagesAfter(lastUserMessageIndex);
721
+ this.devtoolsBridge.emitSnapshot();
568
722
  await this.streamResponse();
569
723
  }
570
724
  /**
@@ -586,11 +740,15 @@ class ChatClient {
586
740
  * Add the result of a client-side tool execution
587
741
  */
588
742
  async addToolResult(result) {
743
+ await this.addToolResultInternal(result);
744
+ }
745
+ async addToolResultInternal(result, context) {
589
746
  this.events.toolResultAdded(
590
747
  result.toolCallId,
591
748
  result.tool,
592
749
  result.output,
593
- result.state || "output-available"
750
+ result.state || "output-available",
751
+ context
594
752
  );
595
753
  this.processor.addToolResult(
596
754
  result.toolCallId,
@@ -626,6 +784,7 @@ class ChatClient {
626
784
  );
627
785
  }
628
786
  this.processor.addToolApprovalResponse(response.id, response.approved);
787
+ this.devtoolsBridge.emitSnapshot();
629
788
  if (this.isLoading) {
630
789
  this.queuePostStreamAction(() => this.checkForContinuation());
631
790
  return;
@@ -743,6 +902,7 @@ class ChatClient {
743
902
  */
744
903
  setMessagesManually(messages) {
745
904
  this.processor.setMessages(messages);
905
+ this.devtoolsBridge.emitSnapshot();
746
906
  }
747
907
  /**
748
908
  * Update options refs (for use in React hooks to avoid recreating client)
@@ -782,6 +942,7 @@ class ChatClient {
782
942
  for (const tool of options.tools) {
783
943
  this.clientToolsRef.current.set(tool.name, tool);
784
944
  }
945
+ this.devtoolsBridge.notifyToolsChanged();
785
946
  }
786
947
  if (options.onResponse !== void 0) {
787
948
  this.callbacksRef.current.onResponse = options.onResponse;
@@ -808,6 +969,11 @@ class ChatClient {
808
969
  this.callbacksRef.current.onCustomEvent = options.onCustomEvent;
809
970
  }
810
971
  }
972
+ dispose() {
973
+ this.unsubscribe();
974
+ this.devtoolsBridge.dispose();
975
+ this.devtoolsMounted = false;
976
+ }
811
977
  }
812
978
  export {
813
979
  ChatClient