@tanstack/ai 0.48.0 → 0.50.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.
Files changed (53) hide show
  1. package/dist/esm/activities/chat/index.js +4 -4
  2. package/dist/esm/activities/chat/index.js.map +1 -1
  3. package/dist/esm/activities/chat/messages.d.ts +7 -0
  4. package/dist/esm/activities/chat/messages.js +270 -64
  5. package/dist/esm/activities/chat/messages.js.map +1 -1
  6. package/dist/esm/activities/chat/stream/processor.js +7 -7
  7. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  8. package/dist/esm/byok/define-provider.d.ts +23 -0
  9. package/dist/esm/byok/define-provider.js +19 -0
  10. package/dist/esm/byok/define-provider.js.map +1 -0
  11. package/dist/esm/byok/errors.d.ts +13 -0
  12. package/dist/esm/byok/errors.js +29 -0
  13. package/dist/esm/byok/errors.js.map +1 -0
  14. package/dist/esm/byok/get-key.d.ts +10 -0
  15. package/dist/esm/byok/get-key.js +26 -0
  16. package/dist/esm/byok/get-key.js.map +1 -0
  17. package/dist/esm/byok/missing.d.ts +11 -0
  18. package/dist/esm/byok/missing.js +30 -0
  19. package/dist/esm/byok/missing.js.map +1 -0
  20. package/dist/esm/byok/providers.d.ts +13 -0
  21. package/dist/esm/byok/providers.js +18 -0
  22. package/dist/esm/byok/providers.js.map +1 -0
  23. package/dist/esm/byok/scrub.d.ts +2 -0
  24. package/dist/esm/byok/scrub.js +17 -0
  25. package/dist/esm/byok/scrub.js.map +1 -0
  26. package/dist/esm/byok/server.d.ts +3 -0
  27. package/dist/esm/byok/server.js +3 -0
  28. package/dist/esm/byok.d.ts +8 -0
  29. package/dist/esm/byok.js +6 -0
  30. package/dist/esm/client.js +1 -1
  31. package/dist/esm/index.js +2 -2
  32. package/dist/esm/interrupt-resume.js +4 -4
  33. package/dist/esm/interrupt-resume.js.map +1 -1
  34. package/dist/esm/types.d.ts +18 -0
  35. package/dist/esm/utilities/ag-ui-wire.d.ts +20 -44
  36. package/dist/esm/utilities/ag-ui-wire.js +143 -34
  37. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  38. package/package.json +10 -2
  39. package/skills/ai-core/chat-experience/SKILL.md +2 -2
  40. package/src/activities/chat/index.ts +3 -4
  41. package/src/activities/chat/messages.ts +344 -61
  42. package/src/activities/chat/stream/processor.ts +19 -10
  43. package/src/byok/define-provider.ts +46 -0
  44. package/src/byok/errors.ts +36 -0
  45. package/src/byok/get-key.ts +27 -0
  46. package/src/byok/missing.ts +43 -0
  47. package/src/byok/providers.ts +27 -0
  48. package/src/byok/scrub.ts +16 -0
  49. package/src/byok/server.ts +3 -0
  50. package/src/byok.ts +17 -0
  51. package/src/interrupt-resume.ts +14 -13
  52. package/src/types.ts +18 -0
  53. package/src/utilities/ag-ui-wire.ts +278 -73
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import {
21
21
  aguiSnapshotMessageToUIMessage,
22
+ coerceCreatedAt,
22
23
  generateMessageId,
23
24
  uiMessageToModelMessages,
24
25
  } from '../messages.js'
@@ -835,11 +836,10 @@ export class StreamProcessor {
835
836
  }
836
837
  : incomingRecord
837
838
  const metadata = mergeMetadata(message.metadata, toMerge)
838
- const createdAtRaw = tanstackMetadata(incomingRecord)?.createdAt
839
- const createdAt =
840
- typeof createdAtRaw === 'string' ? new Date(createdAtRaw) : undefined
841
- const createdAtValid =
842
- createdAt !== undefined && !Number.isNaN(createdAt.getTime())
839
+ const createdAt = coerceCreatedAt(
840
+ tanstackMetadata(incomingRecord)?.createdAt,
841
+ )
842
+ const createdAtValid = createdAt !== undefined
843
843
  this.messages = this.messages.map((msg) =>
844
844
  msg.id === messageId
845
845
  ? {
@@ -1050,8 +1050,10 @@ export class StreamProcessor {
1050
1050
  msg.parts.every((part) => part.type === 'thinking')
1051
1051
  const isToolResultOnly = (msg: UIMessage) =>
1052
1052
  msg.role === 'assistant' &&
1053
- msg.parts.length === 1 &&
1054
- msg.parts[0]?.type === 'tool-result'
1053
+ msg.parts.some((part) => part.type === 'tool-result') &&
1054
+ msg.parts.every(
1055
+ (part) => part.type === 'tool-result' || part.type === 'ui-resource',
1056
+ )
1055
1057
  const flushPending = () => {
1056
1058
  out.push(...pending)
1057
1059
  pending = []
@@ -1109,9 +1111,16 @@ export class StreamProcessor {
1109
1111
 
1110
1112
  const reconciled: Array<UIMessage> = []
1111
1113
  for (const msg of snapshot) {
1114
+ const toolResultParts = msg.parts.filter(
1115
+ (part): part is ToolResultPart => part.type === 'tool-result',
1116
+ )
1112
1117
  const toolResultPart =
1113
- msg.role === 'assistant' && msg.parts.length === 1
1114
- ? msg.parts.find((p): p is ToolResultPart => p.type === 'tool-result')
1118
+ msg.role === 'assistant' &&
1119
+ toolResultParts.length === 1 &&
1120
+ msg.parts.every(
1121
+ (part) => part.type === 'tool-result' || part.type === 'ui-resource',
1122
+ )
1123
+ ? toolResultParts[0]
1115
1124
  : undefined
1116
1125
 
1117
1126
  if (!toolResultPart) {
@@ -1170,7 +1179,7 @@ export class StreamProcessor {
1170
1179
  )
1171
1180
  }
1172
1181
  }
1173
- parts.push(toolResultPart)
1182
+ parts.push(...msg.parts)
1174
1183
  // Replace rather than push into `target.parts`: a snapshot message that
1175
1184
  // arrived already carrying `parts` (TanStack server echoing UIMessages)
1176
1185
  // shares its array with the incoming chunk, and mutating it in place
@@ -0,0 +1,46 @@
1
+ import { isProviderId } from './providers'
2
+
3
+ /**
4
+ * A BYOK provider declared by an adapter. `id` is the `x-byok-<id>` slug and
5
+ * is required — `{ id?: string }` is not a {@link ByokProvider}.
6
+ */
7
+ export interface ByokProvider<TId extends string = string> {
8
+ readonly id: TId
9
+ readonly label: string
10
+ /**
11
+ * Env var names the relay may read. Names only — never put `process.env`
12
+ * values here. This object is imported on the client.
13
+ */
14
+ readonly env?: ReadonlyArray<string>
15
+ }
16
+
17
+ /**
18
+ * Input for {@link defineByokProvider}. `id` cannot be optional: if `TId`
19
+ * includes `undefined`, `id` becomes `never` and the object is unassignable.
20
+ */
21
+ export type ByokProviderInit<TId extends string> = {
22
+ readonly id: undefined extends TId ? never : TId
23
+ readonly label: string
24
+ readonly env?: string | ReadonlyArray<string>
25
+ }
26
+
27
+ function normalizeEnv(
28
+ env: string | ReadonlyArray<string> | undefined,
29
+ ): ReadonlyArray<string> | undefined {
30
+ if (env === undefined) return undefined
31
+ return typeof env === 'string' ? [env] : env
32
+ }
33
+
34
+ export function defineByokProvider<const TId extends string>(
35
+ provider: ByokProviderInit<TId>,
36
+ ): ByokProvider<TId> {
37
+ if (!isProviderId(provider.id)) {
38
+ throw new Error(`Invalid BYOK provider id: ${String(provider.id)}`)
39
+ }
40
+ const env = normalizeEnv(provider.env)
41
+ return {
42
+ id: provider.id,
43
+ label: provider.label,
44
+ ...(env ? { env } : {}),
45
+ }
46
+ }
@@ -0,0 +1,36 @@
1
+ import type { ProviderId } from './providers'
2
+
3
+ export class ByokMissingError extends Error {
4
+ readonly provider: ProviderId
5
+
6
+ constructor(provider: ProviderId) {
7
+ super(`Missing ${provider} API key`)
8
+ this.name = 'ByokMissingError'
9
+ this.provider = provider
10
+ }
11
+ }
12
+
13
+ export class ByokBlockedError extends Error {
14
+ readonly provider: ProviderId
15
+ readonly reason: 'missing' | 'locked'
16
+
17
+ constructor(provider: ProviderId, reason: 'missing' | 'locked') {
18
+ super(
19
+ reason === 'locked'
20
+ ? `${provider} key is locked`
21
+ : `Missing ${provider} API key`,
22
+ )
23
+ this.name = 'ByokBlockedError'
24
+ this.provider = provider
25
+ this.reason = reason
26
+ }
27
+ }
28
+
29
+ export class ByokUnresolvedProviderError extends Error {
30
+ constructor() {
31
+ super(
32
+ 'BYOK is enabled but no provider slug was resolved. Pass byokProvider or forwardedProps.provider.',
33
+ )
34
+ this.name = 'ByokUnresolvedProviderError'
35
+ }
36
+ }
@@ -0,0 +1,27 @@
1
+ import { byokHeaderName, resolveProviderId } from './providers'
2
+ import type { ByokProvider } from './define-provider'
3
+ import type { ProviderId } from './providers'
4
+
5
+ /**
6
+ * Read a key on the relay. Import from `@tanstack/ai/byok/server` so this
7
+ * `process.env` access is not in the client graph.
8
+ *
9
+ * The header wins. A {@link ByokProvider} then tries `provider.env` in order.
10
+ * A slug is header-only.
11
+ */
12
+ export function getByokKey(
13
+ request: Request,
14
+ provider: ProviderId | ByokProvider,
15
+ ): string | null {
16
+ const value = request.headers.get(byokHeaderName(resolveProviderId(provider)))
17
+ if (typeof value === 'string') {
18
+ const trimmed = value.trim()
19
+ if (trimmed.length > 0) return trimmed
20
+ }
21
+ if (typeof provider === 'string') return null
22
+ for (const name of provider.env ?? []) {
23
+ const envValue = process.env[name]
24
+ if (typeof envValue === 'string' && envValue.length > 0) return envValue
25
+ }
26
+ return null
27
+ }
@@ -0,0 +1,43 @@
1
+ import { isProviderId, resolveProviderId } from './providers'
2
+ import type { ByokProvider } from './define-provider'
3
+ import type { ProviderId } from './providers'
4
+
5
+ export interface ByokMissingBody {
6
+ error: {
7
+ type: 'byok_missing'
8
+ provider: ProviderId
9
+ message: string
10
+ }
11
+ }
12
+
13
+ export function isByokMissingBody(value: unknown): value is ByokMissingBody {
14
+ if (typeof value !== 'object' || value === null) return false
15
+ if (!('error' in value)) return false
16
+ const error = value.error
17
+ if (typeof error !== 'object' || error === null) return false
18
+ if (!('type' in error) || error.type !== 'byok_missing') return false
19
+ if (!('provider' in error) || typeof error.provider !== 'string') {
20
+ return false
21
+ }
22
+ if (!isProviderId(error.provider)) return false
23
+ if (!('message' in error) || typeof error.message !== 'string') return false
24
+ return true
25
+ }
26
+
27
+ export function byokMissing(provider: ProviderId | ByokProvider): Response {
28
+ const id = resolveProviderId(provider)
29
+ if (!isProviderId(id)) {
30
+ throw new Error(`Invalid BYOK provider id: ${id}`)
31
+ }
32
+ const body: ByokMissingBody = {
33
+ error: {
34
+ type: 'byok_missing',
35
+ provider: id,
36
+ message: `Missing ${id} API key`,
37
+ },
38
+ }
39
+ return new Response(JSON.stringify(body), {
40
+ status: 401,
41
+ headers: { 'content-type': 'application/json' },
42
+ })
43
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Provider ids are open slugs, not a closed catalog. `@tanstack/ai` does not
3
+ * list adapters. Any matching string is a valid id and becomes `x-byok-<id>`.
4
+ */
5
+ export type ProviderId = string
6
+
7
+ /** `[a-z][a-z0-9-]{0,63}` — lowercase, no underscores, max 64 chars. */
8
+ export const BYOK_PROVIDER_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/
9
+
10
+ export const BYOK_HEADER_PREFIX = 'x-byok-'
11
+
12
+ export function isProviderId(value: unknown): value is ProviderId {
13
+ return typeof value === 'string' && BYOK_PROVIDER_ID_PATTERN.test(value)
14
+ }
15
+
16
+ export function resolveProviderId(
17
+ provider: string | { readonly id: string },
18
+ ): string {
19
+ return typeof provider === 'string' ? provider : provider.id
20
+ }
21
+
22
+ export function byokHeaderName(provider: string): string {
23
+ if (!isProviderId(provider)) {
24
+ throw new Error(`Invalid BYOK provider id: ${provider}`)
25
+ }
26
+ return `${BYOK_HEADER_PREFIX}${provider}`
27
+ }
@@ -0,0 +1,16 @@
1
+ export function maskKey(key: string): string {
2
+ if (key.length <= 4) return '••'
3
+ return key.slice(-4)
4
+ }
5
+
6
+ export function scrubSecrets(
7
+ input: string,
8
+ secrets: ReadonlyArray<string>,
9
+ ): string {
10
+ let next = input
11
+ for (const secret of secrets) {
12
+ if (secret.length === 0) continue
13
+ next = next.split(secret).join('[redacted]')
14
+ }
15
+ return next
16
+ }
@@ -0,0 +1,3 @@
1
+ export { getByokKey } from './get-key'
2
+ export { byokMissing, isByokMissingBody } from './missing'
3
+ export type { ByokMissingBody } from './missing'
package/src/byok.ts ADDED
@@ -0,0 +1,17 @@
1
+ export {
2
+ BYOK_PROVIDER_ID_PATTERN,
3
+ BYOK_HEADER_PREFIX,
4
+ byokHeaderName,
5
+ isProviderId,
6
+ } from './byok/providers'
7
+ export type { ProviderId } from './byok/providers'
8
+ export { defineByokProvider } from './byok/define-provider'
9
+ export type { ByokProvider, ByokProviderInit } from './byok/define-provider'
10
+ export { isByokMissingBody, byokMissing } from './byok/missing'
11
+ export type { ByokMissingBody } from './byok/missing'
12
+ export {
13
+ ByokMissingError,
14
+ ByokBlockedError,
15
+ ByokUnresolvedProviderError,
16
+ } from './byok/errors'
17
+ export { maskKey, scrubSecrets } from './byok/scrub'
@@ -445,6 +445,20 @@ export async function validateInterruptResumeBatch(
445
445
  continue
446
446
  }
447
447
 
448
+ if (entry.status === 'cancelled') {
449
+ if (entry.payload !== undefined) {
450
+ errors.push(
451
+ interruptItemError(
452
+ input,
453
+ record.interruptId,
454
+ 'invalid-payload',
455
+ `Cancelled interrupt ${record.interruptId} must not include a payload.`,
456
+ ),
457
+ )
458
+ }
459
+ continue
460
+ }
461
+
448
462
  const tool = runtimeTool(input.tools, binding.toolName)
449
463
  if (!tool) {
450
464
  errors.push(
@@ -511,19 +525,6 @@ export async function validateInterruptResumeBatch(
511
525
  }
512
526
  }
513
527
 
514
- if (entry.status === 'cancelled') {
515
- if (entry.payload !== undefined) {
516
- errors.push(
517
- interruptItemError(
518
- input,
519
- record.interruptId,
520
- 'invalid-payload',
521
- `Cancelled interrupt ${record.interruptId} must not include a payload.`,
522
- ),
523
- )
524
- }
525
- continue
526
- }
527
528
  if (schemaDrifted) continue
528
529
 
529
530
  if (binding.kind === 'client-tool-execution') {
package/src/types.ts CHANGED
@@ -372,6 +372,10 @@ export interface ModelMessage<
372
372
  toolCalls?: Array<ToolCall>
373
373
  toolCallId?: string
374
374
  thinking?: Array<{ content: string; signature?: string }>
375
+ /** Error reported by an AG-UI tool message. */
376
+ error?: string
377
+ /** Optional AG-UI message metadata. TanStack-owned fields live under `tanstack`. */
378
+ metadata?: Record<string, any>
375
379
  /**
376
380
  * Completed structured output represented by this assistant message.
377
381
  * `content` remains the provider-facing JSON text; this field preserves the
@@ -435,10 +439,14 @@ export interface ToolCallPart<TMetadata = unknown> {
435
439
 
436
440
  export interface ToolResultPart {
437
441
  type: 'tool-result'
442
+ id?: string
443
+ name?: string
438
444
  toolCallId: string
439
445
  content: string | Array<ContentPart>
440
446
  state: ToolResultState
441
447
  error?: string // Error message if state is "error"
448
+ metadata?: Record<string, unknown>
449
+ createdAt?: Date
442
450
  }
443
451
 
444
452
  export interface ThinkingPart {
@@ -524,9 +532,17 @@ export interface TanStackMessageMetadata {
524
532
  signature?: string
525
533
  /** Per-tool-call provider metadata keyed by tool call id (e.g. Gemini thoughtSignature). */
526
534
  toolCallMetadata?: Record<string, unknown>
535
+ toolResult?: {
536
+ id?: string
537
+ createdAt?: string
538
+ content?: Array<ContentPart>
539
+ }
527
540
  structuredOutput?: {
528
541
  status?: 'streaming' | 'complete' | 'error'
542
+ partial?: unknown
543
+ data?: unknown
529
544
  raw?: string
545
+ reasoning?: string
530
546
  errorMessage?: string
531
547
  }
532
548
  uiResources?: Array<UIResourcePart>
@@ -562,6 +578,8 @@ export interface UIMessage<TData = unknown> {
562
578
  role: 'system' | 'user' | 'assistant'
563
579
  parts: Array<MessagePart<TData>>
564
580
  createdAt?: Date
581
+ /** Optional AG-UI sender name. Converters preserve it across wire and persist. */
582
+ name?: string
565
583
  /**
566
584
  * Optional AG-UI metadata bag. TanStack writes the `tanstack` key.
567
585
  * User keys stay at the top.