@tanstack/ai 0.20.1 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/activities/chat/index.d.ts +1 -1
- package/dist/esm/activities/chat/index.js +357 -258
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.d.ts +10 -1
- package/dist/esm/activities/chat/middleware/compose.js +55 -0
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/index.d.ts +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +35 -3
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.d.ts +12 -1
- package/dist/esm/activities/chat/tools/schema-converter.js +13 -4
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/extend-adapter.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +2 -1
- package/dist/esm/middlewares/content-guard.js.map +1 -1
- package/dist/esm/middlewares/otel.js +1 -4
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/utilities/ag-ui-wire.js +4 -1
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/package.json +3 -3
- package/skills/ai-core/middleware/SKILL.md +124 -18
- package/skills/ai-core/structured-outputs/SKILL.md +13 -0
- package/src/activities/chat/index.ts +653 -370
- package/src/activities/chat/messages.ts +1 -1
- package/src/activities/chat/middleware/compose.ts +65 -5
- package/src/activities/chat/middleware/index.ts +1 -0
- package/src/activities/chat/middleware/types.ts +53 -2
- package/src/activities/chat/stream/processor.ts +2 -2
- package/src/activities/chat/tools/schema-converter.ts +22 -7
- package/src/activities/generateImage/index.ts +3 -3
- package/src/extend-adapter.ts +1 -1
- package/src/index.ts +5 -1
- package/src/middlewares/content-guard.ts +1 -1
- package/src/middlewares/otel.ts +7 -10
- package/src/strip-to-spec-middleware.ts +1 -4
- package/src/utilities/ag-ui-wire.ts +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../../src/activities/generateImage/index.ts"],"sourcesContent":["/**\n * Image Activity\n *\n * Generates images from text prompts.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { ImageAdapter } from './adapter'\nimport type { ImageGenerationResult, StreamChunk } from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'image' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract model-specific provider options from an ImageAdapter via ~types.\n * If the model has specific options defined in ModelProviderOptions (and not just via index signature),\n * use those; otherwise fall back to base provider options.\n */\nexport type ImageProviderOptionsForModel<TAdapter, TModel extends string> =\n TAdapter extends ImageAdapter<any, infer BaseOptions, infer ModelOptions, any>\n ? string extends keyof ModelOptions\n ? // ModelOptions is Record<string, unknown> or has index signature - use BaseOptions\n BaseOptions\n : // ModelOptions has explicit keys - check if TModel is one of them\n TModel extends keyof ModelOptions\n ? ModelOptions[TModel]\n : BaseOptions\n : object\n\n/**\n * Extract model-specific size options from an ImageAdapter via ~types.\n * If the model has specific sizes defined, use those; otherwise fall back to string.\n */\nexport type ImageSizeForModel<TAdapter, TModel extends string> =\n TAdapter extends ImageAdapter<any, any, any, infer SizeByName>\n ? string extends keyof SizeByName\n ? // SizeByName has index signature - fall back to string\n string\n : // SizeByName has explicit keys - check if TModel is one of them\n TModel extends keyof SizeByName\n ? SizeByName[TModel]\n : string\n : string\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the image activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The image adapter type\n * @template TStream - Whether to stream the output\n */\nexport type ImageActivityOptions<\n TAdapter extends ImageAdapter<string, any, any, any>,\n TStream extends boolean = false,\n> = {\n /** The image adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** Text description of the desired image(s) */\n prompt: string\n /** Number of images to generate (default: 1) */\n numberOfImages?: number\n /** Image size in WIDTHxHEIGHT format (e.g., \"1024x1024\") */\n size?: ImageSizeForModel<TAdapter, TAdapter['model']>\n /**\n * Whether to stream the image generation result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming transport.\n * When false or not provided, returns a Promise<ImageGenerationResult>.\n *\n * @default false\n */\n stream?: TStream\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} & ({} extends ImageProviderOptionsForModel<TAdapter, TAdapter['model']>\n ? {\n /** Provider-specific options for image generation */ modelOptions?: ImageProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n }\n : {\n /** Provider-specific options for image generation */ modelOptions: ImageProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n })\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the image activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<ImageGenerationResult>\n */\nexport type ImageActivityResult<TStream extends boolean = false> =\n TStream extends true\n ? AsyncIterable<StreamChunk>\n : Promise<ImageGenerationResult>\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Image activity - generates images from text prompts.\n *\n * Uses AI image generation models to create images based on natural language descriptions.\n *\n * @example Generate a single image\n * ```ts\n * import { generateImage } from '@tanstack/ai'\n * import { openaiImage } from '@tanstack/ai-openai'\n *\n * const result = await generateImage({\n * adapter: openaiImage('dall-e-3'),\n * prompt: 'A serene mountain landscape at sunset'\n * })\n *\n * console.log(result.images[0].url)\n * ```\n *\n * @example Generate multiple images\n * ```ts\n * const result = await generateImage({\n * adapter: openaiImage('dall-e-2'),\n * prompt: 'A cute robot mascot',\n * numberOfImages: 4,\n * size: '512x512'\n * })\n *\n * result.images.forEach((image, i) => {\n * console.log(`Image ${i + 1}: ${image.url}`)\n * })\n * ```\n *\n * @example With provider-specific options\n * ```ts\n * const result = await generateImage({\n * adapter: openaiImage('dall-e-3'),\n * prompt: 'A professional headshot photo',\n * size: '1024x1024',\n * modelOptions: {\n * quality: 'hd',\n * style: 'natural'\n * }\n * })\n * ```\n */\nexport function generateImage<\n TAdapter extends ImageAdapter<string, any, any, any>,\n TStream extends boolean = false,\n>(\n options: ImageActivityOptions<TAdapter, TStream>,\n): ImageActivityResult<TStream> {\n if (options.stream) {\n return streamGenerationResult(() =>\n runGenerateImage(options),\n ) as ImageActivityResult<TStream>\n }\n\n return runGenerateImage(options) as ImageActivityResult<TStream>\n}\n\n/**\n * Internal implementation of image generation (always non-streaming).\n * Contains all devtools event emission logic.\n */\nasync function runGenerateImage<\n TAdapter extends ImageAdapter<string, any, any, any>,\n>(\n options: ImageActivityOptions<TAdapter, boolean>,\n): Promise<ImageGenerationResult> {\n const { adapter, stream: _stream, debug: _debug, ...rest } = options\n const model = adapter.model\n const requestId = createId('image')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n\n aiEventClient.emit('image:request:started', {\n requestId,\n provider: adapter.name,\n model,\n prompt: rest.prompt,\n numberOfImages: rest.numberOfImages,\n size: rest.size,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: startTime,\n })\n\n logger.request(`activity=generateImage provider=${adapter.name}`, {\n provider: adapter.name,\n model,\n })\n\n try {\n const result = await adapter.generateImages({ ...rest, model, logger })\n const duration = Date.now() - startTime\n\n aiEventClient.emit('image:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n // GeneratedImage is a discriminated `{ url } | { b64Json }` union, but the\n // wire shape on the devtools event is a plain optional pair. Use\n // conditional spreads so the emitted record only sets the field actually\n // present — `exactOptionalPropertyTypes` rejects `field: undefined`\n // against `field?: string` targets.\n images: result.images.map((image) => ({\n url: image.url,\n b64Json: image.b64Json,\n })),\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n\n if (result.usage) {\n aiEventClient.emit('image:usage', {\n requestId,\n model,\n usage: result.usage,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n }\n\n logger.output(`activity=generateImage count=${result.images.length}`, {\n count: result.images.length,\n })\n\n return result\n } catch (error) {\n logger.errors('generateImage activity failed', {\n error,\n source: 'generateImage',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateImage() function without executing.\n */\nexport function createImageOptions<\n TAdapter extends ImageAdapter<string, any, any, any>,\n TStream extends boolean = false,\n>(\n options: ImageActivityOptions<TAdapter, TStream>,\n): ImageActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n ImageAdapter,\n ImageAdapterConfig,\n AnyImageAdapter,\n} from './adapter'\nexport { BaseImageAdapter } from './adapter'\n"],"names":[],"mappings":";;;AAoBO,MAAM,OAAO;AAsGpB,SAAS,SAAS,QAAwB;AACxC,SAAO,GAAG,MAAM,IAAI,KAAK,IAAA,CAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAC1E;AAmDO,SAAS,cAId,SAC8B;AAC9B,MAAI,QAAQ,QAAQ;AAClB,WAAO;AAAA,MAAuB,MAC5B,iBAAiB,OAAO;AAAA,IAAA;AAAA,EAE5B;AAEA,SAAO,iBAAiB,OAAO;AACjC;AAMA,eAAe,iBAGb,SACgC;AAChC,QAAM,EAAE,SAAS,QAAQ,SAAS,OAAO,QAAQ,GAAG,SAAS;AAC7D,QAAM,QAAQ,QAAQ;AACtB,QAAM,YAAY,SAAS,OAAO;AAClC,QAAM,YAAY,KAAK,IAAA;AACvB,QAAM,SAAyB,mBAAmB,QAAQ,KAAK;AAE/D,gBAAc,KAAK,yBAAyB;AAAA,IAC1C;AAAA,IACA,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA,QAAQ,KAAK;AAAA,IACb,gBAAgB,KAAK;AAAA,IACrB,MAAM,KAAK;AAAA,IACX,cAAc,KAAK;AAAA,IACnB,WAAW;AAAA,EAAA,CACZ;AAED,SAAO,QAAQ,mCAAmC,QAAQ,IAAI,IAAI;AAAA,IAChE,UAAU,QAAQ;AAAA,IAClB;AAAA,EAAA,CACD;AAED,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,eAAe,EAAE,GAAG,MAAM,OAAO,QAAQ;AACtE,UAAM,WAAW,KAAK,IAAA,IAAQ;AAE9B,kBAAc,KAAK,2BAA2B;AAAA,MAC5C;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,QAAQ,OAAO,OAAO,IAAI,CAAC,WAAW;AAAA,QACpC,KAAK,MAAM;AAAA,QACX,SAAS,MAAM;AAAA,MAAA,EACf;AAAA,MACF;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,WAAW,KAAK,IAAA;AAAA,IAAI,CACrB;AAED,QAAI,OAAO,OAAO;AAChB,oBAAc,KAAK,eAAe;AAAA,QAChC;AAAA,QACA;AAAA,QACA,OAAO,OAAO;AAAA,QACd,cAAc,KAAK;AAAA,QACnB,WAAW,KAAK,IAAA;AAAA,MAAI,CACrB;AAAA,IACH;AAEA,WAAO,OAAO,gCAAgC,OAAO,OAAO,MAAM,IAAI;AAAA,MACpE,OAAO,OAAO,OAAO;AAAA,IAAA,CACtB;AAED,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,OAAO,iCAAiC;AAAA,MAC7C;AAAA,MACA,QAAQ;AAAA,IAAA,CACT;AACD,UAAM;AAAA,EACR;AACF;AASO,SAAS,mBAId,SACyC;AACzC,SAAO;AACT;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/activities/generateImage/index.ts"],"sourcesContent":["/**\n * Image Activity\n *\n * Generates images from text prompts.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { ImageAdapter } from './adapter'\nimport type { ImageGenerationResult, StreamChunk } from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'image' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract model-specific provider options from an ImageAdapter via ~types.\n * If the model has specific options defined in ModelProviderOptions (and not just via index signature),\n * use those; otherwise fall back to base provider options.\n */\nexport type ImageProviderOptionsForModel<TAdapter, TModel extends string> =\n TAdapter extends ImageAdapter<any, infer BaseOptions, infer ModelOptions, any>\n ? string extends keyof ModelOptions\n ? // ModelOptions is Record<string, unknown> or has index signature - use BaseOptions\n BaseOptions\n : // ModelOptions has explicit keys - check if TModel is one of them\n TModel extends keyof ModelOptions\n ? ModelOptions[TModel]\n : BaseOptions\n : object\n\n/**\n * Extract model-specific size options from an ImageAdapter via ~types.\n * If the model has specific sizes defined, use those; otherwise fall back to string.\n */\nexport type ImageSizeForModel<TAdapter, TModel extends string> =\n TAdapter extends ImageAdapter<any, any, any, infer SizeByName>\n ? string extends keyof SizeByName\n ? // SizeByName has index signature - fall back to string\n string\n : // SizeByName has explicit keys - check if TModel is one of them\n TModel extends keyof SizeByName\n ? SizeByName[TModel]\n : string\n : string\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the image activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The image adapter type\n * @template TStream - Whether to stream the output\n */\nexport type ImageActivityOptions<\n TAdapter extends ImageAdapter<string, any, any, any>,\n TStream extends boolean = false,\n> = {\n /** The image adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** Text description of the desired image(s) */\n prompt: string\n /** Number of images to generate (default: 1) */\n numberOfImages?: number\n /** Image size in WIDTHxHEIGHT format (e.g., \"1024x1024\") */\n size?: ImageSizeForModel<TAdapter, TAdapter['model']>\n /**\n * Whether to stream the image generation result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming transport.\n * When false or not provided, returns a Promise<ImageGenerationResult>.\n *\n * @default false\n */\n stream?: TStream\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} & ({} extends ImageProviderOptionsForModel<TAdapter, TAdapter['model']>\n ? {\n /** Provider-specific options for image generation */ modelOptions?: ImageProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n }\n : {\n /** Provider-specific options for image generation */ modelOptions: ImageProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n })\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the image activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<ImageGenerationResult>\n */\nexport type ImageActivityResult<TStream extends boolean = false> =\n TStream extends true\n ? AsyncIterable<StreamChunk>\n : Promise<ImageGenerationResult>\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Image activity - generates images from text prompts.\n *\n * Uses AI image generation models to create images based on natural language descriptions.\n *\n * @example Generate a single image\n * ```ts\n * import { generateImage } from '@tanstack/ai'\n * import { openaiImage } from '@tanstack/ai-openai'\n *\n * const result = await generateImage({\n * adapter: openaiImage('dall-e-3'),\n * prompt: 'A serene mountain landscape at sunset'\n * })\n *\n * console.log(result.images[0].url)\n * ```\n *\n * @example Generate multiple images\n * ```ts\n * const result = await generateImage({\n * adapter: openaiImage('dall-e-2'),\n * prompt: 'A cute robot mascot',\n * numberOfImages: 4,\n * size: '512x512'\n * })\n *\n * result.images.forEach((image, i) => {\n * console.log(`Image ${i + 1}: ${image.url}`)\n * })\n * ```\n *\n * @example With provider-specific options\n * ```ts\n * const result = await generateImage({\n * adapter: openaiImage('dall-e-3'),\n * prompt: 'A professional headshot photo',\n * size: '1024x1024',\n * modelOptions: {\n * quality: 'hd',\n * style: 'natural'\n * }\n * })\n * ```\n */\nexport function generateImage<\n TAdapter extends ImageAdapter<string, any, any, any>,\n TStream extends boolean = false,\n>(\n options: ImageActivityOptions<TAdapter, TStream>,\n): ImageActivityResult<TStream> {\n if (options.stream) {\n return streamGenerationResult(() =>\n runGenerateImage(options),\n ) as ImageActivityResult<TStream>\n }\n\n return runGenerateImage(options) as ImageActivityResult<TStream>\n}\n\n/**\n * Internal implementation of image generation (always non-streaming).\n * Contains all devtools event emission logic.\n */\nasync function runGenerateImage<\n TAdapter extends ImageAdapter<string, any, any, any>,\n>(\n options: ImageActivityOptions<TAdapter, boolean>,\n): Promise<ImageGenerationResult> {\n const { adapter, stream: _stream, debug: _debug, ...rest } = options\n const model = adapter.model\n const requestId = createId('image')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n\n aiEventClient.emit('image:request:started', {\n requestId,\n provider: adapter.name,\n model,\n prompt: rest.prompt,\n numberOfImages: rest.numberOfImages,\n size: rest.size,\n modelOptions: rest.modelOptions,\n timestamp: startTime,\n })\n\n logger.request(`activity=generateImage provider=${adapter.name}`, {\n provider: adapter.name,\n model,\n })\n\n try {\n const result = await adapter.generateImages({ ...rest, model, logger })\n const duration = Date.now() - startTime\n\n aiEventClient.emit('image:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n // GeneratedImage is a discriminated `{ url } | { b64Json }` union, but the\n // wire shape on the devtools event is a plain optional pair. Use\n // conditional spreads so the emitted record only sets the field actually\n // present — `exactOptionalPropertyTypes` rejects `field: undefined`\n // against `field?: string` targets.\n images: result.images.map((image) => ({\n url: image.url,\n b64Json: image.b64Json,\n })),\n duration,\n modelOptions: rest.modelOptions,\n timestamp: Date.now(),\n })\n\n if (result.usage) {\n aiEventClient.emit('image:usage', {\n requestId,\n model,\n usage: result.usage,\n modelOptions: rest.modelOptions,\n timestamp: Date.now(),\n })\n }\n\n logger.output(`activity=generateImage count=${result.images.length}`, {\n count: result.images.length,\n })\n\n return result\n } catch (error) {\n logger.errors('generateImage activity failed', {\n error,\n source: 'generateImage',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateImage() function without executing.\n */\nexport function createImageOptions<\n TAdapter extends ImageAdapter<string, any, any, any>,\n TStream extends boolean = false,\n>(\n options: ImageActivityOptions<TAdapter, TStream>,\n): ImageActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n ImageAdapter,\n ImageAdapterConfig,\n AnyImageAdapter,\n} from './adapter'\nexport { BaseImageAdapter } from './adapter'\n"],"names":[],"mappings":";;;AAoBO,MAAM,OAAO;AAsGpB,SAAS,SAAS,QAAwB;AACxC,SAAO,GAAG,MAAM,IAAI,KAAK,IAAA,CAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAC1E;AAmDO,SAAS,cAId,SAC8B;AAC9B,MAAI,QAAQ,QAAQ;AAClB,WAAO;AAAA,MAAuB,MAC5B,iBAAiB,OAAO;AAAA,IAAA;AAAA,EAE5B;AAEA,SAAO,iBAAiB,OAAO;AACjC;AAMA,eAAe,iBAGb,SACgC;AAChC,QAAM,EAAE,SAAS,QAAQ,SAAS,OAAO,QAAQ,GAAG,SAAS;AAC7D,QAAM,QAAQ,QAAQ;AACtB,QAAM,YAAY,SAAS,OAAO;AAClC,QAAM,YAAY,KAAK,IAAA;AACvB,QAAM,SAAyB,mBAAmB,QAAQ,KAAK;AAE/D,gBAAc,KAAK,yBAAyB;AAAA,IAC1C;AAAA,IACA,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA,QAAQ,KAAK;AAAA,IACb,gBAAgB,KAAK;AAAA,IACrB,MAAM,KAAK;AAAA,IACX,cAAc,KAAK;AAAA,IACnB,WAAW;AAAA,EAAA,CACZ;AAED,SAAO,QAAQ,mCAAmC,QAAQ,IAAI,IAAI;AAAA,IAChE,UAAU,QAAQ;AAAA,IAClB;AAAA,EAAA,CACD;AAED,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,eAAe,EAAE,GAAG,MAAM,OAAO,QAAQ;AACtE,UAAM,WAAW,KAAK,IAAA,IAAQ;AAE9B,kBAAc,KAAK,2BAA2B;AAAA,MAC5C;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,QAAQ,OAAO,OAAO,IAAI,CAAC,WAAW;AAAA,QACpC,KAAK,MAAM;AAAA,QACX,SAAS,MAAM;AAAA,MAAA,EACf;AAAA,MACF;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,WAAW,KAAK,IAAA;AAAA,IAAI,CACrB;AAED,QAAI,OAAO,OAAO;AAChB,oBAAc,KAAK,eAAe;AAAA,QAChC;AAAA,QACA;AAAA,QACA,OAAO,OAAO;AAAA,QACd,cAAc,KAAK;AAAA,QACnB,WAAW,KAAK,IAAA;AAAA,MAAI,CACrB;AAAA,IACH;AAEA,WAAO,OAAO,gCAAgC,OAAO,OAAO,MAAM,IAAI;AAAA,MACpE,OAAO,OAAO,OAAO;AAAA,IAAA,CACtB;AAED,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,OAAO,iCAAiC;AAAA,MAC7C;AAAA,MACA,QAAQ;AAAA,IAAA,CACT;AACD,UAAM;AAAA,EACR;AACF;AASO,SAAS,mBAId,SACyC;AACzC,SAAO;AACT;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extend-adapter.js","sources":["../../src/extend-adapter.ts"],"sourcesContent":["import type { Modality } from './types'\n\n// ===========================\n// Extended Model Definition\n// ===========================\n\n/**\n * Definition for a custom model to add to an adapter.\n *\n * @template TName - The model name as a literal string type\n * @template TInput - Array of supported input modalities\n * @template TOptions - Provider options type for this model\n *\n * @example\n * ```typescript\n * const customModels = [\n * createModel('my-custom-model', ['text', 'image']),\n * ] as const\n * ```\n */\nexport interface ExtendedModelDef<\n TName extends string = string,\n TInput extends ReadonlyArray<Modality> = ReadonlyArray<Modality>,\n TOptions = unknown,\n> {\n /** The model name identifier */\n name: TName\n /** Supported input modalities for this model */\n input: TInput\n /** Type brand for provider options - use `{} as YourOptionsType` */\n modelOptions: TOptions\n}\n\n/**\n * Creates a custom model definition for use with `extendAdapter`.\n *\n * This is a helper function that provides proper type inference without\n * requiring manual `as const` casts on individual properties.\n *\n * @template TName - The model name (inferred from argument)\n * @template TInput - The input modalities array (inferred from argument)\n *\n * @param name - The model name identifier (literal string)\n * @param input - Array of supported input modalities\n * @returns A properly typed model definition for use with `extendAdapter`\n *\n * @example\n * ```typescript\n * import { extendAdapter, createModel } from '@tanstack/ai'\n * import { openaiText } from '@tanstack/ai-openai'\n *\n * // Define custom models with full type inference\n * const customModels = [\n * createModel('my-fine-tuned-gpt4', ['text', 'image']),\n * createModel('local-llama', ['text']),\n * ] as const\n *\n * const myOpenai = extendAdapter(openaiText, customModels)\n * ```\n */\nexport function createModel<\n const TName extends string,\n const TInput extends ReadonlyArray<Modality>,\n>(name: TName, input: TInput): ExtendedModelDef<TName, TInput> {\n return {\n name,\n input,\n modelOptions: {}
|
|
1
|
+
{"version":3,"file":"extend-adapter.js","sources":["../../src/extend-adapter.ts"],"sourcesContent":["import type { Modality } from './types'\n\n// ===========================\n// Extended Model Definition\n// ===========================\n\n/**\n * Definition for a custom model to add to an adapter.\n *\n * @template TName - The model name as a literal string type\n * @template TInput - Array of supported input modalities\n * @template TOptions - Provider options type for this model\n *\n * @example\n * ```typescript\n * const customModels = [\n * createModel('my-custom-model', ['text', 'image']),\n * ] as const\n * ```\n */\nexport interface ExtendedModelDef<\n TName extends string = string,\n TInput extends ReadonlyArray<Modality> = ReadonlyArray<Modality>,\n TOptions = unknown,\n> {\n /** The model name identifier */\n name: TName\n /** Supported input modalities for this model */\n input: TInput\n /** Type brand for provider options - use `{} as YourOptionsType` */\n modelOptions: TOptions\n}\n\n/**\n * Creates a custom model definition for use with `extendAdapter`.\n *\n * This is a helper function that provides proper type inference without\n * requiring manual `as const` casts on individual properties.\n *\n * @template TName - The model name (inferred from argument)\n * @template TInput - The input modalities array (inferred from argument)\n *\n * @param name - The model name identifier (literal string)\n * @param input - Array of supported input modalities\n * @returns A properly typed model definition for use with `extendAdapter`\n *\n * @example\n * ```typescript\n * import { extendAdapter, createModel } from '@tanstack/ai'\n * import { openaiText } from '@tanstack/ai-openai'\n *\n * // Define custom models with full type inference\n * const customModels = [\n * createModel('my-fine-tuned-gpt4', ['text', 'image']),\n * createModel('local-llama', ['text']),\n * ] as const\n *\n * const myOpenai = extendAdapter(openaiText, customModels)\n * ```\n */\nexport function createModel<\n const TName extends string,\n const TInput extends ReadonlyArray<Modality>,\n>(name: TName, input: TInput): ExtendedModelDef<TName, TInput> {\n return {\n name,\n input,\n modelOptions: {},\n }\n}\n\n// ===========================\n// Type Extraction Utilities\n// ===========================\n\n/**\n * Extract the model name union from an array of model definitions.\n */\ntype ExtractCustomModelNames<TDefs extends ReadonlyArray<ExtendedModelDef>> =\n TDefs[number]['name']\n\n// ===========================\n// Factory Type Inference\n// ===========================\n\n/**\n * Infer the model parameter type from an adapter factory function.\n * For generic functions like `<T extends Union>(model: T)`, this gets `T` which\n * TypeScript treats as the constraint union when used in parameter position.\n */\ntype InferFactoryModels<TFactory> = TFactory extends (\n model: infer TModel,\n ...args: Array<any>\n) => any\n ? TModel extends string\n ? TModel\n : string\n : string\n\n/**\n * Infer the config parameter type from an adapter factory function.\n */\ntype InferConfig<TFactory> = TFactory extends (\n model: any,\n config?: infer TConfig,\n) => any\n ? TConfig\n : undefined\n\n/**\n * Infer the adapter return type from a factory function.\n */\ntype InferAdapterReturn<TFactory> = TFactory extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn\n : never\n\n// ===========================\n// extendAdapter Function\n// ===========================\n\n/**\n * Extends an existing adapter factory with additional custom models.\n *\n * The extended adapter accepts both original models (with full original type inference)\n * and custom models (with types from your definitions).\n *\n * At runtime, this simply passes through to the original factory - no validation is performed.\n * The original factory's signature is fully preserved, including any config parameters.\n *\n * @param factory - The original adapter factory function (e.g., `openaiText`, `anthropicText`)\n * @param models - Array of custom model definitions with `name` and `input`\n * @returns A new factory function that accepts both original and custom models\n *\n * @example\n * ```typescript\n * import { extendAdapter, createModel } from '@tanstack/ai'\n * import { openaiText } from '@tanstack/ai-openai'\n *\n * // Define custom models\n * const customModels = [\n * createModel('my-fine-tuned-gpt4', ['text', 'image']),\n * createModel('local-llama', ['text']),\n * ] as const\n *\n * // Create extended adapter\n * const myOpenai = extendAdapter(openaiText, customModels)\n *\n * // Use with original models - full type inference preserved\n * const gpt4 = myOpenai('gpt-4o')\n *\n * // Use with custom models\n * const custom = myOpenai('my-fine-tuned-gpt4')\n *\n * // Type error: 'invalid-model' is not a valid model\n * // myOpenai('invalid-model')\n *\n * // Works with chat()\n * chat({\n * adapter: myOpenai('my-fine-tuned-gpt4'),\n * messages: [...]\n * })\n * ```\n */\nexport function extendAdapter<\n TFactory extends (...args: Array<any>) => any,\n const TDefs extends ReadonlyArray<ExtendedModelDef>,\n>(\n factory: TFactory,\n _customModels: TDefs,\n): (\n model: InferFactoryModels<TFactory> | ExtractCustomModelNames<TDefs>,\n ...args: InferConfig<TFactory> extends undefined\n ? []\n : [config?: InferConfig<TFactory>]\n) => InferAdapterReturn<TFactory> {\n // At runtime, we simply pass through to the original factory.\n // The _customModels parameter is only used for type inference.\n // No runtime validation - users are trusted to pass valid model names.\n return factory as any\n}\n"],"names":[],"mappings":"AA4DO,SAAS,YAGd,MAAa,OAAgD;AAC7D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,cAAc,CAAA;AAAA,EAAC;AAEnB;AAgGO,SAAS,cAId,SACA,eAMgC;AAIhC,SAAO;AACT;"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -8,14 +8,14 @@ export { createSpeechOptions } from './activities/generateSpeech/index.js';
|
|
|
8
8
|
export { createTranscriptionOptions } from './activities/generateTranscription/index.js';
|
|
9
9
|
export type { AIAdapter, ImageAdapter, AnyImageAdapter, TextAdapter, AnyTextAdapter, AnySummarizeAdapter, SummarizeAdapter, AnyAudioAdapter, AudioAdapter, AnyTTSAdapter, TTSAdapter, AnyTranscriptionAdapter, TranscriptionAdapter, AnyVideoAdapter, VideoAdapter, } from './activities/index.js';
|
|
10
10
|
export { toolDefinition, type ToolDefinition, type ToolDefinitionInstance, type ToolDefinitionConfig, type ServerTool, type ClientTool, type AnyClientTool, type InferToolName, type InferToolInput, type InferToolOutput, } from './activities/chat/tools/tool-definition.js';
|
|
11
|
-
export { convertSchemaToJsonSchema } from './activities/chat/tools/schema-converter.js';
|
|
11
|
+
export { convertSchemaToJsonSchema, StandardSchemaValidationError, } from './activities/chat/tools/schema-converter.js';
|
|
12
12
|
export { streamToText, toServerSentEventsStream, toServerSentEventsResponse, toHttpStream, toHttpResponse, } from './stream-to-response.js';
|
|
13
13
|
export { ToolCallManager } from './activities/chat/tools/tool-calls.js';
|
|
14
14
|
export type { ProviderTool } from './tools/provider-tool.js';
|
|
15
15
|
export { brandProviderTool } from './tools/provider-tool.js';
|
|
16
16
|
export { maxIterations, untilFinishReason, combineStrategies, } from './activities/chat/agent-loop-strategies.js';
|
|
17
17
|
export { createToolRegistry, createFrozenRegistry, type ToolRegistry, } from './tool-registry.js';
|
|
18
|
-
export type { ChatMiddleware, ChatMiddlewareContext, ChatMiddlewarePhase, ChatMiddlewareConfig, ToolCallHookContext, BeforeToolCallDecision, AfterToolCallInfo, IterationInfo, ToolPhaseCompleteInfo, UsageInfo, FinishInfo, AbortInfo, ErrorInfo, } from './activities/chat/middleware/index.js';
|
|
18
|
+
export type { ChatMiddleware, ChatMiddlewareContext, ChatMiddlewarePhase, ChatMiddlewareConfig, StructuredOutputMiddlewareConfig, ToolCallHookContext, BeforeToolCallDecision, AfterToolCallInfo, IterationInfo, ToolPhaseCompleteInfo, UsageInfo, FinishInfo, AbortInfo, ErrorInfo, } from './activities/chat/middleware/index.js';
|
|
19
19
|
export * from './types.js';
|
|
20
20
|
export type { SystemPrompt, NormalizedSystemPrompt } from './system-prompts.js';
|
|
21
21
|
export { normalizeSystemPrompts } from './system-prompts.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createVideoOptions, generateVideo, getVideoJobStatus } from "./activiti
|
|
|
6
6
|
import { createSpeechOptions, generateSpeech } from "./activities/generateSpeech/index.js";
|
|
7
7
|
import { createTranscriptionOptions, generateTranscription } from "./activities/generateTranscription/index.js";
|
|
8
8
|
import { toolDefinition } from "./activities/chat/tools/tool-definition.js";
|
|
9
|
-
import { convertSchemaToJsonSchema } from "./activities/chat/tools/schema-converter.js";
|
|
9
|
+
import { StandardSchemaValidationError, convertSchemaToJsonSchema } from "./activities/chat/tools/schema-converter.js";
|
|
10
10
|
import { streamToText, toHttpResponse, toHttpStream, toServerSentEventsResponse, toServerSentEventsStream } from "./stream-to-response.js";
|
|
11
11
|
import { ToolCallManager } from "./activities/chat/tools/tool-calls.js";
|
|
12
12
|
import { brandProviderTool } from "./tools/provider-tool.js";
|
|
@@ -32,6 +32,7 @@ export {
|
|
|
32
32
|
ImmediateStrategy,
|
|
33
33
|
PartialJSONParser,
|
|
34
34
|
PunctuationStrategy,
|
|
35
|
+
StandardSchemaValidationError,
|
|
35
36
|
StreamProcessor,
|
|
36
37
|
ToolCallManager,
|
|
37
38
|
WordBoundaryStrategy,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-guard.js","sources":["../../../src/middlewares/content-guard.ts"],"sourcesContent":["import type {\n ChatMiddleware,\n ChatMiddlewareContext,\n} from '../activities/chat/middleware/types'\nimport type { StreamChunk } from '../types'\n\n/**\n * A content guard rule — either a regex pattern with replacement, or a transform function.\n */\nexport type ContentGuardRule =\n | { pattern: RegExp; replacement: string }\n | { fn: (text: string) => string }\n\n/**\n * Information passed to the onFiltered callback.\n */\nexport interface ContentFilteredInfo {\n /** The message ID being filtered */\n messageId: string\n /** The original text before filtering */\n original: string\n /** The filtered text after rules applied */\n filtered: string\n /** Which strategy was used */\n strategy: 'delta' | 'buffered'\n}\n\n/**\n * Options for the content guard middleware.\n */\nexport interface ContentGuardMiddlewareOptions {\n /**\n * Rules to apply to text content. Each rule is either a regex pattern\n * with a replacement string, or a custom transform function.\n * Rules are applied in order. Each rule receives the output of the previous.\n */\n rules: Array<ContentGuardRule>\n\n /**\n * Matching strategy:\n * - 'delta': Apply rules to each delta as it arrives. Fast, real-time,\n * but patterns spanning chunk boundaries may be missed.\n * - 'buffered': Accumulate content and apply rules to settled portions,\n * holding back a look-behind buffer to catch cross-boundary patterns.\n *\n * @default 'buffered'\n */\n strategy?: 'delta' | 'buffered'\n\n /**\n * Number of characters to hold back before emitting (buffered strategy only).\n * Should be at least as long as the longest pattern you expect to match.\n * Buffer is flushed when the stream ends.\n *\n * @default 50\n */\n bufferSize?: number\n\n /**\n * If true, drop the entire chunk when any rule changes the content.\n * @default false\n */\n blockOnMatch?: boolean\n\n /**\n * Callback when content is filtered by any rule.\n */\n onFiltered?: (info: ContentFilteredInfo) => void\n}\n\n/**\n * Apply all rules to a string, returning the transformed result.\n */\nfunction applyRules(text: string, rules: Array<ContentGuardRule>): string {\n let result = text\n for (const rule of rules) {\n if ('pattern' in rule) {\n result = result.replace(rule.pattern, rule.replacement)\n } else {\n result = rule.fn(result)\n }\n }\n return result\n}\n\n/**\n * Creates a middleware that filters or transforms streamed text content.\n *\n * @example\n * ```ts\n * import { contentGuardMiddleware } from '@tanstack/ai/middlewares'\n *\n * const guard = contentGuardMiddleware({\n * rules: [\n * { pattern: /\\b\\d{3}-\\d{2}-\\d{4}\\b/g, replacement: '[SSN REDACTED]' },\n * ],\n * strategy: 'buffered',\n * })\n * ```\n */\nexport function contentGuardMiddleware(\n options: ContentGuardMiddlewareOptions,\n): ChatMiddleware {\n const {\n rules,\n strategy = 'buffered',\n bufferSize = 50,\n blockOnMatch = false,\n onFiltered,\n } = options\n\n if (strategy === 'delta') {\n return createDeltaStrategy(rules, blockOnMatch, onFiltered)\n }\n return createBufferedStrategy(rules, bufferSize, blockOnMatch, onFiltered)\n}\n\nfunction createDeltaStrategy(\n rules: Array<ContentGuardRule>,\n blockOnMatch: boolean,\n onFiltered?: (info: ContentFilteredInfo) => void,\n): ChatMiddleware {\n return {\n name: 'content-guard',\n\n onChunk(_ctx: ChatMiddlewareContext, chunk: StreamChunk) {\n if (chunk.type !== 'TEXT_MESSAGE_CONTENT') return\n\n const original = chunk.delta\n const filtered = applyRules(original, rules)\n\n if (filtered === original) return // unchanged, pass through\n\n if (onFiltered) {\n onFiltered({\n messageId: chunk.messageId,\n original,\n filtered,\n strategy: 'delta',\n })\n }\n\n if (blockOnMatch) return null // drop chunk\n\n // Strip out the previous `content` field by destructuring it away — with\n // `exactOptionalPropertyTypes` we can't assign `content: undefined`\n // against `content?: string`. The replacement event carries only the\n // filtered delta.\n const { content: _strippedContent, ...rest } = chunk\n void _strippedContent\n return {\n ...rest,\n delta: filtered,\n } as StreamChunk\n },\n }\n}\n\nfunction createBufferedStrategy(\n rules: Array<ContentGuardRule>,\n bufferSize: number,\n blockOnMatch: boolean,\n onFiltered?: (info: ContentFilteredInfo) => void,\n): ChatMiddleware {\n let rawAccumulated = ''\n let emittedFilteredLength = 0\n let lastMessageId = ''\n\n function resetState() {\n rawAccumulated = ''\n emittedFilteredLength = 0\n lastMessageId = ''\n }\n\n function flushBuffer(): StreamChunk | null {\n if (rawAccumulated.length === 0) return null\n\n const filtered = applyRules(rawAccumulated, rules)\n\n if (blockOnMatch && filtered !== rawAccumulated) {\n if (onFiltered) {\n onFiltered({\n messageId: lastMessageId,\n original: rawAccumulated,\n filtered,\n strategy: 'buffered',\n })\n }\n resetState()\n return null\n }\n\n const remaining = filtered.slice(emittedFilteredLength)\n if (remaining.length > 0) {\n if (filtered !== rawAccumulated && onFiltered) {\n onFiltered({\n messageId: lastMessageId,\n original: rawAccumulated,\n filtered,\n strategy: 'buffered',\n })\n }\n\n const flushed = {\n type: 'TEXT_MESSAGE_CONTENT',\n messageId: lastMessageId,\n delta: remaining,\n content: filtered,\n timestamp: Date.now(),\n } as StreamChunk\n\n resetState()\n return flushed\n }\n\n resetState()\n return null\n }\n\n return {\n name: 'content-guard',\n\n onStart() {\n resetState()\n },\n\n onChunk(_ctx: ChatMiddlewareContext, chunk: StreamChunk) {\n // Flush buffer on stream end events\n if (chunk.type === 'TEXT_MESSAGE_END' || chunk.type === 'RUN_FINISHED') {\n const flushed = flushBuffer()\n if (flushed) return [flushed, chunk]\n return // pass through end event\n }\n\n if (chunk.type !== 'TEXT_MESSAGE_CONTENT') return // pass through\n\n // Flush buffer on message boundary change\n const pending: Array<StreamChunk> = []\n if (lastMessageId && chunk.messageId !== lastMessageId) {\n const flushed = flushBuffer()\n if (flushed) pending.push(flushed)\n }\n\n rawAccumulated += chunk.delta\n lastMessageId = chunk.messageId\n\n // Apply rules to full accumulated text, buffer in filtered space\n const filtered = applyRules(rawAccumulated, rules)\n const safeFilteredEnd = Math.max(0, filtered.length - bufferSize)\n\n if (safeFilteredEnd <= emittedFilteredLength) {\n return pending.length > 0 ? pending : null\n }\n\n if (blockOnMatch && filtered !== rawAccumulated) {\n if (onFiltered) {\n onFiltered({\n messageId: chunk.messageId,\n original: rawAccumulated,\n filtered,\n strategy: 'buffered',\n })\n }\n return pending.length > 0 ? pending : null\n }\n\n const newDelta = filtered.slice(emittedFilteredLength, safeFilteredEnd)\n\n if (filtered !== rawAccumulated && onFiltered) {\n onFiltered({\n messageId: chunk.messageId,\n original: rawAccumulated,\n filtered,\n strategy: 'buffered',\n })\n }\n\n emittedFilteredLength = safeFilteredEnd\n\n const emitChunk = {\n ...chunk,\n delta: newDelta,\n content: filtered.slice(0, safeFilteredEnd),\n } as StreamChunk\n\n // `pending` was empty before this push iff `emitChunk` is now the only\n // entry — return it directly without re-indexing through `pending[0]`.\n const wasEmpty = pending.length === 0\n pending.push(emitChunk)\n return wasEmpty ? emitChunk : pending\n },\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"content-guard.js","sources":["../../../src/middlewares/content-guard.ts"],"sourcesContent":["import type {\n ChatMiddleware,\n ChatMiddlewareContext,\n} from '../activities/chat/middleware/types'\nimport type { StreamChunk } from '../types'\n\n/**\n * A content guard rule — either a regex pattern with replacement, or a transform function.\n */\nexport type ContentGuardRule =\n | { pattern: RegExp; replacement: string }\n | { fn: (text: string) => string }\n\n/**\n * Information passed to the onFiltered callback.\n */\nexport interface ContentFilteredInfo {\n /** The message ID being filtered */\n messageId: string\n /** The original text before filtering */\n original: string\n /** The filtered text after rules applied */\n filtered: string\n /** Which strategy was used */\n strategy: 'delta' | 'buffered'\n}\n\n/**\n * Options for the content guard middleware.\n */\nexport interface ContentGuardMiddlewareOptions {\n /**\n * Rules to apply to text content. Each rule is either a regex pattern\n * with a replacement string, or a custom transform function.\n * Rules are applied in order. Each rule receives the output of the previous.\n */\n rules: Array<ContentGuardRule>\n\n /**\n * Matching strategy:\n * - 'delta': Apply rules to each delta as it arrives. Fast, real-time,\n * but patterns spanning chunk boundaries may be missed.\n * - 'buffered': Accumulate content and apply rules to settled portions,\n * holding back a look-behind buffer to catch cross-boundary patterns.\n *\n * @default 'buffered'\n */\n strategy?: 'delta' | 'buffered'\n\n /**\n * Number of characters to hold back before emitting (buffered strategy only).\n * Should be at least as long as the longest pattern you expect to match.\n * Buffer is flushed when the stream ends.\n *\n * @default 50\n */\n bufferSize?: number\n\n /**\n * If true, drop the entire chunk when any rule changes the content.\n * @default false\n */\n blockOnMatch?: boolean\n\n /**\n * Callback when content is filtered by any rule.\n */\n onFiltered?: (info: ContentFilteredInfo) => void\n}\n\n/**\n * Apply all rules to a string, returning the transformed result.\n */\nfunction applyRules(text: string, rules: Array<ContentGuardRule>): string {\n let result = text\n for (const rule of rules) {\n if ('pattern' in rule) {\n result = result.replace(rule.pattern, rule.replacement)\n } else {\n result = rule.fn(result)\n }\n }\n return result\n}\n\n/**\n * Creates a middleware that filters or transforms streamed text content.\n *\n * @example\n * ```ts\n * import { contentGuardMiddleware } from '@tanstack/ai/middlewares'\n *\n * const guard = contentGuardMiddleware({\n * rules: [\n * { pattern: /\\b\\d{3}-\\d{2}-\\d{4}\\b/g, replacement: '[SSN REDACTED]' },\n * ],\n * strategy: 'buffered',\n * })\n * ```\n */\nexport function contentGuardMiddleware(\n options: ContentGuardMiddlewareOptions,\n): ChatMiddleware {\n const {\n rules,\n strategy = 'buffered',\n bufferSize = 50,\n blockOnMatch = false,\n onFiltered,\n } = options\n\n if (strategy === 'delta') {\n return createDeltaStrategy(rules, blockOnMatch, onFiltered)\n }\n return createBufferedStrategy(rules, bufferSize, blockOnMatch, onFiltered)\n}\n\nfunction createDeltaStrategy(\n rules: Array<ContentGuardRule>,\n blockOnMatch: boolean,\n onFiltered?: (info: ContentFilteredInfo) => void,\n): ChatMiddleware {\n return {\n name: 'content-guard',\n\n onChunk(_ctx: ChatMiddlewareContext, chunk: StreamChunk) {\n if (chunk.type !== 'TEXT_MESSAGE_CONTENT') return\n\n const original = chunk.delta\n const filtered = applyRules(original, rules)\n\n if (filtered === original) return // unchanged, pass through\n\n if (onFiltered) {\n onFiltered({\n messageId: chunk.messageId,\n original,\n filtered,\n strategy: 'delta',\n })\n }\n\n if (blockOnMatch) return null // drop chunk\n\n // Strip out the previous `content` field by destructuring it away — with\n // `exactOptionalPropertyTypes` we can't assign `content: undefined`\n // against `content?: string`. The replacement event carries only the\n // filtered delta.\n const { content: _strippedContent, ...rest } = chunk\n void _strippedContent\n return {\n ...rest,\n delta: filtered,\n }\n },\n }\n}\n\nfunction createBufferedStrategy(\n rules: Array<ContentGuardRule>,\n bufferSize: number,\n blockOnMatch: boolean,\n onFiltered?: (info: ContentFilteredInfo) => void,\n): ChatMiddleware {\n let rawAccumulated = ''\n let emittedFilteredLength = 0\n let lastMessageId = ''\n\n function resetState() {\n rawAccumulated = ''\n emittedFilteredLength = 0\n lastMessageId = ''\n }\n\n function flushBuffer(): StreamChunk | null {\n if (rawAccumulated.length === 0) return null\n\n const filtered = applyRules(rawAccumulated, rules)\n\n if (blockOnMatch && filtered !== rawAccumulated) {\n if (onFiltered) {\n onFiltered({\n messageId: lastMessageId,\n original: rawAccumulated,\n filtered,\n strategy: 'buffered',\n })\n }\n resetState()\n return null\n }\n\n const remaining = filtered.slice(emittedFilteredLength)\n if (remaining.length > 0) {\n if (filtered !== rawAccumulated && onFiltered) {\n onFiltered({\n messageId: lastMessageId,\n original: rawAccumulated,\n filtered,\n strategy: 'buffered',\n })\n }\n\n const flushed = {\n type: 'TEXT_MESSAGE_CONTENT',\n messageId: lastMessageId,\n delta: remaining,\n content: filtered,\n timestamp: Date.now(),\n } as StreamChunk\n\n resetState()\n return flushed\n }\n\n resetState()\n return null\n }\n\n return {\n name: 'content-guard',\n\n onStart() {\n resetState()\n },\n\n onChunk(_ctx: ChatMiddlewareContext, chunk: StreamChunk) {\n // Flush buffer on stream end events\n if (chunk.type === 'TEXT_MESSAGE_END' || chunk.type === 'RUN_FINISHED') {\n const flushed = flushBuffer()\n if (flushed) return [flushed, chunk]\n return // pass through end event\n }\n\n if (chunk.type !== 'TEXT_MESSAGE_CONTENT') return // pass through\n\n // Flush buffer on message boundary change\n const pending: Array<StreamChunk> = []\n if (lastMessageId && chunk.messageId !== lastMessageId) {\n const flushed = flushBuffer()\n if (flushed) pending.push(flushed)\n }\n\n rawAccumulated += chunk.delta\n lastMessageId = chunk.messageId\n\n // Apply rules to full accumulated text, buffer in filtered space\n const filtered = applyRules(rawAccumulated, rules)\n const safeFilteredEnd = Math.max(0, filtered.length - bufferSize)\n\n if (safeFilteredEnd <= emittedFilteredLength) {\n return pending.length > 0 ? pending : null\n }\n\n if (blockOnMatch && filtered !== rawAccumulated) {\n if (onFiltered) {\n onFiltered({\n messageId: chunk.messageId,\n original: rawAccumulated,\n filtered,\n strategy: 'buffered',\n })\n }\n return pending.length > 0 ? pending : null\n }\n\n const newDelta = filtered.slice(emittedFilteredLength, safeFilteredEnd)\n\n if (filtered !== rawAccumulated && onFiltered) {\n onFiltered({\n messageId: chunk.messageId,\n original: rawAccumulated,\n filtered,\n strategy: 'buffered',\n })\n }\n\n emittedFilteredLength = safeFilteredEnd\n\n const emitChunk = {\n ...chunk,\n delta: newDelta,\n content: filtered.slice(0, safeFilteredEnd),\n } as StreamChunk\n\n // `pending` was empty before this push iff `emitChunk` is now the only\n // entry — return it directly without re-indexing through `pending[0]`.\n const wasEmpty = pending.length === 0\n pending.push(emitChunk)\n return wasEmpty ? emitChunk : pending\n },\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -129,10 +129,7 @@ function otelMiddleware(options) {
|
|
|
129
129
|
const iteration = state.iterationCount - 1;
|
|
130
130
|
safeCall(
|
|
131
131
|
"otel.onSpanEnd",
|
|
132
|
-
() => onSpanEnd?.(
|
|
133
|
-
{ kind: "iteration", ctx, iteration },
|
|
134
|
-
span
|
|
135
|
-
)
|
|
132
|
+
() => onSpanEnd?.({ kind: "iteration", ctx, iteration }, span)
|
|
136
133
|
);
|
|
137
134
|
span.end();
|
|
138
135
|
state.currentIterationSpan = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otel.js","sources":["../../../src/middlewares/otel.ts"],"sourcesContent":["import {\n SpanKind,\n SpanStatusCode,\n context as otelContext,\n trace as otelTrace,\n} from '@opentelemetry/api'\nimport type {\n AttributeValue,\n Exception,\n Meter,\n Span,\n SpanOptions,\n Tracer,\n} from '@opentelemetry/api'\nimport type {\n ChatMiddleware,\n ChatMiddlewareContext,\n} from '../activities/chat/middleware/types'\n\n/**\n * Scope (role) of an OTel span emitted by this middleware.\n *\n * - `chat` — the root span for a single `chat()` call\n * - `iteration` — one per agent-loop iteration (one model call)\n * - `tool` — one per tool execution inside an iteration\n */\nexport type OtelSpanScope = 'chat' | 'iteration' | 'tool'\n\n/**\n * Alias retained for backwards compatibility. Prefer {@link OtelSpanScope}.\n *\n * @deprecated Use `OtelSpanScope` instead — the name shadows OTel's built-in\n * `SpanKind` which is also imported by integrations of this middleware.\n */\nexport type OtelSpanKind = OtelSpanScope\n\n/**\n * Span metadata passed to `spanNameFormatter`, `attributeEnricher`,\n * `onBeforeSpanStart`, and `onSpanEnd`. Discriminated by `kind` so that\n * tool-only fields narrow automatically inside callback bodies.\n */\nexport type OtelSpanInfo<TScope extends OtelSpanScope = OtelSpanScope> =\n TScope extends 'chat'\n ? { kind: 'chat'; ctx: ChatMiddlewareContext }\n : TScope extends 'iteration'\n ? { kind: 'iteration'; ctx: ChatMiddlewareContext; iteration: number }\n : TScope extends 'tool'\n ? {\n kind: 'tool'\n ctx: ChatMiddlewareContext\n iteration: number\n toolName: string\n toolCallId: string\n }\n : never\n\nexport interface OtelMiddlewareOptions {\n /** OTel `Tracer` used to start root, iteration, and tool spans. */\n tracer: Tracer\n /**\n * Optional OTel `Meter`. When provided, the middleware records\n * `gen_ai.client.operation.duration` and `gen_ai.client.token.usage`\n * histograms. Omit to disable metrics without disabling tracing.\n */\n meter?: Meter\n /**\n * When `true`, prompt and completion content is attached to iteration spans\n * as `gen_ai.*.message` / `gen_ai.choice` events. Defaults to `false` so\n * that PII never lands on a span by accident.\n */\n captureContent?: boolean\n /**\n * Invoked on every captured content string before it lands on a span.\n * Return a redacted version. If this function throws, the middleware emits\n * the literal sentinel `\"[redaction_failed]\"` instead of the original text\n * — it never falls back to raw content.\n */\n redact?: (text: string) => string\n /**\n * Maximum characters kept in the per-iteration assistant text buffer used\n * to emit `gen_ai.choice` events. Extra characters are truncated with a\n * trailing `\"…\"` marker. Defaults to 100 000. Set to `0` to disable the\n * cap. Exporters typically truncate long attribute values anyway.\n */\n maxContentLength?: number\n /** Override the default span name for each `kind`. */\n spanNameFormatter?: (info: OtelSpanInfo) => string\n /** Add extra attributes to each span. */\n attributeEnricher?: (info: OtelSpanInfo) => Record<string, AttributeValue>\n /** Mutate `SpanOptions` immediately before `tracer.startSpan(...)`. */\n onBeforeSpanStart?: (info: OtelSpanInfo, options: SpanOptions) => SpanOptions\n /** Fires just before every `span.end()`. */\n onSpanEnd?: (info: OtelSpanInfo, span: Span) => void\n}\n\ninterface RequestState {\n rootSpan: Span\n currentIterationSpan: Span | null\n toolSpans: Map<string, { span: Span; toolName: string }>\n iterationCount: number\n assistantTextBuffer: string\n assistantTextBufferTruncated: boolean\n startTime: number\n}\n\nconst stateByCtx = new WeakMap<ChatMiddlewareContext, RequestState>()\n\nconst DEFAULT_MAX_CONTENT_LENGTH = 100_000\nconst REDACTION_FAILED_SENTINEL = '[redaction_failed]'\n\nfunction serializeContent(content: unknown): string {\n if (typeof content === 'string') return content\n if (!Array.isArray(content)) return ''\n const parts: Array<string> = []\n for (const part of content) {\n if (!part || typeof part !== 'object') continue\n const type = (part as { type?: string }).type\n switch (type) {\n case 'text':\n parts.push(\n (\n (part as { text?: string }).text ??\n (part as { content?: string }).content ??\n ''\n ).toString(),\n )\n break\n case 'image':\n parts.push('[image]')\n break\n case 'audio':\n parts.push('[audio]')\n break\n case 'video':\n parts.push('[video]')\n break\n case 'document':\n parts.push('[document]')\n break\n case undefined:\n parts.push('[unknown]')\n break\n default:\n parts.push(`[${type}]`)\n }\n }\n return parts.join(' ')\n}\n\nfunction messageEventName(role: string): string {\n switch (role) {\n case 'user':\n return 'gen_ai.user.message'\n case 'assistant':\n return 'gen_ai.assistant.message'\n case 'tool':\n return 'gen_ai.tool.message'\n case 'system':\n return 'gen_ai.system.message'\n default:\n return `gen_ai.${role}.message`\n }\n}\n\nfunction errorMessage(err: unknown): string | undefined {\n if (err instanceof Error) return err.message\n if (typeof err === 'string') return err\n if (err && typeof err === 'object' && 'message' in err) {\n const m = (err as { message?: unknown }).message\n if (typeof m === 'string') return m\n }\n return undefined\n}\n\nfunction errorTypeName(err: unknown): string {\n if (err instanceof Error) return err.name || 'Error'\n if (err && typeof err === 'object' && 'name' in err) {\n const n = (err as { name?: unknown }).name\n if (typeof n === 'string') return n\n }\n return 'Error'\n}\n\nfunction safeCall<T>(label: string, fn: () => T): T | undefined {\n try {\n return fn()\n } catch (err) {\n // Keep middleware non-fatal, but surface callback failures so that broken\n // extension points (attributeEnricher, spanNameFormatter, onSpanEnd, ...)\n // are observable. Matches the guarantee documented in docs/advanced/otel.md.\n console.warn(`[otelMiddleware] ${label} failed`, err)\n return undefined\n }\n}\n\nexport function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {\n const {\n tracer,\n meter,\n captureContent = false,\n redact = (s) => s,\n maxContentLength = DEFAULT_MAX_CONTENT_LENGTH,\n spanNameFormatter,\n attributeEnricher,\n onBeforeSpanStart,\n onSpanEnd,\n } = options\n\n const durationHistogram = meter?.createHistogram(\n 'gen_ai.client.operation.duration',\n {\n description: 'GenAI client operation duration',\n unit: 's',\n },\n )\n const tokenHistogram = meter?.createHistogram('gen_ai.client.token.usage', {\n description: 'GenAI client token usage',\n unit: '{token}',\n })\n\n // Redact user content, failing closed to a sentinel string instead of ever\n // letting raw text through. Callers that pass `captureContent: true` with a\n // third-party PII redactor depend on this invariant.\n const redactContent = (text: string): string => {\n try {\n return redact(text)\n } catch (err) {\n console.warn('[otelMiddleware] otel.redact failed', err)\n return REDACTION_FAILED_SENTINEL\n }\n }\n\n const appendAssistantText = (state: RequestState, delta: string): void => {\n if (maxContentLength > 0) {\n if (state.assistantTextBufferTruncated) return\n const remaining = maxContentLength - state.assistantTextBuffer.length\n if (remaining <= 0) {\n state.assistantTextBufferTruncated = true\n state.assistantTextBuffer += '…'\n return\n }\n if (delta.length > remaining) {\n state.assistantTextBuffer += delta.slice(0, remaining) + '…'\n state.assistantTextBufferTruncated = true\n return\n }\n }\n state.assistantTextBuffer += delta\n }\n\n const closeIterationSpan = (\n state: RequestState,\n ctx: ChatMiddlewareContext,\n ): void => {\n if (!state.currentIterationSpan) return\n const span = state.currentIterationSpan\n const iteration = state.iterationCount - 1\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n { kind: 'iteration', ctx, iteration } as OtelSpanInfo<'iteration'>,\n span,\n ),\n )\n span.end()\n state.currentIterationSpan = null\n }\n\n return {\n name: 'otel',\n\n onStart(ctx) {\n safeCall('otel.onStart', () => {\n const info: OtelSpanInfo<'chat'> = { kind: 'chat', ctx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.request.model': ctx.model,\n // NOTE: `gen_ai.operation.name` is deliberately NOT set on the\n // root span. The root represents a `chat()` invocation that may\n // span multiple model calls; only iteration spans correspond to\n // a single chat operation. Backends that map `operation.name=chat`\n // to a \"generation\" event (e.g. PostHog LLM Analytics) would\n // otherwise emit a duplicate generation for the wrapper span.\n },\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n const rootSpan = tracer.startSpan(name, spanOptions)\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) rootSpan.setAttributes(enriched)\n\n stateByCtx.set(ctx, {\n rootSpan,\n currentIterationSpan: null,\n toolSpans: new Map(),\n iterationCount: 0,\n assistantTextBuffer: '',\n assistantTextBufferTruncated: false,\n startTime: Date.now(),\n })\n })\n },\n\n onConfig(ctx, config) {\n if (ctx.phase !== 'beforeModel') return\n safeCall('otel.onConfig', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // The previous iteration's span stays open through tool execution and\n // onUsage so that tool spans nest under it and token attributes land\n // on it. Close it here, just before opening the next iteration.\n closeIterationSpan(state, ctx)\n\n const info: OtelSpanInfo<'iteration'> = {\n kind: 'iteration',\n ctx,\n iteration: ctx.iteration,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model} #${ctx.iteration}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'tanstack.ai.iteration': ctx.iteration,\n }\n if (config.temperature !== undefined)\n baseAttrs['gen_ai.request.temperature'] = config.temperature\n if (config.topP !== undefined)\n baseAttrs['gen_ai.request.top_p'] = config.topP\n if (config.maxTokens !== undefined)\n baseAttrs['gen_ai.request.max_tokens'] = config.maxTokens\n\n const baseOptions: SpanOptions = {\n kind: SpanKind.CLIENT,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(\n otelContext.active(),\n state.rootSpan,\n )\n let iterSpan!: Span\n otelContext.with(parentCtx, () => {\n // Pass the parent context explicitly as the 3rd arg — this is a\n // real-OTel-compatible way to ensure the span is parented to\n // `rootSpan` even when the host app has not registered a context\n // manager (e.g. in tests or minimal setups).\n iterSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) iterSpan.setAttributes(enriched)\n\n state.currentIterationSpan = iterSpan\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n\n if (captureContent) {\n const systemPromptContents = config.systemPrompts.map((p) =>\n typeof p === 'string' ? p : p.content,\n )\n // Anthropic prompt-caching users need to know which prompt carried\n // `cache_control`: it's the one attribute that explains cache\n // hit/miss in observability. Serialise per-prompt metadata as a\n // single JSON span attribute so backends that don't understand\n // GenAI events can still surface it. Kept off span events to\n // avoid breaking the one-event-per-message GenAI semconv contract.\n const systemPromptMetadata = config.systemPrompts.map((p) =>\n typeof p === 'string' || p.metadata === undefined\n ? null\n : p.metadata,\n )\n if (systemPromptMetadata.some((m) => m !== null)) {\n iterSpan.setAttribute(\n 'tanstack.ai.system_prompt.metadata',\n JSON.stringify(systemPromptMetadata),\n )\n }\n // Span events follow the original GenAI semconv (one event per\n // message). Backends that read events get content this way.\n for (const sys of systemPromptContents) {\n iterSpan.addEvent('gen_ai.system.message', {\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n iterSpan.addEvent(messageEventName(m.role), {\n content: redactContent(body),\n })\n }\n\n // Also emit the current GenAI-semconv attribute form\n // (`gen_ai.input.messages`) — backends like PostHog read prompt\n // content from this attribute, not from span events.\n const inputMessages: Array<{ role: string; content: string }> = []\n for (const sys of systemPromptContents) {\n inputMessages.push({\n role: 'system',\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n inputMessages.push({\n role: m.role,\n content: redactContent(body),\n })\n }\n if (inputMessages.length > 0) {\n const inputJson = JSON.stringify(inputMessages)\n // Current OTel GenAI semconv — Sentry / PostHog / Datadog read\n // prompt content from this attribute.\n iterSpan.setAttribute('gen_ai.input.messages', inputJson)\n // Langfuse-native attribute. Highest priority in Langfuse's OTLP\n // ingestion (checked before events and gen_ai.input.messages) so\n // the Input panel populates reliably. Harmless to other backends —\n // the attribute is namespaced and unrecognised keys are ignored.\n iterSpan.setAttribute('langfuse.observation.input', inputJson)\n\n // Mirror the first iteration's input onto the root span and at\n // trace level so Langfuse fills Input on the trace card and the\n // chat-level observation. Later iterations append tool-call /\n // assistant messages that are useful per-iteration but noise at\n // the chat / trace level.\n if (state.iterationCount === 0) {\n state.rootSpan.setAttribute(\n 'langfuse.observation.input',\n inputJson,\n )\n state.rootSpan.setAttribute('langfuse.trace.input', inputJson)\n }\n }\n }\n\n state.iterationCount += 1\n })\n return undefined\n },\n\n onChunk(ctx, chunk) {\n safeCall('otel.onChunk', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n if (captureContent && chunk.type === 'TEXT_MESSAGE_CONTENT') {\n appendAssistantText(state, chunk.delta)\n }\n\n if (chunk.type !== 'RUN_FINISHED') return\n const span = state.currentIterationSpan\n if (!span) return\n\n if (chunk.finishReason) {\n span.setAttribute('gen_ai.response.finish_reasons', [\n chunk.finishReason,\n ])\n }\n if (chunk.model) span.setAttribute('gen_ai.response.model', chunk.model)\n\n // Set usage attributes on the iteration span directly from the chunk\n // so they're available before `onUsage` fires. Histogram recording is\n // deliberately NOT done here — the chat runner always invokes\n // `runOnUsage` when `chunk.usage` is present, and `onUsage` is the\n // canonical place for the metric. Recording in both would double-count.\n if (chunk.usage) {\n span.setAttributes({\n 'gen_ai.usage.input_tokens': chunk.usage.promptTokens,\n 'gen_ai.usage.output_tokens': chunk.usage.completionTokens,\n })\n }\n\n if (captureContent && state.assistantTextBuffer.length > 0) {\n const completion = redactContent(state.assistantTextBuffer)\n const outputJson = JSON.stringify([\n { role: 'assistant', content: completion },\n ])\n // Event form (older semconv) — kept for backends that consume it.\n span.addEvent('gen_ai.choice', { content: completion })\n // Attribute form (current semconv) — required by backends like\n // PostHog that read completion content from `gen_ai.output.messages`.\n span.setAttribute('gen_ai.output.messages', outputJson)\n // Langfuse-native attribute (highest priority in Langfuse mapping).\n span.setAttribute('langfuse.observation.output', outputJson)\n // Mirror to the root span and trace card. Each iteration overwrites,\n // so the final iteration's completion lands on the root — which is\n // the final answer the user saw, not an intermediate tool-call turn.\n state.rootSpan.setAttribute('langfuse.observation.output', outputJson)\n state.rootSpan.setAttribute('langfuse.trace.output', outputJson)\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n }\n\n // Intentionally leave the iteration span open: tool spans started\n // after `RUN_FINISHED` (tool_calls finishReason) must nest under it,\n // and `onUsage` may still fire. The span is closed in `onConfig` when\n // the next iteration starts, or in `onFinish` / `onError` / `onAbort`.\n })\n return undefined\n },\n\n onUsage(ctx, usage) {\n safeCall('otel.onUsage', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // Always record the token histogram — metrics don't depend on having\n // an iteration span, and skipping here would drop metric data if an\n // adapter emits `onUsage` outside the iteration window.\n if (tokenHistogram) {\n const metricAttrs = {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n }\n tokenHistogram.record(usage.promptTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'input',\n })\n tokenHistogram.record(usage.completionTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'output',\n })\n }\n\n const span = state.currentIterationSpan ?? state.rootSpan\n span.setAttributes({\n 'gen_ai.usage.input_tokens': usage.promptTokens,\n 'gen_ai.usage.output_tokens': usage.completionTokens,\n })\n })\n },\n\n onBeforeToolCall(ctx, hookCtx) {\n safeCall('otel.onBeforeToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const parent = state.currentIterationSpan ?? state.rootSpan\n\n const info: OtelSpanInfo<'tool'> = {\n kind: 'tool',\n ctx,\n toolName: hookCtx.toolName,\n toolCallId: hookCtx.toolCallId,\n iteration: state.iterationCount - 1,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `execute_tool ${hookCtx.toolName}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.tool.name': hookCtx.toolName,\n 'gen_ai.tool.call.id': hookCtx.toolCallId,\n 'gen_ai.tool.type': 'function',\n }\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(otelContext.active(), parent)\n let toolSpan!: Span\n otelContext.with(parentCtx, () => {\n toolSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) toolSpan.setAttributes(enriched)\n\n // Stamp the tool args onto the tool span so backends that render an\n // input panel per span (e.g. PostHog) have something to show.\n if (captureContent) {\n const argsBody =\n typeof hookCtx.args === 'string'\n ? hookCtx.args\n : (safeCall('otel.serializeToolArgs', () =>\n JSON.stringify(hookCtx.args ?? null),\n ) ?? '[unserializable_tool_args]')\n const redactedArgs = redactContent(argsBody)\n const toolInputJson = JSON.stringify([\n { role: 'tool', content: redactedArgs },\n ])\n toolSpan.setAttribute('gen_ai.input.messages', toolInputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.input', toolInputJson)\n }\n\n state.toolSpans.set(hookCtx.toolCallId, {\n span: toolSpan,\n toolName: hookCtx.toolName,\n })\n })\n return undefined\n },\n\n onAfterToolCall(ctx, info) {\n safeCall('otel.onAfterToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const entry = state.toolSpans.get(info.toolCallId)\n if (!entry) return\n const { span: toolSpan } = entry\n\n const outcome = info.ok ? 'success' : 'error'\n toolSpan.setAttribute('tanstack.ai.tool.outcome', outcome)\n\n if (!info.ok && info.error !== undefined) {\n toolSpan.recordException(info.error as Exception)\n const msg = errorMessage(info.error)\n toolSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...(msg !== undefined && { message: msg }),\n })\n }\n\n if (captureContent) {\n // Serialization can throw on circular refs or `BigInt` values. If it\n // does, fall back to a sentinel so the rest of this handler (span\n // end, onSpanEnd, toolSpans cleanup) still runs — otherwise the tool\n // span would dangle until the onFinish/onError sweep.\n const body =\n typeof info.result === 'string'\n ? info.result\n : (safeCall('otel.serializeToolResult', () =>\n JSON.stringify(info.result ?? null),\n ) ?? '[unserializable_tool_result]')\n const redactedBody = redactContent(body)\n if (state.currentIterationSpan) {\n state.currentIterationSpan.addEvent('gen_ai.tool.message', {\n content: redactedBody,\n tool_call_id: info.toolCallId,\n })\n }\n // Output panel of the tool span itself — `gen_ai.output.messages` is\n // what current GenAI semconv consumers (e.g. PostHog) read.\n const toolOutputJson = JSON.stringify([\n { role: 'tool', content: redactedBody },\n ])\n toolSpan.setAttribute('gen_ai.output.messages', toolOutputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.output', toolOutputJson)\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolName: info.toolName,\n toolCallId: info.toolCallId,\n iteration: state.iterationCount - 1,\n } as OtelSpanInfo<'tool'>,\n toolSpan,\n ),\n )\n toolSpan.end()\n state.toolSpans.delete(info.toolCallId)\n })\n },\n\n onError(ctx, info) {\n safeCall('otel.onError', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n const errType = errorTypeName(info.error)\n const message = errorMessage(info.error)\n const statusMessage =\n message !== undefined ? { message } : ({} as const)\n const exception = info.error as Exception\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n iterationSpan.recordException(exception)\n iterationSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx,\n iteration: state.iterationCount - 1,\n } as OtelSpanInfo<'iteration'>,\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.recordException(exception)\n span.setStatus({ code: SpanStatusCode.ERROR, ...statusMessage })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n } as OtelSpanInfo<'tool'>,\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n state.rootSpan.recordException(exception)\n state.rootSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'error.type': errType,\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n\n onAbort(ctx, info) {\n safeCall('otel.onAbort', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n const closeCancelled = (span: Span): void => {\n // `gen_ai.completion.reason` is not part of the GenAI semconv; use a\n // TanStack-namespaced attribute so downstream exporters don't treat\n // it as standard. The span status still carries the error code.\n span.setAttribute('tanstack.ai.completion.reason', 'cancelled')\n span.setStatus({ code: SpanStatusCode.ERROR, message: 'cancelled' })\n }\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n closeCancelled(iterationSpan)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx,\n iteration: state.iterationCount - 1,\n } as OtelSpanInfo<'iteration'>,\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n closeCancelled(span)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n } as OtelSpanInfo<'tool'>,\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n closeCancelled(state.rootSpan)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'error.type': 'cancelled',\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n\n onFinish(ctx, info) {\n safeCall('otel.onFinish', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // Close any tool spans that never received `onAfterToolCall` (adapter\n // quirk). Done before the iteration span so the hierarchy is closed\n // in depth-first order.\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.setAttribute('tanstack.ai.tool.outcome', 'unknown')\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n } as OtelSpanInfo<'tool'>,\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n // The final iteration's span is still open because we keep it open\n // through tool execution and `onUsage`. Close it now.\n closeIterationSpan(state, ctx)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n })\n }\n\n if (info.usage) {\n state.rootSpan.setAttributes({\n 'gen_ai.usage.input_tokens': info.usage.promptTokens,\n 'gen_ai.usage.output_tokens': info.usage.completionTokens,\n })\n }\n if (info.finishReason) {\n state.rootSpan.setAttribute('gen_ai.response.finish_reasons', [\n info.finishReason,\n ])\n }\n state.rootSpan.setAttribute(\n 'tanstack.ai.iterations',\n state.iterationCount,\n )\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n }\n}\n"],"names":["otelTrace","otelContext"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"otel.js","sources":["../../../src/middlewares/otel.ts"],"sourcesContent":["import {\n SpanKind,\n SpanStatusCode,\n context as otelContext,\n trace as otelTrace,\n} from '@opentelemetry/api'\nimport type {\n AttributeValue,\n Exception,\n Meter,\n Span,\n SpanOptions,\n Tracer,\n} from '@opentelemetry/api'\nimport type {\n ChatMiddleware,\n ChatMiddlewareContext,\n} from '../activities/chat/middleware/types'\n\n/**\n * Scope (role) of an OTel span emitted by this middleware.\n *\n * - `chat` — the root span for a single `chat()` call\n * - `iteration` — one per agent-loop iteration (one model call)\n * - `tool` — one per tool execution inside an iteration\n */\nexport type OtelSpanScope = 'chat' | 'iteration' | 'tool'\n\n/**\n * Alias retained for backwards compatibility. Prefer {@link OtelSpanScope}.\n *\n * @deprecated Use `OtelSpanScope` instead — the name shadows OTel's built-in\n * `SpanKind` which is also imported by integrations of this middleware.\n */\nexport type OtelSpanKind = OtelSpanScope\n\n/**\n * Span metadata passed to `spanNameFormatter`, `attributeEnricher`,\n * `onBeforeSpanStart`, and `onSpanEnd`. Discriminated by `kind` so that\n * tool-only fields narrow automatically inside callback bodies.\n */\nexport type OtelSpanInfo<TScope extends OtelSpanScope = OtelSpanScope> =\n TScope extends 'chat'\n ? { kind: 'chat'; ctx: ChatMiddlewareContext }\n : TScope extends 'iteration'\n ? { kind: 'iteration'; ctx: ChatMiddlewareContext; iteration: number }\n : TScope extends 'tool'\n ? {\n kind: 'tool'\n ctx: ChatMiddlewareContext\n iteration: number\n toolName: string\n toolCallId: string\n }\n : never\n\nexport interface OtelMiddlewareOptions {\n /** OTel `Tracer` used to start root, iteration, and tool spans. */\n tracer: Tracer\n /**\n * Optional OTel `Meter`. When provided, the middleware records\n * `gen_ai.client.operation.duration` and `gen_ai.client.token.usage`\n * histograms. Omit to disable metrics without disabling tracing.\n */\n meter?: Meter\n /**\n * When `true`, prompt and completion content is attached to iteration spans\n * as `gen_ai.*.message` / `gen_ai.choice` events. Defaults to `false` so\n * that PII never lands on a span by accident.\n */\n captureContent?: boolean\n /**\n * Invoked on every captured content string before it lands on a span.\n * Return a redacted version. If this function throws, the middleware emits\n * the literal sentinel `\"[redaction_failed]\"` instead of the original text\n * — it never falls back to raw content.\n */\n redact?: (text: string) => string\n /**\n * Maximum characters kept in the per-iteration assistant text buffer used\n * to emit `gen_ai.choice` events. Extra characters are truncated with a\n * trailing `\"…\"` marker. Defaults to 100 000. Set to `0` to disable the\n * cap. Exporters typically truncate long attribute values anyway.\n */\n maxContentLength?: number\n /** Override the default span name for each `kind`. */\n spanNameFormatter?: (info: OtelSpanInfo) => string\n /** Add extra attributes to each span. */\n attributeEnricher?: (info: OtelSpanInfo) => Record<string, AttributeValue>\n /** Mutate `SpanOptions` immediately before `tracer.startSpan(...)`. */\n onBeforeSpanStart?: (info: OtelSpanInfo, options: SpanOptions) => SpanOptions\n /** Fires just before every `span.end()`. */\n onSpanEnd?: (info: OtelSpanInfo, span: Span) => void\n}\n\ninterface RequestState {\n rootSpan: Span\n currentIterationSpan: Span | null\n toolSpans: Map<string, { span: Span; toolName: string }>\n iterationCount: number\n assistantTextBuffer: string\n assistantTextBufferTruncated: boolean\n startTime: number\n}\n\nconst stateByCtx = new WeakMap<ChatMiddlewareContext, RequestState>()\n\nconst DEFAULT_MAX_CONTENT_LENGTH = 100_000\nconst REDACTION_FAILED_SENTINEL = '[redaction_failed]'\n\nfunction serializeContent(content: unknown): string {\n if (typeof content === 'string') return content\n if (!Array.isArray(content)) return ''\n const parts: Array<string> = []\n for (const part of content) {\n if (!part || typeof part !== 'object') continue\n const type = (part as { type?: string }).type\n switch (type) {\n case 'text':\n parts.push(\n (\n (part as { text?: string }).text ??\n (part as { content?: string }).content ??\n ''\n ).toString(),\n )\n break\n case 'image':\n parts.push('[image]')\n break\n case 'audio':\n parts.push('[audio]')\n break\n case 'video':\n parts.push('[video]')\n break\n case 'document':\n parts.push('[document]')\n break\n case undefined:\n parts.push('[unknown]')\n break\n default:\n parts.push(`[${type}]`)\n }\n }\n return parts.join(' ')\n}\n\nfunction messageEventName(role: string): string {\n switch (role) {\n case 'user':\n return 'gen_ai.user.message'\n case 'assistant':\n return 'gen_ai.assistant.message'\n case 'tool':\n return 'gen_ai.tool.message'\n case 'system':\n return 'gen_ai.system.message'\n default:\n return `gen_ai.${role}.message`\n }\n}\n\nfunction errorMessage(err: unknown): string | undefined {\n if (err instanceof Error) return err.message\n if (typeof err === 'string') return err\n if (err && typeof err === 'object' && 'message' in err) {\n const m = (err as { message?: unknown }).message\n if (typeof m === 'string') return m\n }\n return undefined\n}\n\nfunction errorTypeName(err: unknown): string {\n if (err instanceof Error) return err.name || 'Error'\n if (err && typeof err === 'object' && 'name' in err) {\n const n = (err as { name?: unknown }).name\n if (typeof n === 'string') return n\n }\n return 'Error'\n}\n\nfunction safeCall<T>(label: string, fn: () => T): T | undefined {\n try {\n return fn()\n } catch (err) {\n // Keep middleware non-fatal, but surface callback failures so that broken\n // extension points (attributeEnricher, spanNameFormatter, onSpanEnd, ...)\n // are observable. Matches the guarantee documented in docs/advanced/otel.md.\n console.warn(`[otelMiddleware] ${label} failed`, err)\n return undefined\n }\n}\n\nexport function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {\n const {\n tracer,\n meter,\n captureContent = false,\n redact = (s) => s,\n maxContentLength = DEFAULT_MAX_CONTENT_LENGTH,\n spanNameFormatter,\n attributeEnricher,\n onBeforeSpanStart,\n onSpanEnd,\n } = options\n\n const durationHistogram = meter?.createHistogram(\n 'gen_ai.client.operation.duration',\n {\n description: 'GenAI client operation duration',\n unit: 's',\n },\n )\n const tokenHistogram = meter?.createHistogram('gen_ai.client.token.usage', {\n description: 'GenAI client token usage',\n unit: '{token}',\n })\n\n // Redact user content, failing closed to a sentinel string instead of ever\n // letting raw text through. Callers that pass `captureContent: true` with a\n // third-party PII redactor depend on this invariant.\n const redactContent = (text: string): string => {\n try {\n return redact(text)\n } catch (err) {\n console.warn('[otelMiddleware] otel.redact failed', err)\n return REDACTION_FAILED_SENTINEL\n }\n }\n\n const appendAssistantText = (state: RequestState, delta: string): void => {\n if (maxContentLength > 0) {\n if (state.assistantTextBufferTruncated) return\n const remaining = maxContentLength - state.assistantTextBuffer.length\n if (remaining <= 0) {\n state.assistantTextBufferTruncated = true\n state.assistantTextBuffer += '…'\n return\n }\n if (delta.length > remaining) {\n state.assistantTextBuffer += delta.slice(0, remaining) + '…'\n state.assistantTextBufferTruncated = true\n return\n }\n }\n state.assistantTextBuffer += delta\n }\n\n const closeIterationSpan = (\n state: RequestState,\n ctx: ChatMiddlewareContext,\n ): void => {\n if (!state.currentIterationSpan) return\n const span = state.currentIterationSpan\n const iteration = state.iterationCount - 1\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'iteration', ctx, iteration }, span),\n )\n span.end()\n state.currentIterationSpan = null\n }\n\n return {\n name: 'otel',\n\n onStart(ctx) {\n safeCall('otel.onStart', () => {\n const info: OtelSpanInfo<'chat'> = { kind: 'chat', ctx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.request.model': ctx.model,\n // NOTE: `gen_ai.operation.name` is deliberately NOT set on the\n // root span. The root represents a `chat()` invocation that may\n // span multiple model calls; only iteration spans correspond to\n // a single chat operation. Backends that map `operation.name=chat`\n // to a \"generation\" event (e.g. PostHog LLM Analytics) would\n // otherwise emit a duplicate generation for the wrapper span.\n },\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n const rootSpan = tracer.startSpan(name, spanOptions)\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) rootSpan.setAttributes(enriched)\n\n stateByCtx.set(ctx, {\n rootSpan,\n currentIterationSpan: null,\n toolSpans: new Map(),\n iterationCount: 0,\n assistantTextBuffer: '',\n assistantTextBufferTruncated: false,\n startTime: Date.now(),\n })\n })\n },\n\n onConfig(ctx, config) {\n if (ctx.phase !== 'beforeModel') return\n safeCall('otel.onConfig', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // The previous iteration's span stays open through tool execution and\n // onUsage so that tool spans nest under it and token attributes land\n // on it. Close it here, just before opening the next iteration.\n closeIterationSpan(state, ctx)\n\n const info: OtelSpanInfo<'iteration'> = {\n kind: 'iteration',\n ctx,\n iteration: ctx.iteration,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model} #${ctx.iteration}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'tanstack.ai.iteration': ctx.iteration,\n }\n if (config.temperature !== undefined)\n baseAttrs['gen_ai.request.temperature'] = config.temperature\n if (config.topP !== undefined)\n baseAttrs['gen_ai.request.top_p'] = config.topP\n if (config.maxTokens !== undefined)\n baseAttrs['gen_ai.request.max_tokens'] = config.maxTokens\n\n const baseOptions: SpanOptions = {\n kind: SpanKind.CLIENT,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(\n otelContext.active(),\n state.rootSpan,\n )\n let iterSpan!: Span\n otelContext.with(parentCtx, () => {\n // Pass the parent context explicitly as the 3rd arg — this is a\n // real-OTel-compatible way to ensure the span is parented to\n // `rootSpan` even when the host app has not registered a context\n // manager (e.g. in tests or minimal setups).\n iterSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) iterSpan.setAttributes(enriched)\n\n state.currentIterationSpan = iterSpan\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n\n if (captureContent) {\n const systemPromptContents = config.systemPrompts.map((p) =>\n typeof p === 'string' ? p : p.content,\n )\n // Anthropic prompt-caching users need to know which prompt carried\n // `cache_control`: it's the one attribute that explains cache\n // hit/miss in observability. Serialise per-prompt metadata as a\n // single JSON span attribute so backends that don't understand\n // GenAI events can still surface it. Kept off span events to\n // avoid breaking the one-event-per-message GenAI semconv contract.\n const systemPromptMetadata = config.systemPrompts.map((p) =>\n typeof p === 'string' || p.metadata === undefined\n ? null\n : p.metadata,\n )\n if (systemPromptMetadata.some((m) => m !== null)) {\n iterSpan.setAttribute(\n 'tanstack.ai.system_prompt.metadata',\n JSON.stringify(systemPromptMetadata),\n )\n }\n // Span events follow the original GenAI semconv (one event per\n // message). Backends that read events get content this way.\n for (const sys of systemPromptContents) {\n iterSpan.addEvent('gen_ai.system.message', {\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n iterSpan.addEvent(messageEventName(m.role), {\n content: redactContent(body),\n })\n }\n\n // Also emit the current GenAI-semconv attribute form\n // (`gen_ai.input.messages`) — backends like PostHog read prompt\n // content from this attribute, not from span events.\n const inputMessages: Array<{ role: string; content: string }> = []\n for (const sys of systemPromptContents) {\n inputMessages.push({\n role: 'system',\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n inputMessages.push({\n role: m.role,\n content: redactContent(body),\n })\n }\n if (inputMessages.length > 0) {\n const inputJson = JSON.stringify(inputMessages)\n // Current OTel GenAI semconv — Sentry / PostHog / Datadog read\n // prompt content from this attribute.\n iterSpan.setAttribute('gen_ai.input.messages', inputJson)\n // Langfuse-native attribute. Highest priority in Langfuse's OTLP\n // ingestion (checked before events and gen_ai.input.messages) so\n // the Input panel populates reliably. Harmless to other backends —\n // the attribute is namespaced and unrecognised keys are ignored.\n iterSpan.setAttribute('langfuse.observation.input', inputJson)\n\n // Mirror the first iteration's input onto the root span and at\n // trace level so Langfuse fills Input on the trace card and the\n // chat-level observation. Later iterations append tool-call /\n // assistant messages that are useful per-iteration but noise at\n // the chat / trace level.\n if (state.iterationCount === 0) {\n state.rootSpan.setAttribute(\n 'langfuse.observation.input',\n inputJson,\n )\n state.rootSpan.setAttribute('langfuse.trace.input', inputJson)\n }\n }\n }\n\n state.iterationCount += 1\n })\n return undefined\n },\n\n onChunk(ctx, chunk) {\n safeCall('otel.onChunk', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n if (captureContent && chunk.type === 'TEXT_MESSAGE_CONTENT') {\n appendAssistantText(state, chunk.delta)\n }\n\n if (chunk.type !== 'RUN_FINISHED') return\n const span = state.currentIterationSpan\n if (!span) return\n\n if (chunk.finishReason) {\n span.setAttribute('gen_ai.response.finish_reasons', [\n chunk.finishReason,\n ])\n }\n if (chunk.model) span.setAttribute('gen_ai.response.model', chunk.model)\n\n // Set usage attributes on the iteration span directly from the chunk\n // so they're available before `onUsage` fires. Histogram recording is\n // deliberately NOT done here — the chat runner always invokes\n // `runOnUsage` when `chunk.usage` is present, and `onUsage` is the\n // canonical place for the metric. Recording in both would double-count.\n if (chunk.usage) {\n span.setAttributes({\n 'gen_ai.usage.input_tokens': chunk.usage.promptTokens,\n 'gen_ai.usage.output_tokens': chunk.usage.completionTokens,\n })\n }\n\n if (captureContent && state.assistantTextBuffer.length > 0) {\n const completion = redactContent(state.assistantTextBuffer)\n const outputJson = JSON.stringify([\n { role: 'assistant', content: completion },\n ])\n // Event form (older semconv) — kept for backends that consume it.\n span.addEvent('gen_ai.choice', { content: completion })\n // Attribute form (current semconv) — required by backends like\n // PostHog that read completion content from `gen_ai.output.messages`.\n span.setAttribute('gen_ai.output.messages', outputJson)\n // Langfuse-native attribute (highest priority in Langfuse mapping).\n span.setAttribute('langfuse.observation.output', outputJson)\n // Mirror to the root span and trace card. Each iteration overwrites,\n // so the final iteration's completion lands on the root — which is\n // the final answer the user saw, not an intermediate tool-call turn.\n state.rootSpan.setAttribute('langfuse.observation.output', outputJson)\n state.rootSpan.setAttribute('langfuse.trace.output', outputJson)\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n }\n\n // Intentionally leave the iteration span open: tool spans started\n // after `RUN_FINISHED` (tool_calls finishReason) must nest under it,\n // and `onUsage` may still fire. The span is closed in `onConfig` when\n // the next iteration starts, or in `onFinish` / `onError` / `onAbort`.\n })\n return undefined\n },\n\n onUsage(ctx, usage) {\n safeCall('otel.onUsage', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // Always record the token histogram — metrics don't depend on having\n // an iteration span, and skipping here would drop metric data if an\n // adapter emits `onUsage` outside the iteration window.\n if (tokenHistogram) {\n const metricAttrs = {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n }\n tokenHistogram.record(usage.promptTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'input',\n })\n tokenHistogram.record(usage.completionTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'output',\n })\n }\n\n const span = state.currentIterationSpan ?? state.rootSpan\n span.setAttributes({\n 'gen_ai.usage.input_tokens': usage.promptTokens,\n 'gen_ai.usage.output_tokens': usage.completionTokens,\n })\n })\n },\n\n onBeforeToolCall(ctx, hookCtx) {\n safeCall('otel.onBeforeToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const parent = state.currentIterationSpan ?? state.rootSpan\n\n const info: OtelSpanInfo<'tool'> = {\n kind: 'tool',\n ctx,\n toolName: hookCtx.toolName,\n toolCallId: hookCtx.toolCallId,\n iteration: state.iterationCount - 1,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `execute_tool ${hookCtx.toolName}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.tool.name': hookCtx.toolName,\n 'gen_ai.tool.call.id': hookCtx.toolCallId,\n 'gen_ai.tool.type': 'function',\n }\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(otelContext.active(), parent)\n let toolSpan!: Span\n otelContext.with(parentCtx, () => {\n toolSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) toolSpan.setAttributes(enriched)\n\n // Stamp the tool args onto the tool span so backends that render an\n // input panel per span (e.g. PostHog) have something to show.\n if (captureContent) {\n const argsBody =\n typeof hookCtx.args === 'string'\n ? hookCtx.args\n : (safeCall('otel.serializeToolArgs', () =>\n JSON.stringify(hookCtx.args ?? null),\n ) ?? '[unserializable_tool_args]')\n const redactedArgs = redactContent(argsBody)\n const toolInputJson = JSON.stringify([\n { role: 'tool', content: redactedArgs },\n ])\n toolSpan.setAttribute('gen_ai.input.messages', toolInputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.input', toolInputJson)\n }\n\n state.toolSpans.set(hookCtx.toolCallId, {\n span: toolSpan,\n toolName: hookCtx.toolName,\n })\n })\n return undefined\n },\n\n onAfterToolCall(ctx, info) {\n safeCall('otel.onAfterToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const entry = state.toolSpans.get(info.toolCallId)\n if (!entry) return\n const { span: toolSpan } = entry\n\n const outcome = info.ok ? 'success' : 'error'\n toolSpan.setAttribute('tanstack.ai.tool.outcome', outcome)\n\n if (!info.ok && info.error !== undefined) {\n toolSpan.recordException(info.error as Exception)\n const msg = errorMessage(info.error)\n toolSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...(msg !== undefined && { message: msg }),\n })\n }\n\n if (captureContent) {\n // Serialization can throw on circular refs or `BigInt` values. If it\n // does, fall back to a sentinel so the rest of this handler (span\n // end, onSpanEnd, toolSpans cleanup) still runs — otherwise the tool\n // span would dangle until the onFinish/onError sweep.\n const body =\n typeof info.result === 'string'\n ? info.result\n : (safeCall('otel.serializeToolResult', () =>\n JSON.stringify(info.result ?? null),\n ) ?? '[unserializable_tool_result]')\n const redactedBody = redactContent(body)\n if (state.currentIterationSpan) {\n state.currentIterationSpan.addEvent('gen_ai.tool.message', {\n content: redactedBody,\n tool_call_id: info.toolCallId,\n })\n }\n // Output panel of the tool span itself — `gen_ai.output.messages` is\n // what current GenAI semconv consumers (e.g. PostHog) read.\n const toolOutputJson = JSON.stringify([\n { role: 'tool', content: redactedBody },\n ])\n toolSpan.setAttribute('gen_ai.output.messages', toolOutputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.output', toolOutputJson)\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolName: info.toolName,\n toolCallId: info.toolCallId,\n iteration: state.iterationCount - 1,\n },\n toolSpan,\n ),\n )\n toolSpan.end()\n state.toolSpans.delete(info.toolCallId)\n })\n },\n\n onError(ctx, info) {\n safeCall('otel.onError', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n const errType = errorTypeName(info.error)\n const message = errorMessage(info.error)\n const statusMessage =\n message !== undefined ? { message } : ({} as const)\n const exception = info.error as Exception\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n iterationSpan.recordException(exception)\n iterationSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.recordException(exception)\n span.setStatus({ code: SpanStatusCode.ERROR, ...statusMessage })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n state.rootSpan.recordException(exception)\n state.rootSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'error.type': errType,\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n\n onAbort(ctx, info) {\n safeCall('otel.onAbort', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n const closeCancelled = (span: Span): void => {\n // `gen_ai.completion.reason` is not part of the GenAI semconv; use a\n // TanStack-namespaced attribute so downstream exporters don't treat\n // it as standard. The span status still carries the error code.\n span.setAttribute('tanstack.ai.completion.reason', 'cancelled')\n span.setStatus({ code: SpanStatusCode.ERROR, message: 'cancelled' })\n }\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n closeCancelled(iterationSpan)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n closeCancelled(span)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n closeCancelled(state.rootSpan)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'error.type': 'cancelled',\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n\n onFinish(ctx, info) {\n safeCall('otel.onFinish', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // Close any tool spans that never received `onAfterToolCall` (adapter\n // quirk). Done before the iteration span so the hierarchy is closed\n // in depth-first order.\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.setAttribute('tanstack.ai.tool.outcome', 'unknown')\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n // The final iteration's span is still open because we keep it open\n // through tool execution and `onUsage`. Close it now.\n closeIterationSpan(state, ctx)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n })\n }\n\n if (info.usage) {\n state.rootSpan.setAttributes({\n 'gen_ai.usage.input_tokens': info.usage.promptTokens,\n 'gen_ai.usage.output_tokens': info.usage.completionTokens,\n })\n }\n if (info.finishReason) {\n state.rootSpan.setAttribute('gen_ai.response.finish_reasons', [\n info.finishReason,\n ])\n }\n state.rootSpan.setAttribute(\n 'tanstack.ai.iterations',\n state.iterationCount,\n )\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n }\n}\n"],"names":["otelTrace","otelContext"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"strip-to-spec-middleware.js","sources":["../../src/strip-to-spec-middleware.ts"],"sourcesContent":["import type { ChatMiddleware } from './activities/chat/middleware/types'\nimport type { StreamChunk } from './types'\n\n/**\n * Strip only the deprecated nested `error` object from RUN_ERROR events.\n * The flat `message`/`code` fields are the spec-compliant form.\n *\n * All other fields pass through unchanged. @ag-ui/core's BaseEventSchema\n * uses `.passthrough()`, so extra fields (model, content, usage,\n * finishReason, toolName, stepId, etc.) are allowed and won't break\n * spec validation or verifyEvents.\n */\nexport function stripToSpec(chunk: StreamChunk): StreamChunk {\n // Only strip the deprecated nested error object from RUN_ERROR\n if (
|
|
1
|
+
{"version":3,"file":"strip-to-spec-middleware.js","sources":["../../src/strip-to-spec-middleware.ts"],"sourcesContent":["import type { ChatMiddleware } from './activities/chat/middleware/types'\nimport type { StreamChunk } from './types'\n\n/**\n * Strip only the deprecated nested `error` object from RUN_ERROR events.\n * The flat `message`/`code` fields are the spec-compliant form.\n *\n * All other fields pass through unchanged. @ag-ui/core's BaseEventSchema\n * uses `.passthrough()`, so extra fields (model, content, usage,\n * finishReason, toolName, stepId, etc.) are allowed and won't break\n * spec validation or verifyEvents.\n */\nexport function stripToSpec(chunk: StreamChunk): StreamChunk {\n // Only strip the deprecated nested error object from RUN_ERROR\n if (chunk.type === 'RUN_ERROR' && 'error' in chunk) {\n const { error: _deprecated, ...rest } = chunk as Record<string, unknown>\n return rest as StreamChunk\n }\n return chunk\n}\n\n/**\n * Middleware that ensures events are AG-UI spec compliant.\n * Currently only strips the deprecated nested `error` object from RUN_ERROR.\n * All other fields pass through unchanged (passthrough allowed by spec).\n */\nexport function stripToSpecMiddleware(): ChatMiddleware {\n return {\n name: 'strip-to-spec',\n onChunk(_ctx, chunk) {\n return stripToSpec(chunk)\n },\n }\n}\n"],"names":[],"mappings":"AAYO,SAAS,YAAY,OAAiC;AAE3D,MAAI,MAAM,SAAS,eAAe,WAAW,OAAO;AAClD,UAAM,EAAE,OAAO,aAAa,GAAG,SAAS;AACxC,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAOO,SAAS,wBAAwC;AACtD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ,MAAM,OAAO;AACnB,aAAO,YAAY,KAAK;AAAA,IAC1B;AAAA,EAAA;AAEJ;"}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
function uiMessagesToWire(messages) {
|
|
2
2
|
const wire = [];
|
|
3
3
|
for (const msg of messages) {
|
|
4
|
-
const parts =
|
|
4
|
+
const parts = (
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- runtime input may be ModelMessage-shaped (no `parts`); cast forces the optional-chain fallback below to remain in scope
|
|
6
|
+
msg.parts ?? []
|
|
7
|
+
);
|
|
5
8
|
if (msg.role === "system") {
|
|
6
9
|
wire.push({
|
|
7
10
|
...msg,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ag-ui-wire.js","sources":["../../../src/utilities/ag-ui-wire.ts"],"sourcesContent":["import type { ContentPart, MessagePart, UIMessage } from '../types'\n\ntype AGUITextInputContent = { type: 'text'; text: string }\ntype AGUIInputContent =\n | AGUITextInputContent\n | (ContentPart & { type: 'image' | 'audio' | 'video' | 'document' })\n\ntype AGUIToolCallMirror = {\n id: string\n type: 'function'\n function: { name: string; arguments: string }\n}\n\ntype AGUIToolMessage = {\n role: 'tool'\n id: string\n toolCallId: string\n content: string\n error?: string\n}\n\ntype AGUIReasoningMessage = {\n role: 'reasoning'\n id: string\n content: string\n}\n\ntype WireAnchorMessage = UIMessage & {\n content?: string | Array<AGUIInputContent>\n toolCalls?: Array<AGUIToolCallMirror>\n}\n\nexport type WireMessage =\n | WireAnchorMessage\n | AGUIToolMessage\n | AGUIReasoningMessage\n\n/**\n * Serialize TanStack `UIMessage`s into the AG-UI `RunAgentInput.messages`\n * wire shape. Each anchor (system/user/assistant) carries the canonical\n * `parts` array verbatim plus AG-UI mirror fields (`content`, `toolCalls`)\n * so AG-UI Zod parsing succeeds. Tool results and thinking parts on\n * assistant messages are additionally emitted as fan-out\n * `{role:'tool',...}` and `{role:'reasoning',...}` entries for strict\n * AG-UI server consumers.\n */\nexport function uiMessagesToWire(\n messages: Array<UIMessage>,\n): Array<WireMessage> {\n const wire: Array<WireMessage> = []\n\n for (const msg of messages) {\n // Defensive: if parts is missing (ModelMessage-shaped input), pass through as-is.\n // UIMessage always has parts; ModelMessage uses content directly.\n const parts: ReadonlyArray<MessagePart> =\n (msg.parts as ReadonlyArray<MessagePart> | undefined) ?? []\n\n if (msg.role === 'system') {\n wire.push({\n ...msg,\n content:\n parts.length > 0\n ? collectText(parts)\n : ((msg as { content?: string }).content ?? ''),\n })\n continue\n }\n\n if (msg.role === 'user') {\n wire.push({\n ...msg,\n content:\n parts.length > 0\n ? collectUserContent(parts)\n : ((msg as { content?: string }).content ?? ''),\n })\n continue\n }\n\n // assistant: emit reasoning fan-outs first, then anchor, then tool fan-outs\n for (const part of parts) {\n if (part.type === 'thinking') {\n wire.push({\n role: 'reasoning',\n id: deriveReasoningId(msg.id, part),\n content: part.content,\n })\n }\n }\n\n const text = collectText(parts)\n const toolCalls = collectToolCalls(parts)\n wire.push({\n ...msg,\n ...(text !== '' && { content: text }),\n ...(toolCalls && { toolCalls }),\n })\n\n for (const part of parts) {\n if (part.type === 'tool-result') {\n wire.push({\n role: 'tool',\n id: deriveToolMessageId(part.toolCallId),\n toolCallId: part.toolCallId,\n content: part.content,\n ...(part.error !== undefined && { error: part.error }),\n })\n }\n }\n }\n\n return wire\n}\n\nfunction collectText(parts: ReadonlyArray<MessagePart>): string {\n // The streamed JSON of a completed structured-output part is the source of\n // truth for multi-turn coherence — emitting it back as assistant content\n // lets the LLM see its own prior structured response. Streaming/errored\n // parts are skipped: they'd ship malformed JSON fragments and confuse the\n // model. `completeStructuredOutputPart` tries hard to populate `raw`\n // (caller → existing buffer → `JSON.stringify(data)`), but the stringify\n // fallback can leave it empty when `data` is unserializable (BigInt,\n // circular). The `p.raw !== ''` guard below is what enforces \"no malformed\n // round-trip\" in that case — without it we'd ship `''` and the model would\n // see an empty assistant turn.\n const out: Array<string> = []\n for (const p of parts) {\n if (p.type === 'text') {\n out.push(p.content)\n } else if (\n p.type === 'structured-output' &&\n p.status === 'complete' &&\n p.raw !== ''\n ) {\n out.push(p.raw)\n }\n }\n return out.join('')\n}\n\nfunction collectUserContent(\n parts: ReadonlyArray<MessagePart>,\n): string | Array<AGUIInputContent> {\n const hasMultimodal = parts.some(\n (p) =>\n p.type === 'image' ||\n p.type === 'audio' ||\n p.type === 'video' ||\n p.type === 'document',\n )\n if (!hasMultimodal) {\n return collectText(parts)\n }\n const out: Array<AGUIInputContent> = []\n for (const p of parts) {\n if (p.type === 'text') {\n out.push({ type: 'text', text: p.content })\n } else if (\n p.type === 'image' ||\n p.type === 'audio' ||\n p.type === 'video' ||\n p.type === 'document'\n ) {\n out.push(p
|
|
1
|
+
{"version":3,"file":"ag-ui-wire.js","sources":["../../../src/utilities/ag-ui-wire.ts"],"sourcesContent":["import type { ContentPart, MessagePart, UIMessage } from '../types'\n\ntype AGUITextInputContent = { type: 'text'; text: string }\ntype AGUIInputContent =\n | AGUITextInputContent\n | (ContentPart & { type: 'image' | 'audio' | 'video' | 'document' })\n\ntype AGUIToolCallMirror = {\n id: string\n type: 'function'\n function: { name: string; arguments: string }\n}\n\ntype AGUIToolMessage = {\n role: 'tool'\n id: string\n toolCallId: string\n content: string\n error?: string\n}\n\ntype AGUIReasoningMessage = {\n role: 'reasoning'\n id: string\n content: string\n}\n\ntype WireAnchorMessage = UIMessage & {\n content?: string | Array<AGUIInputContent>\n toolCalls?: Array<AGUIToolCallMirror>\n}\n\nexport type WireMessage =\n | WireAnchorMessage\n | AGUIToolMessage\n | AGUIReasoningMessage\n\n/**\n * Serialize TanStack `UIMessage`s into the AG-UI `RunAgentInput.messages`\n * wire shape. Each anchor (system/user/assistant) carries the canonical\n * `parts` array verbatim plus AG-UI mirror fields (`content`, `toolCalls`)\n * so AG-UI Zod parsing succeeds. Tool results and thinking parts on\n * assistant messages are additionally emitted as fan-out\n * `{role:'tool',...}` and `{role:'reasoning',...}` entries for strict\n * AG-UI server consumers.\n */\nexport function uiMessagesToWire(\n messages: Array<UIMessage>,\n): Array<WireMessage> {\n const wire: Array<WireMessage> = []\n\n for (const msg of messages) {\n // Defensive: if parts is missing (ModelMessage-shaped input), pass through as-is.\n // UIMessage always has parts; ModelMessage uses content directly.\n const parts: ReadonlyArray<MessagePart> =\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- runtime input may be ModelMessage-shaped (no `parts`); cast forces the optional-chain fallback below to remain in scope\n (msg.parts as ReadonlyArray<MessagePart> | undefined) ?? []\n\n if (msg.role === 'system') {\n wire.push({\n ...msg,\n content:\n parts.length > 0\n ? collectText(parts)\n : ((msg as { content?: string }).content ?? ''),\n })\n continue\n }\n\n if (msg.role === 'user') {\n wire.push({\n ...msg,\n content:\n parts.length > 0\n ? collectUserContent(parts)\n : ((msg as { content?: string }).content ?? ''),\n })\n continue\n }\n\n // assistant: emit reasoning fan-outs first, then anchor, then tool fan-outs\n for (const part of parts) {\n if (part.type === 'thinking') {\n wire.push({\n role: 'reasoning',\n id: deriveReasoningId(msg.id, part),\n content: part.content,\n })\n }\n }\n\n const text = collectText(parts)\n const toolCalls = collectToolCalls(parts)\n wire.push({\n ...msg,\n ...(text !== '' && { content: text }),\n ...(toolCalls && { toolCalls }),\n })\n\n for (const part of parts) {\n if (part.type === 'tool-result') {\n wire.push({\n role: 'tool',\n id: deriveToolMessageId(part.toolCallId),\n toolCallId: part.toolCallId,\n content: part.content,\n ...(part.error !== undefined && { error: part.error }),\n })\n }\n }\n }\n\n return wire\n}\n\nfunction collectText(parts: ReadonlyArray<MessagePart>): string {\n // The streamed JSON of a completed structured-output part is the source of\n // truth for multi-turn coherence — emitting it back as assistant content\n // lets the LLM see its own prior structured response. Streaming/errored\n // parts are skipped: they'd ship malformed JSON fragments and confuse the\n // model. `completeStructuredOutputPart` tries hard to populate `raw`\n // (caller → existing buffer → `JSON.stringify(data)`), but the stringify\n // fallback can leave it empty when `data` is unserializable (BigInt,\n // circular). The `p.raw !== ''` guard below is what enforces \"no malformed\n // round-trip\" in that case — without it we'd ship `''` and the model would\n // see an empty assistant turn.\n const out: Array<string> = []\n for (const p of parts) {\n if (p.type === 'text') {\n out.push(p.content)\n } else if (\n p.type === 'structured-output' &&\n p.status === 'complete' &&\n p.raw !== ''\n ) {\n out.push(p.raw)\n }\n }\n return out.join('')\n}\n\nfunction collectUserContent(\n parts: ReadonlyArray<MessagePart>,\n): string | Array<AGUIInputContent> {\n const hasMultimodal = parts.some(\n (p) =>\n p.type === 'image' ||\n p.type === 'audio' ||\n p.type === 'video' ||\n p.type === 'document',\n )\n if (!hasMultimodal) {\n return collectText(parts)\n }\n const out: Array<AGUIInputContent> = []\n for (const p of parts) {\n if (p.type === 'text') {\n out.push({ type: 'text', text: p.content })\n } else if (\n p.type === 'image' ||\n p.type === 'audio' ||\n p.type === 'video' ||\n p.type === 'document'\n ) {\n out.push(p)\n }\n }\n return out\n}\n\nfunction collectToolCalls(\n parts: ReadonlyArray<MessagePart>,\n): Array<AGUIToolCallMirror> | undefined {\n const calls: Array<AGUIToolCallMirror> = []\n for (const p of parts) {\n if (p.type === 'tool-call') {\n calls.push({\n id: p.id,\n type: 'function',\n function: { name: p.name, arguments: p.arguments },\n })\n }\n }\n return calls.length > 0 ? calls : undefined\n}\n\nfunction deriveReasoningId(messageId: string, part: MessagePart): string {\n return `${messageId}-reasoning-${(part as { id?: string }).id ?? hashContent((part as { content: string }).content)}`\n}\n\nfunction deriveToolMessageId(toolCallId: string): string {\n return `tool-${toolCallId}`\n}\n\nfunction hashContent(s: string): string {\n // Cheap deterministic id suffix; collisions are tolerable since\n // reasoning ids only matter for AG-UI server consumers, not for our\n // own server's dedup logic (which keys on toolCallId, not reasoning id).\n let h = 0\n for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0\n return Math.abs(h).toString(36)\n}\n"],"names":[],"mappings":"AA8CO,SAAS,iBACd,UACoB;AACpB,QAAM,OAA2B,CAAA;AAEjC,aAAW,OAAO,UAAU;AAG1B,UAAM;AAAA;AAAA,MAEH,IAAI,SAAoD,CAAA;AAAA;AAE3D,QAAI,IAAI,SAAS,UAAU;AACzB,WAAK,KAAK;AAAA,QACR,GAAG;AAAA,QACH,SACE,MAAM,SAAS,IACX,YAAY,KAAK,IACf,IAA6B,WAAW;AAAA,MAAA,CACjD;AACD;AAAA,IACF;AAEA,QAAI,IAAI,SAAS,QAAQ;AACvB,WAAK,KAAK;AAAA,QACR,GAAG;AAAA,QACH,SACE,MAAM,SAAS,IACX,mBAAmB,KAAK,IACtB,IAA6B,WAAW;AAAA,MAAA,CACjD;AACD;AAAA,IACF;AAGA,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,SAAS,YAAY;AAC5B,aAAK,KAAK;AAAA,UACR,MAAM;AAAA,UACN,IAAI,kBAAkB,IAAI,IAAI,IAAI;AAAA,UAClC,SAAS,KAAK;AAAA,QAAA,CACf;AAAA,MACH;AAAA,IACF;AAEA,UAAM,OAAO,YAAY,KAAK;AAC9B,UAAM,YAAY,iBAAiB,KAAK;AACxC,SAAK,KAAK;AAAA,MACR,GAAG;AAAA,MACH,GAAI,SAAS,MAAM,EAAE,SAAS,KAAA;AAAA,MAC9B,GAAI,aAAa,EAAE,UAAA;AAAA,IAAU,CAC9B;AAED,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,SAAS,eAAe;AAC/B,aAAK,KAAK;AAAA,UACR,MAAM;AAAA,UACN,IAAI,oBAAoB,KAAK,UAAU;AAAA,UACvC,YAAY,KAAK;AAAA,UACjB,SAAS,KAAK;AAAA,UACd,GAAI,KAAK,UAAU,UAAa,EAAE,OAAO,KAAK,MAAA;AAAA,QAAM,CACrD;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,YAAY,OAA2C;AAW9D,QAAM,MAAqB,CAAA;AAC3B,aAAW,KAAK,OAAO;AACrB,QAAI,EAAE,SAAS,QAAQ;AACrB,UAAI,KAAK,EAAE,OAAO;AAAA,IACpB,WACE,EAAE,SAAS,uBACX,EAAE,WAAW,cACb,EAAE,QAAQ,IACV;AACA,UAAI,KAAK,EAAE,GAAG;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,KAAK,EAAE;AACpB;AAEA,SAAS,mBACP,OACkC;AAClC,QAAM,gBAAgB,MAAM;AAAA,IAC1B,CAAC,MACC,EAAE,SAAS,WACX,EAAE,SAAS,WACX,EAAE,SAAS,WACX,EAAE,SAAS;AAAA,EAAA;AAEf,MAAI,CAAC,eAAe;AAClB,WAAO,YAAY,KAAK;AAAA,EAC1B;AACA,QAAM,MAA+B,CAAA;AACrC,aAAW,KAAK,OAAO;AACrB,QAAI,EAAE,SAAS,QAAQ;AACrB,UAAI,KAAK,EAAE,MAAM,QAAQ,MAAM,EAAE,SAAS;AAAA,IAC5C,WACE,EAAE,SAAS,WACX,EAAE,SAAS,WACX,EAAE,SAAS,WACX,EAAE,SAAS,YACX;AACA,UAAI,KAAK,CAAC;AAAA,IACZ;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,iBACP,OACuC;AACvC,QAAM,QAAmC,CAAA;AACzC,aAAW,KAAK,OAAO;AACrB,QAAI,EAAE,SAAS,aAAa;AAC1B,YAAM,KAAK;AAAA,QACT,IAAI,EAAE;AAAA,QACN,MAAM;AAAA,QACN,UAAU,EAAE,MAAM,EAAE,MAAM,WAAW,EAAE,UAAA;AAAA,MAAU,CAClD;AAAA,IACH;AAAA,EACF;AACA,SAAO,MAAM,SAAS,IAAI,QAAQ;AACpC;AAEA,SAAS,kBAAkB,WAAmB,MAA2B;AACvE,SAAO,GAAG,SAAS,cAAe,KAAyB,MAAM,YAAa,KAA6B,OAAO,CAAC;AACrH;AAEA,SAAS,oBAAoB,YAA4B;AACvD,SAAO,QAAQ,UAAU;AAC3B;AAEA,SAAS,YAAY,GAAmB;AAItC,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAK,KAAK,IAAI,KAAK,EAAE,WAAW,CAAC,IAAK;AACpE,SAAO,KAAK,IAAI,CAAC,EAAE,SAAS,EAAE;AAChC;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "Core TanStack AI library - Open source AI SDK",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,8 +54,9 @@
|
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@ag-ui/core": "^0.0.52",
|
|
57
|
+
"@standard-schema/spec": "^1.1.0",
|
|
57
58
|
"partial-json": "^0.1.7",
|
|
58
|
-
"@tanstack/ai-event-client": "0.3.
|
|
59
|
+
"@tanstack/ai-event-client": "0.3.8"
|
|
59
60
|
},
|
|
60
61
|
"peerDependencies": {
|
|
61
62
|
"@opentelemetry/api": ">=1.9.0"
|
|
@@ -67,7 +68,6 @@
|
|
|
67
68
|
},
|
|
68
69
|
"devDependencies": {
|
|
69
70
|
"@opentelemetry/api": "^1.9.0",
|
|
70
|
-
"@standard-schema/spec": "^1.1.0",
|
|
71
71
|
"@vitest/coverage-v8": "4.0.14",
|
|
72
72
|
"zod": "^4.2.0"
|
|
73
73
|
},
|