UNPKG

@tanstack/ai

Version:

Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.

163 lines (153 loc) 5.19 kB
import { INTERRUPT_PAYLOAD_METADATA_KEY } from './interrupt-definition' import { readUnopenedInterruptBinding } from './interrupt-resume' import type { Interrupt } from './types' /** * `ResumeEntry.metadata` key for a first-party generic request. * * AG-UI `resume` only carries the answer (`interruptId`, `status`, `payload`). * The original request rides here so an ephemeral server can rebuild it. */ export const INTERRUPT_CONTINUATION_METADATA_KEY = 'tanstack:interruptContinuation' as const export const INTERRUPT_CONTINUATION_VERSION = 1 as const export interface GenericInterruptContinuation { v: typeof INTERRUPT_CONTINUATION_VERSION definitionId: string key: string batchIndex: number reason: string message: string expiresAt?: string responseSchemaHash?: string payloadSchemaHash?: string payload?: unknown } export type GenericInterruptContinuationReadResult = | { status: 'absent' } | { status: 'invalid'; message: string } | { status: 'ok'; value: GenericInterruptContinuation } function isRecord(value: unknown): value is Record<string, unknown> { return value !== null && typeof value === 'object' && !Array.isArray(value) } function invalid( message: string, ): Extract<GenericInterruptContinuationReadResult, { status: 'invalid' }> { return { status: 'invalid', message } } /** * Read one generic request from `resume[].metadata`. * * Missing key means this resume item is not a first-party generic continuation. * A present key that fails the shape is a protocol error. */ export function readGenericInterruptContinuation( metadata: unknown, ): GenericInterruptContinuationReadResult { if (metadata === undefined) return { status: 'absent' } if (!isRecord(metadata)) { return invalid('Generic interrupt resume metadata must be an object.') } if ( !Object.prototype.hasOwnProperty.call( metadata, INTERRUPT_CONTINUATION_METADATA_KEY, ) ) { return { status: 'absent' } } const raw = metadata[INTERRUPT_CONTINUATION_METADATA_KEY] if (!isRecord(raw)) { return invalid('Generic interrupt continuation is invalid.') } if ( raw.v !== INTERRUPT_CONTINUATION_VERSION || typeof raw.definitionId !== 'string' || typeof raw.key !== 'string' || typeof raw.reason !== 'string' || typeof raw.message !== 'string' || typeof raw.batchIndex !== 'number' || !Number.isInteger(raw.batchIndex) || raw.batchIndex < 0 || (raw.responseSchemaHash !== undefined && typeof raw.responseSchemaHash !== 'string') || (raw.expiresAt !== undefined && typeof raw.expiresAt !== 'string') || (raw.payloadSchemaHash !== undefined && typeof raw.payloadSchemaHash !== 'string') ) { return invalid('Generic interrupt continuation contains invalid fields.') } return { status: 'ok', value: { v: INTERRUPT_CONTINUATION_VERSION, definitionId: raw.definitionId, key: raw.key, batchIndex: raw.batchIndex, reason: raw.reason, message: raw.message, ...(typeof raw.expiresAt === 'string' ? { expiresAt: raw.expiresAt } : {}), ...(typeof raw.responseSchemaHash === 'string' ? { responseSchemaHash: raw.responseSchemaHash } : {}), ...(typeof raw.payloadSchemaHash === 'string' ? { payloadSchemaHash: raw.payloadSchemaHash } : {}), ...(Object.prototype.hasOwnProperty.call(raw, 'payload') ? { payload: raw.payload } : {}), }, } } /** Put a parsed continuation on `ResumeEntry.metadata`. */ export function wrapGenericInterruptContinuation( continuation: GenericInterruptContinuation, ): Record<string, unknown> { return { [INTERRUPT_CONTINUATION_METADATA_KEY]: continuation } } /** * Build the resume-metadata continuation from an outbound AG-UI interrupt. * * Returns `undefined` when the descriptor is not a first-party generic item. */ export function genericInterruptContinuationFromDescriptor( interrupt: Interrupt, ): GenericInterruptContinuation | undefined { const binding = readUnopenedInterruptBinding(interrupt) if ( binding?.kind !== 'generic' || binding.definitionId === undefined || binding.key === undefined || binding.batchIndex === undefined ) { return undefined } const metadata = isRecord(interrupt.metadata) ? interrupt.metadata : undefined const hasPayload = metadata !== undefined && Object.prototype.hasOwnProperty.call( metadata, INTERRUPT_PAYLOAD_METADATA_KEY, ) return { v: INTERRUPT_CONTINUATION_VERSION, definitionId: binding.definitionId, key: binding.key, batchIndex: binding.batchIndex, reason: interrupt.reason, message: interrupt.message ?? '', ...(interrupt.expiresAt !== undefined ? { expiresAt: interrupt.expiresAt } : {}), ...(binding.responseSchemaHash !== undefined ? { responseSchemaHash: binding.responseSchemaHash } : {}), ...(binding.payloadSchemaHash !== undefined ? { payloadSchemaHash: binding.payloadSchemaHash } : {}), ...(hasPayload ? { payload: metadata[INTERRUPT_PAYLOAD_METADATA_KEY] } : {}), } }