@tanstack/ai 0.45.1 → 0.47.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 (82) hide show
  1. package/dist/esm/activities/chat/index.d.ts +36 -11
  2. package/dist/esm/activities/chat/index.js +462 -66
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/messages.d.ts +1 -0
  5. package/dist/esm/activities/chat/messages.js +12 -7
  6. package/dist/esm/activities/chat/messages.js.map +1 -1
  7. package/dist/esm/activities/chat/middleware/builder.d.ts +7 -2
  8. package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
  9. package/dist/esm/activities/chat/middleware/compose.d.ts +10 -3
  10. package/dist/esm/activities/chat/middleware/compose.js +55 -0
  11. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  12. package/dist/esm/activities/chat/middleware/define.d.ts +6 -3
  13. package/dist/esm/activities/chat/middleware/define.js.map +1 -1
  14. package/dist/esm/activities/chat/middleware/generic-interrupts.d.ts +13 -0
  15. package/dist/esm/activities/chat/middleware/generic-interrupts.js +8 -0
  16. package/dist/esm/activities/chat/middleware/generic-interrupts.js.map +1 -0
  17. package/dist/esm/activities/chat/middleware/index.d.ts +4 -1
  18. package/dist/esm/activities/chat/middleware/types.d.ts +54 -3
  19. package/dist/esm/activities/chat/middleware/types.js +16 -0
  20. package/dist/esm/activities/chat/middleware/types.js.map +1 -0
  21. package/dist/esm/activities/chat/stream/processor.js +18 -5
  22. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  23. package/dist/esm/activities/chat/tools/unique-tool-names.d.ts +20 -0
  24. package/dist/esm/activities/chat/tools/unique-tool-names.js +57 -0
  25. package/dist/esm/activities/chat/tools/unique-tool-names.js.map +1 -0
  26. package/dist/esm/adapter-internals.d.ts +7 -0
  27. package/dist/esm/adapter-internals.js +5 -1
  28. package/dist/esm/client.d.ts +4 -0
  29. package/dist/esm/client.js +3 -1
  30. package/dist/esm/client.js.map +1 -1
  31. package/dist/esm/generic-interrupt-continuation.d.ts +45 -0
  32. package/dist/esm/generic-interrupt-continuation.js +80 -0
  33. package/dist/esm/generic-interrupt-continuation.js.map +1 -0
  34. package/dist/esm/index.d.ts +9 -1
  35. package/dist/esm/index.js +7 -2
  36. package/dist/esm/interrupt-definition.d.ts +113 -0
  37. package/dist/esm/interrupt-definition.js +169 -0
  38. package/dist/esm/interrupt-definition.js.map +1 -0
  39. package/dist/esm/interrupt-resume.d.ts +3 -0
  40. package/dist/esm/interrupt-resume.js +77 -16
  41. package/dist/esm/interrupt-resume.js.map +1 -1
  42. package/dist/esm/interrupts.d.ts +12 -3
  43. package/dist/esm/interrupts.js.map +1 -1
  44. package/dist/esm/middlewares/usage-attributes.d.ts +2 -2
  45. package/dist/esm/middlewares/usage-attributes.js +9 -2
  46. package/dist/esm/middlewares/usage-attributes.js.map +1 -1
  47. package/dist/esm/stream-to-response.d.ts +26 -0
  48. package/dist/esm/stream-to-response.js +1 -1
  49. package/dist/esm/stream-to-response.js.map +1 -1
  50. package/dist/esm/stream-to-websocket.d.ts +123 -0
  51. package/dist/esm/stream-to-websocket.js +249 -0
  52. package/dist/esm/stream-to-websocket.js.map +1 -0
  53. package/dist/esm/types.d.ts +19 -10
  54. package/dist/esm/utilities/chat-params.js +10 -1
  55. package/dist/esm/utilities/chat-params.js.map +1 -1
  56. package/package.json +2 -2
  57. package/skills/ai-core/media-generation/SKILL.md +13 -9
  58. package/skills/ai-core/middleware/SKILL.md +53 -44
  59. package/skills/ai-core/structured-outputs/SKILL.md +59 -55
  60. package/skills/ai-core/tool-calling/SKILL.md +54 -1
  61. package/src/activities/chat/index.ts +1076 -194
  62. package/src/activities/chat/messages.ts +11 -3
  63. package/src/activities/chat/middleware/builder.ts +29 -4
  64. package/src/activities/chat/middleware/compose.ts +95 -5
  65. package/src/activities/chat/middleware/define.ts +13 -3
  66. package/src/activities/chat/middleware/generic-interrupts.ts +26 -0
  67. package/src/activities/chat/middleware/index.ts +15 -0
  68. package/src/activities/chat/middleware/types.ts +127 -2
  69. package/src/activities/chat/stream/processor.ts +21 -0
  70. package/src/activities/chat/tools/unique-tool-names.ts +73 -0
  71. package/src/adapter-internals.ts +24 -0
  72. package/src/client.ts +20 -0
  73. package/src/generic-interrupt-continuation.ts +162 -0
  74. package/src/index.ts +51 -0
  75. package/src/interrupt-definition.ts +581 -0
  76. package/src/interrupt-resume.ts +156 -25
  77. package/src/interrupts.ts +13 -3
  78. package/src/middlewares/usage-attributes.ts +12 -2
  79. package/src/stream-to-response.ts +2 -2
  80. package/src/stream-to-websocket.ts +418 -0
  81. package/src/types.ts +21 -8
  82. package/src/utilities/chat-params.ts +16 -3
@@ -25,6 +25,10 @@ import type {
25
25
  ChatMiddlewareConfig,
26
26
  ChatResumeToolState,
27
27
  } from './activities/chat/middleware/types'
28
+ import type {
29
+ GenericInterruptRequest,
30
+ InterruptDefinition,
31
+ } from './interrupt-definition'
28
32
  import type { Interrupt, RunAgentResumeItem } from './types'
29
33
 
30
34
  /**
@@ -46,6 +50,10 @@ export interface PendingInterruptResumeRecord {
46
50
  interruptId: string
47
51
  payload: unknown
48
52
  binding: InterruptBinding
53
+ /** Present for a first-party generic interrupt. */
54
+ genericRequest?: GenericInterruptRequest<
55
+ InterruptDefinition<any, any, any, any>
56
+ >
49
57
  }
50
58
 
51
59
  export interface ValidateInterruptResumeBatchInput {
@@ -164,6 +172,17 @@ function runtimeTool(
164
172
  return tools.find((tool) => tool.name === name) as RuntimeTool | undefined
165
173
  }
166
174
 
175
+ async function parseSchemaValue(
176
+ schema: unknown,
177
+ value: unknown,
178
+ ): Promise<{ success: true; data: unknown } | { success: false }> {
179
+ if (!isStandardSchema(schema)) return { success: true, data: value }
180
+ const result = await validateWithStandardSchema<unknown>(schema, value)
181
+ return result.success
182
+ ? { success: true, data: result.data }
183
+ : { success: false }
184
+ }
185
+
167
186
  function descriptorResponseSchema(
168
187
  record: PendingInterruptResumeRecord,
169
188
  ): unknown {
@@ -219,9 +238,14 @@ function validateDescriptorSchema(
219
238
  errors: Array<InterruptSubmissionError>,
220
239
  ): unknown {
221
240
  const schema = descriptorResponseSchema(record)
241
+ const responseSchemaHash = binding.responseSchemaHash
242
+ if (schema === undefined && responseSchemaHash === undefined) {
243
+ return undefined
244
+ }
222
245
  if (
223
246
  schema === undefined ||
224
- schemaHash(schema) !== binding.responseSchemaHash
247
+ responseSchemaHash === undefined ||
248
+ schemaHash(schema) !== responseSchemaHash
225
249
  ) {
226
250
  errors.push(
227
251
  interruptItemError(
@@ -276,12 +300,24 @@ export async function validateInterruptResumeBatch(
276
300
  }
277
301
  }
278
302
 
303
+ const pendingGenerics = input.pending.filter(
304
+ (record) => record.binding.kind === 'generic',
305
+ )
306
+ const genericBatchSatisfied =
307
+ pendingGenerics.length > 0 &&
308
+ pendingGenerics.every((record) => resumeById.has(record.interruptId))
309
+
279
310
  let incomplete = false
280
311
  for (const record of input.pending) {
281
312
  const errors = group(record.interruptId)
282
313
  const entry = resumeById.get(record.interruptId)
283
314
  const binding = record.binding
284
315
  if (!entry) {
316
+ // Client tools that share a generic interrupt batch wait for
317
+ // `toolResume`. `continue` re-emits them; `cancel` / `stop` skip them.
318
+ if (genericBatchSatisfied && binding.kind === 'client-tool-execution') {
319
+ continue
320
+ }
285
321
  incomplete = true
286
322
  errors.push(
287
323
  interruptItemError(
@@ -307,19 +343,29 @@ export async function validateInterruptResumeBatch(
307
343
  ),
308
344
  )
309
345
  }
310
- if (
311
- binding.expiresAt !== undefined &&
312
- Date.parse(binding.expiresAt) <= (input.now ?? Date.now())
313
- ) {
314
- errors.push(
315
- interruptItemError(
316
- input,
317
- record.interruptId,
318
- 'expired',
319
- `Interrupt ${record.interruptId} has expired.`,
320
- { source: 'server' },
321
- ),
322
- )
346
+ if (binding.expiresAt !== undefined) {
347
+ const expiresAt = Date.parse(binding.expiresAt)
348
+ if (!Number.isFinite(expiresAt)) {
349
+ errors.push(
350
+ interruptItemError(
351
+ input,
352
+ record.interruptId,
353
+ 'invalid-payload',
354
+ `Interrupt ${record.interruptId} has an invalid expiresAt.`,
355
+ { source: 'server' },
356
+ ),
357
+ )
358
+ } else if (expiresAt <= (input.now ?? Date.now())) {
359
+ errors.push(
360
+ interruptItemError(
361
+ input,
362
+ record.interruptId,
363
+ 'expired',
364
+ `Interrupt ${record.interruptId} has expired.`,
365
+ { source: 'server' },
366
+ ),
367
+ )
368
+ }
323
369
  }
324
370
 
325
371
  const responseSchema = validateDescriptorSchema(
@@ -342,6 +388,28 @@ export async function validateInterruptResumeBatch(
342
388
  continue
343
389
  }
344
390
  if (binding.kind === 'generic') {
391
+ const genericRequest = record.genericRequest
392
+ if (genericRequest !== undefined) {
393
+ const batchIndex = binding.batchIndex
394
+ if (
395
+ binding.definitionId !== genericRequest.definition.id ||
396
+ binding.key !== genericRequest.key ||
397
+ binding.interruptId !== record.interruptId ||
398
+ batchIndex === undefined ||
399
+ !Number.isInteger(batchIndex) ||
400
+ batchIndex < 0
401
+ ) {
402
+ errors.push(
403
+ interruptItemError(
404
+ input,
405
+ record.interruptId,
406
+ 'stale',
407
+ `Generic interrupt ${record.interruptId} has stale definition metadata.`,
408
+ { source: 'server' },
409
+ ),
410
+ )
411
+ }
412
+ }
345
413
  if (entry.status === 'cancelled') {
346
414
  if (entry.payload !== undefined) {
347
415
  errors.push(
@@ -353,6 +421,16 @@ export async function validateInterruptResumeBatch(
353
421
  ),
354
422
  )
355
423
  }
424
+ } else if (genericRequest !== undefined) {
425
+ await pushSchemaIssues({
426
+ request: input,
427
+ errors,
428
+ interruptId: record.interruptId,
429
+ schema: genericRequest.definition.responseSchema,
430
+ value: entry.payload,
431
+ code: 'invalid-payload',
432
+ label: `Interrupt ${record.interruptId} payload is invalid`,
433
+ })
356
434
  } else if (responseSchema !== undefined) {
357
435
  await pushSchemaIssues({
358
436
  request: input,
@@ -616,16 +694,42 @@ export async function validateInterruptResumeBatch(
616
694
  if (!entry) continue
617
695
  const binding = record.binding
618
696
  if (binding.kind === 'generic') {
619
- genericInterrupts.set(
620
- record.interruptId,
621
- entry.status === 'resolved'
622
- ? {
623
- interruptId: record.interruptId,
624
- status: 'resolved',
625
- payload: entry.payload,
626
- }
627
- : { interruptId: record.interruptId, status: 'cancelled' },
697
+ if (entry.status !== 'resolved') {
698
+ genericInterrupts.set(record.interruptId, {
699
+ interruptId: record.interruptId,
700
+ status: 'cancelled',
701
+ })
702
+ continue
703
+ }
704
+ if (record.genericRequest === undefined) {
705
+ genericInterrupts.set(record.interruptId, {
706
+ interruptId: record.interruptId,
707
+ status: 'resolved',
708
+ payload: entry.payload,
709
+ })
710
+ continue
711
+ }
712
+ const parsed = await parseSchemaValue(
713
+ record.genericRequest.definition.responseSchema,
714
+ entry.payload,
628
715
  )
716
+ if (!parsed.success) {
717
+ return {
718
+ errors: [
719
+ interruptItemError(
720
+ input,
721
+ record.interruptId,
722
+ 'invalid-payload',
723
+ `Interrupt ${record.interruptId} payload is invalid.`,
724
+ ),
725
+ ],
726
+ }
727
+ }
728
+ genericInterrupts.set(record.interruptId, {
729
+ interruptId: record.interruptId,
730
+ status: 'resolved',
731
+ payload: parsed.data,
732
+ })
629
733
  continue
630
734
  }
631
735
  if (entry.status === 'cancelled') {
@@ -711,17 +815,44 @@ export function readUnopenedInterruptBinding(
711
815
  const interruptId = stringField(raw, 'interruptId')
712
816
  const responseSchemaHash = stringField(raw, 'responseSchemaHash')
713
817
  const expiresAt = stringField(raw, 'expiresAt')
714
- if (!interruptId || !responseSchemaHash) return undefined
818
+ if (!interruptId || responseSchemaHash === '') return undefined
819
+ if (expiresAt !== undefined && !Number.isFinite(Date.parse(expiresAt))) {
820
+ return undefined
821
+ }
715
822
  const v = INTERRUPT_BINDING_VERSION
716
823
  if (kind === 'generic') {
824
+ const definitionId = stringField(raw, 'definitionId')
825
+ const key = stringField(raw, 'key')
826
+ const batchIndex = raw['batchIndex']
827
+ const payloadSchemaHash = stringField(raw, 'payloadSchemaHash')
828
+ const hasFirstPartyFields =
829
+ definitionId !== undefined ||
830
+ key !== undefined ||
831
+ batchIndex !== undefined ||
832
+ payloadSchemaHash !== undefined
833
+ if (
834
+ hasFirstPartyFields &&
835
+ (!definitionId ||
836
+ !key ||
837
+ typeof batchIndex !== 'number' ||
838
+ !Number.isInteger(batchIndex) ||
839
+ batchIndex < 0)
840
+ ) {
841
+ return undefined
842
+ }
717
843
  return {
718
844
  v,
719
845
  kind,
720
846
  interruptId,
721
- responseSchemaHash,
847
+ ...(responseSchemaHash ? { responseSchemaHash } : {}),
722
848
  ...(expiresAt ? { expiresAt } : {}),
849
+ ...(definitionId ? { definitionId } : {}),
850
+ ...(key ? { key } : {}),
851
+ ...(typeof batchIndex === 'number' ? { batchIndex } : {}),
852
+ ...(payloadSchemaHash ? { payloadSchemaHash } : {}),
723
853
  }
724
854
  }
855
+ if (!responseSchemaHash) return undefined
725
856
  const toolName = stringField(raw, 'toolName')
726
857
  const toolCallId = stringField(raw, 'toolCallId')
727
858
  if (!toolName || !toolCallId) return undefined
package/src/interrupts.ts CHANGED
@@ -84,12 +84,15 @@ interface InterruptBindingBase {
84
84
  interruptId: string
85
85
  interruptedRunId: string
86
86
  generation: number
87
- responseSchemaHash: string
88
87
  expiresAt?: string
89
88
  }
90
89
 
90
+ interface ResponseSchemaInterruptBindingBase extends InterruptBindingBase {
91
+ responseSchemaHash: string
92
+ }
93
+
91
94
  export type InterruptBinding =
92
- | (InterruptBindingBase & {
95
+ | (ResponseSchemaInterruptBindingBase & {
93
96
  kind: 'tool-approval'
94
97
  toolName: string
95
98
  toolCallId: string
@@ -97,7 +100,7 @@ export type InterruptBinding =
97
100
  inputSchemaHash: string
98
101
  approvalSchemaHash: string
99
102
  })
100
- | (InterruptBindingBase & {
103
+ | (ResponseSchemaInterruptBindingBase & {
101
104
  kind: 'client-tool-execution'
102
105
  toolName: string
103
106
  toolCallId: string
@@ -105,6 +108,13 @@ export type InterruptBinding =
105
108
  })
106
109
  | (InterruptBindingBase & {
107
110
  kind: 'generic'
111
+ /** Omitted when the generic interrupt accepts an unvalidated response. */
112
+ responseSchemaHash?: string
113
+ /** Present only for a first-party generic interrupt. */
114
+ definitionId?: string
115
+ key?: string
116
+ batchIndex?: number
117
+ payloadSchemaHash?: string
108
118
  })
109
119
 
110
120
  export type UnopenedInterruptBinding = InterruptBinding extends infer TBinding
@@ -12,8 +12,8 @@ import type { TokenUsage } from '../types'
12
12
  * `gen_ai.usage.cost` and `gen_ai.usage.total_tokens` are de-facto extensions
13
13
  * consumed by backends like PostHog (which otherwise re-derive cost from their
14
14
  * own price tables, losing cache discounts and gateway markup). Fields with no
15
- * semconv or de-facto convention (`costDetails`, `durationSeconds`,
16
- * `unitsBilled`) are TanStack-namespaced.
15
+ * semconv or de-facto convention (`billed`, `costDetails`, and the deprecated
16
+ * `durationSeconds`/`unitsBilled`) are TanStack-namespaced.
17
17
  *
18
18
  * Shared by `otelMiddleware` across every activity (chat and the media
19
19
  * activities) so usage lands identically whichever activity produced the span.
@@ -30,6 +30,16 @@ export function usageAttributes(
30
30
  'gen_ai.usage.input_tokens': usage.promptTokens,
31
31
  'gen_ai.usage.output_tokens': usage.completionTokens,
32
32
  }
33
+ // The self-describing billed quantity: the unit rides along as a string
34
+ // attribute so backends can label/aggregate non-token usage without
35
+ // out-of-band knowledge of the provider.
36
+ if (usage.billed !== undefined) {
37
+ const quantity = firstNumber(usage.billed.quantity)
38
+ if (quantity !== undefined) {
39
+ attrs['tanstack.ai.usage.billed_quantity'] = quantity
40
+ attrs['tanstack.ai.usage.billed_unit'] = usage.billed.unit
41
+ }
42
+ }
33
43
  const optional: Array<[key: string, value: unknown]> = [
34
44
  ['gen_ai.usage.total_tokens', usage.totalTokens],
35
45
  ['gen_ai.usage.cost', usage.cost],
@@ -78,7 +78,7 @@ function combineFailures(
78
78
  )
79
79
  }
80
80
 
81
- function runErrorChunk(
81
+ export function runErrorChunk(
82
82
  error: unknown,
83
83
  ): Extract<StreamChunk, { type: 'RUN_ERROR' }> {
84
84
  const payload = toRunErrorPayload(error)
@@ -366,7 +366,7 @@ export const RUN_ACCEPTED_EVENT = 'run.accepted'
366
366
  * The returned `getId` maps each forwarded chunk to the exact opaque offset
367
367
  * returned by the durability adapter for the SSE `id:` line.
368
368
  */
369
- function durableStreamSource<TOffset extends string>(
369
+ export function durableStreamSource<TOffset extends string>(
370
370
  stream: AsyncIterable<StreamChunk>,
371
371
  durability: StreamDurability<TOffset>,
372
372
  options: {