@tanstack/ai 0.46.0 → 0.47.2

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 (66) hide show
  1. package/dist/esm/activities/chat/index.d.ts +36 -11
  2. package/dist/esm/activities/chat/index.js +440 -79
  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/adapter-internals.d.ts +6 -0
  24. package/dist/esm/adapter-internals.js +4 -1
  25. package/dist/esm/client.d.ts +4 -0
  26. package/dist/esm/client.js +3 -1
  27. package/dist/esm/client.js.map +1 -1
  28. package/dist/esm/generic-interrupt-continuation.d.ts +45 -0
  29. package/dist/esm/generic-interrupt-continuation.js +80 -0
  30. package/dist/esm/generic-interrupt-continuation.js.map +1 -0
  31. package/dist/esm/index.d.ts +6 -1
  32. package/dist/esm/index.js +4 -1
  33. package/dist/esm/interrupt-definition.d.ts +113 -0
  34. package/dist/esm/interrupt-definition.js +169 -0
  35. package/dist/esm/interrupt-definition.js.map +1 -0
  36. package/dist/esm/interrupt-resume.d.ts +3 -0
  37. package/dist/esm/interrupt-resume.js +77 -16
  38. package/dist/esm/interrupt-resume.js.map +1 -1
  39. package/dist/esm/interrupts.d.ts +12 -3
  40. package/dist/esm/interrupts.js.map +1 -1
  41. package/dist/esm/types.d.ts +11 -3
  42. package/dist/esm/utilities/chat-params.js +10 -1
  43. package/dist/esm/utilities/chat-params.js.map +1 -1
  44. package/package.json +3 -3
  45. package/skills/ai-core/media-generation/SKILL.md +4 -1
  46. package/skills/ai-core/middleware/SKILL.md +53 -44
  47. package/skills/ai-core/structured-outputs/SKILL.md +59 -55
  48. package/skills/ai-core/tool-calling/SKILL.md +54 -1
  49. package/src/activities/chat/index.ts +1030 -211
  50. package/src/activities/chat/messages.ts +11 -3
  51. package/src/activities/chat/middleware/builder.ts +29 -4
  52. package/src/activities/chat/middleware/compose.ts +95 -5
  53. package/src/activities/chat/middleware/define.ts +13 -3
  54. package/src/activities/chat/middleware/generic-interrupts.ts +26 -0
  55. package/src/activities/chat/middleware/index.ts +15 -0
  56. package/src/activities/chat/middleware/types.ts +127 -2
  57. package/src/activities/chat/stream/processor.ts +21 -0
  58. package/src/adapter-internals.ts +20 -0
  59. package/src/client.ts +20 -0
  60. package/src/generic-interrupt-continuation.ts +162 -0
  61. package/src/index.ts +34 -0
  62. package/src/interrupt-definition.ts +581 -0
  63. package/src/interrupt-resume.ts +156 -25
  64. package/src/interrupts.ts +13 -3
  65. package/src/types.ts +11 -3
  66. 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
package/src/types.ts CHANGED
@@ -369,6 +369,12 @@ export interface ModelMessage<
369
369
  toolCalls?: Array<ToolCall>
370
370
  toolCallId?: string
371
371
  thinking?: Array<{ content: string; signature?: string }>
372
+ /**
373
+ * Completed structured output represented by this assistant message.
374
+ * `content` remains the provider-facing JSON text; this field preserves the
375
+ * typed UI part across persistence and message conversion.
376
+ */
377
+ structuredOutput?: StructuredOutputPart
372
378
  /**
373
379
  * Optional stable message id. Providers ignore it; it exists so a persisted
374
380
  * transcript can retain the streaming `messageId` and survive the
@@ -1028,8 +1034,7 @@ export interface TextOptions<
1028
1034
 
1029
1035
  /**
1030
1036
  * AG-UI interrupt resume responses supplied by the client on a follow-up run.
1031
- * Threaded through request parsing now so later runtime behavior can resolve
1032
- * upstream-native interrupts.
1037
+ * A first-party generic item carries the original request in `metadata`.
1033
1038
  */
1034
1039
  resume?: Array<RunAgentResumeItem>
1035
1040
 
@@ -1128,7 +1133,10 @@ export type Interrupt = AGUIInterrupt
1128
1133
 
1129
1134
  export type RunFinishedOutcome = AGUIRunFinishedOutcome
1130
1135
 
1131
- export type RunAgentResumeItem = AGUIResumeEntry
1136
+ export type RunAgentResumeItem = AGUIResumeEntry & {
1137
+ /** AG-UI resume metadata. First-party generic requests ride here. */
1138
+ metadata?: Record<string, unknown>
1139
+ }
1132
1140
 
1133
1141
  /**
1134
1142
  * Emitted when a run completes successfully.
@@ -2,7 +2,6 @@ import { AGUIError } from '@ag-ui/core'
2
2
  import type {
3
3
  Context as AGUIContext,
4
4
  Message as AGUIMessage,
5
- ResumeEntry as AGUIResumeEntry,
6
5
  Role as AGUIRole,
7
6
  } from '@ag-ui/core'
8
7
  import type {
@@ -22,6 +21,7 @@ const KNOWN_PART_TYPES = new Set([
22
21
  'tool-call',
23
22
  'tool-result',
24
23
  'thinking',
24
+ 'structured-output',
25
25
  ])
26
26
 
27
27
  function isValidParts(value: unknown): value is Array<{ type: string }> {
@@ -30,6 +30,10 @@ function isValidParts(value: unknown): value is Array<{ type: string }> {
30
30
  if (!p || typeof p !== 'object') return false
31
31
  const type = (p as { type?: unknown }).type
32
32
  if (typeof type !== 'string' || !KNOWN_PART_TYPES.has(type)) return false
33
+ if (type === 'structured-output') {
34
+ const raw = (p as { raw?: unknown }).raw
35
+ if (raw !== undefined && typeof raw !== 'string') return false
36
+ }
33
37
  }
34
38
  return true
35
39
  }
@@ -173,20 +177,29 @@ function validateContext(value: unknown, index: number): AGUIContext {
173
177
  }
174
178
  }
175
179
 
176
- function validateResumeEntry(value: unknown, index: number): AGUIResumeEntry {
180
+ function validateResumeEntry(
181
+ value: unknown,
182
+ index: number,
183
+ ): RunAgentResumeItem {
177
184
  const at = `resume[${index}]`
178
185
  if (!isRecord(value)) invalidBody(`${at} must be an object`)
179
186
  const status = value.status
180
187
  if (status !== 'resolved' && status !== 'cancelled') {
181
188
  invalidBody(`${at}.status must be "resolved" or "cancelled"`)
182
189
  }
183
- const entry: AGUIResumeEntry = {
190
+ const entry: RunAgentResumeItem = {
184
191
  interruptId: requireString(value.interruptId, `${at}.interruptId`),
185
192
  status,
186
193
  }
187
194
  // Omit the key entirely when absent, matching the optional-field shape the
188
195
  // schema produced.
189
196
  if (value.payload !== undefined) entry.payload = value.payload
197
+ if (value.metadata !== undefined) {
198
+ if (!isRecord(value.metadata)) {
199
+ invalidBody(`${at}.metadata must be an object`)
200
+ }
201
+ entry.metadata = value.metadata
202
+ }
190
203
  return entry
191
204
  }
192
205