@tanstack/ai 0.53.0 → 0.55.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/README.md +14 -13
- package/dist/esm/activities/chat/index.js +22 -4
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.d.ts +21 -1
- package/dist/esm/activities/chat/messages.js +50 -1
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +17 -0
- package/dist/esm/activities/chat/stream/processor.js +27 -0
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/generateLiveVideo/adapter.d.ts +69 -0
- package/dist/esm/activities/generateLiveVideo/adapter.js +23 -0
- package/dist/esm/activities/generateLiveVideo/adapter.js.map +1 -0
- package/dist/esm/activities/generateLiveVideo/index.d.ts +99 -0
- package/dist/esm/activities/generateLiveVideo/index.js +162 -0
- package/dist/esm/activities/generateLiveVideo/index.js.map +1 -0
- package/dist/esm/activities/generateVideo/index.js +3 -1
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateWorld/adapter.d.ts +69 -0
- package/dist/esm/activities/generateWorld/adapter.js +23 -0
- package/dist/esm/activities/generateWorld/adapter.js.map +1 -0
- package/dist/esm/activities/generateWorld/index.d.ts +99 -0
- package/dist/esm/activities/generateWorld/index.js +162 -0
- package/dist/esm/activities/generateWorld/index.js.map +1 -0
- package/dist/esm/activities/index.d.ts +8 -2
- package/dist/esm/activities/index.js +11 -7
- package/dist/esm/activities/middleware/types.d.ts +1 -1
- package/dist/esm/client.d.ts +4 -2
- package/dist/esm/client.js +3 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +3 -1
- package/dist/esm/middlewares/otel.js +3 -1
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/types.d.ts +112 -0
- package/package.json +3 -3
- package/skills/ai-core/adapter-configuration/SKILL.md +91 -43
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +39 -21
- package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +5 -0
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +14 -6
- package/skills/ai-core/adapter-configuration/references/grok-adapter.md +33 -25
- package/skills/ai-core/adapter-configuration/references/groq-adapter.md +7 -2
- package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +25 -12
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +19 -9
- package/skills/ai-core/adapter-configuration/references/openrouter-adapter.md +34 -21
- package/skills/ai-core/ag-ui-protocol/SKILL.md +16 -10
- package/skills/ai-core/chat-experience/SKILL.md +228 -108
- package/skills/ai-core/client-persistence/SKILL.md +21 -9
- package/skills/ai-core/custom-backend-integration/SKILL.md +86 -52
- package/skills/ai-core/debug-logging/SKILL.md +100 -18
- package/skills/ai-core/locks/SKILL.md +35 -7
- package/skills/ai-core/media-generation/SKILL.md +136 -61
- package/skills/ai-core/middleware/SKILL.md +174 -69
- package/skills/ai-core/structured-outputs/SKILL.md +98 -49
- package/skills/ai-core/tool-calling/SKILL.md +245 -158
- package/src/activities/chat/index.ts +29 -7
- package/src/activities/chat/messages.ts +60 -0
- package/src/activities/chat/stream/processor.ts +31 -0
- package/src/activities/generateLiveVideo/adapter.ts +99 -0
- package/src/activities/generateLiveVideo/index.ts +339 -0
- package/src/activities/generateVideo/index.ts +3 -4
- package/src/activities/generateWorld/adapter.ts +96 -0
- package/src/activities/generateWorld/index.ts +339 -0
- package/src/activities/index.ts +44 -0
- package/src/activities/middleware/types.ts +2 -0
- package/src/client.ts +8 -0
- package/src/index.ts +8 -0
- package/src/middlewares/otel.ts +2 -0
- package/src/types.ts +128 -0
|
@@ -63,10 +63,12 @@ import {
|
|
|
63
63
|
import { maxIterations as maxIterationsStrategy } from './agent-loop-strategies'
|
|
64
64
|
import { isCancelRequestedReason } from './cancel'
|
|
65
65
|
import {
|
|
66
|
+
appendUiResourceToModelMessages,
|
|
66
67
|
convertMessagesToModelMessages,
|
|
67
68
|
generateMessageId,
|
|
68
69
|
modelMessagesToUIMessages,
|
|
69
70
|
safeJsonStringify,
|
|
71
|
+
uiResourcePartFromCustomValue,
|
|
70
72
|
} from './messages'
|
|
71
73
|
import { MiddlewareRunner } from './middleware/compose'
|
|
72
74
|
import { getRunDetached } from './middleware/run-store'
|
|
@@ -1245,7 +1247,7 @@ class TextEngine<
|
|
|
1245
1247
|
...this.deferredModelRunFinishedChunks,
|
|
1246
1248
|
)
|
|
1247
1249
|
this.deferredModelRunFinishedChunks = []
|
|
1248
|
-
} else {
|
|
1250
|
+
} else if (!this.finalStructuredOutput?.nativeCombined) {
|
|
1249
1251
|
yield* this.flushDeferredModelRunFinishedChunks()
|
|
1250
1252
|
}
|
|
1251
1253
|
} else {
|
|
@@ -1279,6 +1281,9 @@ class TextEngine<
|
|
|
1279
1281
|
) {
|
|
1280
1282
|
if (this.finalStructuredOutput.nativeCombined === true) {
|
|
1281
1283
|
yield* this.harvestCombinedStructuredOutput()
|
|
1284
|
+
if (!this.finalizationError && !this.isCancelled()) {
|
|
1285
|
+
yield* this.flushDeferredModelRunFinishedChunks()
|
|
1286
|
+
}
|
|
1282
1287
|
} else {
|
|
1283
1288
|
yield* this.runStructuredFinalization()
|
|
1284
1289
|
}
|
|
@@ -2767,6 +2772,22 @@ class TextEngine<
|
|
|
2767
2772
|
}
|
|
2768
2773
|
}
|
|
2769
2774
|
|
|
2775
|
+
/**
|
|
2776
|
+
* Record a `ui-resource` CUSTOM chunk on the assistant ModelMessage owning
|
|
2777
|
+
* its `toolCallId` so the resource survives later MESSAGES_SNAPSHOT chunks
|
|
2778
|
+
* (e.g. the interrupt snapshot emitted when the run pauses on a client
|
|
2779
|
+
* tool). Mirrors the anchor-preserving approach used for
|
|
2780
|
+
* `toolCallMetadata` (#867). See #1397.
|
|
2781
|
+
*/
|
|
2782
|
+
private recordEmittedUiResource(value: unknown): void {
|
|
2783
|
+
const part = uiResourcePartFromCustomValue(value)
|
|
2784
|
+
if (!part) return
|
|
2785
|
+
const next = appendUiResourceToModelMessages(this.messages, part)
|
|
2786
|
+
if (next === this.messages) return
|
|
2787
|
+
this.messages = next
|
|
2788
|
+
this.middlewareCtx.messages = this.messages
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2770
2791
|
private buildMessagesSnapshotChunk(): StreamChunk {
|
|
2771
2792
|
const withIds = this.messages.map((message, index) => ({
|
|
2772
2793
|
...message,
|
|
@@ -3926,12 +3947,8 @@ class TextEngine<
|
|
|
3926
3947
|
}
|
|
3927
3948
|
|
|
3928
3949
|
// On success, emit the synthetic `structured-output.complete` carrying
|
|
3929
|
-
// the parsed object + raw text
|
|
3930
|
-
//
|
|
3931
|
-
// terminal RUN_FINISHED has already cleared `activeMessageIds` (the
|
|
3932
|
-
// complete event yields AFTER the loop ends, by which point
|
|
3933
|
-
// `getActiveAssistantMessageId()` returns null and would otherwise drop
|
|
3934
|
-
// the event silently).
|
|
3950
|
+
// the parsed object + raw text before the deferred RUN_FINISHED. Pin
|
|
3951
|
+
// the messageId so the client targets the schema-constrained turn.
|
|
3935
3952
|
if (
|
|
3936
3953
|
this.structuredOutputResult &&
|
|
3937
3954
|
!this.finalizationError &&
|
|
@@ -4432,6 +4449,11 @@ class TextEngine<
|
|
|
4432
4449
|
if (this.hasPublicRunStarted) continue
|
|
4433
4450
|
this.hasPublicRunStarted = true
|
|
4434
4451
|
}
|
|
4452
|
+
// Persist MCP Apps ui-resource emissions onto the tool-call anchor
|
|
4453
|
+
// message so interrupt MESSAGES_SNAPSHOT chunks keep them (#1397).
|
|
4454
|
+
if (spec.type === EventType.CUSTOM && spec.name === 'ui-resource') {
|
|
4455
|
+
this.recordEmittedUiResource((spec as CustomEvent).value)
|
|
4456
|
+
}
|
|
4435
4457
|
yield spec
|
|
4436
4458
|
this.middlewareCtx.chunkIndex++
|
|
4437
4459
|
}
|
|
@@ -387,6 +387,66 @@ function appendUiResources(
|
|
|
387
387
|
return { ...ui, parts: [...ui.parts, ...extra] }
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Build a UIResourcePart from the value of a CUSTOM `ui-resource` chunk
|
|
392
|
+
* emitted via `ctx.emitCustomEvent('ui-resource', ...)` (MCP Apps). The
|
|
393
|
+
* emission-side value carries `resource`/`serverId`/`toolName` plus the
|
|
394
|
+
* `toolCallId` stamped by the tool-call context wrapper — the `type`
|
|
395
|
+
* discriminator is added here. Returns undefined when the value does not
|
|
396
|
+
* match the ui-resource shape.
|
|
397
|
+
*/
|
|
398
|
+
export function uiResourcePartFromCustomValue(
|
|
399
|
+
value: unknown,
|
|
400
|
+
): UIResourcePart | undefined {
|
|
401
|
+
if (!isRecord(value)) return undefined
|
|
402
|
+
const part: unknown = { type: 'ui-resource', ...value }
|
|
403
|
+
return isUiResourcePart(part) ? part : undefined
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Store an emitted ui-resource part on the assistant ModelMessage that owns
|
|
408
|
+
* its `toolCallId` (the tool-call anchor), so it survives later
|
|
409
|
+
* MESSAGES_SNAPSHOT chunks — e.g. the interrupt snapshot emitted when the
|
|
410
|
+
* run pauses on a client tool (#1397). Mirrors how `toolCallMetadata` is
|
|
411
|
+
* preserved on the anchor (#867).
|
|
412
|
+
*
|
|
413
|
+
* Returns the SAME array reference when no anchor owns the tool call or the
|
|
414
|
+
* resource is already stored (idempotent).
|
|
415
|
+
*/
|
|
416
|
+
export function appendUiResourceToModelMessages(
|
|
417
|
+
messages: Array<ModelMessage>,
|
|
418
|
+
part: UIResourcePart,
|
|
419
|
+
): Array<ModelMessage> {
|
|
420
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
421
|
+
const message = messages[index]
|
|
422
|
+
if (!message || message.role !== 'assistant') continue
|
|
423
|
+
const ownsToolCall = message.toolCalls?.some(
|
|
424
|
+
(toolCall) => toolCall.id === part.toolCallId,
|
|
425
|
+
)
|
|
426
|
+
if (!ownsToolCall) continue
|
|
427
|
+
const previous = tanstackMetadata(message)?.uiResources ?? []
|
|
428
|
+
if (
|
|
429
|
+
previous.some((stored) => uiResourceKey(stored) === uiResourceKey(part))
|
|
430
|
+
) {
|
|
431
|
+
return messages
|
|
432
|
+
}
|
|
433
|
+
const nextMessage = {
|
|
434
|
+
...message,
|
|
435
|
+
metadata: {
|
|
436
|
+
...message.metadata,
|
|
437
|
+
tanstack: {
|
|
438
|
+
...tanstackMetadata(message),
|
|
439
|
+
uiResources: [...previous, part],
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
const next = messages.slice()
|
|
444
|
+
next[index] = nextMessage
|
|
445
|
+
return next
|
|
446
|
+
}
|
|
447
|
+
return messages
|
|
448
|
+
}
|
|
449
|
+
|
|
390
450
|
function assistantMetadata(
|
|
391
451
|
uiMessage: UIMessage,
|
|
392
452
|
): UIMessage['metadata'] | undefined {
|
|
@@ -242,6 +242,37 @@ export class StreamProcessor {
|
|
|
242
242
|
this.emitMessagesChange()
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Put older UI messages at the front of the conversation.
|
|
247
|
+
*
|
|
248
|
+
* Skip a message if its id is already in the list. Keep the existing message.
|
|
249
|
+
* Then emit the same messages-change event as `setMessages`.
|
|
250
|
+
*
|
|
251
|
+
* Use this for older history pages. The first hydrate window uses `setMessages`.
|
|
252
|
+
*
|
|
253
|
+
* @param messages Older UI messages in insertion order. The first item is the oldest.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* processor.setMessages([newest])
|
|
258
|
+
* processor.prependMessages([oldest])
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
prependMessages(messages: Array<UIMessage>) {
|
|
262
|
+
const existingIds = new Set(this.messages.map((message) => message.id))
|
|
263
|
+
const olderMessages: Array<UIMessage> = []
|
|
264
|
+
for (const message of messages) {
|
|
265
|
+
const isDuplicate = existingIds.has(message.id)
|
|
266
|
+
if (isDuplicate) {
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
existingIds.add(message.id)
|
|
270
|
+
olderMessages.push(message)
|
|
271
|
+
}
|
|
272
|
+
this.messages = [...olderMessages, ...this.messages]
|
|
273
|
+
this.emitMessagesChange()
|
|
274
|
+
}
|
|
275
|
+
|
|
245
276
|
/**
|
|
246
277
|
* Add a user message to the conversation.
|
|
247
278
|
* Supports both simple string content and multimodal content arrays.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LiveVideoGenerationOptions,
|
|
3
|
+
LiveVideoGenerationResult,
|
|
4
|
+
} from '../../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for live generation adapter instances.
|
|
8
|
+
*
|
|
9
|
+
* @experimental Live generation is an experimental feature and may change.
|
|
10
|
+
*/
|
|
11
|
+
export interface LiveVideoAdapterConfig {
|
|
12
|
+
apiKey?: string
|
|
13
|
+
baseUrl?: string
|
|
14
|
+
timeout?: number
|
|
15
|
+
maxRetries?: number
|
|
16
|
+
headers?: Record<string, string>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Live adapter interface with pre-resolved generics.
|
|
21
|
+
*
|
|
22
|
+
* An adapter is created by a provider function: `provider('model')` → `adapter`.
|
|
23
|
+
* All type resolution happens at the provider call site, not in this interface.
|
|
24
|
+
*
|
|
25
|
+
* Generic parameters:
|
|
26
|
+
* - TModel: The specific model name (e.g. 'helios')
|
|
27
|
+
* - TProviderOptions: Provider-specific options (already resolved)
|
|
28
|
+
*
|
|
29
|
+
* @experimental Live generation is an experimental feature and may change.
|
|
30
|
+
*/
|
|
31
|
+
export interface LiveVideoAdapter<
|
|
32
|
+
TModel extends string = string,
|
|
33
|
+
TProviderOptions extends object = Record<string, unknown>,
|
|
34
|
+
> {
|
|
35
|
+
/** Discriminator for adapter kind - used to determine API shape */
|
|
36
|
+
readonly kind: 'liveVideo'
|
|
37
|
+
/** Adapter name identifier */
|
|
38
|
+
readonly name: string
|
|
39
|
+
/** The model this adapter is configured for */
|
|
40
|
+
readonly model: TModel
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @internal Type-only properties for inference. Not assigned at runtime.
|
|
44
|
+
*/
|
|
45
|
+
'~types': {
|
|
46
|
+
providerOptions: TProviderOptions
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Open a live video session from a prompt.
|
|
51
|
+
*
|
|
52
|
+
* Server adapters typically mint a short-lived token and return it with the
|
|
53
|
+
* prompt so a browser can connect, set the prompt, and start streaming.
|
|
54
|
+
*/
|
|
55
|
+
createLiveVideo: (
|
|
56
|
+
options: LiveVideoGenerationOptions<TProviderOptions>,
|
|
57
|
+
) => Promise<LiveVideoGenerationResult>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A LiveVideoAdapter with any/unknown type parameters.
|
|
62
|
+
* Useful as a constraint in generic functions and interfaces.
|
|
63
|
+
*/
|
|
64
|
+
export type AnyLiveVideoAdapter = LiveVideoAdapter<any, any>
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Abstract base class for live generation adapters.
|
|
68
|
+
* Extend this class to implement a live adapter for a specific provider.
|
|
69
|
+
*
|
|
70
|
+
* @experimental Live generation is an experimental feature and may change.
|
|
71
|
+
*/
|
|
72
|
+
export abstract class BaseLiveVideoAdapter<
|
|
73
|
+
TModel extends string = string,
|
|
74
|
+
TProviderOptions extends object = Record<string, unknown>,
|
|
75
|
+
> implements LiveVideoAdapter<TModel, TProviderOptions> {
|
|
76
|
+
readonly kind = 'liveVideo' as const
|
|
77
|
+
abstract readonly name: string
|
|
78
|
+
readonly model: TModel
|
|
79
|
+
|
|
80
|
+
// Type-only property - never assigned at runtime
|
|
81
|
+
declare '~types': {
|
|
82
|
+
providerOptions: TProviderOptions
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected config: LiveVideoAdapterConfig
|
|
86
|
+
|
|
87
|
+
constructor(model: TModel, config: LiveVideoAdapterConfig = {}) {
|
|
88
|
+
this.config = config
|
|
89
|
+
this.model = model
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
abstract createLiveVideo(
|
|
93
|
+
options: LiveVideoGenerationOptions<TProviderOptions>,
|
|
94
|
+
): Promise<LiveVideoGenerationResult>
|
|
95
|
+
|
|
96
|
+
protected generateId(): string {
|
|
97
|
+
return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}`
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Activity (Experimental)
|
|
3
|
+
*
|
|
4
|
+
* Mints a session token for a live, prompt-steerable video session. Unlike
|
|
5
|
+
* generateVideo (a job that finishes with a URL), the browser then connects
|
|
6
|
+
* with the token, sets the prompt, and streams until stop/close.
|
|
7
|
+
*
|
|
8
|
+
* @experimental Live generation is an experimental feature and may change.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { aiEventClient } from '@tanstack/ai-event-client'
|
|
12
|
+
import { streamGenerationResult } from '../stream-generation-result.js'
|
|
13
|
+
import { resolveDebugOption } from '../../logger/resolve'
|
|
14
|
+
import {
|
|
15
|
+
applyGenerationResultTransforms,
|
|
16
|
+
createGenerationContext,
|
|
17
|
+
runGenerationAbort,
|
|
18
|
+
runGenerationError,
|
|
19
|
+
runGenerationFinish,
|
|
20
|
+
runGenerationStart,
|
|
21
|
+
runGenerationUsage,
|
|
22
|
+
} from '../middleware/run'
|
|
23
|
+
import {
|
|
24
|
+
abortReasonMessage,
|
|
25
|
+
createActivityAbortControls,
|
|
26
|
+
isActivityAbortError,
|
|
27
|
+
raceWithAbort,
|
|
28
|
+
} from '../../utilities/activity-abort'
|
|
29
|
+
import type { InternalLogger } from '../../logger/internal-logger'
|
|
30
|
+
import type { DebugOption } from '../../logger/types'
|
|
31
|
+
import type { GenerationMiddleware } from '../middleware/types'
|
|
32
|
+
import type { LiveVideoAdapter } from './adapter'
|
|
33
|
+
import type { StreamChunk, LiveVideoGenerationResult } from '../../types'
|
|
34
|
+
|
|
35
|
+
// ===========================
|
|
36
|
+
// Activity Kind
|
|
37
|
+
// ===========================
|
|
38
|
+
|
|
39
|
+
/** The adapter kind this activity handles */
|
|
40
|
+
export const kind = 'liveVideo' as const
|
|
41
|
+
|
|
42
|
+
// ===========================
|
|
43
|
+
// Type Extraction Helpers
|
|
44
|
+
// ===========================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract provider options from a LiveVideoAdapter via ~types.
|
|
48
|
+
*/
|
|
49
|
+
export type LiveVideoProviderOptions<TAdapter> = TAdapter extends {
|
|
50
|
+
'~types': { providerOptions: infer P extends object }
|
|
51
|
+
}
|
|
52
|
+
? P
|
|
53
|
+
: object
|
|
54
|
+
|
|
55
|
+
// ===========================
|
|
56
|
+
// Activity Options Type
|
|
57
|
+
// ===========================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Options for the live generation activity.
|
|
61
|
+
* The model is extracted from the adapter's model property.
|
|
62
|
+
*
|
|
63
|
+
* @template TAdapter - The live adapter type
|
|
64
|
+
* @template TStream - Whether to stream the output
|
|
65
|
+
*
|
|
66
|
+
* @experimental Live generation is an experimental feature and may change.
|
|
67
|
+
*/
|
|
68
|
+
export interface LiveVideoActivityOptions<
|
|
69
|
+
TAdapter extends LiveVideoAdapter<string, LiveVideoProviderOptions<TAdapter>>,
|
|
70
|
+
TStream extends boolean = false,
|
|
71
|
+
> {
|
|
72
|
+
/** The live adapter to use (must be created with a model) */
|
|
73
|
+
adapter: TAdapter & { kind: typeof kind }
|
|
74
|
+
/** Natural-language description of the shot or scene */
|
|
75
|
+
prompt: string
|
|
76
|
+
/** Provider-specific options for live generation */
|
|
77
|
+
modelOptions?: LiveVideoProviderOptions<TAdapter>
|
|
78
|
+
/**
|
|
79
|
+
* Whether to wrap the token result as StreamChunks for SSE transport.
|
|
80
|
+
* This is not the live video. When false or omitted, returns
|
|
81
|
+
* Promise<LiveVideoGenerationResult>.
|
|
82
|
+
*
|
|
83
|
+
* @default false
|
|
84
|
+
*/
|
|
85
|
+
stream?: TStream
|
|
86
|
+
/**
|
|
87
|
+
* Enable debug logging. Pass `true` to enable all categories, `false` to
|
|
88
|
+
* silence everything including errors, or a `DebugConfig` object for granular
|
|
89
|
+
* control and/or a custom `Logger`.
|
|
90
|
+
*/
|
|
91
|
+
debug?: DebugOption
|
|
92
|
+
/**
|
|
93
|
+
* Observe-only middleware notified on start, usage, success, and error. Pass
|
|
94
|
+
* `otelMiddleware()` to emit OpenTelemetry spans, or implement the
|
|
95
|
+
* `GenerationMiddleware` contract for a custom backend.
|
|
96
|
+
*/
|
|
97
|
+
middleware?: Array<GenerationMiddleware>
|
|
98
|
+
/** Stable conversation/thread id for correlating this run when persisted. */
|
|
99
|
+
threadId?: string
|
|
100
|
+
/** Stable run id for correlating this run when persisted. */
|
|
101
|
+
runId?: string
|
|
102
|
+
/**
|
|
103
|
+
* Maximum duration of the token mint in milliseconds.
|
|
104
|
+
* No SDK-wide default. Composed with {@link abortSignal}; the first abort wins.
|
|
105
|
+
*/
|
|
106
|
+
timeout?: number
|
|
107
|
+
/**
|
|
108
|
+
* Caller cancellation signal (request disconnects, job/runtime cancellation).
|
|
109
|
+
* Composed with {@link timeout} into an effective signal forwarded to the
|
|
110
|
+
* adapter. Request-specific — not stored on global provider client config.
|
|
111
|
+
*/
|
|
112
|
+
abortSignal?: AbortSignal
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ===========================
|
|
116
|
+
// Activity Result Type
|
|
117
|
+
// ===========================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Result type for the live generation activity.
|
|
121
|
+
* - If stream is true: AsyncIterable<StreamChunk>
|
|
122
|
+
* - Otherwise: Promise<LiveVideoGenerationResult>
|
|
123
|
+
*/
|
|
124
|
+
export type LiveVideoActivityResult<TStream extends boolean = false> =
|
|
125
|
+
TStream extends true
|
|
126
|
+
? AsyncIterable<StreamChunk>
|
|
127
|
+
: Promise<LiveVideoGenerationResult>
|
|
128
|
+
|
|
129
|
+
function createId(prefix: string): string {
|
|
130
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ===========================
|
|
134
|
+
// Activity Implementation
|
|
135
|
+
// ===========================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Live generation activity - opens a live, prompt-steerable video session.
|
|
139
|
+
*
|
|
140
|
+
* @example Mint a session token on the server
|
|
141
|
+
* ```ts
|
|
142
|
+
* import { generateLiveVideo } from '@tanstack/ai'
|
|
143
|
+
* import { reactorVideo } from '@tanstack/ai-reactor'
|
|
144
|
+
*
|
|
145
|
+
* const live = await generateLiveVideo({
|
|
146
|
+
* adapter: reactorVideo('helios'),
|
|
147
|
+
* prompt: 'A red sports car powerslides a mountain hairpin',
|
|
148
|
+
* })
|
|
149
|
+
*
|
|
150
|
+
* // Hand live.token, live.model, and live.prompt to the browser.
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
153
|
+
* @experimental Live generation is an experimental feature and may change.
|
|
154
|
+
*/
|
|
155
|
+
export function generateLiveVideo<
|
|
156
|
+
TAdapter extends LiveVideoAdapter<string, LiveVideoProviderOptions<TAdapter>>,
|
|
157
|
+
TStream extends boolean = false,
|
|
158
|
+
>(
|
|
159
|
+
options: LiveVideoActivityOptions<TAdapter, TStream>,
|
|
160
|
+
): LiveVideoActivityResult<TStream> {
|
|
161
|
+
if (options.stream) {
|
|
162
|
+
return streamGenerationResult(
|
|
163
|
+
(resolved) => runGenerateLiveVideo({ ...options, runId: resolved.runId }),
|
|
164
|
+
options,
|
|
165
|
+
) as LiveVideoActivityResult<TStream>
|
|
166
|
+
}
|
|
167
|
+
return runGenerateLiveVideo(options) as LiveVideoActivityResult<TStream>
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Run the core live generation logic (non-streaming).
|
|
172
|
+
*/
|
|
173
|
+
async function runGenerateLiveVideo<
|
|
174
|
+
TAdapter extends LiveVideoAdapter<string, LiveVideoProviderOptions<TAdapter>>,
|
|
175
|
+
>(
|
|
176
|
+
options: LiveVideoActivityOptions<TAdapter, boolean>,
|
|
177
|
+
): Promise<LiveVideoGenerationResult> {
|
|
178
|
+
const {
|
|
179
|
+
adapter,
|
|
180
|
+
stream: _stream,
|
|
181
|
+
debug: _debug,
|
|
182
|
+
middleware,
|
|
183
|
+
threadId,
|
|
184
|
+
runId,
|
|
185
|
+
timeout,
|
|
186
|
+
abortSignal: callerAbortSignal,
|
|
187
|
+
...rest
|
|
188
|
+
} = options
|
|
189
|
+
const model = adapter.model
|
|
190
|
+
const requestId = createId('liveVideo')
|
|
191
|
+
const startTime = Date.now()
|
|
192
|
+
const logger: InternalLogger = resolveDebugOption(options.debug)
|
|
193
|
+
const abortControls = createActivityAbortControls({
|
|
194
|
+
timeout,
|
|
195
|
+
abortSignal: callerAbortSignal,
|
|
196
|
+
})
|
|
197
|
+
const providerName =
|
|
198
|
+
(adapter as { name?: string; provider?: string }).provider ??
|
|
199
|
+
(adapter as { name?: string }).name ??
|
|
200
|
+
'unknown'
|
|
201
|
+
|
|
202
|
+
const mwCtx = createGenerationContext({
|
|
203
|
+
requestId,
|
|
204
|
+
activity: 'liveVideo',
|
|
205
|
+
provider: adapter.name,
|
|
206
|
+
model,
|
|
207
|
+
modelOptions: rest.modelOptions,
|
|
208
|
+
threadId,
|
|
209
|
+
runId,
|
|
210
|
+
artifactInputs: { prompt: rest.prompt },
|
|
211
|
+
createId,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
await runGenerationStart(middleware, mwCtx)
|
|
215
|
+
|
|
216
|
+
aiEventClient.emit('liveVideo:request:started', {
|
|
217
|
+
requestId,
|
|
218
|
+
provider: adapter.name,
|
|
219
|
+
model,
|
|
220
|
+
prompt: rest.prompt,
|
|
221
|
+
timestamp: startTime,
|
|
222
|
+
...(rest.modelOptions !== undefined && {
|
|
223
|
+
modelOptions: rest.modelOptions as Record<string, unknown>,
|
|
224
|
+
}),
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
logger.request(`activity=generateLiveVideo provider=${providerName}`, {
|
|
228
|
+
provider: providerName,
|
|
229
|
+
model,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const rawResult = await raceWithAbort(
|
|
234
|
+
adapter.createLiveVideo({
|
|
235
|
+
...rest,
|
|
236
|
+
model,
|
|
237
|
+
logger,
|
|
238
|
+
...(abortControls.signal ? { abortSignal: abortControls.signal } : {}),
|
|
239
|
+
}),
|
|
240
|
+
abortControls.signal,
|
|
241
|
+
)
|
|
242
|
+
abortControls.clear()
|
|
243
|
+
const result = await applyGenerationResultTransforms(mwCtx, rawResult)
|
|
244
|
+
const elapsedMs = Date.now() - startTime
|
|
245
|
+
|
|
246
|
+
aiEventClient.emit('liveVideo:request:completed', {
|
|
247
|
+
requestId,
|
|
248
|
+
provider: adapter.name,
|
|
249
|
+
model: result.model,
|
|
250
|
+
prompt: result.prompt,
|
|
251
|
+
status: result.status,
|
|
252
|
+
duration: elapsedMs,
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
...(rest.modelOptions !== undefined && {
|
|
255
|
+
modelOptions: rest.modelOptions as Record<string, unknown>,
|
|
256
|
+
}),
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (result.usage) {
|
|
260
|
+
aiEventClient.emit('liveVideo:usage', {
|
|
261
|
+
requestId,
|
|
262
|
+
model: result.model,
|
|
263
|
+
usage: result.usage,
|
|
264
|
+
timestamp: Date.now(),
|
|
265
|
+
...(rest.modelOptions !== undefined && {
|
|
266
|
+
modelOptions: rest.modelOptions as Record<string, unknown>,
|
|
267
|
+
}),
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
logger.output(`activity=generateLiveVideo provider=${providerName}`, {
|
|
272
|
+
model: result.model,
|
|
273
|
+
status: result.status,
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)
|
|
277
|
+
await runGenerationFinish(middleware, mwCtx, {
|
|
278
|
+
duration: elapsedMs,
|
|
279
|
+
usage: result.usage,
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
return result
|
|
283
|
+
} catch (error) {
|
|
284
|
+
abortControls.clear()
|
|
285
|
+
const elapsedMs = Date.now() - startTime
|
|
286
|
+
const err = error as Error
|
|
287
|
+
aiEventClient.emit('liveVideo:request:error', {
|
|
288
|
+
requestId,
|
|
289
|
+
provider: adapter.name,
|
|
290
|
+
model,
|
|
291
|
+
error: { message: err.message, name: err.name },
|
|
292
|
+
duration: elapsedMs,
|
|
293
|
+
timestamp: Date.now(),
|
|
294
|
+
...(rest.modelOptions !== undefined && {
|
|
295
|
+
modelOptions: rest.modelOptions as Record<string, unknown>,
|
|
296
|
+
}),
|
|
297
|
+
})
|
|
298
|
+
if (isActivityAbortError(error, abortControls.signal)) {
|
|
299
|
+
await runGenerationAbort(middleware, mwCtx, {
|
|
300
|
+
reason: abortReasonMessage(error, abortControls.signal),
|
|
301
|
+
duration: elapsedMs,
|
|
302
|
+
})
|
|
303
|
+
} else {
|
|
304
|
+
await runGenerationError(middleware, mwCtx, {
|
|
305
|
+
error,
|
|
306
|
+
duration: elapsedMs,
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
logger.errors('generateLiveVideo activity failed', {
|
|
310
|
+
error,
|
|
311
|
+
source: 'generateLiveVideo',
|
|
312
|
+
})
|
|
313
|
+
throw error
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ===========================
|
|
318
|
+
// Options Factory
|
|
319
|
+
// ===========================
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Create typed options for the generateLiveVideo() function without executing.
|
|
323
|
+
*/
|
|
324
|
+
export function createLiveVideoOptions<
|
|
325
|
+
TAdapter extends LiveVideoAdapter<string, LiveVideoProviderOptions<TAdapter>>,
|
|
326
|
+
TStream extends boolean = false,
|
|
327
|
+
>(
|
|
328
|
+
options: LiveVideoActivityOptions<TAdapter, TStream>,
|
|
329
|
+
): LiveVideoActivityOptions<TAdapter, TStream> {
|
|
330
|
+
return options
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Re-export adapter types
|
|
334
|
+
export type {
|
|
335
|
+
LiveVideoAdapter,
|
|
336
|
+
LiveVideoAdapterConfig,
|
|
337
|
+
AnyLiveVideoAdapter,
|
|
338
|
+
} from './adapter'
|
|
339
|
+
export { BaseLiveVideoAdapter } from './adapter'
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Video Activity (Experimental)
|
|
3
3
|
*
|
|
4
|
-
* Generates videos from text prompts
|
|
4
|
+
* Generates videos from text prompts. Adapters use a jobs/polling
|
|
5
|
+
* architecture: create a job, poll for status, then fetch a download URL.
|
|
6
|
+
* For a live, prompt-steerable stream, use generateLiveVideo().
|
|
5
7
|
* This is a self-contained module with implementation, types, and JSDoc.
|
|
6
8
|
*
|
|
7
9
|
* @experimental Video generation is an experimental feature and may change.
|
|
@@ -537,9 +539,6 @@ async function runCreateVideoJob<
|
|
|
537
539
|
|
|
538
540
|
const mwCtx = contextFor(videoRunIdForJob(adapter.name, jobResult.jobId))
|
|
539
541
|
await runGenerationStart(middleware, mwCtx)
|
|
540
|
-
// Transforms see the submission result (no url yet, so nothing to copy into a
|
|
541
|
-
// blob store) purely so the run record captures the jobId and any prompt
|
|
542
|
-
// inputs. No finish hook: the run is still running.
|
|
543
542
|
return await applyGenerationResultTransforms(mwCtx, jobResult)
|
|
544
543
|
}
|
|
545
544
|
|