@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.
- package/dist/esm/activities/chat/index.js +4 -4
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.d.ts +7 -0
- package/dist/esm/activities/chat/messages.js +270 -64
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.js +7 -7
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/byok/define-provider.d.ts +23 -0
- package/dist/esm/byok/define-provider.js +19 -0
- package/dist/esm/byok/define-provider.js.map +1 -0
- package/dist/esm/byok/errors.d.ts +13 -0
- package/dist/esm/byok/errors.js +29 -0
- package/dist/esm/byok/errors.js.map +1 -0
- package/dist/esm/byok/get-key.d.ts +10 -0
- package/dist/esm/byok/get-key.js +26 -0
- package/dist/esm/byok/get-key.js.map +1 -0
- package/dist/esm/byok/missing.d.ts +11 -0
- package/dist/esm/byok/missing.js +30 -0
- package/dist/esm/byok/missing.js.map +1 -0
- package/dist/esm/byok/providers.d.ts +13 -0
- package/dist/esm/byok/providers.js +18 -0
- package/dist/esm/byok/providers.js.map +1 -0
- package/dist/esm/byok/scrub.d.ts +2 -0
- package/dist/esm/byok/scrub.js +17 -0
- package/dist/esm/byok/scrub.js.map +1 -0
- package/dist/esm/byok/server.d.ts +3 -0
- package/dist/esm/byok/server.js +3 -0
- package/dist/esm/byok.d.ts +8 -0
- package/dist/esm/byok.js +6 -0
- package/dist/esm/client.js +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/interrupt-resume.js +4 -4
- package/dist/esm/interrupt-resume.js.map +1 -1
- package/dist/esm/types.d.ts +18 -0
- package/dist/esm/utilities/ag-ui-wire.d.ts +20 -44
- package/dist/esm/utilities/ag-ui-wire.js +143 -34
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/package.json +10 -2
- package/skills/ai-core/chat-experience/SKILL.md +2 -2
- package/src/activities/chat/index.ts +3 -4
- package/src/activities/chat/messages.ts +344 -61
- package/src/activities/chat/stream/processor.ts +19 -10
- package/src/byok/define-provider.ts +46 -0
- package/src/byok/errors.ts +36 -0
- package/src/byok/get-key.ts +27 -0
- package/src/byok/missing.ts +43 -0
- package/src/byok/providers.ts +27 -0
- package/src/byok/scrub.ts +16 -0
- package/src/byok/server.ts +3 -0
- package/src/byok.ts +17 -0
- package/src/interrupt-resume.ts +14 -13
- package/src/types.ts +18 -0
- 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
|
|
839
|
-
|
|
840
|
-
|
|
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.
|
|
1054
|
-
msg.parts
|
|
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' &&
|
|
1114
|
-
|
|
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(
|
|
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
|
+
}
|
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'
|
package/src/interrupt-resume.ts
CHANGED
|
@@ -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.
|