@tanstack/ai 0.19.1 → 0.20.1

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 (93) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +9 -3
  2. package/dist/esm/activities/chat/adapter.js +3 -1
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/index.d.ts +13 -4
  5. package/dist/esm/activities/chat/index.js +46 -18
  6. package/dist/esm/activities/chat/index.js.map +1 -1
  7. package/dist/esm/activities/chat/messages.js +1 -1
  8. package/dist/esm/activities/chat/messages.js.map +1 -1
  9. package/dist/esm/activities/chat/middleware/compose.js +2 -0
  10. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  11. package/dist/esm/activities/chat/middleware/types.d.ts +8 -7
  12. package/dist/esm/activities/chat/stream/processor.d.ts +8 -8
  13. package/dist/esm/activities/chat/stream/processor.js +29 -21
  14. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  15. package/dist/esm/activities/chat/stream/strategies.d.ts +3 -3
  16. package/dist/esm/activities/chat/stream/strategies.js +4 -4
  17. package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
  18. package/dist/esm/activities/chat/tools/lazy-tool-manager.js +5 -0
  19. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  20. package/dist/esm/activities/chat/tools/schema-converter.d.ts +1 -1
  21. package/dist/esm/activities/chat/tools/schema-converter.js +36 -35
  22. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  23. package/dist/esm/activities/chat/tools/tool-calls.d.ts +2 -2
  24. package/dist/esm/activities/chat/tools/tool-calls.js +17 -9
  25. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  26. package/dist/esm/activities/chat/tools/tool-definition.js +1 -1
  27. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  28. package/dist/esm/activities/generateAudio/adapter.js +3 -1
  29. package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
  30. package/dist/esm/activities/generateAudio/index.d.ts +5 -1
  31. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  32. package/dist/esm/activities/generateImage/adapter.js +3 -1
  33. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  34. package/dist/esm/activities/generateImage/index.js +5 -0
  35. package/dist/esm/activities/generateImage/index.js.map +1 -1
  36. package/dist/esm/activities/generateSpeech/adapter.js +3 -1
  37. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  38. package/dist/esm/activities/generateTranscription/adapter.js +3 -1
  39. package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
  40. package/dist/esm/activities/generateVideo/adapter.js +3 -1
  41. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  42. package/dist/esm/activities/stream-generation-result.js +6 -2
  43. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  44. package/dist/esm/activities/summarize/adapter.js +3 -1
  45. package/dist/esm/activities/summarize/adapter.js.map +1 -1
  46. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +1 -1
  47. package/dist/esm/activities/summarize/chat-stream-summarize.js +5 -0
  48. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  49. package/dist/esm/activities/summarize/index.js.map +1 -1
  50. package/dist/esm/index.d.ts +3 -0
  51. package/dist/esm/index.js +4 -0
  52. package/dist/esm/index.js.map +1 -1
  53. package/dist/esm/logger/internal-logger.js +2 -0
  54. package/dist/esm/logger/internal-logger.js.map +1 -1
  55. package/dist/esm/middlewares/content-guard.js +5 -4
  56. package/dist/esm/middlewares/content-guard.js.map +1 -1
  57. package/dist/esm/middlewares/otel.js +38 -16
  58. package/dist/esm/middlewares/otel.js.map +1 -1
  59. package/dist/esm/realtime/index.d.ts +1 -1
  60. package/dist/esm/realtime/index.js.map +1 -1
  61. package/dist/esm/system-prompts.d.ts +66 -0
  62. package/dist/esm/system-prompts.js +23 -0
  63. package/dist/esm/system-prompts.js.map +1 -0
  64. package/dist/esm/tools/provider-tool.d.ts +9 -0
  65. package/dist/esm/tools/provider-tool.js +7 -0
  66. package/dist/esm/tools/provider-tool.js.map +1 -0
  67. package/dist/esm/types.d.ts +22 -7
  68. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  69. package/dist/esm/utilities/chat-params.js.map +1 -1
  70. package/package.json +2 -2
  71. package/src/activities/chat/adapter.ts +11 -2
  72. package/src/activities/chat/index.ts +46 -27
  73. package/src/activities/chat/messages.ts +2 -1
  74. package/src/activities/chat/middleware/types.ts +14 -11
  75. package/src/activities/chat/stream/processor.ts +19 -19
  76. package/src/activities/chat/stream/strategies.ts +3 -3
  77. package/src/activities/chat/tools/schema-converter.ts +77 -52
  78. package/src/activities/chat/tools/tool-calls.ts +18 -11
  79. package/src/activities/chat/tools/tool-definition.ts +6 -1
  80. package/src/activities/generateAudio/index.ts +5 -4
  81. package/src/activities/generateImage/index.ts +5 -0
  82. package/src/activities/stream-generation-result.ts +11 -2
  83. package/src/activities/summarize/chat-stream-summarize.ts +5 -2
  84. package/src/activities/summarize/index.ts +2 -2
  85. package/src/index.ts +5 -0
  86. package/src/middlewares/content-guard.ts +11 -3
  87. package/src/middlewares/otel.ts +47 -16
  88. package/src/realtime/index.ts +1 -1
  89. package/src/system-prompts.ts +98 -0
  90. package/src/tools/provider-tool.ts +14 -0
  91. package/src/types.ts +28 -9
  92. package/src/utilities/ag-ui-wire.ts +2 -2
  93. package/src/utilities/chat-params.ts +2 -2
@@ -1,11 +1,29 @@
1
- /* eslint-disable @typescript-eslint/no-unnecessary-condition */
2
-
3
1
  import type {
4
2
  StandardJSONSchemaV1,
5
3
  StandardSchemaV1,
6
4
  } from '@standard-schema/spec'
7
5
  import type { JSONSchema, SchemaInput } from '../../../types'
8
6
 
7
+ /**
8
+ * Build a JSONSchema object from any plain key/value source. The `JSONSchema`
9
+ * interface's `[key: string]: any` index signature makes every property
10
+ * assignable through bracket access without a type cast — copying keys here
11
+ * lets us narrow either `Record<string, unknown>` (returned by
12
+ * `~standard.jsonSchema.input()`) or a `JSONSchema` (from the SchemaInput
13
+ * pass-through arm) into the typed view used by the rest of this module.
14
+ *
15
+ * Accepts `object` so callers don't need a cast when narrowing from union
16
+ * types like `SchemaInput`.
17
+ */
18
+ function toJsonSchema(obj: object): JSONSchema {
19
+ const result: JSONSchema = {}
20
+ for (const [key, value] of Object.entries(obj)) {
21
+ if (key === '$schema') continue // not needed by LLM providers
22
+ result[key] = value
23
+ }
24
+ return result
25
+ }
26
+
9
27
  /**
10
28
  * Check if a value is a Standard JSON Schema compliant schema.
11
29
  * Standard JSON Schema compliant libraries (Zod v4+, ArkType, Valibot with toStandardJsonSchema, etc.)
@@ -19,6 +37,7 @@ export function isStandardJSONSchema(
19
37
  schema !== null &&
20
38
  '~standard' in schema &&
21
39
  typeof (schema as StandardJSONSchemaV1)['~standard'] === 'object' &&
40
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for caller-provided unknown; type assertion narrows but doesn't validate the wire payload
22
41
  (schema as StandardJSONSchemaV1)['~standard'].version === 1 &&
23
42
  typeof (schema as StandardJSONSchemaV1)['~standard'].jsonSchema ===
24
43
  'object' &&
@@ -37,9 +56,9 @@ export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
37
56
  schema !== null &&
38
57
  '~standard' in schema &&
39
58
  typeof schema['~standard'] === 'object' &&
40
- schema !== null &&
41
59
  schema['~standard'] !== null &&
42
60
  'version' in schema['~standard'] &&
61
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for caller-provided unknown; in-operator narrows but doesn't validate the wire payload
43
62
  schema['~standard'].version === 1 &&
44
63
  'validate' in schema['~standard'] &&
45
64
  typeof schema['~standard'].validate === 'function'
@@ -58,19 +77,20 @@ export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
58
77
  * @returns Transformed schema compatible with OpenAI structured output
59
78
  */
60
79
  function makeStructuredOutputCompatible(
61
- schema: Record<string, any>,
80
+ schema: JSONSchema,
62
81
  originalRequired: Array<string> = [],
63
- ): Record<string, any> {
64
- const result = { ...schema }
82
+ ): JSONSchema {
83
+ const result: JSONSchema = { ...schema }
65
84
 
66
85
  // Handle object types
67
86
  if (result.type === 'object' && result.properties) {
68
- const properties = { ...result.properties }
87
+ const properties: Record<string, JSONSchema> = { ...result.properties }
69
88
  const allPropertyNames = Object.keys(properties)
70
89
 
71
90
  // Transform each property
72
91
  for (const propName of allPropertyNames) {
73
92
  const prop = properties[propName]
93
+ if (!prop) continue
74
94
  const wasOptional = !originalRequired.includes(propName)
75
95
 
76
96
  // Recursively transform nested objects/arrays
@@ -83,12 +103,12 @@ function makeStructuredOutputCompatible(
83
103
  ? { ...transformed, type: ['object', 'null'] }
84
104
  : transformed
85
105
  } else if (prop.type === 'array' && prop.items) {
86
- const transformed = {
106
+ const items = Array.isArray(prop.items) ? prop.items[0] : prop.items
107
+ const transformed: JSONSchema = {
87
108
  ...prop,
88
- items: makeStructuredOutputCompatible(
89
- prop.items,
90
- prop.items.required || [],
91
- ),
109
+ items: items
110
+ ? makeStructuredOutputCompatible(items, items.required || [])
111
+ : prop.items,
92
112
  }
93
113
  properties[propName] = wasOptional
94
114
  ? { ...transformed, type: ['array', 'null'] }
@@ -118,10 +138,10 @@ function makeStructuredOutputCompatible(
118
138
 
119
139
  // Handle array types with object items
120
140
  if (result.type === 'array' && result.items) {
121
- result.items = makeStructuredOutputCompatible(
122
- result.items,
123
- result.items.required || [],
124
- )
141
+ const items = Array.isArray(result.items) ? result.items[0] : result.items
142
+ if (items) {
143
+ result.items = makeStructuredOutputCompatible(items, items.required || [])
144
+ }
125
145
  }
126
146
 
127
147
  return result
@@ -216,42 +236,33 @@ export function convertSchemaToJsonSchema(
216
236
  target: 'draft-07',
217
237
  })
218
238
 
219
- let result = jsonSchema
220
-
221
- if (typeof result === 'object' && '$schema' in result) {
222
- // Remove $schema property as it's not needed for LLM providers
223
- const { $schema, ...rest } = result
224
- result = rest
225
- }
239
+ // Rebuild structurally so the typed JSONSchema view is acquired without
240
+ // a `Record<string, unknown> as JSONSchema` cast; `toJsonSchema()` also
241
+ // drops the `$schema` key which LLM providers don't need.
242
+ let result: JSONSchema = toJsonSchema(jsonSchema)
226
243
 
227
244
  // Ensure object schemas always have type: "object"
245
+ // If it has properties (even empty), it should be an object type
246
+ if ('properties' in result && !result.type) {
247
+ result.type = 'object'
248
+ }
228
249
 
229
- if (typeof result === 'object') {
230
- // If it has properties (even empty), it should be an object type
231
- if ('properties' in result && !result.type) {
232
- result.type = 'object'
233
- }
234
-
235
- // Ensure properties exists for object types (even if empty)
236
- if (result.type === 'object' && !('properties' in result)) {
237
- result.properties = {}
238
- }
250
+ // Ensure properties exists for object types (even if empty)
251
+ if (result.type === 'object' && !('properties' in result)) {
252
+ result.properties = {}
253
+ }
239
254
 
240
- // Ensure required exists for object types (even if empty array)
241
- if (result.type === 'object' && !('required' in result)) {
242
- result.required = []
243
- }
255
+ // Ensure required exists for object types (even if empty array)
256
+ if (result.type === 'object' && !('required' in result)) {
257
+ result.required = []
258
+ }
244
259
 
245
- // Apply structured output transformation if requested
246
- if (forStructuredOutput) {
247
- result = makeStructuredOutputCompatible(
248
- result,
249
- (result.required as Array<string>) || [],
250
- )
251
- }
260
+ // Apply structured output transformation if requested
261
+ if (forStructuredOutput) {
262
+ result = makeStructuredOutputCompatible(result, result.required || [])
252
263
  }
253
264
 
254
- return result as JSONSchema
265
+ return result
255
266
  }
256
267
 
257
268
  // Detect Standard Schema validators (Zod, ArkType, Valibot, …) that don't
@@ -271,14 +282,25 @@ export function convertSchemaToJsonSchema(
271
282
  // If it's not a Standard JSON Schema, assume it's already a JSONSchema and pass through
272
283
  // Still apply structured output transformation if requested
273
284
 
274
- if (forStructuredOutput && typeof schema === 'object') {
275
- return makeStructuredOutputCompatible(
276
- schema as Record<string, any>,
277
- ((schema as JSONSchema).required as Array<string>) || [],
278
- ) as JSONSchema
285
+ // At this branch, `schema` is the plain `JSONSchema` arm of `SchemaInput`
286
+ // (the two `~standard` arms were handled above). When no transformation
287
+ // is requested we pass the schema through by reference to preserve
288
+ // identity for callers that compare via `===`.
289
+ if (typeof schema !== 'object') {
290
+ // The SchemaInput union is object-shaped on every arm; if we ever hit a
291
+ // non-object here, propagate it untouched and let the downstream
292
+ // provider error loudly rather than silently widen.
293
+ return schema
279
294
  }
280
295
 
281
- return schema as JSONSchema
296
+ if (forStructuredOutput) {
297
+ // Build a typed view structurally so we don't need a SchemaInput→JSONSchema
298
+ // cast on the transformation path.
299
+ const typedView = toJsonSchema(schema)
300
+ return makeStructuredOutputCompatible(typedView, typedView.required || [])
301
+ }
302
+
303
+ return schema
282
304
  }
283
305
 
284
306
  /**
@@ -293,7 +315,10 @@ export async function validateWithStandardSchema<T>(
293
315
  data: unknown,
294
316
  ): Promise<
295
317
  | { success: true; data: T }
296
- | { success: false; issues: Array<{ message: string; path?: Array<string> }> }
318
+ | {
319
+ success: false
320
+ issues: Array<{ message: string; path?: Array<string> | undefined }>
321
+ }
297
322
  > {
298
323
  if (!isStandardSchema(schema)) {
299
324
  // If it's not a Standard Schema, just return the data as-is
@@ -81,8 +81,8 @@ export class MiddlewareAbortError extends Error {
81
81
  * ```
82
82
  */
83
83
  export class ToolCallManager {
84
- private toolCallsMap = new Map<number, ToolCall>()
85
- private tools: ReadonlyArray<Tool>
84
+ private readonly toolCallsMap = new Map<number, ToolCall>()
85
+ private readonly tools: ReadonlyArray<Tool>
86
86
 
87
87
  constructor(tools: ReadonlyArray<Tool>) {
88
88
  this.tools = tools
@@ -320,14 +320,16 @@ async function* executeWithEventPolling<T>(
320
320
  ])
321
321
 
322
322
  // Flush any pending events
323
- while (pendingEvents.length > 0) {
324
- yield pendingEvents.shift()!
323
+ let event: CustomEvent | undefined
324
+ while ((event = pendingEvents.shift()) !== undefined) {
325
+ yield event
325
326
  }
326
327
  }
327
328
 
328
329
  // Final flush in case events were emitted right at completion
329
- while (pendingEvents.length > 0) {
330
- yield pendingEvents.shift()!
330
+ let event: CustomEvent | undefined
331
+ while ((event = pendingEvents.shift()) !== undefined) {
332
+ yield event
331
333
  }
332
334
 
333
335
  return state.result
@@ -404,13 +406,17 @@ async function* executeServerTool(
404
406
  ): AsyncGenerator<CustomEvent, void, void> {
405
407
  const startTime = Date.now()
406
408
  try {
407
- const executionPromise = Promise.resolve(tool.execute!(input, context))
409
+ if (!tool.execute) {
410
+ throw new Error(`Tool ${toolName} has no execute() implementation`)
411
+ }
412
+ const executionPromise = Promise.resolve(tool.execute(input, context))
408
413
  let result = yield* executeWithEventPolling(executionPromise, pendingEvents)
409
414
  const duration = Date.now() - startTime
410
415
 
411
416
  // Flush remaining events
412
- while (pendingEvents.length > 0) {
413
- yield pendingEvents.shift()!
417
+ let pendingEvent: CustomEvent | undefined
418
+ while ((pendingEvent = pendingEvents.shift()) !== undefined) {
419
+ yield pendingEvent
414
420
  }
415
421
 
416
422
  // Validate output against outputSchema if provided
@@ -448,8 +454,9 @@ async function* executeServerTool(
448
454
  const duration = Date.now() - startTime
449
455
 
450
456
  // Flush remaining events
451
- while (pendingEvents.length > 0) {
452
- yield pendingEvents.shift()!
457
+ let pendingEvent: CustomEvent | undefined
458
+ while ((pendingEvent = pendingEvents.shift()) !== undefined) {
459
+ yield pendingEvent
453
460
  }
454
461
 
455
462
  if (error instanceof MiddlewareAbortError) {
@@ -29,6 +29,11 @@ export interface ClientTool<
29
29
  __toolSide: 'client'
30
30
  name: TName
31
31
  description: string
32
+ // Note: `inputSchema` / `outputSchema` stay as bare optionals (not
33
+ // widened to `| undefined`). They participate in inference via
34
+ // `InferToolInput` / `InferToolOutput` — widening with `| undefined`
35
+ // breaks the `infer TInput extends StandardJSONSchemaV1<...>` chain
36
+ // because `undefined` doesn't extend the schema constraint.
32
37
  inputSchema?: TInput
33
38
  outputSchema?: TOutput
34
39
  needsApproval?: boolean
@@ -215,7 +220,7 @@ export function toolDefinition<
215
220
  return {
216
221
  __toolSide: 'client',
217
222
  ...config,
218
- execute,
223
+ ...(execute !== undefined && { execute }),
219
224
  }
220
225
  },
221
226
  }
@@ -27,10 +27,11 @@ export const kind = 'audio' as const
27
27
  /**
28
28
  * Extract provider options from an AudioAdapter via ~types.
29
29
  */
30
- export type AudioProviderOptions<TAdapter> =
31
- TAdapter extends AudioAdapter<any, any>
32
- ? TAdapter['~types']['providerOptions']
33
- : object
30
+ export type AudioProviderOptions<TAdapter> = TAdapter extends {
31
+ '~types': { providerOptions: infer P extends object }
32
+ }
33
+ ? P
34
+ : object
34
35
 
35
36
  // ===========================
36
37
  // Activity Options Type
@@ -227,6 +227,11 @@ async function runGenerateImage<
227
227
  requestId,
228
228
  provider: adapter.name,
229
229
  model,
230
+ // GeneratedImage is a discriminated `{ url } | { b64Json }` union, but the
231
+ // wire shape on the devtools event is a plain optional pair. Use
232
+ // conditional spreads so the emitted record only sets the field actually
233
+ // present — `exactOptionalPropertyTypes` rejects `field: undefined`
234
+ // against `field?: string` targets.
230
235
  images: result.images.map((image) => ({
231
236
  url: image.url,
232
237
  b64Json: image.b64Json,
@@ -55,12 +55,21 @@ export async function* streamGenerationResult<TResult>(
55
55
  }
56
56
  } catch (error: unknown) {
57
57
  const payload = toRunErrorPayload(error, 'Generation failed')
58
+ // `code` is omitted entirely when undefined so the event matches the
59
+ // AG-UI `code?: string` shape under `exactOptionalPropertyTypes`. The
60
+ // deprecated nested `error` form preserves the same conditional
61
+ // structure for backward compatibility.
62
+ const codeFields =
63
+ payload.code !== undefined ? { code: payload.code } : undefined
58
64
  yield {
59
65
  type: EventType.RUN_ERROR,
60
66
  message: payload.message,
61
- code: payload.code,
67
+ ...codeFields,
62
68
  // Deprecated nested form for backward compatibility
63
- error: payload,
69
+ error: {
70
+ message: payload.message,
71
+ ...codeFields,
72
+ },
64
73
  timestamp: Date.now(),
65
74
  }
66
75
  }
@@ -46,7 +46,7 @@ export class ChatStreamSummarizeAdapter<
46
46
  > extends BaseSummarizeAdapter<TModel, TProviderOptions> {
47
47
  readonly name: string
48
48
 
49
- private textAdapter: ChatStreamCapable
49
+ private readonly textAdapter: ChatStreamCapable
50
50
 
51
51
  constructor(
52
52
  textAdapter: ChatStreamCapable,
@@ -124,7 +124,7 @@ export class ChatStreamSummarizeAdapter<
124
124
  return { id, model, summary, usage }
125
125
  }
126
126
 
127
- async *summarizeStream(
127
+ override async *summarizeStream(
128
128
  options: SummarizationOptions<TProviderOptions>,
129
129
  ): AsyncIterable<StreamChunk> {
130
130
  const systemPrompt = this.buildSummarizationPrompt(options)
@@ -221,6 +221,9 @@ export class ChatStreamSummarizeAdapter<
221
221
  case 'concise':
222
222
  prompt += 'Provide a very concise summary in 1-2 sentences. '
223
223
  break
224
+ case undefined:
225
+ prompt += 'Provide a clear and concise summary. '
226
+ break
224
227
  default:
225
228
  prompt += 'Provide a clear and concise summary. '
226
229
  }
@@ -159,7 +159,7 @@ export function summarize<
159
159
 
160
160
  if (stream) {
161
161
  return runStreamingSummarize(
162
- options as unknown as SummarizeActivityOptions<
162
+ options as SummarizeActivityOptions<
163
163
  SummarizeAdapter<string, object>,
164
164
  true
165
165
  >,
@@ -167,7 +167,7 @@ export function summarize<
167
167
  }
168
168
 
169
169
  return runSummarize(
170
- options as unknown as SummarizeActivityOptions<
170
+ options as SummarizeActivityOptions<
171
171
  SummarizeAdapter<string, object>,
172
172
  false
173
173
  >,
package/src/index.ts CHANGED
@@ -69,6 +69,7 @@ export { ToolCallManager } from './activities/chat/tools/tool-calls'
69
69
 
70
70
  // Provider tool type
71
71
  export type { ProviderTool } from './tools/provider-tool'
72
+ export { brandProviderTool } from './tools/provider-tool'
72
73
 
73
74
  // Agent loop strategies
74
75
  export {
@@ -104,6 +105,10 @@ export type {
104
105
  // All types
105
106
  export * from './types'
106
107
 
108
+ // System prompts (type + normaliser used by adapters)
109
+ export type { SystemPrompt, NormalizedSystemPrompt } from './system-prompts'
110
+ export { normalizeSystemPrompts } from './system-prompts'
111
+
107
112
  // Utility functions
108
113
  export { detectImageMimeType } from './utils'
109
114
 
@@ -142,10 +142,15 @@ function createDeltaStrategy(
142
142
 
143
143
  if (blockOnMatch) return null // drop chunk
144
144
 
145
+ // Strip out the previous `content` field by destructuring it away — with
146
+ // `exactOptionalPropertyTypes` we can't assign `content: undefined`
147
+ // against `content?: string`. The replacement event carries only the
148
+ // filtered delta.
149
+ const { content: _strippedContent, ...rest } = chunk
150
+ void _strippedContent
145
151
  return {
146
- ...chunk,
152
+ ...rest,
147
153
  delta: filtered,
148
- content: undefined,
149
154
  } as StreamChunk
150
155
  },
151
156
  }
@@ -278,8 +283,11 @@ function createBufferedStrategy(
278
283
  content: filtered.slice(0, safeFilteredEnd),
279
284
  } as StreamChunk
280
285
 
286
+ // `pending` was empty before this push iff `emitChunk` is now the only
287
+ // entry — return it directly without re-indexing through `pending[0]`.
288
+ const wasEmpty = pending.length === 0
281
289
  pending.push(emitChunk)
282
- return pending.length === 1 ? pending[0]! : pending
290
+ return wasEmpty ? emitChunk : pending
283
291
  },
284
292
  }
285
293
  }
@@ -137,8 +137,11 @@ function serializeContent(content: unknown): string {
137
137
  case 'document':
138
138
  parts.push('[document]')
139
139
  break
140
+ case undefined:
141
+ parts.push('[unknown]')
142
+ break
140
143
  default:
141
- parts.push(`[${type ?? 'unknown'}]`)
144
+ parts.push(`[${type}]`)
142
145
  }
143
146
  }
144
147
  return parts.join(' ')
@@ -372,9 +375,29 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
372
375
  state.assistantTextBufferTruncated = false
373
376
 
374
377
  if (captureContent) {
378
+ const systemPromptContents = config.systemPrompts.map((p) =>
379
+ typeof p === 'string' ? p : p.content,
380
+ )
381
+ // Anthropic prompt-caching users need to know which prompt carried
382
+ // `cache_control`: it's the one attribute that explains cache
383
+ // hit/miss in observability. Serialise per-prompt metadata as a
384
+ // single JSON span attribute so backends that don't understand
385
+ // GenAI events can still surface it. Kept off span events to
386
+ // avoid breaking the one-event-per-message GenAI semconv contract.
387
+ const systemPromptMetadata = config.systemPrompts.map((p) =>
388
+ typeof p === 'string' || p.metadata === undefined
389
+ ? null
390
+ : p.metadata,
391
+ )
392
+ if (systemPromptMetadata.some((m) => m !== null)) {
393
+ iterSpan.setAttribute(
394
+ 'tanstack.ai.system_prompt.metadata',
395
+ JSON.stringify(systemPromptMetadata),
396
+ )
397
+ }
375
398
  // Span events follow the original GenAI semconv (one event per
376
399
  // message). Backends that read events get content this way.
377
- for (const sys of config.systemPrompts) {
400
+ for (const sys of systemPromptContents) {
378
401
  iterSpan.addEvent('gen_ai.system.message', {
379
402
  content: redactContent(sys),
380
403
  })
@@ -391,7 +414,7 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
391
414
  // (`gen_ai.input.messages`) — backends like PostHog read prompt
392
415
  // content from this attribute, not from span events.
393
416
  const inputMessages: Array<{ role: string; content: string }> = []
394
- for (const sys of config.systemPrompts) {
417
+ for (const sys of systemPromptContents) {
395
418
  inputMessages.push({
396
419
  role: 'system',
397
420
  content: redactContent(sys),
@@ -610,9 +633,10 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
610
633
 
611
634
  if (!info.ok && info.error !== undefined) {
612
635
  toolSpan.recordException(info.error as Exception)
636
+ const msg = errorMessage(info.error)
613
637
  toolSpan.setStatus({
614
638
  code: SpanStatusCode.ERROR,
615
- message: errorMessage(info.error),
639
+ ...(msg !== undefined && { message: msg }),
616
640
  })
617
641
  }
618
642
 
@@ -668,13 +692,16 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
668
692
 
669
693
  const errType = errorTypeName(info.error)
670
694
  const message = errorMessage(info.error)
695
+ const statusMessage =
696
+ message !== undefined ? { message } : ({} as const)
671
697
  const exception = info.error as Exception
672
698
 
673
- if (state.currentIterationSpan) {
674
- state.currentIterationSpan.recordException(exception)
675
- state.currentIterationSpan.setStatus({
699
+ const iterationSpan = state.currentIterationSpan
700
+ if (iterationSpan) {
701
+ iterationSpan.recordException(exception)
702
+ iterationSpan.setStatus({
676
703
  code: SpanStatusCode.ERROR,
677
- message,
704
+ ...statusMessage,
678
705
  })
679
706
  safeCall('otel.onSpanEnd', () =>
680
707
  onSpanEnd?.(
@@ -683,17 +710,17 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
683
710
  ctx,
684
711
  iteration: state.iterationCount - 1,
685
712
  } as OtelSpanInfo<'iteration'>,
686
- state.currentIterationSpan!,
713
+ iterationSpan,
687
714
  ),
688
715
  )
689
- state.currentIterationSpan.end()
716
+ iterationSpan.end()
690
717
  state.currentIterationSpan = null
691
718
  }
692
719
 
693
720
  for (const [id, entry] of state.toolSpans) {
694
721
  const { span, toolName } = entry
695
722
  span.recordException(exception)
696
- span.setStatus({ code: SpanStatusCode.ERROR, message })
723
+ span.setStatus({ code: SpanStatusCode.ERROR, ...statusMessage })
697
724
  safeCall('otel.onSpanEnd', () =>
698
725
  onSpanEnd?.(
699
726
  {
@@ -711,7 +738,10 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
711
738
  }
712
739
 
713
740
  state.rootSpan.recordException(exception)
714
- state.rootSpan.setStatus({ code: SpanStatusCode.ERROR, message })
741
+ state.rootSpan.setStatus({
742
+ code: SpanStatusCode.ERROR,
743
+ ...statusMessage,
744
+ })
715
745
 
716
746
  if (durationHistogram) {
717
747
  durationHistogram.record(info.duration / 1000, {
@@ -743,8 +773,9 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
743
773
  span.setStatus({ code: SpanStatusCode.ERROR, message: 'cancelled' })
744
774
  }
745
775
 
746
- if (state.currentIterationSpan) {
747
- closeCancelled(state.currentIterationSpan)
776
+ const iterationSpan = state.currentIterationSpan
777
+ if (iterationSpan) {
778
+ closeCancelled(iterationSpan)
748
779
  safeCall('otel.onSpanEnd', () =>
749
780
  onSpanEnd?.(
750
781
  {
@@ -752,10 +783,10 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
752
783
  ctx,
753
784
  iteration: state.iterationCount - 1,
754
785
  } as OtelSpanInfo<'iteration'>,
755
- state.currentIterationSpan!,
786
+ iterationSpan,
756
787
  ),
757
788
  )
758
- state.currentIterationSpan.end()
789
+ iterationSpan.end()
759
790
  state.currentIterationSpan = null
760
791
  }
761
792
  for (const [id, entry] of state.toolSpans) {
@@ -1,7 +1,7 @@
1
1
  import type { RealtimeToken, RealtimeTokenOptions } from './types'
2
2
 
3
3
  // Re-export all types
4
- export * from './types'
4
+ export type * from './types'
5
5
 
6
6
  /**
7
7
  * Generate a realtime token using the provided adapter.