@tanstack/ai 0.44.0 → 0.45.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/adapter.d.ts +13 -1
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.js +126 -68
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +24 -27
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.js +14 -13
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/approval-schema.js +11 -8
- package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -1
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.js +40 -31
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +3 -2
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +2 -0
- package/dist/esm/adapter-internals.js +3 -1
- package/dist/esm/interrupt-resume.js +24 -20
- package/dist/esm/interrupt-resume.js.map +1 -1
- package/dist/esm/interrupts.js +2 -1
- package/dist/esm/interrupts.js.map +1 -1
- package/dist/esm/logger/console-logger.js +1 -3
- package/dist/esm/logger/console-logger.js.map +1 -1
- package/dist/esm/logger/resolve.js +2 -1
- package/dist/esm/logger/resolve.js.map +1 -1
- package/dist/esm/stream-durability.js +1 -1
- package/dist/esm/stream-durability.js.map +1 -1
- package/dist/esm/types.d.ts +19 -16
- package/dist/esm/utilities/chat-params.js +1 -3
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/media-prompt.js +1 -3
- package/dist/esm/utilities/media-prompt.js.map +1 -1
- package/dist/esm/utilities/structured-output-events.d.ts +17 -0
- package/dist/esm/utilities/structured-output-events.js +32 -0
- package/dist/esm/utilities/structured-output-events.js.map +1 -0
- package/dist/esm/utilities/structured-output-text.d.ts +7 -0
- package/dist/esm/utilities/structured-output-text.js +50 -0
- package/dist/esm/utilities/structured-output-text.js.map +1 -0
- package/package.json +2 -2
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +6 -2
- package/skills/ai-core/media-generation/SKILL.md +80 -34
- package/skills/ai-core/structured-outputs/SKILL.md +73 -1
- package/skills/ai-core/tool-calling/SKILL.md +1 -1
- package/src/activities/chat/adapter.ts +16 -1
- package/src/activities/chat/index.ts +155 -49
- package/src/activities/chat/stream/processor.ts +6 -3
- package/src/activities/chat/tools/tool-calls.ts +12 -4
- package/src/adapter-internals.ts +8 -0
- package/src/types.ts +27 -19
- package/src/utilities/structured-output-events.ts +44 -0
- package/src/utilities/structured-output-text.ts +63 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/generateVideo/index.ts"],"sourcesContent":["/**\n * Video Activity (Experimental)\n *\n * Generates videos from text prompts using a jobs/polling architecture.\n * This is a self-contained module with implementation, types, and JSDoc.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { toRunErrorPayload } from '../error-payload'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n applyGenerationResultTransforms,\n createGenerationContext,\n runGenerationAbort,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport {\n abortReasonMessage,\n createActivityAbortControls,\n isActivityAbortError,\n raceWithAbort,\n toAbortError,\n} from '../../utilities/activity-abort'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type {\n GenerationMiddleware,\n GenerationMiddlewareContext,\n} from '../middleware/types'\nimport type { VideoAdapter } from './adapter'\nimport type {\n MediaPrompt,\n MediaPromptFor,\n PersistedArtifactRef,\n StreamChunk,\n TokenUsage,\n VideoJobResult,\n VideoStatusResult,\n VideoUrlResult,\n} from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'video' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract provider options from a VideoAdapter via ~types.\n */\nexport type VideoProviderOptions<TAdapter> =\n TAdapter extends VideoAdapter<any, any, any, any, any, any>\n ? TAdapter['~types']['providerOptions']\n : object\n\n/**\n * Extract the size type for a VideoAdapter's model via ~types.\n */\nexport type VideoSizeForAdapter<TAdapter> =\n TAdapter extends VideoAdapter<\n infer TModel,\n any,\n any,\n infer TSizeMap,\n any,\n any\n >\n ? TModel extends keyof TSizeMap\n ? TSizeMap[TModel]\n : string\n : string\n\n/**\n * Extract the prompt type a model accepts from a VideoAdapter via ~types.\n * Mirrors `ImagePromptForModel`: models in the adapter's input-modality map\n * get a `prompt` narrowed to text + their supported part types; adapters\n * without a map fall back to the full MediaPrompt.\n */\nexport type VideoPromptForAdapter<TAdapter> =\n TAdapter extends VideoAdapter<\n infer TModel,\n any,\n any,\n any,\n infer ModsByName,\n any\n >\n ? string extends keyof ModsByName\n ? MediaPrompt\n : TModel extends keyof ModsByName\n ? MediaPromptFor<ModsByName[TModel][number]>\n : MediaPrompt\n : MediaPrompt\n\n/**\n * Extract the duration type for a VideoAdapter's model via ~types.\n * Mirrors `VideoSizeForAdapter`. Falls back to `number` for adapters that\n * haven't declared per-model duration constraints.\n */\nexport type VideoDurationForAdapter<TAdapter> =\n TAdapter extends VideoAdapter<\n infer TModel,\n any,\n any,\n any,\n any,\n infer TDurationMap\n >\n ? TModel extends keyof TDurationMap\n ? TDurationMap[TModel]\n : number\n : number\n\n// ===========================\n// Activity Options Types\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n// ===========================\n\n/**\n * Base options shared by all video activity operations.\n * The model is extracted from the adapter's model property.\n */\ninterface VideoActivityBaseOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n> {\n /** The video adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n}\n\n/**\n * Options for creating a new video generation job.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The video adapter type\n * @template TStream - Whether to stream the output\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport type VideoCreateOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n TStream extends boolean = false,\n> = VideoActivityBaseOptions<TAdapter> & {\n /** Request type - create a new job (default if not specified) */\n request?: 'create'\n /**\n * Description of the desired video. Either a plain string, or — for models\n * that support image-conditioned generation — an ordered array of content\n * parts interleaving text with image inputs. Image parts may carry\n * `metadata.role` (`'start_frame' | 'end_frame' | 'reference' |\n * 'character'`) to disambiguate intent; positional fallback otherwise. The\n * accepted part types are narrowed per model via the adapter's\n * input-modality map.\n */\n prompt: VideoPromptForAdapter<TAdapter>\n /** Video size — format depends on the provider (e.g., \"16:9\", \"1280x720\") */\n size?: VideoSizeForAdapter<TAdapter>\n /**\n * Video duration in seconds. Adapters that declare a per-model duration\n * map narrow this to the model's valid union (e.g. `4 | 6 | 8` for Veo 3).\n * Pass `adapter.snapDuration(seconds)` to coerce raw seconds to a valid\n * value.\n */\n duration?: VideoDurationForAdapter<TAdapter>\n /**\n * Whether to stream the video generation lifecycle.\n * When true, returns an AsyncIterable<StreamChunk> that handles the full\n * job lifecycle: create job, poll for status, yield updates, and yield final result.\n * When false or not provided, returns a Promise<VideoJobResult>.\n *\n * @default false\n */\n stream?: TStream\n /** Polling interval in milliseconds (stream mode only). @default 2000 */\n pollingInterval?: number\n /** Maximum time to wait before timing out in milliseconds (stream mode only). @default 600000 */\n maxDuration?: number\n /**\n * Custom run id (stream mode only) — the id stamped on the emitted\n * `RUN_STARTED` / `RUN_FINISHED` chunks.\n *\n * IGNORED by a non-streaming submit. That run spans two calls, and its id is\n * derived from the provider's job instead, so {@link getVideoJobStatus} can\n * recompute it from the `jobId` you already have to poll with. Honoring a\n * custom id here would reintroduce the failure this avoids: a caller who set\n * it on the submit and forgot it on the poll would silently open a second\n * record while the first sat unfinished forever.\n */\n runId?: string\n /**\n * Stable conversation/thread id for correlating this run when persisted.\n *\n * Also the `threadId` stamped on the emitted `RUN_STARTED` / `RUN_FINISHED`\n * chunks; when omitted a throwaway id is minted for those chunks only, and\n * the persisted run record carries NO thread link rather than a fabricated\n * one. Pass it whenever persistence is on — it is the slot a reloading client\n * hydrates by, so a run stored without it can only be fetched by run id.\n */\n threadId?: string\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, `withGenerationPersistence()`\n * to persist the run, or implement the `GenerationMiddleware` contract for a\n * custom backend.\n *\n * In streaming mode one run covers the full create→poll→complete lifecycle:\n * `onStart` at submission, a terminal `onFinish`/`onError` when the job\n * settles, and `onAbort` if the consumer abandons the stream.\n *\n * In NON-streaming mode the call only SUBMITS the job, so it only opens the\n * run: no terminal hook fires here, because the video does not exist yet.\n * Pass the same `middleware` and `threadId` to {@link getVideoJobStatus}; the\n * poll that observes a terminal job state finishes the run and is where the\n * result and its artifacts are recorded. Nothing else has to be threaded\n * through — both calls derive the run id from the provider's `jobId`, the one\n * id a poller cannot be missing.\n *\n * Because the job id only exists once the provider accepts the job, `onStart`\n * fires AFTER the submit request rather than before it — an observer's span\n * therefore covers the run from acceptance onward, not the submit round-trip.\n * A submission that FAILS has no job to key on, so it opens and immediately\n * fails a run under this call's `requestId`: the thread's latest run reports\n * the failure (a client hydrating the slot sees it) even though there is no\n * job to resume.\n */\n middleware?: Array<GenerationMiddleware>\n /**\n * Maximum duration of this activity invocation in milliseconds.\n * No SDK-wide default — choose a value suitable for the provider and job.\n * Composed with {@link abortSignal}; the first abort wins.\n *\n * In stream mode this bounds the full create→poll→complete lifecycle and\n * complements {@link maxDuration} (which defaults to 10 minutes). When both\n * are set, the shorter limit wins via signal composition against the\n * polling deadline.\n */\n timeout?: number\n /**\n * Caller cancellation signal (request disconnects, job/runtime cancellation).\n * Composed with {@link timeout} into an effective signal forwarded to the\n * adapter on job submission. Request-specific — not stored on global\n * provider client config.\n */\n abortSignal?: AbortSignal\n} & ({} extends VideoProviderOptions<TAdapter>\n ? {\n /** Provider-specific options for video generation */ modelOptions?: VideoProviderOptions<TAdapter>\n }\n : {\n /** Provider-specific options for video generation */ modelOptions: VideoProviderOptions<TAdapter>\n })\n\n/**\n * Options for polling the status of a video generation job.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface VideoStatusOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n> extends VideoActivityBaseOptions<TAdapter> {\n /** Request type - get job status */\n request: 'status'\n /** The job ID to check status for */\n jobId: string\n}\n\n/**\n * Options for getting the URL of a completed video.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface VideoUrlOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n> extends VideoActivityBaseOptions<TAdapter> {\n /** Request type - get video URL */\n request: 'url'\n /** The job ID to get URL for */\n jobId: string\n}\n\n/**\n * Union type for all video activity options.\n * Discriminated by the `request` field.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport type VideoActivityOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n TRequest extends 'create' | 'status' | 'url' = 'create',\n TStream extends boolean = false,\n> = TRequest extends 'status'\n ? VideoStatusOptions<TAdapter>\n : TRequest extends 'url'\n ? VideoUrlOptions<TAdapter>\n : VideoCreateOptions<TAdapter, TStream>\n\n// ===========================\n// Activity Result Types\n// ===========================\n\n/**\n * Result type for the video activity, based on request type and streaming.\n * - If stream is true (create request): AsyncIterable<StreamChunk>\n * - Otherwise: Promise<VideoJobResult | VideoStatusResult | VideoUrlResult>\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport type VideoActivityResult<\n TRequest extends 'create' | 'status' | 'url' = 'create',\n TStream extends boolean = false,\n> = TRequest extends 'status'\n ? Promise<VideoStatusResult>\n : TRequest extends 'url'\n ? Promise<VideoUrlResult>\n : TStream extends true\n ? AsyncIterable<StreamChunk>\n : Promise<VideoJobResult>\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Generate video - creates a video generation job from a text prompt.\n *\n * Uses AI video generation models to create videos based on natural language descriptions.\n * Unlike image generation, video generation is asynchronous and requires polling for completion.\n *\n * When `stream: true` is passed, handles the full job lifecycle automatically:\n * create job → poll for status → stream updates → yield final result.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @example Create a video generation job\n * ```ts\n * import { generateVideo, getVideoJobStatus } from '@tanstack/ai'\n * import { openaiVideo } from '@tanstack/ai-openai'\n *\n * // Start a video generation job\n * const { jobId } = await generateVideo({\n * adapter: openaiVideo('sora-2'),\n * prompt: 'A cat chasing a dog in a sunny park'\n * })\n *\n * console.log('Job started:', jobId)\n *\n * // The submission only OPENS the run; the poll that sees a terminal state is\n * // what completes it. The `jobId` is the whole correlation — pass the same\n * // `middleware` and `threadId` when you use them.\n * const status = await getVideoJobStatus({\n * adapter: openaiVideo('sora-2'),\n * jobId,\n * })\n * ```\n *\n * @example Stream the full video generation lifecycle\n * ```ts\n * import { generateVideo, toServerSentEventsResponse } from '@tanstack/ai'\n * import { openaiVideo } from '@tanstack/ai-openai'\n *\n * const stream = generateVideo({\n * adapter: openaiVideo('sora-2'),\n * prompt: 'A cat chasing a dog in a sunny park',\n * stream: true,\n * pollingInterval: 3000,\n * })\n *\n * return toServerSentEventsResponse(stream)\n * ```\n */\nexport function generateVideo<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n TStream extends boolean = false,\n>(\n options: VideoCreateOptions<TAdapter, TStream>,\n): VideoActivityResult<'create', TStream> {\n if (options.stream) {\n return runStreamingVideoGeneration(\n options as VideoCreateOptions<TAdapter, true>,\n ) as VideoActivityResult<'create', TStream>\n }\n\n return runCreateVideoJob(options) as VideoActivityResult<'create', TStream>\n}\n\n/**\n * The run id a non-streaming video job is filed under, derived from the\n * provider job itself.\n *\n * A submit-and-poll run spans two calls in two different requests, so the two\n * halves need to agree on an id. Deriving it from the `jobId` — the one id a\n * poller structurally cannot be missing, because it cannot poll without it —\n * means no correlation state has to survive the boundary and there is no\n * \"forgot to pass the run id\" failure to document. The provider is part of the\n * key so two providers' job-id spaces cannot collide, and both halves are\n * percent-encoded so the joined string stays unambiguous (and url-safe, since\n * run ids end up in storage keys and query strings).\n */\nfunction videoRunIdForJob(provider: string, jobId: string): string {\n return `video:${encodeURIComponent(provider)}:${encodeURIComponent(jobId)}`\n}\n\n/**\n * Internal implementation of non-streaming video job creation.\n *\n * Submitting a job OPENS a run, it does not complete one: the video does not\n * exist yet, and the bytes only appear on a later poll. So this fires `onStart`\n * and runs the result transforms over the submission result — the jobId lands\n * on the run record, which is what lets a later request resume polling — but\n * fires NO terminal hook. {@link getVideoJobStatus} finishes the run when the\n * job settles, keyed on the same derived id.\n *\n * `onStart` therefore runs AFTER the submit request: the run's id comes from\n * the job, which does not exist until the provider accepts it. A submission\n * that fails has no job, so it opens and immediately fails a run under this\n * call's `requestId` — terminal and unresumable by construction, but it puts\n * the failure where a client hydrating the thread will see it instead of\n * showing nothing.\n */\nasync function runCreateVideoJob<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n>(options: VideoCreateOptions<TAdapter, boolean>): Promise<VideoJobResult> {\n const {\n adapter,\n prompt,\n size,\n duration,\n modelOptions,\n middleware,\n timeout,\n abortSignal: callerAbortSignal,\n } = options\n const model = adapter.model\n const requestId = createId('video')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const abortControls = createActivityAbortControls({\n timeout,\n abortSignal: callerAbortSignal,\n })\n const providerName =\n (adapter as { name?: string; provider?: string }).provider ??\n (adapter as { name?: string }).name ??\n 'unknown'\n\n // `runId` is resolved per outcome (from the job, or absent on failure), so the\n // context is built once the outcome is known. `options.runId` is deliberately\n // not consulted: in non-streaming mode the run id is always the derived one,\n // the single rule that keeps the two calls in agreement.\n const contextFor = (runId?: string): GenerationMiddlewareContext =>\n createGenerationContext({\n requestId,\n activity: 'video',\n provider: adapter.name,\n model,\n modelOptions,\n // Deliberately the CALLER's `threadId` — no minted fallback. A thread id\n // nobody else knows would file the run in a slot no client could hydrate,\n // which is worse than no link because it looks like one. Mirrors the\n // streaming path.\n threadId: options.threadId,\n runId,\n artifactInputs: { prompt },\n createId,\n })\n\n logger.request(`activity=generateVideo provider=${providerName}`, {\n provider: providerName,\n model,\n })\n\n let jobResult: VideoJobResult\n try {\n jobResult = await raceWithAbort(\n adapter.createVideoJob({\n model,\n prompt,\n size,\n duration,\n modelOptions,\n logger,\n ...(abortControls.signal ? { abortSignal: abortControls.signal } : {}),\n }),\n abortControls.signal,\n )\n abortControls.clear()\n } catch (error) {\n abortControls.clear()\n // No jobId exists, so this run can only be keyed on the request. Start it\n // just to fail it: `generationRuns.update` on an unknown run id is a no-op\n // by contract, so without the `onStart` the failure would persist nowhere.\n const failedCtx = contextFor()\n await runGenerationStart(middleware, failedCtx)\n const elapsed = Date.now() - startTime\n if (isActivityAbortError(error, abortControls.signal)) {\n await runGenerationAbort(middleware, failedCtx, {\n reason: abortReasonMessage(error, abortControls.signal),\n duration: elapsed,\n })\n } else {\n await runGenerationError(middleware, failedCtx, {\n error,\n duration: elapsed,\n })\n }\n logger.errors('generateVideo activity failed', {\n error,\n source: 'generateVideo',\n })\n throw error\n }\n\n logger.output(`activity=generateVideo jobId=${jobResult.jobId}`, {\n jobId: jobResult.jobId,\n model: jobResult.model,\n })\n\n const mwCtx = contextFor(videoRunIdForJob(adapter.name, jobResult.jobId))\n await runGenerationStart(middleware, mwCtx)\n // Transforms see the submission result (no url yet, so nothing to copy into a\n // blob store) purely so the run record captures the jobId and any prompt\n // inputs. No finish hook: the run is still running.\n return await applyGenerationResultTransforms(mwCtx, jobResult)\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (!signal) {\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n if (signal.aborted) {\n return Promise.reject(toAbortError(signal.reason))\n }\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort)\n resolve()\n }, ms)\n const onAbort = () => {\n clearTimeout(timer)\n signal.removeEventListener('abort', onAbort)\n reject(toAbortError(signal.reason))\n }\n signal.addEventListener('abort', onAbort, { once: true })\n })\n}\n\n/**\n * Internal streaming implementation for video generation.\n * Handles the full job lifecycle: create job → poll for status → stream updates → yield final result.\n */\nasync function* runStreamingVideoGeneration<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n>(options: VideoCreateOptions<TAdapter, true>): AsyncIterable<StreamChunk> {\n const {\n adapter,\n prompt,\n size,\n duration,\n modelOptions,\n middleware,\n timeout,\n abortSignal: callerAbortSignal,\n } = options\n const model = adapter.model\n const runId = options.runId ?? createId('run')\n const requestId = createId('video')\n const obsStartTime = Date.now()\n const pollingInterval = options.pollingInterval ?? 2000\n const maxDuration = options.maxDuration ?? 600_000\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const abortControls = createActivityAbortControls({\n timeout,\n abortSignal: callerAbortSignal,\n })\n const providerName =\n (adapter as { name?: string; provider?: string }).provider ??\n (adapter as { name?: string }).name ??\n 'unknown'\n\n // The wire needs a thread id on every RUN_* chunk, so one is minted when the\n // caller passes none — matching `streamGenerationResult`, which the other\n // activities stream through.\n const wireThreadId = options.threadId ?? createId('thread')\n\n yield {\n type: 'RUN_STARTED',\n runId,\n threadId: wireThreadId,\n timestamp: Date.now(),\n } as StreamChunk\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'video',\n provider: adapter.name,\n model,\n modelOptions,\n // Identity has to reach the middleware, not just the chunks: persistence\n // keys the run record on these, and without them it falls back to the\n // internal `requestId` and records no thread link at all.\n //\n // Deliberately the CALLER's `threadId`, never `wireThreadId`: a minted id is\n // known to nobody, so persisting it would file the run in a slot no client\n // could ever hydrate — worse than recording no link, because it looks like\n // one. This mirrors `generateImage`.\n threadId: options.threadId,\n runId,\n artifactInputs: { prompt },\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n logger.request(\n `activity=generateVideo provider=${providerName} stream=true`,\n {\n provider: providerName,\n model,\n },\n )\n\n // Tracks whether a terminal observer event (finish/error/abort) has already\n // fired, so the `finally` below can fire one on abandonment without\n // double-firing.\n let settled = false\n try {\n // Create the video generation job\n const jobResult = await raceWithAbort(\n adapter.createVideoJob({\n model,\n prompt,\n size,\n duration,\n modelOptions,\n logger,\n ...(abortControls.signal ? { abortSignal: abortControls.signal } : {}),\n }),\n abortControls.signal,\n )\n\n yield {\n type: 'CUSTOM',\n name: 'video:job:created',\n value: { jobId: jobResult.jobId },\n timestamp: Date.now(),\n }\n\n // Poll for completion\n const startTime = Date.now()\n while (Date.now() - startTime < maxDuration) {\n await sleep(pollingInterval, abortControls.signal)\n\n const statusResult = await adapter.getVideoStatus(jobResult.jobId)\n\n yield {\n type: 'CUSTOM',\n name: 'video:status',\n value: {\n jobId: jobResult.jobId,\n status: statusResult.status,\n progress: statusResult.progress,\n error: statusResult.error,\n },\n timestamp: Date.now(),\n }\n\n if (statusResult.status === 'completed') {\n const urlResult = await adapter.getVideoUrl(jobResult.jobId)\n\n logger.output(\n `activity=generateVideo jobId=${jobResult.jobId} status=completed`,\n {\n jobId: jobResult.jobId,\n url: urlResult.url,\n },\n )\n\n // Run the result transforms before anything observes the result, the\n // same as every other media activity. This is what lets persistence\n // copy the video into a blob store, attach its artifact refs, and\n // rewrite `url` to a durable app-origin one — so the chunk below and\n // the stored run record carry the SAME urls. Skipping it leaves a\n // result whose only url is the provider's expiring link.\n const rawResult = {\n jobId: jobResult.jobId,\n status: 'completed' as const,\n url: urlResult.url,\n expiresAt: urlResult.expiresAt,\n ...(urlResult.usage ? { usage: urlResult.usage } : {}),\n }\n const result = await applyGenerationResultTransforms(mwCtx, rawResult)\n\n // Fire finish before yielding the terminal chunks: the generation has\n // succeeded, so a consumer that stops reading after `generation:result`\n // (without pulling `RUN_FINISHED`) must not trip the abandonment path in\n // `finally`, which would otherwise report a spurious cancellation.\n if (urlResult.usage)\n await runGenerationUsage(middleware, mwCtx, urlResult.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration: Date.now() - obsStartTime,\n usage: urlResult.usage,\n })\n settled = true\n abortControls.clear()\n\n yield {\n type: 'CUSTOM',\n name: 'generation:result',\n value: result,\n timestamp: Date.now(),\n }\n\n yield {\n type: 'RUN_FINISHED',\n runId,\n threadId: wireThreadId,\n finishReason: 'stop',\n timestamp: Date.now(),\n } as StreamChunk\n return\n }\n\n if (statusResult.status === 'failed') {\n throw new Error(statusResult.error || 'Video generation failed')\n }\n }\n\n throw new Error('Video generation timed out')\n } catch (error: unknown) {\n abortControls.clear()\n const payload = toRunErrorPayload(error, 'Video generation failed')\n // Mark settled before firing terminal hooks: if a user error-hook throws,\n // the `finally` below must still not double-fire onAbort over the same op\n // (which would mask the original error and end the span twice).\n settled = true\n const elapsed = Date.now() - obsStartTime\n if (isActivityAbortError(error, abortControls.signal)) {\n await runGenerationAbort(middleware, mwCtx, {\n reason: abortReasonMessage(error, abortControls.signal),\n duration: elapsed,\n })\n } else {\n await runGenerationError(middleware, mwCtx, {\n error,\n duration: elapsed,\n })\n }\n logger.errors('generateVideo activity failed', {\n message: payload.message,\n code: payload.code,\n source: 'generateVideo',\n })\n yield {\n type: 'RUN_ERROR',\n runId,\n threadId: wireThreadId,\n message: payload.message,\n code: payload.code,\n error: payload,\n timestamp: Date.now(),\n } as StreamChunk\n } finally {\n abortControls.clear()\n if (!settled) {\n // The consumer abandoned the stream (broke the `for await` loop or\n // disconnected) before completion, so the generator is being unwound at\n // a `yield` without reaching finish/error. Fire `onAbort` — a cancel, not\n // an error — so otelMiddleware ends its span instead of leaking it.\n await runGenerationAbort(middleware, mwCtx, {\n reason: 'Video generation stream abandoned before completion',\n duration: Date.now() - obsStartTime,\n })\n }\n }\n}\n\n/**\n * Options for {@link getVideoJobStatus}.\n *\n * The run this poll finishes is identified by `adapter` + `jobId` alone — the\n * same pair the submitting `generateVideo()` call derived it from — so there is\n * no run id to thread through. Pass the submission's `threadId` and the same\n * `middleware`.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface VideoJobStatusOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n> {\n /** The video adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** The job ID to check status for */\n jobId: string\n /**\n * The scope the run is filed under. Must match the submission's `threadId` —\n * generation persistence REFUSES a run without a scope (a run filed under\n * none can never be hydrated by one), so omitting it throws rather than\n * quietly filing the finished video somewhere unreachable.\n */\n threadId?: string\n /**\n * Observe-only middleware. Hooks fire ONLY on the poll that observes a\n * terminal job state: `onStart` (resuming the submission's run), then the\n * result transforms — which is where persistence copies the video into a blob\n * store and rewrites `url` to a durable one, so the returned result carries\n * the same urls as the stored record — then `onFinish`, or `onError` when the\n * job failed. Intermediate polls invoke nothing, so a middleware is not\n * charged for the wait.\n */\n middleware?: Array<GenerationMiddleware>\n}\n\n/**\n * The status of a video job, plus the video itself once the job completed.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface VideoJobStatusResult {\n /** Job identifier */\n jobId: string\n status: 'pending' | 'processing' | 'completed' | 'failed'\n progress?: number\n url?: string\n /** When the provider url expires, if it reported one. */\n expiresAt?: Date\n error?: string\n usage?: TokenUsage\n /** Durable artifact references, when generation persistence is wired. */\n artifacts?: Array<PersistedArtifactRef>\n}\n\n/**\n * Get video job status - returns the current status, progress, and URL if available.\n *\n * This function combines status checking and URL retrieval. If the job is completed,\n * it will automatically fetch and include the video URL.\n *\n * It is also where a non-streaming `generateVideo()` run ENDS: pass the same\n * `middleware` and `threadId`, and the poll that first sees a terminal job state\n * finishes the run (recording the result and its artifacts) or fails it. The run\n * is identified by `adapter` + `jobId`, exactly what the submission derived it\n * from, so there is nothing else to carry between the two calls.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @example Check job status\n * ```ts\n * import { getVideoJobStatus } from '@tanstack/ai'\n * import { openaiVideo } from '@tanstack/ai-openai'\n *\n * const result = await getVideoJobStatus({\n * adapter: openaiVideo('sora-2'),\n * jobId: 'job-123'\n * })\n *\n * console.log('Status:', result.status)\n * console.log('Progress:', result.progress)\n * if (result.url) {\n * console.log('Video URL:', result.url)\n * }\n * ```\n *\n * @example Submit and poll one persisted run\n * ```ts\n * import { generateVideo, getVideoJobStatus } from '@tanstack/ai'\n * import { withGenerationPersistence } from '@tanstack/ai-persistence'\n * import { openaiVideo } from '@tanstack/ai-openai'\n *\n * const adapter = openaiVideo('sora-2')\n * const middleware = [withGenerationPersistence(persistence)]\n *\n * // Opens the run (status `running`, jobId recorded). Its run id is derived\n * // from the provider job, so nothing has to be stored to resume it.\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: 'A cat chasing a dog in a sunny park',\n * threadId,\n * middleware,\n * })\n *\n * // Completes the SAME run once the job settles — this is what writes the\n * // video, its artifacts, and the terminal status. Works from a different\n * // request or process: the jobId is the only correlation.\n * const status = await getVideoJobStatus({\n * adapter,\n * jobId,\n * threadId,\n * middleware,\n * })\n * ```\n */\nexport async function getVideoJobStatus<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n>(options: VideoJobStatusOptions<TAdapter>): Promise<VideoJobStatusResult> {\n const { adapter, jobId, middleware } = options\n const requestId = createId('video-status')\n const startTime = Date.now()\n\n // Built per call but only USED on a terminal poll — `onStart` is what\n // registers the result transforms, so it has to run in the same call that\n // applies them.\n const terminalContext = (): GenerationMiddlewareContext =>\n createGenerationContext({\n requestId,\n activity: 'video',\n provider: adapter.name,\n model: adapter.model,\n threadId: options.threadId,\n // Recomputed, never passed in: the submitting call derived the same id\n // from the same provider + job, so the two halves agree without the\n // caller carrying anything but the jobId they must already have.\n runId: videoRunIdForJob(adapter.name, jobId),\n // Deliberately no `artifactInputs`: the submission already persisted any\n // prompt inputs under this run, and passing them again would store a\n // second copy of every input image.\n createId,\n })\n\n aiEventClient.emit('video:request:started', {\n requestId,\n provider: adapter.name,\n model: adapter.model,\n requestType: 'status',\n jobId,\n timestamp: startTime,\n })\n\n // Get status first\n const statusResult = await adapter.getVideoStatus(jobId)\n\n // If completed, also get the URL\n if (statusResult.status === 'completed') {\n let urlResult: VideoUrlResult\n // Scoped tightly to the provider call: a middleware hook that throws must\n // surface as itself, not be relabelled \"failed to get video URL\" and then\n // re-reported to the very middleware that threw.\n try {\n urlResult = await adapter.getVideoUrl(jobId)\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : 'Failed to get video URL'\n aiEventClient.emit('video:request:completed', {\n requestId,\n provider: adapter.name,\n model: adapter.model,\n requestType: 'status',\n jobId,\n status: 'failed',\n progress: statusResult.progress,\n error: errorMessage,\n duration: Date.now() - startTime,\n timestamp: Date.now(),\n })\n // Provider reported completed but result fetch failed — treat as failed,\n // and fail the run with it: the job is terminal, so nothing later will.\n await runGenerationError(middleware, terminalContext(), {\n error,\n duration: Date.now() - startTime,\n })\n return {\n jobId,\n status: 'failed' as const,\n progress: statusResult.progress,\n error: errorMessage,\n }\n }\n\n aiEventClient.emit('video:request:completed', {\n requestId,\n provider: adapter.name,\n model: adapter.model,\n requestType: 'status',\n jobId,\n status: statusResult.status,\n progress: statusResult.progress,\n url: urlResult.url,\n duration: Date.now() - startTime,\n timestamp: Date.now(),\n })\n if (urlResult.usage) {\n aiEventClient.emit('video:usage', {\n requestId,\n model: adapter.model,\n usage: urlResult.usage,\n timestamp: Date.now(),\n })\n }\n\n const mwCtx = terminalContext()\n await runGenerationStart(middleware, mwCtx)\n const result = await applyGenerationResultTransforms<VideoJobStatusResult>(\n mwCtx,\n {\n jobId,\n status: 'completed',\n ...(statusResult.progress !== undefined\n ? { progress: statusResult.progress }\n : {}),\n url: urlResult.url,\n ...(urlResult.expiresAt ? { expiresAt: urlResult.expiresAt } : {}),\n ...(urlResult.usage ? { usage: urlResult.usage } : {}),\n },\n )\n if (urlResult.usage)\n await runGenerationUsage(middleware, mwCtx, urlResult.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration: Date.now() - startTime,\n usage: urlResult.usage,\n })\n return result\n }\n\n aiEventClient.emit('video:request:completed', {\n requestId,\n provider: adapter.name,\n model: adapter.model,\n requestType: 'status',\n jobId,\n status: statusResult.status,\n progress: statusResult.progress,\n error: statusResult.error,\n duration: Date.now() - startTime,\n timestamp: Date.now(),\n })\n\n // A failed job is terminal for the run too: without this the record would sit\n // at `running` forever, indistinguishable from a job still being worked on.\n if (statusResult.status === 'failed') {\n await runGenerationError(middleware, terminalContext(), {\n error: new Error(statusResult.error || 'Video generation failed'),\n duration: Date.now() - startTime,\n })\n }\n\n // Return status for non-completed jobs\n return {\n jobId,\n status: statusResult.status,\n progress: statusResult.progress,\n error: statusResult.error,\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateVideo() function without executing.\n */\nexport function createVideoOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n TStream extends boolean = false,\n>(\n options: VideoCreateOptions<TAdapter, TStream>,\n): VideoCreateOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n VideoAdapter,\n VideoAdapterConfig,\n AnyVideoAdapter,\n} from './adapter'\nexport { BaseVideoAdapter } from './adapter'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/generateVideo/index.ts"],"sourcesContent":["/**\n * Video Activity (Experimental)\n *\n * Generates videos from text prompts using a jobs/polling architecture.\n * This is a self-contained module with implementation, types, and JSDoc.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { toRunErrorPayload } from '../error-payload'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n applyGenerationResultTransforms,\n createGenerationContext,\n runGenerationAbort,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport {\n abortReasonMessage,\n createActivityAbortControls,\n isActivityAbortError,\n raceWithAbort,\n toAbortError,\n} from '../../utilities/activity-abort'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type {\n GenerationMiddleware,\n GenerationMiddlewareContext,\n} from '../middleware/types'\nimport type { VideoAdapter } from './adapter'\nimport type {\n MediaPrompt,\n MediaPromptFor,\n PersistedArtifactRef,\n StreamChunk,\n TokenUsage,\n VideoJobResult,\n VideoStatusResult,\n VideoUrlResult,\n} from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'video' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract provider options from a VideoAdapter via ~types.\n */\nexport type VideoProviderOptions<TAdapter> =\n TAdapter extends VideoAdapter<any, any, any, any, any, any>\n ? TAdapter['~types']['providerOptions']\n : object\n\n/**\n * Extract the size type for a VideoAdapter's model via ~types.\n */\nexport type VideoSizeForAdapter<TAdapter> =\n TAdapter extends VideoAdapter<\n infer TModel,\n any,\n any,\n infer TSizeMap,\n any,\n any\n >\n ? TModel extends keyof TSizeMap\n ? TSizeMap[TModel]\n : string\n : string\n\n/**\n * Extract the prompt type a model accepts from a VideoAdapter via ~types.\n * Mirrors `ImagePromptForModel`: models in the adapter's input-modality map\n * get a `prompt` narrowed to text + their supported part types; adapters\n * without a map fall back to the full MediaPrompt.\n */\nexport type VideoPromptForAdapter<TAdapter> =\n TAdapter extends VideoAdapter<\n infer TModel,\n any,\n any,\n any,\n infer ModsByName,\n any\n >\n ? string extends keyof ModsByName\n ? MediaPrompt\n : TModel extends keyof ModsByName\n ? MediaPromptFor<ModsByName[TModel][number]>\n : MediaPrompt\n : MediaPrompt\n\n/**\n * Extract the duration type for a VideoAdapter's model via ~types.\n * Mirrors `VideoSizeForAdapter`. Falls back to `number` for adapters that\n * haven't declared per-model duration constraints.\n */\nexport type VideoDurationForAdapter<TAdapter> =\n TAdapter extends VideoAdapter<\n infer TModel,\n any,\n any,\n any,\n any,\n infer TDurationMap\n >\n ? TModel extends keyof TDurationMap\n ? TDurationMap[TModel]\n : number\n : number\n\n// ===========================\n// Activity Options Types\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n// ===========================\n\n/**\n * Base options shared by all video activity operations.\n * The model is extracted from the adapter's model property.\n */\ninterface VideoActivityBaseOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n> {\n /** The video adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n}\n\n/**\n * Options for creating a new video generation job.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The video adapter type\n * @template TStream - Whether to stream the output\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport type VideoCreateOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n TStream extends boolean = false,\n> = VideoActivityBaseOptions<TAdapter> & {\n /** Request type - create a new job (default if not specified) */\n request?: 'create'\n /**\n * Description of the desired video. Either a plain string, or — for models\n * that support image-conditioned generation — an ordered array of content\n * parts interleaving text with image inputs. Image parts may carry\n * `metadata.role` (`'start_frame' | 'end_frame' | 'reference' |\n * 'character'`) to disambiguate intent; positional fallback otherwise. The\n * accepted part types are narrowed per model via the adapter's\n * input-modality map.\n */\n prompt: VideoPromptForAdapter<TAdapter>\n /** Video size — format depends on the provider (e.g., \"16:9\", \"1280x720\") */\n size?: VideoSizeForAdapter<TAdapter>\n /**\n * Video duration in seconds. Adapters that declare a per-model duration\n * map narrow this to the model's valid union (e.g. `4 | 6 | 8` for Veo 3).\n * Pass `adapter.snapDuration(seconds)` to coerce raw seconds to a valid\n * value.\n */\n duration?: VideoDurationForAdapter<TAdapter>\n /**\n * Whether to stream the video generation lifecycle.\n * When true, returns an AsyncIterable<StreamChunk> that handles the full\n * job lifecycle: create job, poll for status, yield updates, and yield final result.\n * When false or not provided, returns a Promise<VideoJobResult>.\n *\n * @default false\n */\n stream?: TStream\n /** Polling interval in milliseconds (stream mode only). @default 2000 */\n pollingInterval?: number\n /** Maximum time to wait before timing out in milliseconds (stream mode only). @default 600000 */\n maxDuration?: number\n /**\n * Custom run id (stream mode only) — the id stamped on the emitted\n * `RUN_STARTED` / `RUN_FINISHED` chunks.\n *\n * IGNORED by a non-streaming submit. That run spans two calls, and its id is\n * derived from the provider's job instead, so {@link getVideoJobStatus} can\n * recompute it from the `jobId` you already have to poll with. Honoring a\n * custom id here would reintroduce the failure this avoids: a caller who set\n * it on the submit and forgot it on the poll would silently open a second\n * record while the first sat unfinished forever.\n */\n runId?: string\n /**\n * Stable conversation/thread id for correlating this run when persisted.\n *\n * Also the `threadId` stamped on the emitted `RUN_STARTED` / `RUN_FINISHED`\n * chunks; when omitted a throwaway id is minted for those chunks only, and\n * the persisted run record carries NO thread link rather than a fabricated\n * one. Pass it whenever persistence is on — it is the slot a reloading client\n * hydrates by, so a run stored without it can only be fetched by run id.\n */\n threadId?: string\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, `withGenerationPersistence()`\n * to persist the run, or implement the `GenerationMiddleware` contract for a\n * custom backend.\n *\n * In streaming mode one run covers the full create→poll→complete lifecycle:\n * `onStart` at submission, a terminal `onFinish`/`onError` when the job\n * settles, and `onAbort` if the consumer abandons the stream.\n *\n * In NON-streaming mode the call only SUBMITS the job, so it only opens the\n * run: no terminal hook fires here, because the video does not exist yet.\n * Pass the same `middleware` and `threadId` to {@link getVideoJobStatus}; the\n * poll that observes a terminal job state finishes the run and is where the\n * result and its artifacts are recorded. Nothing else has to be threaded\n * through — both calls derive the run id from the provider's `jobId`, the one\n * id a poller cannot be missing.\n *\n * Because the job id only exists once the provider accepts the job, `onStart`\n * fires AFTER the submit request rather than before it — an observer's span\n * therefore covers the run from acceptance onward, not the submit round-trip.\n * A submission that FAILS has no job to key on, so it opens and immediately\n * fails a run under this call's `requestId`: the thread's latest run reports\n * the failure (a client hydrating the slot sees it) even though there is no\n * job to resume.\n */\n middleware?: Array<GenerationMiddleware>\n /**\n * Maximum duration of this activity invocation in milliseconds.\n * No SDK-wide default — choose a value suitable for the provider and job.\n * Composed with {@link abortSignal}; the first abort wins.\n *\n * In stream mode this bounds the full create→poll→complete lifecycle and\n * complements {@link maxDuration} (which defaults to 10 minutes). When both\n * are set, the shorter limit wins via signal composition against the\n * polling deadline.\n */\n timeout?: number\n /**\n * Caller cancellation signal (request disconnects, job/runtime cancellation).\n * Composed with {@link timeout} into an effective signal forwarded to the\n * adapter on job submission. Request-specific — not stored on global\n * provider client config.\n */\n abortSignal?: AbortSignal\n} & ({} extends VideoProviderOptions<TAdapter>\n ? {\n /** Provider-specific options for video generation */ modelOptions?: VideoProviderOptions<TAdapter>\n }\n : {\n /** Provider-specific options for video generation */ modelOptions: VideoProviderOptions<TAdapter>\n })\n\n/**\n * Options for polling the status of a video generation job.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface VideoStatusOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n> extends VideoActivityBaseOptions<TAdapter> {\n /** Request type - get job status */\n request: 'status'\n /** The job ID to check status for */\n jobId: string\n}\n\n/**\n * Options for getting the URL of a completed video.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface VideoUrlOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n> extends VideoActivityBaseOptions<TAdapter> {\n /** Request type - get video URL */\n request: 'url'\n /** The job ID to get URL for */\n jobId: string\n}\n\n/**\n * Union type for all video activity options.\n * Discriminated by the `request` field.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport type VideoActivityOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n TRequest extends 'create' | 'status' | 'url' = 'create',\n TStream extends boolean = false,\n> = TRequest extends 'status'\n ? VideoStatusOptions<TAdapter>\n : TRequest extends 'url'\n ? VideoUrlOptions<TAdapter>\n : VideoCreateOptions<TAdapter, TStream>\n\n// ===========================\n// Activity Result Types\n// ===========================\n\n/**\n * Result type for the video activity, based on request type and streaming.\n * - If stream is true (create request): AsyncIterable<StreamChunk>\n * - Otherwise: Promise<VideoJobResult | VideoStatusResult | VideoUrlResult>\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport type VideoActivityResult<\n TRequest extends 'create' | 'status' | 'url' = 'create',\n TStream extends boolean = false,\n> = TRequest extends 'status'\n ? Promise<VideoStatusResult>\n : TRequest extends 'url'\n ? Promise<VideoUrlResult>\n : TStream extends true\n ? AsyncIterable<StreamChunk>\n : Promise<VideoJobResult>\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Generate video - creates a video generation job from a text prompt.\n *\n * Uses AI video generation models to create videos based on natural language descriptions.\n * Unlike image generation, video generation is asynchronous and requires polling for completion.\n *\n * When `stream: true` is passed, handles the full job lifecycle automatically:\n * create job → poll for status → stream updates → yield final result.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @example Create a video generation job\n * ```ts\n * import { generateVideo, getVideoJobStatus } from '@tanstack/ai'\n * import { openaiVideo } from '@tanstack/ai-openai'\n *\n * // Start a video generation job\n * const { jobId } = await generateVideo({\n * adapter: openaiVideo('sora-2'),\n * prompt: 'A cat chasing a dog in a sunny park'\n * })\n *\n * console.log('Job started:', jobId)\n *\n * // The submission only OPENS the run; the poll that sees a terminal state is\n * // what completes it. The `jobId` is the whole correlation — pass the same\n * // `middleware` and `threadId` when you use them.\n * const status = await getVideoJobStatus({\n * adapter: openaiVideo('sora-2'),\n * jobId,\n * })\n * ```\n *\n * @example Stream the full video generation lifecycle\n * ```ts\n * import { generateVideo, toServerSentEventsResponse } from '@tanstack/ai'\n * import { openaiVideo } from '@tanstack/ai-openai'\n *\n * const stream = generateVideo({\n * adapter: openaiVideo('sora-2'),\n * prompt: 'A cat chasing a dog in a sunny park',\n * stream: true,\n * pollingInterval: 3000,\n * })\n *\n * return toServerSentEventsResponse(stream)\n * ```\n */\nexport function generateVideo<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n TStream extends boolean = false,\n>(\n options: VideoCreateOptions<TAdapter, TStream>,\n): VideoActivityResult<'create', TStream> {\n if (options.stream) {\n return runStreamingVideoGeneration(\n options as VideoCreateOptions<TAdapter, true>,\n ) as VideoActivityResult<'create', TStream>\n }\n\n return runCreateVideoJob(options) as VideoActivityResult<'create', TStream>\n}\n\n/**\n * The run id a non-streaming video job is filed under, derived from the\n * provider job itself.\n *\n * A submit-and-poll run spans two calls in two different requests, so the two\n * halves need to agree on an id. Deriving it from the `jobId` — the one id a\n * poller structurally cannot be missing, because it cannot poll without it —\n * means no correlation state has to survive the boundary and there is no\n * \"forgot to pass the run id\" failure to document. The provider is part of the\n * key so two providers' job-id spaces cannot collide, and both halves are\n * percent-encoded so the joined string stays unambiguous (and url-safe, since\n * run ids end up in storage keys and query strings).\n */\nfunction videoRunIdForJob(provider: string, jobId: string): string {\n return `video:${encodeURIComponent(provider)}:${encodeURIComponent(jobId)}`\n}\n\n/**\n * Internal implementation of non-streaming video job creation.\n *\n * Submitting a job OPENS a run, it does not complete one: the video does not\n * exist yet, and the bytes only appear on a later poll. So this fires `onStart`\n * and runs the result transforms over the submission result — the jobId lands\n * on the run record, which is what lets a later request resume polling — but\n * fires NO terminal hook. {@link getVideoJobStatus} finishes the run when the\n * job settles, keyed on the same derived id.\n *\n * `onStart` therefore runs AFTER the submit request: the run's id comes from\n * the job, which does not exist until the provider accepts it. A submission\n * that fails has no job, so it opens and immediately fails a run under this\n * call's `requestId` — terminal and unresumable by construction, but it puts\n * the failure where a client hydrating the thread will see it instead of\n * showing nothing.\n */\nasync function runCreateVideoJob<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n>(options: VideoCreateOptions<TAdapter, boolean>): Promise<VideoJobResult> {\n const {\n adapter,\n prompt,\n size,\n duration,\n modelOptions,\n middleware,\n timeout,\n abortSignal: callerAbortSignal,\n } = options\n const model = adapter.model\n const requestId = createId('video')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const abortControls = createActivityAbortControls({\n timeout,\n abortSignal: callerAbortSignal,\n })\n const providerName =\n (adapter as { name?: string; provider?: string }).provider ??\n (adapter as { name?: string }).name ??\n 'unknown'\n\n // `runId` is resolved per outcome (from the job, or absent on failure), so the\n // context is built once the outcome is known. `options.runId` is deliberately\n // not consulted: in non-streaming mode the run id is always the derived one,\n // the single rule that keeps the two calls in agreement.\n const contextFor = (runId?: string): GenerationMiddlewareContext =>\n createGenerationContext({\n requestId,\n activity: 'video',\n provider: adapter.name,\n model,\n modelOptions,\n // Deliberately the CALLER's `threadId` — no minted fallback. A thread id\n // nobody else knows would file the run in a slot no client could hydrate,\n // which is worse than no link because it looks like one. Mirrors the\n // streaming path.\n threadId: options.threadId,\n runId,\n artifactInputs: { prompt },\n createId,\n })\n\n logger.request(`activity=generateVideo provider=${providerName}`, {\n provider: providerName,\n model,\n })\n\n let jobResult: VideoJobResult\n try {\n jobResult = await raceWithAbort(\n adapter.createVideoJob({\n model,\n prompt,\n size,\n duration,\n modelOptions,\n logger,\n ...(abortControls.signal ? { abortSignal: abortControls.signal } : {}),\n }),\n abortControls.signal,\n )\n abortControls.clear()\n } catch (error) {\n abortControls.clear()\n // No jobId exists, so this run can only be keyed on the request. Start it\n // just to fail it: `generationRuns.update` on an unknown run id is a no-op\n // by contract, so without the `onStart` the failure would persist nowhere.\n const failedCtx = contextFor()\n await runGenerationStart(middleware, failedCtx)\n const elapsed = Date.now() - startTime\n if (isActivityAbortError(error, abortControls.signal)) {\n await runGenerationAbort(middleware, failedCtx, {\n reason: abortReasonMessage(error, abortControls.signal),\n duration: elapsed,\n })\n } else {\n await runGenerationError(middleware, failedCtx, {\n error,\n duration: elapsed,\n })\n }\n logger.errors('generateVideo activity failed', {\n error,\n source: 'generateVideo',\n })\n throw error\n }\n\n logger.output(`activity=generateVideo jobId=${jobResult.jobId}`, {\n jobId: jobResult.jobId,\n model: jobResult.model,\n })\n\n const mwCtx = contextFor(videoRunIdForJob(adapter.name, jobResult.jobId))\n await runGenerationStart(middleware, mwCtx)\n // Transforms see the submission result (no url yet, so nothing to copy into a\n // blob store) purely so the run record captures the jobId and any prompt\n // inputs. No finish hook: the run is still running.\n return await applyGenerationResultTransforms(mwCtx, jobResult)\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (!signal) {\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n if (signal.aborted) {\n return Promise.reject(toAbortError(signal.reason))\n }\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort)\n resolve()\n }, ms)\n const onAbort = () => {\n clearTimeout(timer)\n signal.removeEventListener('abort', onAbort)\n reject(toAbortError(signal.reason))\n }\n signal.addEventListener('abort', onAbort, { once: true })\n })\n}\n\n/**\n * Internal streaming implementation for video generation.\n * Handles the full job lifecycle: create job → poll for status → stream updates → yield final result.\n */\nasync function* runStreamingVideoGeneration<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n>(options: VideoCreateOptions<TAdapter, true>): AsyncIterable<StreamChunk> {\n const {\n adapter,\n prompt,\n size,\n duration,\n modelOptions,\n middleware,\n timeout,\n abortSignal: callerAbortSignal,\n } = options\n const model = adapter.model\n const runId = options.runId ?? createId('run')\n const requestId = createId('video')\n const obsStartTime = Date.now()\n const pollingInterval = options.pollingInterval ?? 2000\n const maxDuration = options.maxDuration ?? 600_000\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const abortControls = createActivityAbortControls({\n timeout,\n abortSignal: callerAbortSignal,\n })\n const providerName =\n (adapter as { name?: string; provider?: string }).provider ??\n (adapter as { name?: string }).name ??\n 'unknown'\n\n // The wire needs a thread id on every RUN_* chunk, so one is minted when the\n // caller passes none — matching `streamGenerationResult`, which the other\n // activities stream through.\n const wireThreadId = options.threadId ?? createId('thread')\n\n yield {\n type: 'RUN_STARTED',\n runId,\n threadId: wireThreadId,\n timestamp: Date.now(),\n } as StreamChunk\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'video',\n provider: adapter.name,\n model,\n modelOptions,\n // Identity has to reach the middleware, not just the chunks: persistence\n // keys the run record on these, and without them it falls back to the\n // internal `requestId` and records no thread link at all.\n //\n // Deliberately the CALLER's `threadId`, never `wireThreadId`: a minted id is\n // known to nobody, so persisting it would file the run in a slot no client\n // could ever hydrate — worse than recording no link, because it looks like\n // one. This mirrors `generateImage`.\n threadId: options.threadId,\n runId,\n artifactInputs: { prompt },\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n logger.request(\n `activity=generateVideo provider=${providerName} stream=true`,\n {\n provider: providerName,\n model,\n },\n )\n\n // Tracks whether a terminal observer event (finish/error/abort) has already\n // fired, so the `finally` below can fire one on abandonment without\n // double-firing.\n let settled = false\n try {\n // Create the video generation job\n const jobResult = await raceWithAbort(\n adapter.createVideoJob({\n model,\n prompt,\n size,\n duration,\n modelOptions,\n logger,\n ...(abortControls.signal ? { abortSignal: abortControls.signal } : {}),\n }),\n abortControls.signal,\n )\n\n yield {\n type: 'CUSTOM',\n name: 'video:job:created',\n value: { jobId: jobResult.jobId },\n timestamp: Date.now(),\n }\n\n // Poll for completion\n const startTime = Date.now()\n while (Date.now() - startTime < maxDuration) {\n await sleep(pollingInterval, abortControls.signal)\n\n const statusResult = await adapter.getVideoStatus(jobResult.jobId)\n\n yield {\n type: 'CUSTOM',\n name: 'video:status',\n value: {\n jobId: jobResult.jobId,\n status: statusResult.status,\n progress: statusResult.progress,\n error: statusResult.error,\n },\n timestamp: Date.now(),\n }\n\n if (statusResult.status === 'completed') {\n const urlResult = await adapter.getVideoUrl(jobResult.jobId)\n\n logger.output(\n `activity=generateVideo jobId=${jobResult.jobId} status=completed`,\n {\n jobId: jobResult.jobId,\n url: urlResult.url,\n },\n )\n\n // Run the result transforms before anything observes the result, the\n // same as every other media activity. This is what lets persistence\n // copy the video into a blob store, attach its artifact refs, and\n // rewrite `url` to a durable app-origin one — so the chunk below and\n // the stored run record carry the SAME urls. Skipping it leaves a\n // result whose only url is the provider's expiring link.\n const rawResult = {\n jobId: jobResult.jobId,\n status: 'completed' as const,\n url: urlResult.url,\n expiresAt: urlResult.expiresAt,\n ...(urlResult.usage ? { usage: urlResult.usage } : {}),\n }\n const result = await applyGenerationResultTransforms(mwCtx, rawResult)\n\n // Fire finish before yielding the terminal chunks: the generation has\n // succeeded, so a consumer that stops reading after `generation:result`\n // (without pulling `RUN_FINISHED`) must not trip the abandonment path in\n // `finally`, which would otherwise report a spurious cancellation.\n if (urlResult.usage)\n await runGenerationUsage(middleware, mwCtx, urlResult.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration: Date.now() - obsStartTime,\n usage: urlResult.usage,\n })\n settled = true\n abortControls.clear()\n\n yield {\n type: 'CUSTOM',\n name: 'generation:result',\n value: result,\n timestamp: Date.now(),\n }\n\n yield {\n type: 'RUN_FINISHED',\n runId,\n threadId: wireThreadId,\n finishReason: 'stop',\n timestamp: Date.now(),\n } as StreamChunk\n return\n }\n\n if (statusResult.status === 'failed') {\n throw new Error(statusResult.error || 'Video generation failed')\n }\n }\n\n throw new Error('Video generation timed out')\n } catch (error: unknown) {\n abortControls.clear()\n const payload = toRunErrorPayload(error, 'Video generation failed')\n // Mark settled before firing terminal hooks: if a user error-hook throws,\n // the `finally` below must still not double-fire onAbort over the same op\n // (which would mask the original error and end the span twice).\n settled = true\n const elapsed = Date.now() - obsStartTime\n if (isActivityAbortError(error, abortControls.signal)) {\n await runGenerationAbort(middleware, mwCtx, {\n reason: abortReasonMessage(error, abortControls.signal),\n duration: elapsed,\n })\n } else {\n await runGenerationError(middleware, mwCtx, {\n error,\n duration: elapsed,\n })\n }\n logger.errors('generateVideo activity failed', {\n message: payload.message,\n code: payload.code,\n source: 'generateVideo',\n })\n yield {\n type: 'RUN_ERROR',\n runId,\n threadId: wireThreadId,\n message: payload.message,\n code: payload.code,\n error: payload,\n timestamp: Date.now(),\n } as StreamChunk\n } finally {\n abortControls.clear()\n if (!settled) {\n // The consumer abandoned the stream (broke the `for await` loop or\n // disconnected) before completion, so the generator is being unwound at\n // a `yield` without reaching finish/error. Fire `onAbort` — a cancel, not\n // an error — so otelMiddleware ends its span instead of leaking it.\n await runGenerationAbort(middleware, mwCtx, {\n reason: 'Video generation stream abandoned before completion',\n duration: Date.now() - obsStartTime,\n })\n }\n }\n}\n\n/**\n * Options for {@link getVideoJobStatus}.\n *\n * The run this poll finishes is identified by `adapter` + `jobId` alone — the\n * same pair the submitting `generateVideo()` call derived it from — so there is\n * no run id to thread through. Pass the submission's `threadId` and the same\n * `middleware`.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface VideoJobStatusOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n> {\n /** The video adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** The job ID to check status for */\n jobId: string\n /**\n * The scope the run is filed under. Must match the submission's `threadId` —\n * generation persistence REFUSES a run without a scope (a run filed under\n * none can never be hydrated by one), so omitting it throws rather than\n * quietly filing the finished video somewhere unreachable.\n */\n threadId?: string\n /**\n * Observe-only middleware. Hooks fire ONLY on the poll that observes a\n * terminal job state: `onStart` (resuming the submission's run), then the\n * result transforms — which is where persistence copies the video into a blob\n * store and rewrites `url` to a durable one, so the returned result carries\n * the same urls as the stored record — then `onFinish`, or `onError` when the\n * job failed. Intermediate polls invoke nothing, so a middleware is not\n * charged for the wait.\n */\n middleware?: Array<GenerationMiddleware>\n}\n\n/**\n * The status of a video job, plus the video itself once the job completed.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface VideoJobStatusResult {\n /** Job identifier */\n jobId: string\n status: 'pending' | 'processing' | 'completed' | 'failed'\n progress?: number\n url?: string\n /** When the provider url expires, if it reported one. */\n expiresAt?: Date\n error?: string\n usage?: TokenUsage\n /** Durable artifact references, when generation persistence is wired. */\n artifacts?: Array<PersistedArtifactRef>\n}\n\n/**\n * Get video job status - returns the current status, progress, and URL if available.\n *\n * This function combines status checking and URL retrieval. If the job is completed,\n * it will automatically fetch and include the video URL.\n *\n * It is also where a non-streaming `generateVideo()` run ENDS: pass the same\n * `middleware` and `threadId`, and the poll that first sees a terminal job state\n * finishes the run (recording the result and its artifacts) or fails it. The run\n * is identified by `adapter` + `jobId`, exactly what the submission derived it\n * from, so there is nothing else to carry between the two calls.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @example Check job status\n * ```ts\n * import { getVideoJobStatus } from '@tanstack/ai'\n * import { openaiVideo } from '@tanstack/ai-openai'\n *\n * const result = await getVideoJobStatus({\n * adapter: openaiVideo('sora-2'),\n * jobId: 'job-123'\n * })\n *\n * console.log('Status:', result.status)\n * console.log('Progress:', result.progress)\n * if (result.url) {\n * console.log('Video URL:', result.url)\n * }\n * ```\n *\n * @example Submit and poll one persisted run\n * ```ts\n * import { generateVideo, getVideoJobStatus } from '@tanstack/ai'\n * import { withGenerationPersistence } from '@tanstack/ai-persistence'\n * import { openaiVideo } from '@tanstack/ai-openai'\n *\n * const adapter = openaiVideo('sora-2')\n * const middleware = [withGenerationPersistence(persistence)]\n *\n * // Opens the run (status `running`, jobId recorded). Its run id is derived\n * // from the provider job, so nothing has to be stored to resume it.\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: 'A cat chasing a dog in a sunny park',\n * threadId,\n * middleware,\n * })\n *\n * // Completes the SAME run once the job settles — this is what writes the\n * // video, its artifacts, and the terminal status. Works from a different\n * // request or process: the jobId is the only correlation.\n * const status = await getVideoJobStatus({\n * adapter,\n * jobId,\n * threadId,\n * middleware,\n * })\n * ```\n */\nexport async function getVideoJobStatus<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n>(options: VideoJobStatusOptions<TAdapter>): Promise<VideoJobStatusResult> {\n const { adapter, jobId, middleware } = options\n const requestId = createId('video-status')\n const startTime = Date.now()\n\n // Built per call but only USED on a terminal poll — `onStart` is what\n // registers the result transforms, so it has to run in the same call that\n // applies them.\n const terminalContext = (): GenerationMiddlewareContext =>\n createGenerationContext({\n requestId,\n activity: 'video',\n provider: adapter.name,\n model: adapter.model,\n threadId: options.threadId,\n // Recomputed, never passed in: the submitting call derived the same id\n // from the same provider + job, so the two halves agree without the\n // caller carrying anything but the jobId they must already have.\n runId: videoRunIdForJob(adapter.name, jobId),\n // Deliberately no `artifactInputs`: the submission already persisted any\n // prompt inputs under this run, and passing them again would store a\n // second copy of every input image.\n createId,\n })\n\n aiEventClient.emit('video:request:started', {\n requestId,\n provider: adapter.name,\n model: adapter.model,\n requestType: 'status',\n jobId,\n timestamp: startTime,\n })\n\n // Get status first\n const statusResult = await adapter.getVideoStatus(jobId)\n\n // If completed, also get the URL\n if (statusResult.status === 'completed') {\n let urlResult: VideoUrlResult\n // Scoped tightly to the provider call: a middleware hook that throws must\n // surface as itself, not be relabelled \"failed to get video URL\" and then\n // re-reported to the very middleware that threw.\n try {\n urlResult = await adapter.getVideoUrl(jobId)\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : 'Failed to get video URL'\n aiEventClient.emit('video:request:completed', {\n requestId,\n provider: adapter.name,\n model: adapter.model,\n requestType: 'status',\n jobId,\n status: 'failed',\n progress: statusResult.progress,\n error: errorMessage,\n duration: Date.now() - startTime,\n timestamp: Date.now(),\n })\n // Provider reported completed but result fetch failed — treat as failed,\n // and fail the run with it: the job is terminal, so nothing later will.\n await runGenerationError(middleware, terminalContext(), {\n error,\n duration: Date.now() - startTime,\n })\n return {\n jobId,\n status: 'failed' as const,\n progress: statusResult.progress,\n error: errorMessage,\n }\n }\n\n aiEventClient.emit('video:request:completed', {\n requestId,\n provider: adapter.name,\n model: adapter.model,\n requestType: 'status',\n jobId,\n status: statusResult.status,\n progress: statusResult.progress,\n url: urlResult.url,\n duration: Date.now() - startTime,\n timestamp: Date.now(),\n })\n if (urlResult.usage) {\n aiEventClient.emit('video:usage', {\n requestId,\n model: adapter.model,\n usage: urlResult.usage,\n timestamp: Date.now(),\n })\n }\n\n const mwCtx = terminalContext()\n await runGenerationStart(middleware, mwCtx)\n const result = await applyGenerationResultTransforms<VideoJobStatusResult>(\n mwCtx,\n {\n jobId,\n status: 'completed',\n ...(statusResult.progress !== undefined\n ? { progress: statusResult.progress }\n : {}),\n url: urlResult.url,\n ...(urlResult.expiresAt ? { expiresAt: urlResult.expiresAt } : {}),\n ...(urlResult.usage ? { usage: urlResult.usage } : {}),\n },\n )\n if (urlResult.usage)\n await runGenerationUsage(middleware, mwCtx, urlResult.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration: Date.now() - startTime,\n usage: urlResult.usage,\n })\n return result\n }\n\n aiEventClient.emit('video:request:completed', {\n requestId,\n provider: adapter.name,\n model: adapter.model,\n requestType: 'status',\n jobId,\n status: statusResult.status,\n progress: statusResult.progress,\n error: statusResult.error,\n duration: Date.now() - startTime,\n timestamp: Date.now(),\n })\n\n // A failed job is terminal for the run too: without this the record would sit\n // at `running` forever, indistinguishable from a job still being worked on.\n if (statusResult.status === 'failed') {\n await runGenerationError(middleware, terminalContext(), {\n error: new Error(statusResult.error || 'Video generation failed'),\n duration: Date.now() - startTime,\n })\n }\n\n // Return status for non-completed jobs\n return {\n jobId,\n status: statusResult.status,\n progress: statusResult.progress,\n error: statusResult.error,\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateVideo() function without executing.\n */\nexport function createVideoOptions<\n TAdapter extends VideoAdapter<string, any, any, any, any, any>,\n TStream extends boolean = false,\n>(\n options: VideoCreateOptions<TAdapter, TStream>,\n): VideoCreateOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n VideoAdapter,\n VideoAdapterConfig,\n AnyVideoAdapter,\n} from './adapter'\nexport { BaseVideoAdapter } from './adapter'\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-stream-summarize.js","names":[],"sources":["../../../../src/activities/summarize/chat-stream-summarize.ts"],"sourcesContent":["import { EventType } from '@ag-ui/core'\nimport { toRunErrorPayload } from '../error-payload'\nimport { MAX_TOKENS_KEYS } from '../../utilities/sampling-keys'\nimport { BaseSummarizeAdapter } from './adapter'\nimport type {\n StreamChunk,\n SummarizationOptions,\n SummarizationResult,\n TextOptions,\n} from '../../types'\n\n/**\n * Minimal contract for a text adapter that supports `chatStream`. Lets\n * `ChatStreamSummarizeAdapter` work with any text adapter without coupling\n * to a specific implementation.\n *\n * The provider-options shape is intentionally `any` here — the wrapper only\n * forwards `modelOptions` straight through, so a text adapter with a richer\n * per-model options type (e.g. `ResolveProviderOptions<TModel>`) is still\n * acceptable. Summarize-level type safety is enforced via\n * `SummarizationOptions<TProviderOptions>` on the wrapper itself.\n */\nexport interface ChatStreamCapable {\n chatStream: (options: TextOptions<any>) => AsyncIterable<StreamChunk>\n}\n\n/**\n * Provider-native max-output-tokens key per summarize-adapter `name`. summarize\n * is provider-agnostic and forwards `modelOptions` opaquely to the wrapped text\n * adapter, so `maxLength` must be written under the exact key the underlying\n * provider reads — no adapter reads a generic `maxTokens`. Ollama is the one\n * exception: it nests sampling under `options`, so it has no entry here and is\n * handled as a special nested case in `applyMaxLength`/`applyDefaultTemperature`.\n *\n * Keep in sync with each adapter's wire mapping:\n * - OpenAI (Responses): `max_output_tokens`\n * - Anthropic / Grok: `max_tokens`\n * - Groq: `max_completion_tokens`\n * - Gemini: `maxOutputTokens`\n * - OpenRouter: `maxCompletionTokens`\n * - Ollama: nested `options.num_predict` (no entry — see `applyMaxLength`)\n */\nconst MAX_TOKENS_KEY_BY_ADAPTER: Record<string, string> = {\n openai: 'max_output_tokens',\n anthropic: 'max_tokens',\n grok: 'max_tokens',\n groq: 'max_completion_tokens',\n gemini: 'maxOutputTokens',\n openrouter: 'maxCompletionTokens',\n}\n\n/**\n * Every flat key any supported provider uses to cap output tokens (plus the\n * generic `maxTokens` spelling no adapter reads). Used to detect a\n * caller-supplied token limit so the summarize default never overrides an\n * explicit caller value. Shared with the OTel middleware via\n * `MAX_TOKENS_KEYS` so the two spelling sets cannot drift.\n */\nconst KNOWN_MAX_TOKENS_KEYS = MAX_TOKENS_KEYS\n\n/**\n * Whether `applyMaxLength` knows how to place a token limit for this adapter\n * `name` (either the nested Ollama shape or a flat provider-native key).\n * Used to surface a warning when `maxLength` would otherwise be silently\n * dropped for an unrecognised adapter name.\n */\nfunction isKnownMaxTokensAdapter(adapterName: string): boolean {\n return (\n adapterName === 'ollama' ||\n MAX_TOKENS_KEY_BY_ADAPTER[adapterName] !== undefined\n )\n}\n\n/**\n * Apply the low-temperature summarize default to a working copy of the\n * caller's `modelOptions`, placed where the wrapped provider actually reads\n * it (nested under `options` for Ollama, flat otherwise). The caller always\n * wins: if they already set `temperature` in that location, it is untouched.\n */\nfunction applyDefaultTemperature(\n adapterName: string,\n temperature: number,\n modelOptions: Record<string, unknown>,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...modelOptions }\n\n if (adapterName === 'ollama') {\n const existing =\n merged.options && typeof merged.options === 'object'\n ? (merged.options as Record<string, unknown>)\n : undefined\n if (existing && 'temperature' in existing) return merged\n merged.options = { temperature, ...existing }\n return merged\n }\n\n if ('temperature' in merged) return merged\n merged.temperature = temperature\n return merged\n}\n\n/**\n * Resolve `maxLength` to the provider-native max-output-tokens key for the\n * given summarize-adapter `name` (this wrapper's OWN `name`, not the wrapped\n * text adapter's) and merge it into a working copy of the caller's\n * `modelOptions`. The caller always wins: if they already set any recognised\n * token-limit key (flat or, for Ollama, nested `options.num_predict`), the\n * default is left untouched. Unknown/unrecognised adapter names fall back to\n * NOT setting a token key (the prompt hint still asks the model to stay under\n * `maxLength`) rather than writing a dead key no provider reads.\n *\n * Caveat (intentional): \"caller wins\" keys off ANY recognised spelling in\n * `KNOWN_MAX_TOKENS_KEYS`, but only the adapter's native key is read on the\n * wire. So a caller who sets a NON-native spelling for this provider — e.g.\n * `maxTokens`, or Anthropic's `max_tokens` against an OpenAI adapter — suppresses\n * the summarize default WITHOUT getting their own value applied either: neither\n * cap reaches the wire. This favours never clobbering a migration leftover over\n * guaranteeing a cap; the prompt-level hint still asks the model to stay under\n * `maxLength`. Rename the key to the provider-native spelling to forward it.\n */\nfunction applyMaxLength(\n adapterName: string,\n maxLength: number,\n modelOptions: Record<string, unknown>,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...modelOptions }\n\n if (adapterName === 'ollama') {\n // Honor a caller-set limit in either shape: a recognised flat key (e.g.\n // left over from a migration) or the nested `options.num_predict`.\n const callerSetFlatLimit = KNOWN_MAX_TOKENS_KEYS.some(\n (k) => typeof merged[k] === 'number',\n )\n const existing =\n merged.options && typeof merged.options === 'object'\n ? (merged.options as Record<string, unknown>)\n : undefined\n if (\n callerSetFlatLimit ||\n (existing && typeof existing.num_predict === 'number')\n ) {\n return merged\n }\n merged.options = { num_predict: maxLength, ...existing }\n return merged\n }\n\n const key = MAX_TOKENS_KEY_BY_ADAPTER[adapterName]\n if (key === undefined) return merged\n\n const callerSetLimit = KNOWN_MAX_TOKENS_KEYS.some(\n (k) => typeof merged[k] === 'number',\n )\n if (callerSetLimit) return merged\n\n merged[key] = maxLength\n return merged\n}\n\n/**\n * Extract the per-model `modelOptions` type a text adapter accepts. Used by\n * provider summarize factories so their `modelOptions` IntelliSense matches\n * what the underlying text adapter actually understands.\n */\nexport type InferTextProviderOptions<TAdapter> = TAdapter extends {\n '~types': { providerOptions: infer P }\n}\n ? P extends object\n ? P\n : object\n : object\n\n/**\n * Summarize adapter that wraps any `ChatStreamCapable` text adapter and\n * prompts it for summarization. Not tied to any wire format.\n */\nexport class ChatStreamSummarizeAdapter<\n TModel extends string,\n TProviderOptions extends object = Record<string, unknown>,\n> extends BaseSummarizeAdapter<TModel, TProviderOptions> {\n readonly name: string\n\n private readonly textAdapter: ChatStreamCapable\n\n constructor(\n textAdapter: ChatStreamCapable,\n model: TModel,\n name: string = 'chat-stream-summarize',\n ) {\n super({}, model)\n this.name = name\n this.textAdapter = textAdapter\n }\n\n async summarize(\n options: SummarizationOptions<TProviderOptions>,\n ): Promise<SummarizationResult> {\n const systemPrompt = this.buildSummarizationPrompt(options)\n\n let summary = ''\n const id = this.generateId()\n let model = options.model\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }\n\n options.logger.request(\n `activity=summarize provider=${this.name} model=${options.model} text-length=${options.text.length} maxLength=${options.maxLength ?? 'unset'}`,\n { provider: this.name, model: options.model },\n )\n\n try {\n for await (const chunk of this.textAdapter.chatStream(\n this.buildTextOptions(options, systemPrompt),\n )) {\n if (chunk.type === 'TEXT_MESSAGE_CONTENT') {\n if (chunk.content) {\n summary = chunk.content\n } else if (chunk.delta) {\n // Append delta only when present — a content-less chunk with no\n // delta would otherwise concat literal `'undefined'`.\n summary += chunk.delta\n }\n model = chunk.model || model\n }\n if (chunk.type === 'RUN_FINISHED') {\n if (chunk.usage) {\n usage = chunk.usage\n }\n }\n // Surface failures: the underlying chatStream emits RUN_ERROR instead\n // of throwing, so without this branch summarize() would return an\n // empty summary and pretend a failed run succeeded.\n if (chunk.type === 'RUN_ERROR') {\n const message =\n (chunk.error && typeof chunk.error.message === 'string'\n ? chunk.error.message\n : null) ?? 'Summarization failed'\n const code =\n chunk.error && typeof chunk.error.code === 'string'\n ? chunk.error.code\n : undefined\n const err = new Error(message)\n if (code) {\n ;(err as Error & { code?: string }).code = code\n }\n throw err\n }\n }\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n options.logger.errors(`${this.name}.summarize fatal`, {\n error: toRunErrorPayload(error, `${this.name}.summarize failed`),\n source: `${this.name}.summarize`,\n })\n throw error\n }\n\n return { id, model, summary, usage }\n }\n\n override async *summarizeStream(\n options: SummarizationOptions<TProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const systemPrompt = this.buildSummarizationPrompt(options)\n\n options.logger.request(\n `activity=summarizeStream provider=${this.name} model=${options.model} text-length=${options.text.length} maxLength=${options.maxLength ?? 'unset'}`,\n { provider: this.name, model: options.model },\n )\n\n const id = this.generateId()\n let summary = ''\n let model = options.model\n let usage: SummarizationResult['usage'] = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n }\n\n try {\n for await (const chunk of this.textAdapter.chatStream(\n this.buildTextOptions(options, systemPrompt),\n )) {\n // Accumulate the same way `summarize()` does so consumers see deltas\n // AND the terminal `generation:result` event below carries the same\n // final summary that non-streaming returns.\n if (chunk.type === 'TEXT_MESSAGE_CONTENT') {\n if (chunk.content) {\n summary = chunk.content\n } else if (chunk.delta) {\n summary += chunk.delta\n }\n if (chunk.model) model = chunk.model\n }\n\n // Emit the GenerationClient-shaped result event just before the\n // terminal RUN_FINISHED so subscribers (useSummarize) populate\n // `result` before flipping `status` to success.\n if (chunk.type === 'RUN_FINISHED') {\n if (chunk.usage) usage = chunk.usage\n if (chunk.model) model = chunk.model\n yield {\n type: EventType.CUSTOM,\n name: 'generation:result',\n value: { id, model, summary, usage } satisfies SummarizationResult,\n model,\n timestamp: Date.now(),\n }\n }\n\n yield chunk\n }\n } catch (error: unknown) {\n options.logger.errors(`${this.name}.summarizeStream fatal`, {\n error: toRunErrorPayload(error, `${this.name}.summarizeStream failed`),\n source: `${this.name}.summarizeStream`,\n })\n throw error\n }\n }\n\n /**\n * Build the TextOptions passed to the underlying chatStream. Provider\n * `modelOptions` from the summarize call are forwarded as-is so knobs like\n * Anthropic cache headers, Gemini safety settings, or Ollama tuning params\n * still reach the wire layer.\n */\n protected buildTextOptions(\n options: SummarizationOptions<TProviderOptions>,\n systemPrompt: string,\n ): TextOptions<TProviderOptions> {\n // Sampling knobs now live in provider-native `modelOptions`. Apply the\n // low-temperature default where the wrapped provider actually reads it\n // (nested under `options` for Ollama, flat otherwise) so callers can still\n // override it. Resolving the placement from this summarize adapter's OWN\n // `name` keeps the default off the wire correctly per provider — a flat\n // `temperature` would be silently dropped by Ollama while still showing up\n // in OTel.\n let working: Record<string, unknown> = {\n ...(options.modelOptions as Record<string, unknown> | undefined),\n }\n working = applyDefaultTemperature(this.name, 0.3, working)\n // `maxLength` must reach the wire under the provider-native token key (it\n // differs per provider, and no adapter reads a generic `maxTokens`).\n // Resolve it from this summarize adapter's `name` (the constructor arg,\n // not the wrapped text adapter's name), never overriding a caller-supplied\n // token limit.\n if (options.maxLength !== undefined) {\n if (!isKnownMaxTokensAdapter(this.name)) {\n options.logger.warn(\n `summarize: maxLength=${options.maxLength} could not be mapped to a provider token key for adapter name \"${this.name}\" — it was dropped from modelOptions (the prompt still asks the model to stay under it). Construct ChatStreamSummarizeAdapter with a recognised provider name to forward the cap.`,\n { provider: this.name },\n )\n }\n working = applyMaxLength(this.name, options.maxLength, working)\n }\n const modelOptions = working as TProviderOptions\n\n return {\n model: options.model,\n messages: [{ role: 'user', content: options.text }],\n systemPrompts: [systemPrompt],\n modelOptions,\n logger: options.logger,\n // Forward the run identity so the wrapped chat stamps it onto RUN_STARTED\n // (chat uses `runId` as its `runIdOverride`). Conditional spreads keep the\n // fields absent when unset, under `exactOptionalPropertyTypes`.\n ...(options.runId !== undefined ? { runId: options.runId } : {}),\n ...(options.threadId !== undefined ? { threadId: options.threadId } : {}),\n }\n }\n\n protected buildSummarizationPrompt(\n options: SummarizationOptions<TProviderOptions>,\n ): string {\n let prompt = 'You are a professional summarizer. '\n\n switch (options.style) {\n case 'bullet-points':\n prompt += 'Provide a summary in bullet point format. '\n break\n case 'paragraph':\n prompt += 'Provide a summary in paragraph format. '\n break\n case 'concise':\n prompt += 'Provide a very concise summary in 1-2 sentences. '\n break\n case undefined:\n prompt += 'Provide a clear and concise summary. '\n break\n default:\n prompt += 'Provide a clear and concise summary. '\n }\n\n if (options.focus && options.focus.length > 0) {\n prompt += `Focus on the following aspects: ${options.focus.join(', ')}. `\n }\n\n if (options.maxLength) {\n prompt += `Keep the summary under ${options.maxLength} tokens. `\n }\n\n return prompt\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0CA,IAAM,4BAAoD;CACxD,QAAQ;CACR,WAAW;CACX,MAAM;CACN,MAAM;CACN,QAAQ;CACR,YAAY;AACd;;;;;;;;AASA,IAAM,wBAAwB;;;;;;;AAQ9B,SAAS,wBAAwB,aAA8B;CAC7D,OACE,gBAAgB,YAChB,0BAA0B,iBAAiB,KAAA;AAE/C;;;;;;;AAQA,SAAS,wBACP,aACA,aACA,cACyB;CACzB,MAAM,SAAkC,EAAE,GAAG,aAAa;CAE1D,IAAI,gBAAgB,UAAU;EAC5B,MAAM,WACJ,OAAO,WAAW,OAAO,OAAO,YAAY,WACvC,OAAO,UACR,KAAA;EACN,IAAI,YAAY,iBAAiB,UAAU,OAAO;EAClD,OAAO,UAAU;GAAE;GAAa,GAAG;EAAS;EAC5C,OAAO;CACT;CAEA,IAAI,iBAAiB,QAAQ,OAAO;CACpC,OAAO,cAAc;CACrB,OAAO;AACT;;;;;;;;;;;;;;;;;;;;AAqBA,SAAS,eACP,aACA,WACA,cACyB;CACzB,MAAM,SAAkC,EAAE,GAAG,aAAa;CAE1D,IAAI,gBAAgB,UAAU;EAG5B,MAAM,qBAAqB,sBAAsB,MAC9C,MAAM,OAAO,OAAO,OAAO,QAC9B;EACA,MAAM,WACJ,OAAO,WAAW,OAAO,OAAO,YAAY,WACvC,OAAO,UACR,KAAA;EACN,IACE,sBACC,YAAY,OAAO,SAAS,gBAAgB,UAE7C,OAAO;EAET,OAAO,UAAU;GAAE,aAAa;GAAW,GAAG;EAAS;EACvD,OAAO;CACT;CAEA,MAAM,MAAM,0BAA0B;CACtC,IAAI,QAAQ,KAAA,GAAW,OAAO;CAK9B,IAHuB,sBAAsB,MAC1C,MAAM,OAAO,OAAO,OAAO,QAE1B,GAAgB,OAAO;CAE3B,OAAO,OAAO;CACd,OAAO;AACT;;;;;AAmBA,IAAa,6BAAb,cAGU,qBAA+C;CACvD;CAEA;CAEA,YACE,aACA,OACA,OAAe,yBACf;EACA,MAAM,CAAC,GAAG,KAAK;EACf,KAAK,OAAO;EACZ,KAAK,cAAc;CACrB;CAEA,MAAM,UACJ,SAC8B;EAC9B,MAAM,eAAe,KAAK,yBAAyB,OAAO;EAE1D,IAAI,UAAU;EACd,MAAM,KAAK,KAAK,WAAW;EAC3B,IAAI,QAAQ,QAAQ;EACpB,IAAI,QAAQ;GAAE,cAAc;GAAG,kBAAkB;GAAG,aAAa;EAAE;EAEnE,QAAQ,OAAO,QACb,+BAA+B,KAAK,KAAK,SAAS,QAAQ,MAAM,eAAe,QAAQ,KAAK,OAAO,aAAa,QAAQ,aAAa,WACrI;GAAE,UAAU,KAAK;GAAM,OAAO,QAAQ;EAAM,CAC9C;EAEA,IAAI;GACF,WAAW,MAAM,SAAS,KAAK,YAAY,WACzC,KAAK,iBAAiB,SAAS,YAAY,CAC7C,GAAG;IACD,IAAI,MAAM,SAAS,wBAAwB;KACzC,IAAI,MAAM,SACR,UAAU,MAAM;UACX,IAAI,MAAM,OAGf,WAAW,MAAM;KAEnB,QAAQ,MAAM,SAAS;IACzB;IACA,IAAI,MAAM,SAAS;SACb,MAAM,OACR,QAAQ,MAAM;IAAA;IAMlB,IAAI,MAAM,SAAS,aAAa;KAC9B,MAAM,WACH,MAAM,SAAS,OAAO,MAAM,MAAM,YAAY,WAC3C,MAAM,MAAM,UACZ,SAAS;KACf,MAAM,OACJ,MAAM,SAAS,OAAO,MAAM,MAAM,SAAS,WACvC,MAAM,MAAM,OACZ,KAAA;KACN,MAAM,MAAM,IAAI,MAAM,OAAO;KAC7B,IAAI,MACD,IAAmC,OAAO;KAE7C,MAAM;IACR;GACF;EACF,SAAS,OAAgB;GAGvB,QAAQ,OAAO,OAAO,GAAG,KAAK,KAAK,mBAAmB;IACpD,OAAO,kBAAkB,OAAO,GAAG,KAAK,KAAK,kBAAkB;IAC/D,QAAQ,GAAG,KAAK,KAAK;GACvB,CAAC;GACD,MAAM;EACR;EAEA,OAAO;GAAE;GAAI;GAAO;GAAS;EAAM;CACrC;CAEA,OAAgB,gBACd,SAC4B;EAC5B,MAAM,eAAe,KAAK,yBAAyB,OAAO;EAE1D,QAAQ,OAAO,QACb,qCAAqC,KAAK,KAAK,SAAS,QAAQ,MAAM,eAAe,QAAQ,KAAK,OAAO,aAAa,QAAQ,aAAa,WAC3I;GAAE,UAAU,KAAK;GAAM,OAAO,QAAQ;EAAM,CAC9C;EAEA,MAAM,KAAK,KAAK,WAAW;EAC3B,IAAI,UAAU;EACd,IAAI,QAAQ,QAAQ;EACpB,IAAI,QAAsC;GACxC,cAAc;GACd,kBAAkB;GAClB,aAAa;EACf;EAEA,IAAI;GACF,WAAW,MAAM,SAAS,KAAK,YAAY,WACzC,KAAK,iBAAiB,SAAS,YAAY,CAC7C,GAAG;IAID,IAAI,MAAM,SAAS,wBAAwB;KACzC,IAAI,MAAM,SACR,UAAU,MAAM;UACX,IAAI,MAAM,OACf,WAAW,MAAM;KAEnB,IAAI,MAAM,OAAO,QAAQ,MAAM;IACjC;IAKA,IAAI,MAAM,SAAS,gBAAgB;KACjC,IAAI,MAAM,OAAO,QAAQ,MAAM;KAC/B,IAAI,MAAM,OAAO,QAAQ,MAAM;KAC/B,MAAM;MACJ,MAAM,UAAU;MAChB,MAAM;MACN,OAAO;OAAE;OAAI;OAAO;OAAS;MAAM;MACnC;MACA,WAAW,KAAK,IAAI;KACtB;IACF;IAEA,MAAM;GACR;EACF,SAAS,OAAgB;GACvB,QAAQ,OAAO,OAAO,GAAG,KAAK,KAAK,yBAAyB;IAC1D,OAAO,kBAAkB,OAAO,GAAG,KAAK,KAAK,wBAAwB;IACrE,QAAQ,GAAG,KAAK,KAAK;GACvB,CAAC;GACD,MAAM;EACR;CACF;;;;;;;CAQA,iBACE,SACA,cAC+B;EAQ/B,IAAI,UAAmC,EACrC,GAAI,QAAQ,aACd;EACA,UAAU,wBAAwB,KAAK,MAAM,IAAK,OAAO;EAMzD,IAAI,QAAQ,cAAc,KAAA,GAAW;GACnC,IAAI,CAAC,wBAAwB,KAAK,IAAI,GACpC,QAAQ,OAAO,KACb,wBAAwB,QAAQ,UAAU,iEAAiE,KAAK,KAAK,oLACrH,EAAE,UAAU,KAAK,KAAK,CACxB;GAEF,UAAU,eAAe,KAAK,MAAM,QAAQ,WAAW,OAAO;EAChE;EACA,MAAM,eAAe;EAErB,OAAO;GACL,OAAO,QAAQ;GACf,UAAU,CAAC;IAAE,MAAM;IAAQ,SAAS,QAAQ;GAAK,CAAC;GAClD,eAAe,CAAC,YAAY;GAC5B;GACA,QAAQ,QAAQ;GAIhB,GAAI,QAAQ,UAAU,KAAA,IAAY,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;GAC9D,GAAI,QAAQ,aAAa,KAAA,IAAY,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;EACzE;CACF;CAEA,yBACE,SACQ;EACR,IAAI,SAAS;EAEb,QAAQ,QAAQ,OAAhB;GACE,KAAK;IACH,UAAU;IACV;GACF,KAAK;IACH,UAAU;IACV;GACF,KAAK;IACH,UAAU;IACV;GACF,KAAK,KAAA;IACH,UAAU;IACV;GACF,SACE,UAAU;EACd;EAEA,IAAI,QAAQ,SAAS,QAAQ,MAAM,SAAS,GAC1C,UAAU,mCAAmC,QAAQ,MAAM,KAAK,IAAI,EAAE;EAGxE,IAAI,QAAQ,WACV,UAAU,0BAA0B,QAAQ,UAAU;EAGxD,OAAO;CACT;AACF"}
|
|
1
|
+
{"version":3,"file":"chat-stream-summarize.js","names":[],"sources":["../../../../src/activities/summarize/chat-stream-summarize.ts"],"sourcesContent":["import { EventType } from '@ag-ui/core'\nimport { toRunErrorPayload } from '../error-payload'\nimport { MAX_TOKENS_KEYS } from '../../utilities/sampling-keys'\nimport { BaseSummarizeAdapter } from './adapter'\nimport type {\n StreamChunk,\n SummarizationOptions,\n SummarizationResult,\n TextOptions,\n} from '../../types'\n\n/**\n * Minimal contract for a text adapter that supports `chatStream`. Lets\n * `ChatStreamSummarizeAdapter` work with any text adapter without coupling\n * to a specific implementation.\n *\n * The provider-options shape is intentionally `any` here — the wrapper only\n * forwards `modelOptions` straight through, so a text adapter with a richer\n * per-model options type (e.g. `ResolveProviderOptions<TModel>`) is still\n * acceptable. Summarize-level type safety is enforced via\n * `SummarizationOptions<TProviderOptions>` on the wrapper itself.\n */\nexport interface ChatStreamCapable {\n chatStream: (options: TextOptions<any>) => AsyncIterable<StreamChunk>\n}\n\n/**\n * Provider-native max-output-tokens key per summarize-adapter `name`. summarize\n * is provider-agnostic and forwards `modelOptions` opaquely to the wrapped text\n * adapter, so `maxLength` must be written under the exact key the underlying\n * provider reads — no adapter reads a generic `maxTokens`. Ollama is the one\n * exception: it nests sampling under `options`, so it has no entry here and is\n * handled as a special nested case in `applyMaxLength`/`applyDefaultTemperature`.\n *\n * Keep in sync with each adapter's wire mapping:\n * - OpenAI (Responses): `max_output_tokens`\n * - Anthropic / Grok: `max_tokens`\n * - Groq: `max_completion_tokens`\n * - Gemini: `maxOutputTokens`\n * - OpenRouter: `maxCompletionTokens`\n * - Ollama: nested `options.num_predict` (no entry — see `applyMaxLength`)\n */\nconst MAX_TOKENS_KEY_BY_ADAPTER: Record<string, string> = {\n openai: 'max_output_tokens',\n anthropic: 'max_tokens',\n grok: 'max_tokens',\n groq: 'max_completion_tokens',\n gemini: 'maxOutputTokens',\n openrouter: 'maxCompletionTokens',\n}\n\n/**\n * Every flat key any supported provider uses to cap output tokens (plus the\n * generic `maxTokens` spelling no adapter reads). Used to detect a\n * caller-supplied token limit so the summarize default never overrides an\n * explicit caller value. Shared with the OTel middleware via\n * `MAX_TOKENS_KEYS` so the two spelling sets cannot drift.\n */\nconst KNOWN_MAX_TOKENS_KEYS = MAX_TOKENS_KEYS\n\n/**\n * Whether `applyMaxLength` knows how to place a token limit for this adapter\n * `name` (either the nested Ollama shape or a flat provider-native key).\n * Used to surface a warning when `maxLength` would otherwise be silently\n * dropped for an unrecognised adapter name.\n */\nfunction isKnownMaxTokensAdapter(adapterName: string): boolean {\n return (\n adapterName === 'ollama' ||\n MAX_TOKENS_KEY_BY_ADAPTER[adapterName] !== undefined\n )\n}\n\n/**\n * Apply the low-temperature summarize default to a working copy of the\n * caller's `modelOptions`, placed where the wrapped provider actually reads\n * it (nested under `options` for Ollama, flat otherwise). The caller always\n * wins: if they already set `temperature` in that location, it is untouched.\n */\nfunction applyDefaultTemperature(\n adapterName: string,\n temperature: number,\n modelOptions: Record<string, unknown>,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...modelOptions }\n\n if (adapterName === 'ollama') {\n const existing =\n merged.options && typeof merged.options === 'object'\n ? (merged.options as Record<string, unknown>)\n : undefined\n if (existing && 'temperature' in existing) return merged\n merged.options = { temperature, ...existing }\n return merged\n }\n\n if ('temperature' in merged) return merged\n merged.temperature = temperature\n return merged\n}\n\n/**\n * Resolve `maxLength` to the provider-native max-output-tokens key for the\n * given summarize-adapter `name` (this wrapper's OWN `name`, not the wrapped\n * text adapter's) and merge it into a working copy of the caller's\n * `modelOptions`. The caller always wins: if they already set any recognised\n * token-limit key (flat or, for Ollama, nested `options.num_predict`), the\n * default is left untouched. Unknown/unrecognised adapter names fall back to\n * NOT setting a token key (the prompt hint still asks the model to stay under\n * `maxLength`) rather than writing a dead key no provider reads.\n *\n * Caveat (intentional): \"caller wins\" keys off ANY recognised spelling in\n * `KNOWN_MAX_TOKENS_KEYS`, but only the adapter's native key is read on the\n * wire. So a caller who sets a NON-native spelling for this provider — e.g.\n * `maxTokens`, or Anthropic's `max_tokens` against an OpenAI adapter — suppresses\n * the summarize default WITHOUT getting their own value applied either: neither\n * cap reaches the wire. This favours never clobbering a migration leftover over\n * guaranteeing a cap; the prompt-level hint still asks the model to stay under\n * `maxLength`. Rename the key to the provider-native spelling to forward it.\n */\nfunction applyMaxLength(\n adapterName: string,\n maxLength: number,\n modelOptions: Record<string, unknown>,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...modelOptions }\n\n if (adapterName === 'ollama') {\n // Honor a caller-set limit in either shape: a recognised flat key (e.g.\n // left over from a migration) or the nested `options.num_predict`.\n const callerSetFlatLimit = KNOWN_MAX_TOKENS_KEYS.some(\n (k) => typeof merged[k] === 'number',\n )\n const existing =\n merged.options && typeof merged.options === 'object'\n ? (merged.options as Record<string, unknown>)\n : undefined\n if (\n callerSetFlatLimit ||\n (existing && typeof existing.num_predict === 'number')\n ) {\n return merged\n }\n merged.options = { num_predict: maxLength, ...existing }\n return merged\n }\n\n const key = MAX_TOKENS_KEY_BY_ADAPTER[adapterName]\n if (key === undefined) return merged\n\n const callerSetLimit = KNOWN_MAX_TOKENS_KEYS.some(\n (k) => typeof merged[k] === 'number',\n )\n if (callerSetLimit) return merged\n\n merged[key] = maxLength\n return merged\n}\n\n/**\n * Extract the per-model `modelOptions` type a text adapter accepts. Used by\n * provider summarize factories so their `modelOptions` IntelliSense matches\n * what the underlying text adapter actually understands.\n */\nexport type InferTextProviderOptions<TAdapter> = TAdapter extends {\n '~types': { providerOptions: infer P }\n}\n ? P extends object\n ? P\n : object\n : object\n\n/**\n * Summarize adapter that wraps any `ChatStreamCapable` text adapter and\n * prompts it for summarization. Not tied to any wire format.\n */\nexport class ChatStreamSummarizeAdapter<\n TModel extends string,\n TProviderOptions extends object = Record<string, unknown>,\n> extends BaseSummarizeAdapter<TModel, TProviderOptions> {\n readonly name: string\n\n private readonly textAdapter: ChatStreamCapable\n\n constructor(\n textAdapter: ChatStreamCapable,\n model: TModel,\n name: string = 'chat-stream-summarize',\n ) {\n super({}, model)\n this.name = name\n this.textAdapter = textAdapter\n }\n\n async summarize(\n options: SummarizationOptions<TProviderOptions>,\n ): Promise<SummarizationResult> {\n const systemPrompt = this.buildSummarizationPrompt(options)\n\n let summary = ''\n const id = this.generateId()\n let model = options.model\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }\n\n options.logger.request(\n `activity=summarize provider=${this.name} model=${options.model} text-length=${options.text.length} maxLength=${options.maxLength ?? 'unset'}`,\n { provider: this.name, model: options.model },\n )\n\n try {\n for await (const chunk of this.textAdapter.chatStream(\n this.buildTextOptions(options, systemPrompt),\n )) {\n if (chunk.type === 'TEXT_MESSAGE_CONTENT') {\n if (chunk.content) {\n summary = chunk.content\n } else if (chunk.delta) {\n // Append delta only when present — a content-less chunk with no\n // delta would otherwise concat literal `'undefined'`.\n summary += chunk.delta\n }\n model = chunk.model || model\n }\n if (chunk.type === 'RUN_FINISHED') {\n if (chunk.usage) {\n usage = chunk.usage\n }\n }\n // Surface failures: the underlying chatStream emits RUN_ERROR instead\n // of throwing, so without this branch summarize() would return an\n // empty summary and pretend a failed run succeeded.\n if (chunk.type === 'RUN_ERROR') {\n const message =\n (chunk.error && typeof chunk.error.message === 'string'\n ? chunk.error.message\n : null) ?? 'Summarization failed'\n const code =\n chunk.error && typeof chunk.error.code === 'string'\n ? chunk.error.code\n : undefined\n const err = new Error(message)\n if (code) {\n ;(err as Error & { code?: string }).code = code\n }\n throw err\n }\n }\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n options.logger.errors(`${this.name}.summarize fatal`, {\n error: toRunErrorPayload(error, `${this.name}.summarize failed`),\n source: `${this.name}.summarize`,\n })\n throw error\n }\n\n return { id, model, summary, usage }\n }\n\n override async *summarizeStream(\n options: SummarizationOptions<TProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const systemPrompt = this.buildSummarizationPrompt(options)\n\n options.logger.request(\n `activity=summarizeStream provider=${this.name} model=${options.model} text-length=${options.text.length} maxLength=${options.maxLength ?? 'unset'}`,\n { provider: this.name, model: options.model },\n )\n\n const id = this.generateId()\n let summary = ''\n let model = options.model\n let usage: SummarizationResult['usage'] = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n }\n\n try {\n for await (const chunk of this.textAdapter.chatStream(\n this.buildTextOptions(options, systemPrompt),\n )) {\n // Accumulate the same way `summarize()` does so consumers see deltas\n // AND the terminal `generation:result` event below carries the same\n // final summary that non-streaming returns.\n if (chunk.type === 'TEXT_MESSAGE_CONTENT') {\n if (chunk.content) {\n summary = chunk.content\n } else if (chunk.delta) {\n summary += chunk.delta\n }\n if (chunk.model) model = chunk.model\n }\n\n // Emit the GenerationClient-shaped result event just before the\n // terminal RUN_FINISHED so subscribers (useSummarize) populate\n // `result` before flipping `status` to success.\n if (chunk.type === 'RUN_FINISHED') {\n if (chunk.usage) usage = chunk.usage\n if (chunk.model) model = chunk.model\n yield {\n type: EventType.CUSTOM,\n name: 'generation:result',\n value: { id, model, summary, usage } satisfies SummarizationResult,\n model,\n timestamp: Date.now(),\n }\n }\n\n yield chunk\n }\n } catch (error: unknown) {\n options.logger.errors(`${this.name}.summarizeStream fatal`, {\n error: toRunErrorPayload(error, `${this.name}.summarizeStream failed`),\n source: `${this.name}.summarizeStream`,\n })\n throw error\n }\n }\n\n /**\n * Build the TextOptions passed to the underlying chatStream. Provider\n * `modelOptions` from the summarize call are forwarded as-is so knobs like\n * Anthropic cache headers, Gemini safety settings, or Ollama tuning params\n * still reach the wire layer.\n */\n protected buildTextOptions(\n options: SummarizationOptions<TProviderOptions>,\n systemPrompt: string,\n ): TextOptions<TProviderOptions> {\n // Sampling knobs now live in provider-native `modelOptions`. Apply the\n // low-temperature default where the wrapped provider actually reads it\n // (nested under `options` for Ollama, flat otherwise) so callers can still\n // override it. Resolving the placement from this summarize adapter's OWN\n // `name` keeps the default off the wire correctly per provider — a flat\n // `temperature` would be silently dropped by Ollama while still showing up\n // in OTel.\n let working: Record<string, unknown> = {\n ...(options.modelOptions as Record<string, unknown> | undefined),\n }\n working = applyDefaultTemperature(this.name, 0.3, working)\n // `maxLength` must reach the wire under the provider-native token key (it\n // differs per provider, and no adapter reads a generic `maxTokens`).\n // Resolve it from this summarize adapter's `name` (the constructor arg,\n // not the wrapped text adapter's name), never overriding a caller-supplied\n // token limit.\n if (options.maxLength !== undefined) {\n if (!isKnownMaxTokensAdapter(this.name)) {\n options.logger.warn(\n `summarize: maxLength=${options.maxLength} could not be mapped to a provider token key for adapter name \"${this.name}\" — it was dropped from modelOptions (the prompt still asks the model to stay under it). Construct ChatStreamSummarizeAdapter with a recognised provider name to forward the cap.`,\n { provider: this.name },\n )\n }\n working = applyMaxLength(this.name, options.maxLength, working)\n }\n const modelOptions = working as TProviderOptions\n\n return {\n model: options.model,\n messages: [{ role: 'user', content: options.text }],\n systemPrompts: [systemPrompt],\n modelOptions,\n logger: options.logger,\n // Forward the run identity so the wrapped chat stamps it onto RUN_STARTED\n // (chat uses `runId` as its `runIdOverride`). Conditional spreads keep the\n // fields absent when unset, under `exactOptionalPropertyTypes`.\n ...(options.runId !== undefined ? { runId: options.runId } : {}),\n ...(options.threadId !== undefined ? { threadId: options.threadId } : {}),\n }\n }\n\n protected buildSummarizationPrompt(\n options: SummarizationOptions<TProviderOptions>,\n ): string {\n let prompt = 'You are a professional summarizer. '\n\n switch (options.style) {\n case 'bullet-points':\n prompt += 'Provide a summary in bullet point format. '\n break\n case 'paragraph':\n prompt += 'Provide a summary in paragraph format. '\n break\n case 'concise':\n prompt += 'Provide a very concise summary in 1-2 sentences. '\n break\n case undefined:\n prompt += 'Provide a clear and concise summary. '\n break\n default:\n prompt += 'Provide a clear and concise summary. '\n }\n\n if (options.focus && options.focus.length > 0) {\n prompt += `Focus on the following aspects: ${options.focus.join(', ')}. `\n }\n\n if (options.maxLength) {\n prompt += `Keep the summary under ${options.maxLength} tokens. `\n }\n\n return prompt\n }\n}\n"],"mappings":"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"}
|
|
@@ -6,3 +6,5 @@ export { toRunErrorPayload, toRunErrorRawEvent, } from './activities/error-paylo
|
|
|
6
6
|
export { getSandboxRuntime, provideSandboxRuntime, SandboxRuntimeCapability, } from './activities/chat/middleware/sandbox-runtime.js';
|
|
7
7
|
export { getRunDisconnect, provideRunDisconnect, RunDisconnectCapability, } from './activities/chat/middleware/run-disconnect.js';
|
|
8
8
|
export { getPendingTurn, PendingTurnCapability, providePendingTurn, } from './activities/chat/middleware/pending-turn.js';
|
|
9
|
+
export { appendOutputSchemaInstruction, parseJsonFromAssistantText, } from './utilities/structured-output-text.js';
|
|
10
|
+
export { structuredOutputCompleteChunk, structuredOutputStartChunk, } from './utilities/structured-output-events.js';
|
|
@@ -4,4 +4,6 @@ import { resolveDebugOption } from "./logger/resolve.js";
|
|
|
4
4
|
import { SandboxRuntimeCapability, getSandboxRuntime, provideSandboxRuntime } from "./activities/chat/middleware/sandbox-runtime.js";
|
|
5
5
|
import { RunDisconnectCapability, getRunDisconnect, provideRunDisconnect } from "./activities/chat/middleware/run-disconnect.js";
|
|
6
6
|
import { PendingTurnCapability, getPendingTurn, providePendingTurn } from "./activities/chat/middleware/pending-turn.js";
|
|
7
|
-
|
|
7
|
+
import { appendOutputSchemaInstruction, parseJsonFromAssistantText } from "./utilities/structured-output-text.js";
|
|
8
|
+
import { structuredOutputCompleteChunk, structuredOutputStartChunk } from "./utilities/structured-output-events.js";
|
|
9
|
+
export { InternalLogger, PendingTurnCapability, RunDisconnectCapability, SandboxRuntimeCapability, appendOutputSchemaInstruction, getPendingTurn, getRunDisconnect, getSandboxRuntime, parseJsonFromAssistantText, providePendingTurn, provideRunDisconnect, provideSandboxRuntime, resolveDebugOption, structuredOutputCompleteChunk, structuredOutputStartChunk, toRunErrorPayload, toRunErrorRawEvent };
|
|
@@ -205,27 +205,31 @@ async function validateInterruptResumeBatch(input) {
|
|
|
205
205
|
code: "invalid-payload",
|
|
206
206
|
label: `Approval ${record.interruptId} envelope is invalid`
|
|
207
207
|
});
|
|
208
|
-
if (approved && envelope?.editedArgs !== void 0)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
208
|
+
if (approved && envelope?.editedArgs !== void 0) {
|
|
209
|
+
if (tool.inputSchema === void 0) errors.push(interruptItemError(input, record.interruptId, "invalid-edited-args", `Approval ${record.interruptId} cannot edit arguments without an input schema.`));
|
|
210
|
+
else await pushSchemaIssues({
|
|
211
|
+
request: input,
|
|
212
|
+
errors,
|
|
213
|
+
interruptId: record.interruptId,
|
|
214
|
+
schema: tool.inputSchema,
|
|
215
|
+
value: envelope.editedArgs,
|
|
216
|
+
code: "invalid-edited-args",
|
|
217
|
+
label: `Approval ${record.interruptId} edited arguments are invalid`
|
|
218
|
+
});
|
|
219
|
+
}
|
|
218
220
|
const branch = approved ? approval.branches.approve : approval.branches.reject;
|
|
219
|
-
if (branch)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
if (branch) {
|
|
222
|
+
if (!envelope) errors.push(interruptItemError(input, record.interruptId, "invalid-payload", `Approval ${record.interruptId} requires a payload for the ${approved ? "approve" : "reject"} decision.`));
|
|
223
|
+
else await pushSchemaIssues({
|
|
224
|
+
request: input,
|
|
225
|
+
errors,
|
|
226
|
+
interruptId: record.interruptId,
|
|
227
|
+
schema: branch.source,
|
|
228
|
+
value: envelope.payload,
|
|
229
|
+
code: "invalid-payload",
|
|
230
|
+
label: `Approval ${record.interruptId} payload is invalid`
|
|
231
|
+
});
|
|
232
|
+
}
|
|
229
233
|
}
|
|
230
234
|
for (const entry of input.resume ?? []) if (!pendingById.has(entry.interruptId)) {
|
|
231
235
|
incomplete = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interrupt-resume.js","names":[],"sources":["../../src/interrupt-resume.ts"],"sourcesContent":["import {\n INTERRUPT_BINDING_VERSION,\n canonicalizeInterruptResolutions,\n} from './interrupts'\nimport {\n canonicalInterruptJson,\n digestInterruptJson,\n} from './interrupt-serialization'\nimport {\n hashSchemaInput,\n normalizeApprovalSchema,\n} from './activities/chat/tools/approval-schema'\nimport {\n isStandardSchema,\n validateWithStandardSchema,\n} from './activities/chat/tools/schema-converter'\nimport type {\n InterruptBinding,\n InterruptSubmissionError,\n ItemInterruptErrorCode,\n ToolApprovalResolution,\n UnopenedInterruptBinding,\n} from './interrupts'\nimport type {\n ChatMiddlewareConfig,\n ChatResumeToolState,\n} from './activities/chat/middleware/types'\nimport type { Interrupt, RunAgentResumeItem } from './types'\n\n/**\n * The `Interrupt.metadata` key under which this package's resume binding\n * travels.\n *\n * Exported so anything that produces an interrupt this package must later\n * resume — an application middleware raising a generic pause, a future\n * workflow-to-AG-UI projection — attaches the binding through\n * {@link withInterruptBinding} rather than copying the string. Everything\n * outside this key is the plain AG-UI envelope and is left untouched.\n */\nexport const INTERRUPT_BINDING_METADATA_KEY = 'tanstack:interruptBinding'\n\nconst interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY\n\n/** The persistence-neutral shape required to validate an interrupt resume. */\nexport interface PendingInterruptResumeRecord {\n interruptId: string\n payload: unknown\n binding: InterruptBinding\n}\n\nexport interface ValidateInterruptResumeBatchInput {\n threadId: string\n interruptedRunId: string\n generation: number\n pending: ReadonlyArray<PendingInterruptResumeRecord>\n resume?: ReadonlyArray<RunAgentResumeItem>\n tools: ChatMiddlewareConfig['tools']\n now?: number\n}\n\nexport interface ValidatedInterruptResumeBatch {\n errors: ReadonlyArray<InterruptSubmissionError>\n resolutions?: ReadonlyArray<RunAgentResumeItem>\n canonicalResolutions?: string\n fingerprint?: string\n resumeToolState?: ChatResumeToolState\n}\n\nexport class InterruptResumeValidationError extends Error {\n override readonly name = 'InterruptResumeValidationError'\n\n constructor(readonly errors: ReadonlyArray<InterruptSubmissionError>) {\n super(errors.map((error) => error.message).join(' '))\n }\n}\n\nfunction objectValue(value: unknown): Record<string, unknown> | null {\n return value && typeof value === 'object'\n ? (value as Record<string, unknown>)\n : null\n}\n\nfunction stringField(\n value: Record<string, unknown>,\n key: string,\n): string | undefined {\n return typeof value[key] === 'string' ? value[key] : undefined\n}\n\nfunction normalizeIssuePath(\n path: ReadonlyArray<unknown> | undefined,\n): ReadonlyArray<string | number> | undefined {\n if (!path) return undefined\n return path.map((segment) => {\n if (typeof segment === 'string' || typeof segment === 'number') {\n return segment\n }\n const record = objectValue(segment)\n const key = record?.key\n return typeof key === 'number' ? key : String(key ?? segment)\n })\n}\n\nexport function interruptItemError(\n input: Pick<\n ValidateInterruptResumeBatchInput,\n 'threadId' | 'interruptedRunId' | 'generation'\n >,\n interruptId: string,\n code: ItemInterruptErrorCode,\n message: string,\n options?: {\n path?: ReadonlyArray<string | number>\n source?: 'client' | 'server'\n retryable?: boolean\n },\n): InterruptSubmissionError {\n return {\n scope: 'item',\n threadId: input.threadId,\n interruptedRunId: input.interruptedRunId,\n generation: input.generation,\n interruptId,\n code,\n message,\n source: options?.source ?? 'client',\n retryable: options?.retryable ?? false,\n ...(options?.path ? { path: options.path } : {}),\n }\n}\n\nasync function validateSchemaValue(input: {\n schema: unknown\n value: unknown\n onIssue: (message: string, path?: ReadonlyArray<string | number>) => void\n}): Promise<void> {\n if (isStandardSchema(input.schema)) {\n const result = await validateWithStandardSchema<unknown>(\n input.schema,\n input.value,\n )\n if (!result.success) {\n for (const issue of result.issues) {\n input.onIssue(issue.message, normalizeIssuePath(issue.path))\n }\n }\n return\n }\n\n // A non-Standard-Schema value (a raw JSON Schema, e.g. a generic interrupt's\n // wire responseSchema) is not validated by the library. The application\n // validates the resume value itself if it needs to; otherwise it flows\n // through as-is.\n}\n\ntype RuntimeTool = ChatMiddlewareConfig['tools'][number] & {\n approvalSchema?: Parameters<typeof normalizeApprovalSchema>[0]\n}\n\nfunction runtimeTool(\n tools: ChatMiddlewareConfig['tools'],\n name: string,\n): RuntimeTool | undefined {\n return tools.find((tool) => tool.name === name) as RuntimeTool | undefined\n}\n\nfunction descriptorResponseSchema(\n record: PendingInterruptResumeRecord,\n): unknown {\n return objectValue(record.payload)?.responseSchema\n}\n\nfunction schemaHash(schema: unknown): string {\n return digestInterruptJson(canonicalInterruptJson(schema))\n}\n\nasync function pushSchemaIssues(input: {\n request: ValidateInterruptResumeBatchInput\n errors: Array<InterruptSubmissionError>\n interruptId: string\n schema: unknown\n value: unknown\n code: ItemInterruptErrorCode\n label: string\n}): Promise<void> {\n try {\n await validateSchemaValue({\n schema: input.schema,\n value: input.value,\n onIssue: (message, path) => {\n input.errors.push(\n interruptItemError(\n input.request,\n input.interruptId,\n input.code,\n `${input.label}: ${message}`,\n { path },\n ),\n )\n },\n })\n } catch (error) {\n input.errors.push(\n interruptItemError(\n input.request,\n input.interruptId,\n 'invalid-response-schema',\n `${input.label} could not be validated: ${error instanceof Error ? error.message : String(error)}`,\n { source: 'server' },\n ),\n )\n }\n}\n\nfunction validateDescriptorSchema(\n input: ValidateInterruptResumeBatchInput,\n record: PendingInterruptResumeRecord,\n binding: InterruptBinding,\n errors: Array<InterruptSubmissionError>,\n): unknown {\n const schema = descriptorResponseSchema(record)\n if (\n schema === undefined ||\n schemaHash(schema) !== binding.responseSchemaHash\n ) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-response-schema',\n `Interrupt ${record.interruptId} response schema no longer matches its binding.`,\n { source: 'server' },\n ),\n )\n }\n return schema\n}\n\n/**\n * Validate and translate a complete interrupt batch before any tool executes.\n * Used by ephemeral chat resume; a durable layer may share the same validator.\n */\nexport async function validateInterruptResumeBatch(\n input: ValidateInterruptResumeBatchInput,\n): Promise<ValidatedInterruptResumeBatch> {\n const grouped = new Map<string, Array<InterruptSubmissionError>>()\n const batchErrors: Array<InterruptSubmissionError> = []\n const group = (interruptId: string): Array<InterruptSubmissionError> => {\n const existing = grouped.get(interruptId)\n if (existing) return existing\n const created: Array<InterruptSubmissionError> = []\n grouped.set(interruptId, created)\n return created\n }\n const pendingById = new Map(\n input.pending.map((record) => [record.interruptId, record]),\n )\n const resumeById = new Map<string, RunAgentResumeItem>()\n const counts = new Map<string, number>()\n for (const entry of input.resume ?? []) {\n counts.set(entry.interruptId, (counts.get(entry.interruptId) ?? 0) + 1)\n if (!resumeById.has(entry.interruptId))\n resumeById.set(entry.interruptId, entry)\n }\n\n for (const [interruptId, count] of counts) {\n if (count > 1) {\n group(interruptId).push(\n interruptItemError(\n input,\n interruptId,\n 'conflict',\n `Interrupt ${interruptId} has duplicate resume entries.`,\n ),\n )\n }\n }\n\n let incomplete = false\n for (const record of input.pending) {\n const errors = group(record.interruptId)\n const entry = resumeById.get(record.interruptId)\n const binding = record.binding\n if (!entry) {\n incomplete = true\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'unknown-interrupt',\n `Missing resume entry for interrupt ${record.interruptId}.`,\n ),\n )\n }\n if (\n binding.interruptedRunId !== input.interruptedRunId ||\n binding.generation !== input.generation ||\n binding.interruptId !== record.interruptId\n ) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'stale',\n `Interrupt ${record.interruptId} has stale correlation metadata.`,\n { source: 'server' },\n ),\n )\n }\n if (\n binding.expiresAt !== undefined &&\n Date.parse(binding.expiresAt) <= (input.now ?? Date.now())\n ) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'expired',\n `Interrupt ${record.interruptId} has expired.`,\n { source: 'server' },\n ),\n )\n }\n\n const responseSchema = validateDescriptorSchema(\n input,\n record,\n binding,\n errors,\n )\n if (!entry) continue\n const entryStatus: unknown = entry.status\n if (entryStatus !== 'resolved' && entryStatus !== 'cancelled') {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-payload',\n `Interrupt ${record.interruptId} has invalid status ${String(entryStatus)}.`,\n ),\n )\n continue\n }\n if (binding.kind === 'generic') {\n if (entry.status === 'cancelled') {\n if (entry.payload !== undefined) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-payload',\n `Cancelled interrupt ${record.interruptId} must not include a payload.`,\n ),\n )\n }\n } else if (responseSchema !== undefined) {\n await pushSchemaIssues({\n request: input,\n errors,\n interruptId: record.interruptId,\n schema: responseSchema,\n value: entry.payload,\n code: 'invalid-payload',\n label: `Interrupt ${record.interruptId} payload is invalid`,\n })\n }\n continue\n }\n\n const tool = runtimeTool(input.tools, binding.toolName)\n if (!tool) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'stale',\n `Tool ${binding.toolName} is unavailable for interrupt ${record.interruptId}.`,\n { source: 'server' },\n ),\n )\n continue\n }\n\n let approval: ReturnType<typeof normalizeApprovalSchema> | undefined\n let schemaDrifted = false\n if (binding.kind === 'client-tool-execution') {\n if (hashSchemaInput(tool.outputSchema) !== binding.outputSchemaHash) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'stale',\n `Tool ${binding.toolName} output schema has changed.`,\n { source: 'server' },\n ),\n )\n schemaDrifted = true\n }\n } else {\n try {\n approval = normalizeApprovalSchema(\n tool.approvalSchema,\n tool.inputSchema,\n )\n } catch {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'stale',\n `Tool ${binding.toolName} approval schema is unavailable.`,\n { source: 'server' },\n ),\n )\n schemaDrifted = true\n }\n if (\n approval !== undefined &&\n (hashSchemaInput(tool.inputSchema) !== binding.inputSchemaHash ||\n approval.approvalSchemaHash !== binding.approvalSchemaHash ||\n approval.responseSchemaHash !== binding.responseSchemaHash)\n ) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'stale',\n `Tool ${binding.toolName} approval schema has changed.`,\n { source: 'server' },\n ),\n )\n schemaDrifted = true\n }\n }\n\n if (entry.status === 'cancelled') {\n if (entry.payload !== undefined) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-payload',\n `Cancelled interrupt ${record.interruptId} must not include a payload.`,\n ),\n )\n }\n continue\n }\n if (schemaDrifted) continue\n\n if (binding.kind === 'client-tool-execution') {\n if (responseSchema !== undefined) {\n await pushSchemaIssues({\n request: input,\n errors,\n interruptId: record.interruptId,\n schema: responseSchema,\n value: entry.payload,\n code: 'invalid-tool-output',\n label: `Tool ${binding.toolName} output is invalid`,\n })\n }\n if (tool.outputSchema !== undefined) {\n await pushSchemaIssues({\n request: input,\n errors,\n interruptId: record.interruptId,\n schema: tool.outputSchema,\n value: entry.payload,\n code: 'invalid-tool-output',\n label: `Tool ${binding.toolName} output is invalid`,\n })\n }\n continue\n }\n\n if (approval === undefined) continue\n const envelope = objectValue(entry.payload)\n const approved =\n typeof entry.payload === 'boolean'\n ? entry.payload\n : typeof envelope?.approved === 'boolean'\n ? envelope.approved\n : undefined\n if (approved === undefined) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-payload',\n `Approval ${record.interruptId} must be a boolean or decision envelope.`,\n ),\n )\n continue\n }\n if (envelope) {\n await pushSchemaIssues({\n request: input,\n errors,\n interruptId: record.interruptId,\n schema: approval.responseSchema,\n value: entry.payload,\n code: 'invalid-payload',\n label: `Approval ${record.interruptId} envelope is invalid`,\n })\n }\n if (approved && envelope?.editedArgs !== undefined) {\n if (tool.inputSchema === undefined) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-edited-args',\n `Approval ${record.interruptId} cannot edit arguments without an input schema.`,\n ),\n )\n } else {\n await pushSchemaIssues({\n request: input,\n errors,\n interruptId: record.interruptId,\n schema: tool.inputSchema,\n value: envelope.editedArgs,\n code: 'invalid-edited-args',\n label: `Approval ${record.interruptId} edited arguments are invalid`,\n })\n }\n }\n const branch = approved\n ? approval.branches.approve\n : approval.branches.reject\n if (branch) {\n if (!envelope) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-payload',\n `Approval ${record.interruptId} requires a payload for the ${approved ? 'approve' : 'reject'} decision.`,\n ),\n )\n } else {\n await pushSchemaIssues({\n request: input,\n errors,\n interruptId: record.interruptId,\n schema: branch.source,\n value: envelope.payload,\n code: 'invalid-payload',\n label: `Approval ${record.interruptId} payload is invalid`,\n })\n }\n }\n }\n\n for (const entry of input.resume ?? []) {\n if (!pendingById.has(entry.interruptId)) {\n incomplete = true\n group(entry.interruptId).push(\n interruptItemError(\n input,\n entry.interruptId,\n 'unknown-interrupt',\n `Resume entry references unknown interrupt ${entry.interruptId}.`,\n ),\n )\n }\n }\n\n if (incomplete) {\n batchErrors.push({\n scope: 'batch',\n threadId: input.threadId,\n interruptedRunId: input.interruptedRunId,\n generation: input.generation,\n code: 'incomplete-batch',\n message:\n 'Resume entries must resolve or cancel the complete interrupt batch.',\n source: 'client',\n retryable: false,\n interruptIds: input.pending.map((record) => record.interruptId),\n })\n }\n\n const itemErrors = [...grouped.entries()]\n .sort(([left], [right]) => left.localeCompare(right))\n .flatMap(([, errors]) => errors)\n if (itemErrors.length > 0) {\n batchErrors.push({\n scope: 'batch',\n threadId: input.threadId,\n interruptedRunId: input.interruptedRunId,\n generation: input.generation,\n code: 'item-validation-failed',\n message: 'One or more interrupt resolutions are invalid.',\n source: 'client',\n retryable: false,\n interruptIds: input.pending.map((record) => record.interruptId),\n })\n return { errors: [...itemErrors, ...batchErrors] }\n }\n\n const canonical = canonicalizeInterruptResolutions(input.resume ?? [])\n const approvals = new Map<string, ToolApprovalResolution>()\n const clientToolResults = new Map<string, unknown>()\n const genericInterrupts = new Map<\n string,\n | { interruptId: string; status: 'resolved'; payload: unknown }\n | { interruptId: string; status: 'cancelled' }\n >()\n const deniedToolResults = new Map<string, unknown>()\n const cancelledToolCallIds = new Set<string>()\n\n for (const record of input.pending) {\n const entry = resumeById.get(record.interruptId)\n if (!entry) continue\n const binding = record.binding\n if (binding.kind === 'generic') {\n genericInterrupts.set(\n record.interruptId,\n entry.status === 'resolved'\n ? {\n interruptId: record.interruptId,\n status: 'resolved',\n payload: entry.payload,\n }\n : { interruptId: record.interruptId, status: 'cancelled' },\n )\n continue\n }\n if (entry.status === 'cancelled') {\n cancelledToolCallIds.add(binding.toolCallId)\n continue\n }\n if (binding.kind === 'client-tool-execution') {\n clientToolResults.set(binding.toolCallId, entry.payload)\n continue\n }\n const envelope = objectValue(entry.payload)\n const resolution: ToolApprovalResolution =\n typeof entry.payload === 'boolean'\n ? entry.payload\n : envelope?.approved === true\n ? {\n approved: true,\n ...(envelope.editedArgs !== undefined\n ? { editedArgs: envelope.editedArgs }\n : {}),\n ...(envelope.payload !== undefined\n ? { payload: envelope.payload }\n : {}),\n }\n : {\n approved: false,\n ...(envelope?.payload !== undefined\n ? { payload: envelope.payload }\n : {}),\n }\n approvals.set(binding.toolCallId, resolution)\n if (\n resolution === false ||\n (typeof resolution === 'object' && !resolution.approved)\n ) {\n deniedToolResults.set(\n binding.toolCallId,\n typeof resolution === 'object' ? resolution.payload : undefined,\n )\n }\n }\n\n return {\n errors: [],\n resolutions: canonical.resolutions,\n canonicalResolutions: canonical.canonicalResolutions,\n fingerprint: canonical.fingerprint,\n resumeToolState: {\n approvals,\n clientToolResults,\n genericInterrupts,\n deniedToolResults,\n cancelledToolCallIds,\n },\n }\n}\n\n/**\n * Is this a binding written by a version of the protocol we understand?\n *\n * A missing `v` is read as {@link INTERRUPT_BINDING_VERSION} so bindings\n * written before the field existed still resume. A `v` we don't recognise is\n * rejected outright — a newer or foreign producer's binding must not be\n * duck-typed into ours.\n */\nfunction isSupportedBindingVersion(raw: Record<string, unknown>): boolean {\n const version = raw['v']\n if (version === undefined) return true\n return version === INTERRUPT_BINDING_VERSION\n}\n\nexport function readUnopenedInterruptBinding(\n descriptor: Interrupt,\n): UnopenedInterruptBinding | undefined {\n const metadata = objectValue(descriptor.metadata)\n const raw = metadata\n ? objectValue(metadata[interruptBindingMetadataKey])\n : null\n if (!raw || stringField(raw, 'interruptId') !== descriptor.id)\n return undefined\n if (!isSupportedBindingVersion(raw)) return undefined\n const kind = stringField(raw, 'kind')\n const interruptId = stringField(raw, 'interruptId')\n const responseSchemaHash = stringField(raw, 'responseSchemaHash')\n const expiresAt = stringField(raw, 'expiresAt')\n if (!interruptId || !responseSchemaHash) return undefined\n const v = INTERRUPT_BINDING_VERSION\n if (kind === 'generic') {\n return {\n v,\n kind,\n interruptId,\n responseSchemaHash,\n ...(expiresAt ? { expiresAt } : {}),\n }\n }\n const toolName = stringField(raw, 'toolName')\n const toolCallId = stringField(raw, 'toolCallId')\n if (!toolName || !toolCallId) return undefined\n if (kind === 'client-tool-execution') {\n const outputSchemaHash = stringField(raw, 'outputSchemaHash')\n if (!outputSchemaHash) return undefined\n return {\n v,\n kind,\n interruptId,\n toolName,\n toolCallId,\n outputSchemaHash,\n responseSchemaHash,\n ...(expiresAt ? { expiresAt } : {}),\n }\n }\n if (kind === 'tool-approval') {\n const inputSchemaHash = stringField(raw, 'inputSchemaHash')\n const approvalSchemaHash = stringField(raw, 'approvalSchemaHash')\n if (!inputSchemaHash || !approvalSchemaHash) return undefined\n return {\n v,\n kind,\n interruptId,\n toolName,\n toolCallId,\n originalArgs: raw.originalArgs,\n inputSchemaHash,\n approvalSchemaHash,\n responseSchemaHash,\n ...(expiresAt ? { expiresAt } : {}),\n }\n }\n return undefined\n}\n\n/**\n * Attach a resume binding to an interrupt descriptor, under\n * {@link INTERRUPT_BINDING_METADATA_KEY}.\n *\n * This is the supported way to make an interrupt resumable by this package.\n * The descriptor keeps its AG-UI shape; only `metadata` gains the namespaced\n * key. Pass the unopened form (no `interruptedRunId` / `generation`) when\n * emitting from inside a run — those fields are stamped as the run finishes.\n */\nexport function withInterruptBinding(\n descriptor: Interrupt,\n binding: UnopenedInterruptBinding | InterruptBinding,\n): Interrupt {\n return {\n ...descriptor,\n metadata: {\n ...descriptor.metadata,\n [interruptBindingMetadataKey]: {\n ...binding,\n v: INTERRUPT_BINDING_VERSION,\n interruptId: descriptor.id,\n },\n },\n }\n}\n\n/**\n * Read the opened resume binding off a descriptor, or `undefined` when the\n * descriptor carries no binding of a version we understand.\n *\n * `undefined` means \"this interrupt is not ours to resume\" — it is not a\n * failure to recover from by inventing a binding.\n */\nexport function readInterruptBinding(\n descriptor: Interrupt,\n): InterruptBinding | undefined {\n const unopened = readUnopenedInterruptBinding(descriptor)\n if (!unopened) return undefined\n const metadata = objectValue(descriptor.metadata)\n const raw = metadata\n ? objectValue(metadata[interruptBindingMetadataKey])\n : null\n if (!raw) return undefined\n const interruptedRunId = stringField(raw, 'interruptedRunId')\n const generation = raw['generation']\n if (\n !interruptedRunId ||\n typeof generation !== 'number' ||\n !Number.isInteger(generation) ||\n generation < 0\n ) {\n return undefined\n }\n return { ...unopened, interruptedRunId, generation }\n}\n\nexport function withoutInterruptBinding(descriptor: Interrupt): Interrupt {\n const metadata = objectValue(descriptor.metadata)\n if (!metadata || !(interruptBindingMetadataKey in metadata)) return descriptor\n const publicMetadata = { ...metadata }\n delete publicMetadata[interruptBindingMetadataKey]\n return { ...descriptor, metadata: publicMetadata }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"interrupt-resume.js","names":[],"sources":["../../src/interrupt-resume.ts"],"sourcesContent":["import {\n INTERRUPT_BINDING_VERSION,\n canonicalizeInterruptResolutions,\n} from './interrupts'\nimport {\n canonicalInterruptJson,\n digestInterruptJson,\n} from './interrupt-serialization'\nimport {\n hashSchemaInput,\n normalizeApprovalSchema,\n} from './activities/chat/tools/approval-schema'\nimport {\n isStandardSchema,\n validateWithStandardSchema,\n} from './activities/chat/tools/schema-converter'\nimport type {\n InterruptBinding,\n InterruptSubmissionError,\n ItemInterruptErrorCode,\n ToolApprovalResolution,\n UnopenedInterruptBinding,\n} from './interrupts'\nimport type {\n ChatMiddlewareConfig,\n ChatResumeToolState,\n} from './activities/chat/middleware/types'\nimport type { Interrupt, RunAgentResumeItem } from './types'\n\n/**\n * The `Interrupt.metadata` key under which this package's resume binding\n * travels.\n *\n * Exported so anything that produces an interrupt this package must later\n * resume — an application middleware raising a generic pause, a future\n * workflow-to-AG-UI projection — attaches the binding through\n * {@link withInterruptBinding} rather than copying the string. Everything\n * outside this key is the plain AG-UI envelope and is left untouched.\n */\nexport const INTERRUPT_BINDING_METADATA_KEY = 'tanstack:interruptBinding'\n\nconst interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY\n\n/** The persistence-neutral shape required to validate an interrupt resume. */\nexport interface PendingInterruptResumeRecord {\n interruptId: string\n payload: unknown\n binding: InterruptBinding\n}\n\nexport interface ValidateInterruptResumeBatchInput {\n threadId: string\n interruptedRunId: string\n generation: number\n pending: ReadonlyArray<PendingInterruptResumeRecord>\n resume?: ReadonlyArray<RunAgentResumeItem>\n tools: ChatMiddlewareConfig['tools']\n now?: number\n}\n\nexport interface ValidatedInterruptResumeBatch {\n errors: ReadonlyArray<InterruptSubmissionError>\n resolutions?: ReadonlyArray<RunAgentResumeItem>\n canonicalResolutions?: string\n fingerprint?: string\n resumeToolState?: ChatResumeToolState\n}\n\nexport class InterruptResumeValidationError extends Error {\n override readonly name = 'InterruptResumeValidationError'\n\n constructor(readonly errors: ReadonlyArray<InterruptSubmissionError>) {\n super(errors.map((error) => error.message).join(' '))\n }\n}\n\nfunction objectValue(value: unknown): Record<string, unknown> | null {\n return value && typeof value === 'object'\n ? (value as Record<string, unknown>)\n : null\n}\n\nfunction stringField(\n value: Record<string, unknown>,\n key: string,\n): string | undefined {\n return typeof value[key] === 'string' ? value[key] : undefined\n}\n\nfunction normalizeIssuePath(\n path: ReadonlyArray<unknown> | undefined,\n): ReadonlyArray<string | number> | undefined {\n if (!path) return undefined\n return path.map((segment) => {\n if (typeof segment === 'string' || typeof segment === 'number') {\n return segment\n }\n const record = objectValue(segment)\n const key = record?.key\n return typeof key === 'number' ? key : String(key ?? segment)\n })\n}\n\nexport function interruptItemError(\n input: Pick<\n ValidateInterruptResumeBatchInput,\n 'threadId' | 'interruptedRunId' | 'generation'\n >,\n interruptId: string,\n code: ItemInterruptErrorCode,\n message: string,\n options?: {\n path?: ReadonlyArray<string | number>\n source?: 'client' | 'server'\n retryable?: boolean\n },\n): InterruptSubmissionError {\n return {\n scope: 'item',\n threadId: input.threadId,\n interruptedRunId: input.interruptedRunId,\n generation: input.generation,\n interruptId,\n code,\n message,\n source: options?.source ?? 'client',\n retryable: options?.retryable ?? false,\n ...(options?.path ? { path: options.path } : {}),\n }\n}\n\nasync function validateSchemaValue(input: {\n schema: unknown\n value: unknown\n onIssue: (message: string, path?: ReadonlyArray<string | number>) => void\n}): Promise<void> {\n if (isStandardSchema(input.schema)) {\n const result = await validateWithStandardSchema<unknown>(\n input.schema,\n input.value,\n )\n if (!result.success) {\n for (const issue of result.issues) {\n input.onIssue(issue.message, normalizeIssuePath(issue.path))\n }\n }\n return\n }\n\n // A non-Standard-Schema value (a raw JSON Schema, e.g. a generic interrupt's\n // wire responseSchema) is not validated by the library. The application\n // validates the resume value itself if it needs to; otherwise it flows\n // through as-is.\n}\n\ntype RuntimeTool = ChatMiddlewareConfig['tools'][number] & {\n approvalSchema?: Parameters<typeof normalizeApprovalSchema>[0]\n}\n\nfunction runtimeTool(\n tools: ChatMiddlewareConfig['tools'],\n name: string,\n): RuntimeTool | undefined {\n return tools.find((tool) => tool.name === name) as RuntimeTool | undefined\n}\n\nfunction descriptorResponseSchema(\n record: PendingInterruptResumeRecord,\n): unknown {\n return objectValue(record.payload)?.responseSchema\n}\n\nfunction schemaHash(schema: unknown): string {\n return digestInterruptJson(canonicalInterruptJson(schema))\n}\n\nasync function pushSchemaIssues(input: {\n request: ValidateInterruptResumeBatchInput\n errors: Array<InterruptSubmissionError>\n interruptId: string\n schema: unknown\n value: unknown\n code: ItemInterruptErrorCode\n label: string\n}): Promise<void> {\n try {\n await validateSchemaValue({\n schema: input.schema,\n value: input.value,\n onIssue: (message, path) => {\n input.errors.push(\n interruptItemError(\n input.request,\n input.interruptId,\n input.code,\n `${input.label}: ${message}`,\n { path },\n ),\n )\n },\n })\n } catch (error) {\n input.errors.push(\n interruptItemError(\n input.request,\n input.interruptId,\n 'invalid-response-schema',\n `${input.label} could not be validated: ${error instanceof Error ? error.message : String(error)}`,\n { source: 'server' },\n ),\n )\n }\n}\n\nfunction validateDescriptorSchema(\n input: ValidateInterruptResumeBatchInput,\n record: PendingInterruptResumeRecord,\n binding: InterruptBinding,\n errors: Array<InterruptSubmissionError>,\n): unknown {\n const schema = descriptorResponseSchema(record)\n if (\n schema === undefined ||\n schemaHash(schema) !== binding.responseSchemaHash\n ) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-response-schema',\n `Interrupt ${record.interruptId} response schema no longer matches its binding.`,\n { source: 'server' },\n ),\n )\n }\n return schema\n}\n\n/**\n * Validate and translate a complete interrupt batch before any tool executes.\n * Used by ephemeral chat resume; a durable layer may share the same validator.\n */\nexport async function validateInterruptResumeBatch(\n input: ValidateInterruptResumeBatchInput,\n): Promise<ValidatedInterruptResumeBatch> {\n const grouped = new Map<string, Array<InterruptSubmissionError>>()\n const batchErrors: Array<InterruptSubmissionError> = []\n const group = (interruptId: string): Array<InterruptSubmissionError> => {\n const existing = grouped.get(interruptId)\n if (existing) return existing\n const created: Array<InterruptSubmissionError> = []\n grouped.set(interruptId, created)\n return created\n }\n const pendingById = new Map(\n input.pending.map((record) => [record.interruptId, record]),\n )\n const resumeById = new Map<string, RunAgentResumeItem>()\n const counts = new Map<string, number>()\n for (const entry of input.resume ?? []) {\n counts.set(entry.interruptId, (counts.get(entry.interruptId) ?? 0) + 1)\n if (!resumeById.has(entry.interruptId))\n resumeById.set(entry.interruptId, entry)\n }\n\n for (const [interruptId, count] of counts) {\n if (count > 1) {\n group(interruptId).push(\n interruptItemError(\n input,\n interruptId,\n 'conflict',\n `Interrupt ${interruptId} has duplicate resume entries.`,\n ),\n )\n }\n }\n\n let incomplete = false\n for (const record of input.pending) {\n const errors = group(record.interruptId)\n const entry = resumeById.get(record.interruptId)\n const binding = record.binding\n if (!entry) {\n incomplete = true\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'unknown-interrupt',\n `Missing resume entry for interrupt ${record.interruptId}.`,\n ),\n )\n }\n if (\n binding.interruptedRunId !== input.interruptedRunId ||\n binding.generation !== input.generation ||\n binding.interruptId !== record.interruptId\n ) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'stale',\n `Interrupt ${record.interruptId} has stale correlation metadata.`,\n { source: 'server' },\n ),\n )\n }\n if (\n binding.expiresAt !== undefined &&\n Date.parse(binding.expiresAt) <= (input.now ?? Date.now())\n ) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'expired',\n `Interrupt ${record.interruptId} has expired.`,\n { source: 'server' },\n ),\n )\n }\n\n const responseSchema = validateDescriptorSchema(\n input,\n record,\n binding,\n errors,\n )\n if (!entry) continue\n const entryStatus: unknown = entry.status\n if (entryStatus !== 'resolved' && entryStatus !== 'cancelled') {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-payload',\n `Interrupt ${record.interruptId} has invalid status ${String(entryStatus)}.`,\n ),\n )\n continue\n }\n if (binding.kind === 'generic') {\n if (entry.status === 'cancelled') {\n if (entry.payload !== undefined) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-payload',\n `Cancelled interrupt ${record.interruptId} must not include a payload.`,\n ),\n )\n }\n } else if (responseSchema !== undefined) {\n await pushSchemaIssues({\n request: input,\n errors,\n interruptId: record.interruptId,\n schema: responseSchema,\n value: entry.payload,\n code: 'invalid-payload',\n label: `Interrupt ${record.interruptId} payload is invalid`,\n })\n }\n continue\n }\n\n const tool = runtimeTool(input.tools, binding.toolName)\n if (!tool) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'stale',\n `Tool ${binding.toolName} is unavailable for interrupt ${record.interruptId}.`,\n { source: 'server' },\n ),\n )\n continue\n }\n\n let approval: ReturnType<typeof normalizeApprovalSchema> | undefined\n let schemaDrifted = false\n if (binding.kind === 'client-tool-execution') {\n if (hashSchemaInput(tool.outputSchema) !== binding.outputSchemaHash) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'stale',\n `Tool ${binding.toolName} output schema has changed.`,\n { source: 'server' },\n ),\n )\n schemaDrifted = true\n }\n } else {\n try {\n approval = normalizeApprovalSchema(\n tool.approvalSchema,\n tool.inputSchema,\n )\n } catch {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'stale',\n `Tool ${binding.toolName} approval schema is unavailable.`,\n { source: 'server' },\n ),\n )\n schemaDrifted = true\n }\n if (\n approval !== undefined &&\n (hashSchemaInput(tool.inputSchema) !== binding.inputSchemaHash ||\n approval.approvalSchemaHash !== binding.approvalSchemaHash ||\n approval.responseSchemaHash !== binding.responseSchemaHash)\n ) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'stale',\n `Tool ${binding.toolName} approval schema has changed.`,\n { source: 'server' },\n ),\n )\n schemaDrifted = true\n }\n }\n\n if (entry.status === 'cancelled') {\n if (entry.payload !== undefined) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-payload',\n `Cancelled interrupt ${record.interruptId} must not include a payload.`,\n ),\n )\n }\n continue\n }\n if (schemaDrifted) continue\n\n if (binding.kind === 'client-tool-execution') {\n if (responseSchema !== undefined) {\n await pushSchemaIssues({\n request: input,\n errors,\n interruptId: record.interruptId,\n schema: responseSchema,\n value: entry.payload,\n code: 'invalid-tool-output',\n label: `Tool ${binding.toolName} output is invalid`,\n })\n }\n if (tool.outputSchema !== undefined) {\n await pushSchemaIssues({\n request: input,\n errors,\n interruptId: record.interruptId,\n schema: tool.outputSchema,\n value: entry.payload,\n code: 'invalid-tool-output',\n label: `Tool ${binding.toolName} output is invalid`,\n })\n }\n continue\n }\n\n if (approval === undefined) continue\n const envelope = objectValue(entry.payload)\n const approved =\n typeof entry.payload === 'boolean'\n ? entry.payload\n : typeof envelope?.approved === 'boolean'\n ? envelope.approved\n : undefined\n if (approved === undefined) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-payload',\n `Approval ${record.interruptId} must be a boolean or decision envelope.`,\n ),\n )\n continue\n }\n if (envelope) {\n await pushSchemaIssues({\n request: input,\n errors,\n interruptId: record.interruptId,\n schema: approval.responseSchema,\n value: entry.payload,\n code: 'invalid-payload',\n label: `Approval ${record.interruptId} envelope is invalid`,\n })\n }\n if (approved && envelope?.editedArgs !== undefined) {\n if (tool.inputSchema === undefined) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-edited-args',\n `Approval ${record.interruptId} cannot edit arguments without an input schema.`,\n ),\n )\n } else {\n await pushSchemaIssues({\n request: input,\n errors,\n interruptId: record.interruptId,\n schema: tool.inputSchema,\n value: envelope.editedArgs,\n code: 'invalid-edited-args',\n label: `Approval ${record.interruptId} edited arguments are invalid`,\n })\n }\n }\n const branch = approved\n ? approval.branches.approve\n : approval.branches.reject\n if (branch) {\n if (!envelope) {\n errors.push(\n interruptItemError(\n input,\n record.interruptId,\n 'invalid-payload',\n `Approval ${record.interruptId} requires a payload for the ${approved ? 'approve' : 'reject'} decision.`,\n ),\n )\n } else {\n await pushSchemaIssues({\n request: input,\n errors,\n interruptId: record.interruptId,\n schema: branch.source,\n value: envelope.payload,\n code: 'invalid-payload',\n label: `Approval ${record.interruptId} payload is invalid`,\n })\n }\n }\n }\n\n for (const entry of input.resume ?? []) {\n if (!pendingById.has(entry.interruptId)) {\n incomplete = true\n group(entry.interruptId).push(\n interruptItemError(\n input,\n entry.interruptId,\n 'unknown-interrupt',\n `Resume entry references unknown interrupt ${entry.interruptId}.`,\n ),\n )\n }\n }\n\n if (incomplete) {\n batchErrors.push({\n scope: 'batch',\n threadId: input.threadId,\n interruptedRunId: input.interruptedRunId,\n generation: input.generation,\n code: 'incomplete-batch',\n message:\n 'Resume entries must resolve or cancel the complete interrupt batch.',\n source: 'client',\n retryable: false,\n interruptIds: input.pending.map((record) => record.interruptId),\n })\n }\n\n const itemErrors = [...grouped.entries()]\n .sort(([left], [right]) => left.localeCompare(right))\n .flatMap(([, errors]) => errors)\n if (itemErrors.length > 0) {\n batchErrors.push({\n scope: 'batch',\n threadId: input.threadId,\n interruptedRunId: input.interruptedRunId,\n generation: input.generation,\n code: 'item-validation-failed',\n message: 'One or more interrupt resolutions are invalid.',\n source: 'client',\n retryable: false,\n interruptIds: input.pending.map((record) => record.interruptId),\n })\n return { errors: [...itemErrors, ...batchErrors] }\n }\n\n const canonical = canonicalizeInterruptResolutions(input.resume ?? [])\n const approvals = new Map<string, ToolApprovalResolution>()\n const clientToolResults = new Map<string, unknown>()\n const genericInterrupts = new Map<\n string,\n | { interruptId: string; status: 'resolved'; payload: unknown }\n | { interruptId: string; status: 'cancelled' }\n >()\n const deniedToolResults = new Map<string, unknown>()\n const cancelledToolCallIds = new Set<string>()\n\n for (const record of input.pending) {\n const entry = resumeById.get(record.interruptId)\n if (!entry) continue\n const binding = record.binding\n if (binding.kind === 'generic') {\n genericInterrupts.set(\n record.interruptId,\n entry.status === 'resolved'\n ? {\n interruptId: record.interruptId,\n status: 'resolved',\n payload: entry.payload,\n }\n : { interruptId: record.interruptId, status: 'cancelled' },\n )\n continue\n }\n if (entry.status === 'cancelled') {\n cancelledToolCallIds.add(binding.toolCallId)\n continue\n }\n if (binding.kind === 'client-tool-execution') {\n clientToolResults.set(binding.toolCallId, entry.payload)\n continue\n }\n const envelope = objectValue(entry.payload)\n const resolution: ToolApprovalResolution =\n typeof entry.payload === 'boolean'\n ? entry.payload\n : envelope?.approved === true\n ? {\n approved: true,\n ...(envelope.editedArgs !== undefined\n ? { editedArgs: envelope.editedArgs }\n : {}),\n ...(envelope.payload !== undefined\n ? { payload: envelope.payload }\n : {}),\n }\n : {\n approved: false,\n ...(envelope?.payload !== undefined\n ? { payload: envelope.payload }\n : {}),\n }\n approvals.set(binding.toolCallId, resolution)\n if (\n resolution === false ||\n (typeof resolution === 'object' && !resolution.approved)\n ) {\n deniedToolResults.set(\n binding.toolCallId,\n typeof resolution === 'object' ? resolution.payload : undefined,\n )\n }\n }\n\n return {\n errors: [],\n resolutions: canonical.resolutions,\n canonicalResolutions: canonical.canonicalResolutions,\n fingerprint: canonical.fingerprint,\n resumeToolState: {\n approvals,\n clientToolResults,\n genericInterrupts,\n deniedToolResults,\n cancelledToolCallIds,\n },\n }\n}\n\n/**\n * Is this a binding written by a version of the protocol we understand?\n *\n * A missing `v` is read as {@link INTERRUPT_BINDING_VERSION} so bindings\n * written before the field existed still resume. A `v` we don't recognise is\n * rejected outright — a newer or foreign producer's binding must not be\n * duck-typed into ours.\n */\nfunction isSupportedBindingVersion(raw: Record<string, unknown>): boolean {\n const version = raw['v']\n if (version === undefined) return true\n return version === INTERRUPT_BINDING_VERSION\n}\n\nexport function readUnopenedInterruptBinding(\n descriptor: Interrupt,\n): UnopenedInterruptBinding | undefined {\n const metadata = objectValue(descriptor.metadata)\n const raw = metadata\n ? objectValue(metadata[interruptBindingMetadataKey])\n : null\n if (!raw || stringField(raw, 'interruptId') !== descriptor.id)\n return undefined\n if (!isSupportedBindingVersion(raw)) return undefined\n const kind = stringField(raw, 'kind')\n const interruptId = stringField(raw, 'interruptId')\n const responseSchemaHash = stringField(raw, 'responseSchemaHash')\n const expiresAt = stringField(raw, 'expiresAt')\n if (!interruptId || !responseSchemaHash) return undefined\n const v = INTERRUPT_BINDING_VERSION\n if (kind === 'generic') {\n return {\n v,\n kind,\n interruptId,\n responseSchemaHash,\n ...(expiresAt ? { expiresAt } : {}),\n }\n }\n const toolName = stringField(raw, 'toolName')\n const toolCallId = stringField(raw, 'toolCallId')\n if (!toolName || !toolCallId) return undefined\n if (kind === 'client-tool-execution') {\n const outputSchemaHash = stringField(raw, 'outputSchemaHash')\n if (!outputSchemaHash) return undefined\n return {\n v,\n kind,\n interruptId,\n toolName,\n toolCallId,\n outputSchemaHash,\n responseSchemaHash,\n ...(expiresAt ? { expiresAt } : {}),\n }\n }\n if (kind === 'tool-approval') {\n const inputSchemaHash = stringField(raw, 'inputSchemaHash')\n const approvalSchemaHash = stringField(raw, 'approvalSchemaHash')\n if (!inputSchemaHash || !approvalSchemaHash) return undefined\n return {\n v,\n kind,\n interruptId,\n toolName,\n toolCallId,\n originalArgs: raw.originalArgs,\n inputSchemaHash,\n approvalSchemaHash,\n responseSchemaHash,\n ...(expiresAt ? { expiresAt } : {}),\n }\n }\n return undefined\n}\n\n/**\n * Attach a resume binding to an interrupt descriptor, under\n * {@link INTERRUPT_BINDING_METADATA_KEY}.\n *\n * This is the supported way to make an interrupt resumable by this package.\n * The descriptor keeps its AG-UI shape; only `metadata` gains the namespaced\n * key. Pass the unopened form (no `interruptedRunId` / `generation`) when\n * emitting from inside a run — those fields are stamped as the run finishes.\n */\nexport function withInterruptBinding(\n descriptor: Interrupt,\n binding: UnopenedInterruptBinding | InterruptBinding,\n): Interrupt {\n return {\n ...descriptor,\n metadata: {\n ...descriptor.metadata,\n [interruptBindingMetadataKey]: {\n ...binding,\n v: INTERRUPT_BINDING_VERSION,\n interruptId: descriptor.id,\n },\n },\n }\n}\n\n/**\n * Read the opened resume binding off a descriptor, or `undefined` when the\n * descriptor carries no binding of a version we understand.\n *\n * `undefined` means \"this interrupt is not ours to resume\" — it is not a\n * failure to recover from by inventing a binding.\n */\nexport function readInterruptBinding(\n descriptor: Interrupt,\n): InterruptBinding | undefined {\n const unopened = readUnopenedInterruptBinding(descriptor)\n if (!unopened) return undefined\n const metadata = objectValue(descriptor.metadata)\n const raw = metadata\n ? objectValue(metadata[interruptBindingMetadataKey])\n : null\n if (!raw) return undefined\n const interruptedRunId = stringField(raw, 'interruptedRunId')\n const generation = raw['generation']\n if (\n !interruptedRunId ||\n typeof generation !== 'number' ||\n !Number.isInteger(generation) ||\n generation < 0\n ) {\n return undefined\n }\n return { ...unopened, interruptedRunId, generation }\n}\n\nexport function withoutInterruptBinding(descriptor: Interrupt): Interrupt {\n const metadata = objectValue(descriptor.metadata)\n if (!metadata || !(interruptBindingMetadataKey in metadata)) return descriptor\n const publicMetadata = { ...metadata }\n delete publicMetadata[interruptBindingMetadataKey]\n return { ...descriptor, metadata: publicMetadata }\n}\n"],"mappings":"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"}
|
package/dist/esm/interrupts.js
CHANGED
|
@@ -17,7 +17,8 @@ import { canonicalInterruptJson, cloneAndDeepFreezeJson, digestInterruptJson } f
|
|
|
17
17
|
*/
|
|
18
18
|
var INTERRUPT_BINDING_VERSION = 1;
|
|
19
19
|
function canonicalizeInterruptResolutions(resolutions) {
|
|
20
|
-
const
|
|
20
|
+
const sorted = [...resolutions].sort((left, right) => left.interruptId.localeCompare(right.interruptId));
|
|
21
|
+
const frozen = cloneAndDeepFreezeJson(sorted);
|
|
21
22
|
const canonicalResolutions = canonicalInterruptJson(frozen);
|
|
22
23
|
return Object.freeze({
|
|
23
24
|
resolutions: frozen,
|