@tanstack/ai 0.22.0 → 0.23.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.
@@ -1,4 +1,4 @@
1
- import { JSONSchema, ModelMessage, StreamChunk, Tool, ToolCall } from '../../../types.js';
1
+ import { JSONSchema, ModelMessage, StreamChunk, Tool, ToolCall, UsageTotals } from '../../../types.js';
2
2
  import { SystemPrompt } from '../../../system-prompts.js';
3
3
  /**
4
4
  * Phase of the chat middleware lifecycle.
@@ -20,6 +20,8 @@ export interface ChatMiddlewareContext {
20
20
  requestId: string;
21
21
  /** Unique identifier for this stream */
22
22
  streamId: string;
23
+ /** AG-UI run identifier for correlating client and server events */
24
+ runId: string;
23
25
  /**
24
26
  * AG-UI thread identifier — a stable per-conversation ID used to
25
27
  * correlate client and server devtools events. Resolves to the
@@ -201,11 +203,12 @@ export interface ToolPhaseCompleteInfo {
201
203
  /**
202
204
  * Token usage statistics passed to the onUsage hook.
203
205
  * Extracted from the RUN_FINISHED chunk when usage data is present.
206
+ *
207
+ * Includes optional provider-reported `cost`/`costDetails` (see {@link UsageTotals}).
208
+ * Kept as an interface extending `UsageTotals` to preserve declaration merging for
209
+ * this publicly exported type.
204
210
  */
205
- export interface UsageInfo {
206
- promptTokens: number;
207
- completionTokens: number;
208
- totalTokens: number;
211
+ export interface UsageInfo extends UsageTotals {
209
212
  }
210
213
  /**
211
214
  * Information passed to onFinish.
@@ -217,12 +220,8 @@ export interface FinishInfo {
217
220
  duration: number;
218
221
  /** Final accumulated text content */
219
222
  content: string;
220
- /** Final usage totals, if available */
221
- usage?: {
222
- promptTokens: number;
223
- completionTokens: number;
224
- totalTokens: number;
225
- } | undefined;
223
+ /** Final usage totals, if available (optionally including provider-reported cost) */
224
+ usage?: UsageTotals | undefined;
226
225
  }
227
226
  /**
228
227
  * Information passed to onAbort.
@@ -25,6 +25,17 @@ export interface StreamProcessorEvents {
25
25
  onTextUpdate?: (messageId: string, content: string) => void;
26
26
  onToolCallStateChange?: (messageId: string, toolCallId: string, state: ToolCallState, args: string) => void;
27
27
  onThinkingUpdate?: (messageId: string, stepId: string, content: string) => void;
28
+ onStructuredOutputChange?: (args: {
29
+ phase: 'start' | 'update' | 'complete' | 'error';
30
+ messageId: string;
31
+ status: 'streaming' | 'complete' | 'error';
32
+ raw: string;
33
+ partial?: unknown;
34
+ data?: unknown;
35
+ reasoning?: string;
36
+ errorMessage?: string;
37
+ delta?: string;
38
+ }) => void;
28
39
  }
29
40
  /**
30
41
  * Options for StreamProcessor
@@ -69,6 +80,7 @@ export declare class StreamProcessor {
69
80
  private pendingManualMessageId;
70
81
  private pendingThinkingStepId;
71
82
  private readonly structuredMessageIds;
83
+ private readonly structuredOutputUpdateBatches;
72
84
  private readonly activeRuns;
73
85
  private finishReason;
74
86
  private hasError;
@@ -359,6 +371,9 @@ export declare class StreamProcessor {
359
371
  * @see docs/chat-architecture.md#uimessage-part-ordering-invariants — Replace vs. push logic
360
372
  */
361
373
  private emitTextUpdateForMessage;
374
+ private queueStructuredOutputUpdate;
375
+ private flushStructuredOutputUpdate;
376
+ private emitStructuredOutputChange;
362
377
  /**
363
378
  * Emit messages change event
364
379
  */
@@ -2,6 +2,7 @@ import { generateMessageId, uiMessageToModelMessages } from "../messages.js";
2
2
  import { defaultJSONParser } from "./json-parser.js";
3
3
  import { updateToolCallWithOutput, updateToolResultPart, updateToolCallApprovalResponse, appendStructuredOutputDelta, updateToolCallPart, errorStructuredOutputPart, updateThinkingPart, completeStructuredOutputPart, updateToolCallApproval, updateTextPart } from "./message-updaters.js";
4
4
  import { ImmediateStrategy } from "./strategies.js";
5
+ const STRUCTURED_OUTPUT_UPDATE_BATCH_SIZE = 12;
5
6
  class StreamProcessor {
6
7
  chunkStrategy;
7
8
  events;
@@ -16,6 +17,7 @@ class StreamProcessor {
16
17
  pendingManualMessageId = null;
17
18
  pendingThinkingStepId = null;
18
19
  structuredMessageIds = /* @__PURE__ */ new Set();
20
+ structuredOutputUpdateBatches = /* @__PURE__ */ new Map();
19
21
  // Run tracking (for concurrent run safety)
20
22
  activeRuns = /* @__PURE__ */ new Set();
21
23
  // Shared stream state
@@ -205,6 +207,9 @@ class StreamProcessor {
205
207
  for (const id of this.structuredMessageIds) {
206
208
  if (!keptIds.has(id)) this.structuredMessageIds.delete(id);
207
209
  }
210
+ for (const id of this.structuredOutputUpdateBatches.keys()) {
211
+ if (!keptIds.has(id)) this.structuredOutputUpdateBatches.delete(id);
212
+ }
208
213
  for (const id of this.messageStates.keys()) {
209
214
  if (!keptIds.has(id)) this.messageStates.delete(id);
210
215
  }
@@ -226,6 +231,7 @@ class StreamProcessor {
226
231
  this.activeMessageIds.clear();
227
232
  this.toolCallToMessage.clear();
228
233
  this.structuredMessageIds.clear();
234
+ this.structuredOutputUpdateBatches.clear();
229
235
  this.pendingManualMessageId = null;
230
236
  this.emitMessagesChange();
231
237
  }
@@ -579,6 +585,7 @@ class StreamProcessor {
579
585
  delta2
580
586
  );
581
587
  state.totalTextContent += delta2;
588
+ this.queueStructuredOutputUpdate(messageId, delta2);
582
589
  this.emitMessagesChange();
583
590
  }
584
591
  return;
@@ -846,12 +853,14 @@ class StreamProcessor {
846
853
  );
847
854
  }
848
855
  if (this.structuredMessageIds.has(messageId)) {
856
+ this.flushStructuredOutputUpdate(messageId);
849
857
  this.messages = errorStructuredOutputPart(
850
858
  this.messages,
851
859
  messageId,
852
860
  errorMessage
853
861
  );
854
862
  this.structuredMessageIds.delete(messageId);
863
+ this.emitStructuredOutputChange(messageId, "error");
855
864
  this.emitMessagesChange();
856
865
  }
857
866
  this.events.onError?.(new Error(errorMessage));
@@ -994,6 +1003,13 @@ class StreamProcessor {
994
1003
  if (targetId) {
995
1004
  this.ensureAssistantMessage(targetId);
996
1005
  this.structuredMessageIds.add(targetId);
1006
+ this.structuredOutputUpdateBatches.delete(targetId);
1007
+ this.events.onStructuredOutputChange?.({
1008
+ phase: "start",
1009
+ messageId: targetId,
1010
+ status: "streaming",
1011
+ raw: ""
1012
+ });
997
1013
  }
998
1014
  return;
999
1015
  }
@@ -1001,6 +1017,7 @@ class StreamProcessor {
1001
1017
  const v = chunk.value;
1002
1018
  const targetId = v.messageId ?? messageId;
1003
1019
  if (targetId) {
1020
+ this.flushStructuredOutputUpdate(targetId);
1004
1021
  this.messages = completeStructuredOutputPart(
1005
1022
  this.messages,
1006
1023
  targetId,
@@ -1009,6 +1026,7 @@ class StreamProcessor {
1009
1026
  v.reasoning
1010
1027
  );
1011
1028
  this.structuredMessageIds.delete(targetId);
1029
+ this.emitStructuredOutputChange(targetId, "complete");
1012
1030
  this.emitMessagesChange();
1013
1031
  }
1014
1032
  }
@@ -1132,6 +1150,40 @@ class StreamProcessor {
1132
1150
  this.emitMessagesChange();
1133
1151
  this.events.onTextUpdate?.(messageId, state.currentSegmentText);
1134
1152
  }
1153
+ queueStructuredOutputUpdate(messageId, delta) {
1154
+ const existing = this.structuredOutputUpdateBatches.get(messageId);
1155
+ const next = {
1156
+ delta: `${existing?.delta ?? ""}${delta}`,
1157
+ chunkCount: (existing?.chunkCount ?? 0) + 1
1158
+ };
1159
+ this.structuredOutputUpdateBatches.set(messageId, next);
1160
+ if (next.chunkCount >= STRUCTURED_OUTPUT_UPDATE_BATCH_SIZE) {
1161
+ this.flushStructuredOutputUpdate(messageId);
1162
+ }
1163
+ }
1164
+ flushStructuredOutputUpdate(messageId) {
1165
+ const batch = this.structuredOutputUpdateBatches.get(messageId);
1166
+ if (!batch || batch.chunkCount === 0) return;
1167
+ this.structuredOutputUpdateBatches.delete(messageId);
1168
+ this.emitStructuredOutputChange(messageId, "update", batch.delta);
1169
+ }
1170
+ emitStructuredOutputChange(messageId, phase, delta) {
1171
+ const part = this.messages.find((message) => message.id === messageId)?.parts.find(
1172
+ (messagePart) => messagePart.type === "structured-output"
1173
+ );
1174
+ if (!part) return;
1175
+ this.events.onStructuredOutputChange?.({
1176
+ phase,
1177
+ messageId,
1178
+ status: part.status,
1179
+ raw: part.raw,
1180
+ ...part.partial !== void 0 ? { partial: part.partial } : {},
1181
+ ...part.data !== void 0 ? { data: part.data } : {},
1182
+ ...part.reasoning !== void 0 ? { reasoning: part.reasoning } : {},
1183
+ ...part.errorMessage !== void 0 ? { errorMessage: part.errorMessage } : {},
1184
+ ...delta !== void 0 ? { delta } : {}
1185
+ });
1186
+ }
1135
1187
  /**
1136
1188
  * Emit messages change event
1137
1189
  */
@@ -1163,13 +1215,16 @@ class StreamProcessor {
1163
1215
  }
1164
1216
  }
1165
1217
  for (const messageId of this.structuredMessageIds) {
1218
+ this.flushStructuredOutputUpdate(messageId);
1166
1219
  this.messages = errorStructuredOutputPart(
1167
1220
  this.messages,
1168
1221
  messageId,
1169
1222
  "Stream ended without structured-output.complete"
1170
1223
  );
1224
+ this.emitStructuredOutputChange(messageId, "error");
1171
1225
  }
1172
1226
  this.structuredMessageIds.clear();
1227
+ this.structuredOutputUpdateBatches.clear();
1173
1228
  this.activeMessageIds.clear();
1174
1229
  if (lastAssistantMessage && !this.hasError) {
1175
1230
  if (this.isWhitespaceOnlyMessage(lastAssistantMessage)) {
@@ -1283,6 +1338,7 @@ class StreamProcessor {
1283
1338
  this.activeRuns.clear();
1284
1339
  this.toolCallToMessage.clear();
1285
1340
  this.structuredMessageIds.clear();
1341
+ this.structuredOutputUpdateBatches.clear();
1286
1342
  this.pendingManualMessageId = null;
1287
1343
  this.pendingThinkingStepId = null;
1288
1344
  this.finishReason = null;