@tanstack/ai 0.59.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/esm/activities/chat/adapter.d.ts +9 -0
- package/dist/esm/activities/chat/adapter.js +1 -0
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/agents/define-agent.d.ts +17 -5
- package/dist/esm/activities/chat/agents/define-agent.js.map +1 -1
- package/dist/esm/activities/chat/agents/spawn.d.ts +2 -0
- package/dist/esm/activities/chat/agents/spawn.js +8 -5
- package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
- package/dist/esm/activities/chat/index.js +289 -78
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +35 -19
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
- package/dist/esm/activities/chat/middleware/types.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
- package/dist/esm/activities/chat/stream/message-updaters.js +11 -3
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +11 -10
- package/dist/esm/activities/chat/stream/processor.js +51 -28
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +57 -16
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
- package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/activities/embed/adapter.d.ts +7 -0
- package/dist/esm/activities/embed/adapter.js +1 -0
- package/dist/esm/activities/embed/adapter.js.map +1 -1
- package/dist/esm/activities/embed/index.js +2 -0
- package/dist/esm/activities/embed/index.js.map +1 -1
- package/dist/esm/activities/evaluate/adapter.d.ts +4 -0
- package/dist/esm/activities/evaluate/adapter.js.map +1 -1
- package/dist/esm/activities/evaluate/index.d.ts +4 -0
- package/dist/esm/activities/evaluate/index.js +3 -1
- package/dist/esm/activities/evaluate/index.js.map +1 -1
- package/dist/esm/activities/files/adapter.d.ts +97 -0
- package/dist/esm/activities/files/adapter.js +45 -0
- package/dist/esm/activities/files/adapter.js.map +1 -0
- package/dist/esm/activities/files/index.d.ts +66 -0
- package/dist/esm/activities/files/index.js +78 -0
- package/dist/esm/activities/files/index.js.map +1 -0
- package/dist/esm/activities/generateImage/adapter.d.ts +8 -0
- package/dist/esm/activities/generateImage/adapter.js +1 -0
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.js +2 -0
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.d.ts +8 -0
- package/dist/esm/activities/generateVideo/adapter.js +1 -0
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +3 -0
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateWorld/adapter.d.ts +4 -2
- package/dist/esm/activities/generateWorld/adapter.js.map +1 -1
- package/dist/esm/activities/generateWorld/index.d.ts +4 -3
- package/dist/esm/activities/generateWorld/index.js +5 -4
- package/dist/esm/activities/generateWorld/index.js.map +1 -1
- package/dist/esm/activities/index.d.ts +6 -3
- package/dist/esm/activities/index.js +13 -11
- package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -0
- package/dist/esm/activities/summarize/chat-stream-summarize.js +8 -8
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/client.d.ts +3 -1
- package/dist/esm/client.js +2 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +4 -3
- package/dist/esm/index.js +5 -3
- package/dist/esm/interrupt-resume.js +29 -4
- package/dist/esm/interrupt-resume.js.map +1 -1
- package/dist/esm/middlewares/otel.d.ts +5 -2
- package/dist/esm/middlewares/otel.js +114 -0
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/types.d.ts +114 -14
- package/dist/esm/utilities/ag-ui-wire.js +39 -16
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/content-source.d.ts +60 -0
- package/dist/esm/utilities/content-source.js +85 -0
- package/dist/esm/utilities/content-source.js.map +1 -0
- package/dist/esm/utilities/provider-executed.d.ts +7 -0
- package/dist/esm/utilities/provider-executed.js +10 -1
- package/dist/esm/utilities/provider-executed.js.map +1 -1
- package/dist/esm/utilities/tool-result.d.ts +14 -3
- package/dist/esm/utilities/tool-result.js +26 -3
- package/dist/esm/utilities/tool-result.js.map +1 -1
- package/package.json +4 -4
- package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
- package/skills/ai-core/chat-experience/SKILL.md +134 -0
- package/skills/ai-core/media-generation/SKILL.md +8 -0
- package/skills/ai-core/tool-calling/SKILL.md +103 -0
- package/src/activities/chat/adapter.ts +10 -0
- package/src/activities/chat/agents/define-agent.ts +20 -3
- package/src/activities/chat/agents/spawn.ts +20 -12
- package/src/activities/chat/index.ts +458 -99
- package/src/activities/chat/messages.ts +52 -6
- package/src/activities/chat/middleware/types.ts +1 -0
- package/src/activities/chat/stream/message-updaters.ts +27 -2
- package/src/activities/chat/stream/processor.ts +81 -49
- package/src/activities/chat/tools/tool-calls.ts +104 -9
- package/src/activities/chat/tools/tool-definition.ts +8 -0
- package/src/activities/embed/adapter.ts +7 -0
- package/src/activities/embed/index.ts +5 -0
- package/src/activities/evaluate/adapter.ts +4 -0
- package/src/activities/evaluate/index.ts +6 -0
- package/src/activities/files/adapter.ts +120 -0
- package/src/activities/files/index.ts +113 -0
- package/src/activities/generateImage/adapter.ts +8 -0
- package/src/activities/generateImage/index.ts +4 -0
- package/src/activities/generateVideo/adapter.ts +8 -0
- package/src/activities/generateVideo/index.ts +7 -0
- package/src/activities/generateWorld/adapter.ts +4 -2
- package/src/activities/generateWorld/index.ts +7 -6
- package/src/activities/index.ts +25 -1
- package/src/activities/summarize/chat-stream-summarize.ts +22 -12
- package/src/client.ts +8 -0
- package/src/index.ts +17 -0
- package/src/interrupt-resume.ts +55 -4
- package/src/middlewares/otel.ts +161 -3
- package/src/types.ts +114 -14
- package/src/utilities/ag-ui-wire.ts +72 -17
- package/src/utilities/content-source.ts +138 -0
- package/src/utilities/provider-executed.ts +13 -0
- package/src/utilities/tool-result.ts +45 -3
|
@@ -27,6 +27,12 @@ export interface EmbeddingAdapter<TModel extends string = string, TProviderOptio
|
|
|
27
27
|
readonly kind: 'embedding';
|
|
28
28
|
/** Adapter name identifier */
|
|
29
29
|
readonly name: string;
|
|
30
|
+
/**
|
|
31
|
+
* Declares that this adapter can consume `{ type: 'file' }` content
|
|
32
|
+
* sources (provider Files API references). `embed()` rejects file sources
|
|
33
|
+
* in preflight for adapters that don't declare this.
|
|
34
|
+
*/
|
|
35
|
+
readonly supportsFileSources?: boolean;
|
|
30
36
|
/** The model this adapter is configured for */
|
|
31
37
|
readonly model: TModel;
|
|
32
38
|
/**
|
|
@@ -56,6 +62,7 @@ export type AnyEmbeddingAdapter = EmbeddingAdapter<any, any, any, any>;
|
|
|
56
62
|
export declare abstract class BaseEmbeddingAdapter<TModel extends string = string, TProviderOptions extends object = Record<string, unknown>, TModelProviderOptionsByName extends Record<string, any> = Record<string, any>, TModelInputModalitiesByName extends EmbeddingModelInputModalitiesByName = EmbeddingModelInputModalitiesByName> implements EmbeddingAdapter<TModel, TProviderOptions, TModelProviderOptionsByName, TModelInputModalitiesByName> {
|
|
57
63
|
readonly kind: "embedding";
|
|
58
64
|
abstract readonly name: string;
|
|
65
|
+
readonly supportsFileSources: boolean;
|
|
59
66
|
readonly model: TModel;
|
|
60
67
|
'~types': {
|
|
61
68
|
providerOptions: TProviderOptions;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.js","names":[],"sources":["../../../../src/activities/embed/adapter.ts"],"sourcesContent":["import type {\n EmbeddingModelInputModalitiesByName,\n EmbeddingOptions,\n EmbeddingResult,\n} from '../../types'\n\n/**\n * Configuration for embedding adapter instances\n */\nexport interface EmbeddingAdapterConfig {\n apiKey?: string\n baseUrl?: string\n timeout?: number\n maxRetries?: number\n headers?: Record<string, string>\n}\n\n/**\n * Embedding adapter interface with pre-resolved generics.\n *\n * An adapter is created by a provider function: `provider('model')` → `adapter`\n * All type resolution happens at the provider call site, not in this interface.\n *\n * Generic parameters:\n * - TModel: The specific model name (e.g., 'text-embedding-3-small')\n * - TProviderOptions: Base provider-specific options (already resolved)\n * - TModelProviderOptionsByName: Map from model name to its specific provider options\n * - TModelInputModalitiesByName: Map from model name to the input modalities it\n * accepts (constrains the `input` item types at compile time)\n */\nexport interface EmbeddingAdapter<\n TModel extends string = string,\n TProviderOptions extends object = Record<string, unknown>,\n TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,\n TModelInputModalitiesByName extends EmbeddingModelInputModalitiesByName =\n EmbeddingModelInputModalitiesByName,\n> {\n /** Discriminator for adapter kind */\n readonly kind: 'embedding'\n /** Adapter name identifier */\n readonly name: string\n /** The model this adapter is configured for */\n readonly model: TModel\n\n /**\n * @internal Type-only properties for inference. Not assigned at runtime.\n */\n '~types': {\n providerOptions: TProviderOptions\n modelProviderOptionsByName: TModelProviderOptionsByName\n modelInputModalitiesByName: TModelInputModalitiesByName\n }\n\n /**\n * Generate embeddings for the input items (one vector per item)\n */\n createEmbeddings: (\n options: EmbeddingOptions<TProviderOptions>,\n ) => Promise<EmbeddingResult>\n}\n\n/**\n * An EmbeddingAdapter with any/unknown type parameters.\n * Useful as a constraint in generic functions and interfaces.\n */\nexport type AnyEmbeddingAdapter = EmbeddingAdapter<any, any, any, any>\n\n/**\n * Abstract base class for embedding adapters.\n * Extend this class to implement an embedding adapter for a specific provider.\n *\n * Generic parameters match EmbeddingAdapter - all pre-resolved by the provider function.\n */\nexport abstract class BaseEmbeddingAdapter<\n TModel extends string = string,\n TProviderOptions extends object = Record<string, unknown>,\n TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,\n TModelInputModalitiesByName extends EmbeddingModelInputModalitiesByName =\n EmbeddingModelInputModalitiesByName,\n> implements EmbeddingAdapter<\n TModel,\n TProviderOptions,\n TModelProviderOptionsByName,\n TModelInputModalitiesByName\n> {\n readonly kind = 'embedding' as const\n abstract readonly name: string\n readonly model: TModel\n\n // Type-only property - never assigned at runtime\n declare '~types': {\n providerOptions: TProviderOptions\n modelProviderOptionsByName: TModelProviderOptionsByName\n modelInputModalitiesByName: TModelInputModalitiesByName\n }\n\n protected config: EmbeddingAdapterConfig\n\n constructor(model: TModel, config: EmbeddingAdapterConfig = {}) {\n this.config = config\n this.model = model\n }\n\n abstract createEmbeddings(\n options: EmbeddingOptions<TProviderOptions>,\n ): Promise<EmbeddingResult>\n\n protected generateId(prefix?: string): string {\n const p = prefix ?? this.name\n return `${p}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n }\n}\n"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"adapter.js","names":[],"sources":["../../../../src/activities/embed/adapter.ts"],"sourcesContent":["import type {\n EmbeddingModelInputModalitiesByName,\n EmbeddingOptions,\n EmbeddingResult,\n} from '../../types'\n\n/**\n * Configuration for embedding adapter instances\n */\nexport interface EmbeddingAdapterConfig {\n apiKey?: string\n baseUrl?: string\n timeout?: number\n maxRetries?: number\n headers?: Record<string, string>\n}\n\n/**\n * Embedding adapter interface with pre-resolved generics.\n *\n * An adapter is created by a provider function: `provider('model')` → `adapter`\n * All type resolution happens at the provider call site, not in this interface.\n *\n * Generic parameters:\n * - TModel: The specific model name (e.g., 'text-embedding-3-small')\n * - TProviderOptions: Base provider-specific options (already resolved)\n * - TModelProviderOptionsByName: Map from model name to its specific provider options\n * - TModelInputModalitiesByName: Map from model name to the input modalities it\n * accepts (constrains the `input` item types at compile time)\n */\nexport interface EmbeddingAdapter<\n TModel extends string = string,\n TProviderOptions extends object = Record<string, unknown>,\n TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,\n TModelInputModalitiesByName extends EmbeddingModelInputModalitiesByName =\n EmbeddingModelInputModalitiesByName,\n> {\n /** Discriminator for adapter kind */\n readonly kind: 'embedding'\n /** Adapter name identifier */\n readonly name: string\n /**\n * Declares that this adapter can consume `{ type: 'file' }` content\n * sources (provider Files API references). `embed()` rejects file sources\n * in preflight for adapters that don't declare this.\n */\n readonly supportsFileSources?: boolean\n /** The model this adapter is configured for */\n readonly model: TModel\n\n /**\n * @internal Type-only properties for inference. Not assigned at runtime.\n */\n '~types': {\n providerOptions: TProviderOptions\n modelProviderOptionsByName: TModelProviderOptionsByName\n modelInputModalitiesByName: TModelInputModalitiesByName\n }\n\n /**\n * Generate embeddings for the input items (one vector per item)\n */\n createEmbeddings: (\n options: EmbeddingOptions<TProviderOptions>,\n ) => Promise<EmbeddingResult>\n}\n\n/**\n * An EmbeddingAdapter with any/unknown type parameters.\n * Useful as a constraint in generic functions and interfaces.\n */\nexport type AnyEmbeddingAdapter = EmbeddingAdapter<any, any, any, any>\n\n/**\n * Abstract base class for embedding adapters.\n * Extend this class to implement an embedding adapter for a specific provider.\n *\n * Generic parameters match EmbeddingAdapter - all pre-resolved by the provider function.\n */\nexport abstract class BaseEmbeddingAdapter<\n TModel extends string = string,\n TProviderOptions extends object = Record<string, unknown>,\n TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,\n TModelInputModalitiesByName extends EmbeddingModelInputModalitiesByName =\n EmbeddingModelInputModalitiesByName,\n> implements EmbeddingAdapter<\n TModel,\n TProviderOptions,\n TModelProviderOptionsByName,\n TModelInputModalitiesByName\n> {\n readonly kind = 'embedding' as const\n abstract readonly name: string\n readonly supportsFileSources: boolean = false\n readonly model: TModel\n\n // Type-only property - never assigned at runtime\n declare '~types': {\n providerOptions: TProviderOptions\n modelProviderOptionsByName: TModelProviderOptionsByName\n modelInputModalitiesByName: TModelInputModalitiesByName\n }\n\n protected config: EmbeddingAdapterConfig\n\n constructor(model: TModel, config: EmbeddingAdapterConfig = {}) {\n this.config = config\n this.model = model\n }\n\n abstract createEmbeddings(\n options: EmbeddingOptions<TProviderOptions>,\n ): Promise<EmbeddingResult>\n\n protected generateId(prefix?: string): string {\n const p = prefix ?? this.name\n return `${p}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n }\n}\n"],"mappings":";;;;;;;AA+EA,IAAsB,uBAAtB,MAWE;CACA,OAAgB;CAEhB,sBAAwC;CACxC;CASA;CAEA,YAAY,OAAe,SAAiC,CAAC,GAAG;EAC9D,KAAK,SAAS;EACd,KAAK,QAAQ;CACf;CAMA,WAAqB,QAAyB;EAE5C,OAAO,GADG,UAAU,KAAK,KACb,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;CACpE;AACF"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveDebugOption } from "../../logger/resolve.js";
|
|
2
|
+
import { assertPromptFileSourceSupport } from "../../utilities/content-source.js";
|
|
2
3
|
import { createGenerationContext, runGenerationError, runGenerationFinish, runGenerationStart, runGenerationUsage } from "../middleware/run.js";
|
|
3
4
|
import { countEmbeddingInputModalities } from "../../utilities/embedding-input.js";
|
|
4
5
|
import "./adapter.js";
|
|
@@ -63,6 +64,7 @@ function createId(prefix) {
|
|
|
63
64
|
*/
|
|
64
65
|
async function embed(options) {
|
|
65
66
|
const { adapter, middleware } = options;
|
|
67
|
+
assertPromptFileSourceSupport(adapter, options.input);
|
|
66
68
|
const model = adapter.model;
|
|
67
69
|
const requestId = createId("embedding");
|
|
68
70
|
const startTime = Date.now();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/embed/index.ts"],"sourcesContent":["/**\n * Embed Activity\n *\n * Generates embedding vectors from text and (for multimodal models) image\n * inputs. This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n createGenerationContext,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport { countEmbeddingInputModalities } from '../../utilities/embedding-input'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { GenerationMiddleware } from '../middleware/types'\nimport type { EmbeddingAdapter } from './adapter'\nimport type {\n EmbeddingInputItem,\n EmbeddingInputItemFor,\n EmbeddingResult,\n} from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'embedding' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract model-specific provider options from an EmbeddingAdapter 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 EmbedProviderOptionsForModel<TAdapter, TModel extends string> =\n TAdapter extends EmbeddingAdapter<\n any,\n infer BaseOptions,\n infer ModelOptions,\n any\n >\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 the input type a model accepts from an EmbeddingAdapter via ~types.\n * Adapters declare a per-model input-modality map; models in the map get an\n * `input` narrowed to their supported item types (text-only models accept\n * `string | TextPart`), so unsupported items fail at compile time. Adapters\n * without a map fall back to the full EmbeddingInputItem union.\n */\nexport type EmbeddingInputForModel<TAdapter, TModel extends string> =\n TAdapter extends EmbeddingAdapter<any, any, any, infer ModsByName>\n ? string extends keyof ModsByName\n ? // No explicit map - accept the full union\n EmbeddingInputItem | Array<EmbeddingInputItem>\n : TModel extends keyof ModsByName\n ?\n | EmbeddingInputItemFor<ModsByName[TModel][number]>\n | Array<EmbeddingInputItemFor<ModsByName[TModel][number]>>\n : EmbeddingInputItem | Array<EmbeddingInputItem>\n : EmbeddingInputItem | Array<EmbeddingInputItem>\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the embed activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The embedding adapter type\n */\nexport type EmbedOptions<\n TAdapter extends EmbeddingAdapter<string, any, any, any>,\n> = {\n /** The embedding adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /**\n * What to embed: a single item or an array of items. Each item in the array\n * produces exactly one vector. An item is a plain string, a text part, an\n * image part, or — for models that embed text and image together — a fused\n * item written as a nested array of parts (`[textPart, imagePart]`), the\n * same `Array<ContentPart>` shape chat messages use. The accepted item types\n * are narrowed per model via the adapter's input-modality map.\n */\n input: EmbeddingInputForModel<TAdapter, TAdapter['model']>\n /**\n * Requested output dimensionality. Supported by models with Matryoshka /\n * configurable dimensions; adapters for fixed-dimension models throw a\n * clear runtime error when this is set.\n */\n dimensions?: number\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, or implement the\n * `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n} & ({} extends EmbedProviderOptionsForModel<TAdapter, TAdapter['model']>\n ? {\n /** Provider-specific options for embedding generation */ modelOptions?: EmbedProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n }\n : {\n /** Provider-specific options for embedding generation */ modelOptions: EmbedProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n })\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 * Embed activity - generates embedding vectors from text and image inputs.\n *\n * Accepts a single item or an array of items; the result always carries an\n * `embeddings` array with one vector per input item, in input order.\n *\n * @example Embed a single text\n * ```ts\n * import { embed } from '@tanstack/ai'\n * import { openaiEmbedding } from '@tanstack/ai-openai'\n *\n * const result = await embed({\n * adapter: openaiEmbedding('text-embedding-3-small'),\n * input: 'a red guitar',\n * })\n *\n * console.log(result.embeddings[0].vector)\n * ```\n *\n * @example Batch with requested dimensions\n * ```ts\n * const result = await embed({\n * adapter: openaiEmbedding('text-embedding-3-large'),\n * input: ['a red guitar', 'a blue drum kit'],\n * dimensions: 1024,\n * })\n * ```\n *\n * @example Multimodal embedding (text + image fused into one vector)\n * ```ts\n * import { cohereEmbedding } from '@tanstack/ai-cohere'\n *\n * // A nested array of parts fuses them into a single vector. The outer array\n * // is the item list, so this embeds one fused item into one vector.\n * const result = await embed({\n * adapter: cohereEmbedding('embed-v4.0'),\n * input: [\n * [\n * { type: 'text', content: 'product photo' },\n * { type: 'image', source: { type: 'data', value: base64, mimeType: 'image/png' } },\n * ],\n * ],\n * modelOptions: { inputType: 'search_document' },\n * })\n * ```\n */\nexport async function embed<\n TAdapter extends EmbeddingAdapter<string, any, any, any>,\n>(options: EmbedOptions<TAdapter>): Promise<EmbeddingResult> {\n const { adapter, middleware } = options\n const model = adapter.model\n const requestId = createId('embedding')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const modelOptions = (options as { modelOptions?: Record<string, unknown> })\n .modelOptions\n\n // Normalize once: adapters always receive an array of items.\n const inputItems: Array<EmbeddingInputItem> = Array.isArray(options.input)\n ? options.input\n : [options.input]\n const { textInputCount, imageInputCount } =\n countEmbeddingInputModalities(inputItems)\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'embedding',\n provider: adapter.name,\n model,\n modelOptions,\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n aiEventClient.emit('embedding:request:started', {\n requestId,\n provider: adapter.name,\n model,\n inputCount: inputItems.length,\n textInputCount,\n imageInputCount,\n dimensions: options.dimensions,\n modelOptions,\n timestamp: startTime,\n })\n\n logger.request(`activity=embed provider=${adapter.name} model=${model}`, {\n provider: adapter.name,\n model,\n })\n\n try {\n const result = await adapter.createEmbeddings({\n model,\n input: inputItems,\n dimensions: options.dimensions,\n modelOptions,\n logger,\n })\n const duration = Date.now() - startTime\n\n aiEventClient.emit('embedding:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n embeddingCount: result.embeddings.length,\n dimensions: result.embeddings[0]?.vector.length,\n duration,\n modelOptions,\n timestamp: Date.now(),\n })\n\n logger.output(`activity=embed count=${result.embeddings.length}`, {\n embeddingCount: result.embeddings.length,\n })\n\n if (result.usage) {\n aiEventClient.emit('embedding:usage', {\n requestId,\n model,\n usage: result.usage,\n timestamp: Date.now(),\n })\n await runGenerationUsage(middleware, mwCtx, result.usage)\n }\n await runGenerationFinish(middleware, mwCtx, {\n duration,\n usage: result.usage,\n })\n\n return result\n } catch (error) {\n const duration = Date.now() - startTime\n const err = error as Error\n aiEventClient.emit('embedding:request:error', {\n requestId,\n provider: adapter.name,\n model,\n error: { message: err.message, name: err.name },\n duration,\n modelOptions,\n timestamp: Date.now(),\n })\n await runGenerationError(middleware, mwCtx, {\n error,\n duration,\n })\n logger.errors('embed activity failed', {\n error,\n source: 'embed',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the embed() function without executing.\n */\nexport function createEmbedOptions<\n TAdapter extends EmbeddingAdapter<string, any, any, any>,\n>(options: EmbedOptions<TAdapter>): EmbedOptions<TAdapter> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n EmbeddingAdapter,\n EmbeddingAdapterConfig,\n AnyEmbeddingAdapter,\n} from './adapter'\nexport { BaseEmbeddingAdapter } from './adapter'\n"],"mappings":";;;;;;;;;;;;;AAgCA,IAAa,OAAO;AAsGpB,SAAS,SAAS,QAAwB;CACxC,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;AACzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,eAAsB,MAEpB,SAA2D;CAC3D,MAAM,EAAE,SAAS,eAAe;CAChC,MAAM,QAAQ,QAAQ;CACtB,MAAM,YAAY,SAAS,WAAW;CACtC,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,SAAyB,mBAAmB,QAAQ,KAAK;CAC/D,MAAM,eAAgB,QACnB;CAGH,MAAM,aAAwC,MAAM,QAAQ,QAAQ,KAAK,IACrE,QAAQ,QACR,CAAC,QAAQ,KAAK;CAClB,MAAM,EAAE,gBAAgB,oBACtB,8BAA8B,UAAU;CAE1C,MAAM,QAAQ,wBAAwB;EACpC;EACA,UAAU;EACV,UAAU,QAAQ;EAClB;EACA;EACA;CACF,CAAC;CAED,MAAM,mBAAmB,YAAY,KAAK;CAE1C,cAAc,KAAK,6BAA6B;EAC9C;EACA,UAAU,QAAQ;EAClB;EACA,YAAY,WAAW;EACvB;EACA;EACA,YAAY,QAAQ;EACpB;EACA,WAAW;CACb,CAAC;CAED,OAAO,QAAQ,2BAA2B,QAAQ,KAAK,SAAS,SAAS;EACvE,UAAU,QAAQ;EAClB;CACF,CAAC;CAED,IAAI;EACF,MAAM,SAAS,MAAM,QAAQ,iBAAiB;GAC5C;GACA,OAAO;GACP,YAAY,QAAQ;GACpB;GACA;EACF,CAAC;EACD,MAAM,WAAW,KAAK,IAAI,IAAI;EAE9B,cAAc,KAAK,+BAA+B;GAChD;GACA,UAAU,QAAQ;GAClB;GACA,gBAAgB,OAAO,WAAW;GAClC,YAAY,OAAO,WAAW,EAAE,EAAE,OAAO;GACzC;GACA;GACA,WAAW,KAAK,IAAI;EACtB,CAAC;EAED,OAAO,OAAO,wBAAwB,OAAO,WAAW,UAAU,EAChE,gBAAgB,OAAO,WAAW,OACpC,CAAC;EAED,IAAI,OAAO,OAAO;GAChB,cAAc,KAAK,mBAAmB;IACpC;IACA;IACA,OAAO,OAAO;IACd,WAAW,KAAK,IAAI;GACtB,CAAC;GACD,MAAM,mBAAmB,YAAY,OAAO,OAAO,KAAK;EAC1D;EACA,MAAM,oBAAoB,YAAY,OAAO;GAC3C;GACA,OAAO,OAAO;EAChB,CAAC;EAED,OAAO;CACT,SAAS,OAAO;EACd,MAAM,WAAW,KAAK,IAAI,IAAI;EAC9B,MAAM,MAAM;EACZ,cAAc,KAAK,2BAA2B;GAC5C;GACA,UAAU,QAAQ;GAClB;GACA,OAAO;IAAE,SAAS,IAAI;IAAS,MAAM,IAAI;GAAK;GAC9C;GACA;GACA,WAAW,KAAK,IAAI;EACtB,CAAC;EACD,MAAM,mBAAmB,YAAY,OAAO;GAC1C;GACA;EACF,CAAC;EACD,OAAO,OAAO,yBAAyB;GACrC;GACA,QAAQ;EACV,CAAC;EACD,MAAM;CACR;AACF;;;;AASA,SAAgB,mBAEd,SAAyD;CACzD,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/embed/index.ts"],"sourcesContent":["/**\n * Embed Activity\n *\n * Generates embedding vectors from text and (for multimodal models) image\n * inputs. This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n createGenerationContext,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport { countEmbeddingInputModalities } from '../../utilities/embedding-input'\nimport { assertPromptFileSourceSupport } from '../../utilities/content-source'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { GenerationMiddleware } from '../middleware/types'\nimport type { EmbeddingAdapter } from './adapter'\nimport type {\n EmbeddingInputItem,\n EmbeddingInputItemFor,\n EmbeddingResult,\n} from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'embedding' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract model-specific provider options from an EmbeddingAdapter 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 EmbedProviderOptionsForModel<TAdapter, TModel extends string> =\n TAdapter extends EmbeddingAdapter<\n any,\n infer BaseOptions,\n infer ModelOptions,\n any\n >\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 the input type a model accepts from an EmbeddingAdapter via ~types.\n * Adapters declare a per-model input-modality map; models in the map get an\n * `input` narrowed to their supported item types (text-only models accept\n * `string | TextPart`), so unsupported items fail at compile time. Adapters\n * without a map fall back to the full EmbeddingInputItem union.\n */\nexport type EmbeddingInputForModel<TAdapter, TModel extends string> =\n TAdapter extends EmbeddingAdapter<any, any, any, infer ModsByName>\n ? string extends keyof ModsByName\n ? // No explicit map - accept the full union\n EmbeddingInputItem | Array<EmbeddingInputItem>\n : TModel extends keyof ModsByName\n ?\n | EmbeddingInputItemFor<ModsByName[TModel][number]>\n | Array<EmbeddingInputItemFor<ModsByName[TModel][number]>>\n : EmbeddingInputItem | Array<EmbeddingInputItem>\n : EmbeddingInputItem | Array<EmbeddingInputItem>\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the embed activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The embedding adapter type\n */\nexport type EmbedOptions<\n TAdapter extends EmbeddingAdapter<string, any, any, any>,\n> = {\n /** The embedding adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /**\n * What to embed: a single item or an array of items. Each item in the array\n * produces exactly one vector. An item is a plain string, a text part, an\n * image part, or — for models that embed text and image together — a fused\n * item written as a nested array of parts (`[textPart, imagePart]`), the\n * same `Array<ContentPart>` shape chat messages use. The accepted item types\n * are narrowed per model via the adapter's input-modality map.\n */\n input: EmbeddingInputForModel<TAdapter, TAdapter['model']>\n /**\n * Requested output dimensionality. Supported by models with Matryoshka /\n * configurable dimensions; adapters for fixed-dimension models throw a\n * clear runtime error when this is set.\n */\n dimensions?: number\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, or implement the\n * `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n} & ({} extends EmbedProviderOptionsForModel<TAdapter, TAdapter['model']>\n ? {\n /** Provider-specific options for embedding generation */ modelOptions?: EmbedProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n }\n : {\n /** Provider-specific options for embedding generation */ modelOptions: EmbedProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n })\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 * Embed activity - generates embedding vectors from text and image inputs.\n *\n * Accepts a single item or an array of items; the result always carries an\n * `embeddings` array with one vector per input item, in input order.\n *\n * @example Embed a single text\n * ```ts\n * import { embed } from '@tanstack/ai'\n * import { openaiEmbedding } from '@tanstack/ai-openai'\n *\n * const result = await embed({\n * adapter: openaiEmbedding('text-embedding-3-small'),\n * input: 'a red guitar',\n * })\n *\n * console.log(result.embeddings[0].vector)\n * ```\n *\n * @example Batch with requested dimensions\n * ```ts\n * const result = await embed({\n * adapter: openaiEmbedding('text-embedding-3-large'),\n * input: ['a red guitar', 'a blue drum kit'],\n * dimensions: 1024,\n * })\n * ```\n *\n * @example Multimodal embedding (text + image fused into one vector)\n * ```ts\n * import { cohereEmbedding } from '@tanstack/ai-cohere'\n *\n * // A nested array of parts fuses them into a single vector. The outer array\n * // is the item list, so this embeds one fused item into one vector.\n * const result = await embed({\n * adapter: cohereEmbedding('embed-v4.0'),\n * input: [\n * [\n * { type: 'text', content: 'product photo' },\n * { type: 'image', source: { type: 'data', value: base64, mimeType: 'image/png' } },\n * ],\n * ],\n * modelOptions: { inputType: 'search_document' },\n * })\n * ```\n */\nexport async function embed<\n TAdapter extends EmbeddingAdapter<string, any, any, any>,\n>(options: EmbedOptions<TAdapter>): Promise<EmbeddingResult> {\n const { adapter, middleware } = options\n // Fail closed on `{ type: 'file' }` sources before middleware start. No\n // embedding adapter consumes file handles today; this matches chat /\n // generateImage / generateVideo.\n assertPromptFileSourceSupport(adapter, options.input)\n const model = adapter.model\n const requestId = createId('embedding')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const modelOptions = (options as { modelOptions?: Record<string, unknown> })\n .modelOptions\n\n // Normalize once: adapters always receive an array of items.\n const inputItems: Array<EmbeddingInputItem> = Array.isArray(options.input)\n ? options.input\n : [options.input]\n const { textInputCount, imageInputCount } =\n countEmbeddingInputModalities(inputItems)\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'embedding',\n provider: adapter.name,\n model,\n modelOptions,\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n aiEventClient.emit('embedding:request:started', {\n requestId,\n provider: adapter.name,\n model,\n inputCount: inputItems.length,\n textInputCount,\n imageInputCount,\n dimensions: options.dimensions,\n modelOptions,\n timestamp: startTime,\n })\n\n logger.request(`activity=embed provider=${adapter.name} model=${model}`, {\n provider: adapter.name,\n model,\n })\n\n try {\n const result = await adapter.createEmbeddings({\n model,\n input: inputItems,\n dimensions: options.dimensions,\n modelOptions,\n logger,\n })\n const duration = Date.now() - startTime\n\n aiEventClient.emit('embedding:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n embeddingCount: result.embeddings.length,\n dimensions: result.embeddings[0]?.vector.length,\n duration,\n modelOptions,\n timestamp: Date.now(),\n })\n\n logger.output(`activity=embed count=${result.embeddings.length}`, {\n embeddingCount: result.embeddings.length,\n })\n\n if (result.usage) {\n aiEventClient.emit('embedding:usage', {\n requestId,\n model,\n usage: result.usage,\n timestamp: Date.now(),\n })\n await runGenerationUsage(middleware, mwCtx, result.usage)\n }\n await runGenerationFinish(middleware, mwCtx, {\n duration,\n usage: result.usage,\n })\n\n return result\n } catch (error) {\n const duration = Date.now() - startTime\n const err = error as Error\n aiEventClient.emit('embedding:request:error', {\n requestId,\n provider: adapter.name,\n model,\n error: { message: err.message, name: err.name },\n duration,\n modelOptions,\n timestamp: Date.now(),\n })\n await runGenerationError(middleware, mwCtx, {\n error,\n duration,\n })\n logger.errors('embed activity failed', {\n error,\n source: 'embed',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the embed() function without executing.\n */\nexport function createEmbedOptions<\n TAdapter extends EmbeddingAdapter<string, any, any, any>,\n>(options: EmbedOptions<TAdapter>): EmbedOptions<TAdapter> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n EmbeddingAdapter,\n EmbeddingAdapterConfig,\n AnyEmbeddingAdapter,\n} from './adapter'\nexport { BaseEmbeddingAdapter } from './adapter'\n"],"mappings":";;;;;;;;;;;;;;AAiCA,IAAa,OAAO;AAsGpB,SAAS,SAAS,QAAwB;CACxC,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;AACzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,eAAsB,MAEpB,SAA2D;CAC3D,MAAM,EAAE,SAAS,eAAe;CAIhC,8BAA8B,SAAS,QAAQ,KAAK;CACpD,MAAM,QAAQ,QAAQ;CACtB,MAAM,YAAY,SAAS,WAAW;CACtC,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,SAAyB,mBAAmB,QAAQ,KAAK;CAC/D,MAAM,eAAgB,QACnB;CAGH,MAAM,aAAwC,MAAM,QAAQ,QAAQ,KAAK,IACrE,QAAQ,QACR,CAAC,QAAQ,KAAK;CAClB,MAAM,EAAE,gBAAgB,oBACtB,8BAA8B,UAAU;CAE1C,MAAM,QAAQ,wBAAwB;EACpC;EACA,UAAU;EACV,UAAU,QAAQ;EAClB;EACA;EACA;CACF,CAAC;CAED,MAAM,mBAAmB,YAAY,KAAK;CAE1C,cAAc,KAAK,6BAA6B;EAC9C;EACA,UAAU,QAAQ;EAClB;EACA,YAAY,WAAW;EACvB;EACA;EACA,YAAY,QAAQ;EACpB;EACA,WAAW;CACb,CAAC;CAED,OAAO,QAAQ,2BAA2B,QAAQ,KAAK,SAAS,SAAS;EACvE,UAAU,QAAQ;EAClB;CACF,CAAC;CAED,IAAI;EACF,MAAM,SAAS,MAAM,QAAQ,iBAAiB;GAC5C;GACA,OAAO;GACP,YAAY,QAAQ;GACpB;GACA;EACF,CAAC;EACD,MAAM,WAAW,KAAK,IAAI,IAAI;EAE9B,cAAc,KAAK,+BAA+B;GAChD;GACA,UAAU,QAAQ;GAClB;GACA,gBAAgB,OAAO,WAAW;GAClC,YAAY,OAAO,WAAW,EAAE,EAAE,OAAO;GACzC;GACA;GACA,WAAW,KAAK,IAAI;EACtB,CAAC;EAED,OAAO,OAAO,wBAAwB,OAAO,WAAW,UAAU,EAChE,gBAAgB,OAAO,WAAW,OACpC,CAAC;EAED,IAAI,OAAO,OAAO;GAChB,cAAc,KAAK,mBAAmB;IACpC;IACA;IACA,OAAO,OAAO;IACd,WAAW,KAAK,IAAI;GACtB,CAAC;GACD,MAAM,mBAAmB,YAAY,OAAO,OAAO,KAAK;EAC1D;EACA,MAAM,oBAAoB,YAAY,OAAO;GAC3C;GACA,OAAO,OAAO;EAChB,CAAC;EAED,OAAO;CACT,SAAS,OAAO;EACd,MAAM,WAAW,KAAK,IAAI,IAAI;EAC9B,MAAM,MAAM;EACZ,cAAc,KAAK,2BAA2B;GAC5C;GACA,UAAU,QAAQ;GAClB;GACA,OAAO;IAAE,SAAS,IAAI;IAAS,MAAM,IAAI;GAAK;GAC9C;GACA;GACA,WAAW,KAAK,IAAI;EACtB,CAAC;EACD,MAAM,mBAAmB,YAAY,OAAO;GAC1C;GACA;EACF,CAAC;EACD,OAAO,OAAO,yBAAyB;GACrC;GACA,QAAQ;EACV,CAAC;EACD,MAAM;CACR;AACF;;;;AASA,SAAgB,mBAEd,SAAyD;CACzD,OAAO;AACT"}
|
|
@@ -105,6 +105,10 @@ export interface EvaluateAdapterResult {
|
|
|
105
105
|
model: string;
|
|
106
106
|
answers: Record<string, WireAnswer>;
|
|
107
107
|
usage: TokenUsage;
|
|
108
|
+
/** Provider response id, for example to look the request up later. */
|
|
109
|
+
id?: string;
|
|
110
|
+
/** Upstream provider that served the request, when a router reports it. */
|
|
111
|
+
provider?: string;
|
|
108
112
|
}
|
|
109
113
|
/**
|
|
110
114
|
* Evaluate adapter interface with pre-resolved generics.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.js","names":[],"sources":["../../../../src/activities/evaluate/adapter.ts"],"sourcesContent":["import type { InternalLogger } from '../../logger/internal-logger'\nimport type { TokenUsage } from '../../types'\n\n/**\n * Configuration for evaluate adapter instances.\n */\nexport interface EvaluateAdapterConfig {\n apiKey?: string\n baseUrl?: string\n timeout?: number\n headers?: Record<string, string>\n}\n\n/**\n * Shared JSON value for `state` and question `instructions`.\n * A JSON array is one value, not a batch.\n */\nexport type EvaluateJsonValue = string | object | Array<unknown>\n\n/** Content the model judges. A JSON array is one state, not a batch. */\nexport type EvaluateState = EvaluateJsonValue\n\n/** Question text. Matches TypeSafe: string, object, or array. */\nexport type EvaluateInstructions = EvaluateJsonValue\n\n/**\n * TypeSafe choice question on the adapter wire.\n *\n * Generic parameters:\n * - TOptions: option key to description (or `null` when the key is enough)\n */\nexport interface WireChoiceQuestion<\n TOptions extends Record<string, string | null> = Record<\n string,\n string | null\n >,\n> {\n type: 'choice'\n instructions: EvaluateInstructions\n criteria: TOptions\n}\n\n/**\n * TypeSafe score question on the adapter wire.\n *\n * Generic parameters:\n * - TLevels: ordered level labels, at least two\n */\nexport interface WireScoreQuestion<\n TLevels extends ReadonlyArray<string> = ReadonlyArray<string>,\n> {\n type: 'score'\n instructions: EvaluateInstructions\n criteria: TLevels\n}\n\n/**\n * TypeSafe yes/no question on the adapter wire.\n * Public helpers call this `boolean`. The wire type is `noul`.\n */\nexport interface WireNoulQuestion {\n type: 'noul'\n instructions: EvaluateInstructions\n criteria?: {\n true?: string\n false?: string\n }\n}\n\n/** Question payload adapters send to the provider. */\nexport type WireQuestion =\n | WireChoiceQuestion\n | WireScoreQuestion\n | WireNoulQuestion\n\n/** TypeSafe choice answer. Adapters do not invent a public `.value`. */\nexport interface WireChoiceAnswer {\n type: 'choice'\n choice: string\n probabilities: Record<string, number>\n confidence: number\n}\n\n/** TypeSafe score answer. `score` is the raw fraction. */\nexport interface WireScoreAnswer {\n type: 'score'\n score: number\n legend: Record<string, string>\n probabilities: Record<string, number>\n confidence: number\n}\n\n/** TypeSafe yes/no answer. `noul` is P(true). */\nexport interface WireNoulAnswer {\n type: 'noul'\n noul: number\n}\n\n/** Provider payload for one question. The activity maps this to a unified answer. */\nexport type WireAnswer = WireChoiceAnswer | WireScoreAnswer | WireNoulAnswer\n\n/**\n * Options passed to {@link EvaluateAdapter.evaluate}.\n */\nexport interface EvaluateOptions<\n TProviderOptions extends object = Record<string, unknown>,\n> {\n model: string\n /** Shared state every question judges. A JSON array is one state, not a batch. */\n state: EvaluateState\n /** TypeSafe wire questions, keyed by the caller's question ids. */\n questions: Record<string, WireQuestion>\n /** Provider-specific options forwarded by `decide()`. */\n modelOptions?: TProviderOptions\n /** Forwarded to the provider request for cancellation. */\n abortSignal?: AbortSignal\n /**\n * Internal logger threaded from `decide()`. Adapters must call\n * `logger.request()` before the provider call and `logger.errors()` in catch\n * blocks.\n */\n logger: InternalLogger\n}\n\n/**\n * Provider-level evaluate result. Adapters return the wire payload plus usage.\n * The activity maps answers to the unified public shape.\n */\nexport interface EvaluateAdapterResult {\n /** Resolved model id from the provider. */\n model: string\n answers: Record<string, WireAnswer>\n usage: TokenUsage\n}\n\n/**\n * Evaluate adapter interface with pre-resolved generics.\n *\n * An adapter is created by a provider function: `provider('model')` → `adapter`.\n * All type resolution happens at the provider call site, not in this interface.\n *\n * Generic parameters:\n * - TModel: The specific model name (e.g. `'jev-latest'`)\n * - TProviderOptions: Provider-specific options (already resolved)\n */\nexport interface EvaluateAdapter<\n TModel extends string = string,\n TProviderOptions extends object = Record<string, unknown>,\n> {\n /** Discriminator for adapter kind */\n readonly kind: 'evaluate'\n /** Adapter name identifier */\n readonly name: string\n /** The model this adapter is configured for */\n readonly model: TModel\n\n /**\n * @internal Type-only properties for inference. Not assigned at runtime.\n */\n '~types': {\n providerOptions: TProviderOptions\n }\n\n /**\n * Evaluate typed questions against `state`. Return the provider payload.\n * Do not invent unified `.value` fields. The activity maps wire answers.\n */\n evaluate: (\n options: EvaluateOptions<TProviderOptions>,\n ) => Promise<EvaluateAdapterResult>\n}\n\n/**\n * An EvaluateAdapter with any/unknown type parameters.\n * Useful as a constraint in generic functions and interfaces.\n */\nexport type AnyEvaluateAdapter = EvaluateAdapter<any, any>\n\n/**\n * Abstract base class for evaluate adapters.\n * Extend this class to implement an evaluate adapter for a specific provider.\n *\n * Generic parameters match EvaluateAdapter. The provider function resolves them.\n */\nexport abstract class BaseEvaluateAdapter<\n TModel extends string = string,\n TProviderOptions extends object = Record<string, unknown>,\n> implements EvaluateAdapter<TModel, TProviderOptions> {\n readonly kind = 'evaluate' as const\n abstract readonly name: string\n readonly model: TModel\n\n // Type-only property - never assigned at runtime\n declare '~types': {\n providerOptions: TProviderOptions\n }\n\n protected config: EvaluateAdapterConfig\n\n constructor(config: EvaluateAdapterConfig = {}, model: TModel) {\n this.config = config\n this.model = model\n }\n\n abstract evaluate(\n options: EvaluateOptions<TProviderOptions>,\n ): Promise<EvaluateAdapterResult>\n\n protected generateId(): string {\n return `${this.name}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n }\n}\n"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"adapter.js","names":[],"sources":["../../../../src/activities/evaluate/adapter.ts"],"sourcesContent":["import type { InternalLogger } from '../../logger/internal-logger'\nimport type { TokenUsage } from '../../types'\n\n/**\n * Configuration for evaluate adapter instances.\n */\nexport interface EvaluateAdapterConfig {\n apiKey?: string\n baseUrl?: string\n timeout?: number\n headers?: Record<string, string>\n}\n\n/**\n * Shared JSON value for `state` and question `instructions`.\n * A JSON array is one value, not a batch.\n */\nexport type EvaluateJsonValue = string | object | Array<unknown>\n\n/** Content the model judges. A JSON array is one state, not a batch. */\nexport type EvaluateState = EvaluateJsonValue\n\n/** Question text. Matches TypeSafe: string, object, or array. */\nexport type EvaluateInstructions = EvaluateJsonValue\n\n/**\n * TypeSafe choice question on the adapter wire.\n *\n * Generic parameters:\n * - TOptions: option key to description (or `null` when the key is enough)\n */\nexport interface WireChoiceQuestion<\n TOptions extends Record<string, string | null> = Record<\n string,\n string | null\n >,\n> {\n type: 'choice'\n instructions: EvaluateInstructions\n criteria: TOptions\n}\n\n/**\n * TypeSafe score question on the adapter wire.\n *\n * Generic parameters:\n * - TLevels: ordered level labels, at least two\n */\nexport interface WireScoreQuestion<\n TLevels extends ReadonlyArray<string> = ReadonlyArray<string>,\n> {\n type: 'score'\n instructions: EvaluateInstructions\n criteria: TLevels\n}\n\n/**\n * TypeSafe yes/no question on the adapter wire.\n * Public helpers call this `boolean`. The wire type is `noul`.\n */\nexport interface WireNoulQuestion {\n type: 'noul'\n instructions: EvaluateInstructions\n criteria?: {\n true?: string\n false?: string\n }\n}\n\n/** Question payload adapters send to the provider. */\nexport type WireQuestion =\n | WireChoiceQuestion\n | WireScoreQuestion\n | WireNoulQuestion\n\n/** TypeSafe choice answer. Adapters do not invent a public `.value`. */\nexport interface WireChoiceAnswer {\n type: 'choice'\n choice: string\n probabilities: Record<string, number>\n confidence: number\n}\n\n/** TypeSafe score answer. `score` is the raw fraction. */\nexport interface WireScoreAnswer {\n type: 'score'\n score: number\n legend: Record<string, string>\n probabilities: Record<string, number>\n confidence: number\n}\n\n/** TypeSafe yes/no answer. `noul` is P(true). */\nexport interface WireNoulAnswer {\n type: 'noul'\n noul: number\n}\n\n/** Provider payload for one question. The activity maps this to a unified answer. */\nexport type WireAnswer = WireChoiceAnswer | WireScoreAnswer | WireNoulAnswer\n\n/**\n * Options passed to {@link EvaluateAdapter.evaluate}.\n */\nexport interface EvaluateOptions<\n TProviderOptions extends object = Record<string, unknown>,\n> {\n model: string\n /** Shared state every question judges. A JSON array is one state, not a batch. */\n state: EvaluateState\n /** TypeSafe wire questions, keyed by the caller's question ids. */\n questions: Record<string, WireQuestion>\n /** Provider-specific options forwarded by `decide()`. */\n modelOptions?: TProviderOptions\n /** Forwarded to the provider request for cancellation. */\n abortSignal?: AbortSignal\n /**\n * Internal logger threaded from `decide()`. Adapters must call\n * `logger.request()` before the provider call and `logger.errors()` in catch\n * blocks.\n */\n logger: InternalLogger\n}\n\n/**\n * Provider-level evaluate result. Adapters return the wire payload plus usage.\n * The activity maps answers to the unified public shape.\n */\nexport interface EvaluateAdapterResult {\n /** Resolved model id from the provider. */\n model: string\n answers: Record<string, WireAnswer>\n usage: TokenUsage\n /** Provider response id, for example to look the request up later. */\n id?: string\n /** Upstream provider that served the request, when a router reports it. */\n provider?: string\n}\n\n/**\n * Evaluate adapter interface with pre-resolved generics.\n *\n * An adapter is created by a provider function: `provider('model')` → `adapter`.\n * All type resolution happens at the provider call site, not in this interface.\n *\n * Generic parameters:\n * - TModel: The specific model name (e.g. `'jev-latest'`)\n * - TProviderOptions: Provider-specific options (already resolved)\n */\nexport interface EvaluateAdapter<\n TModel extends string = string,\n TProviderOptions extends object = Record<string, unknown>,\n> {\n /** Discriminator for adapter kind */\n readonly kind: 'evaluate'\n /** Adapter name identifier */\n readonly name: string\n /** The model this adapter is configured for */\n readonly model: TModel\n\n /**\n * @internal Type-only properties for inference. Not assigned at runtime.\n */\n '~types': {\n providerOptions: TProviderOptions\n }\n\n /**\n * Evaluate typed questions against `state`. Return the provider payload.\n * Do not invent unified `.value` fields. The activity maps wire answers.\n */\n evaluate: (\n options: EvaluateOptions<TProviderOptions>,\n ) => Promise<EvaluateAdapterResult>\n}\n\n/**\n * An EvaluateAdapter with any/unknown type parameters.\n * Useful as a constraint in generic functions and interfaces.\n */\nexport type AnyEvaluateAdapter = EvaluateAdapter<any, any>\n\n/**\n * Abstract base class for evaluate adapters.\n * Extend this class to implement an evaluate adapter for a specific provider.\n *\n * Generic parameters match EvaluateAdapter. The provider function resolves them.\n */\nexport abstract class BaseEvaluateAdapter<\n TModel extends string = string,\n TProviderOptions extends object = Record<string, unknown>,\n> implements EvaluateAdapter<TModel, TProviderOptions> {\n readonly kind = 'evaluate' as const\n abstract readonly name: string\n readonly model: TModel\n\n // Type-only property - never assigned at runtime\n declare '~types': {\n providerOptions: TProviderOptions\n }\n\n protected config: EvaluateAdapterConfig\n\n constructor(config: EvaluateAdapterConfig = {}, model: TModel) {\n this.config = config\n this.model = model\n }\n\n abstract evaluate(\n options: EvaluateOptions<TProviderOptions>,\n ): Promise<EvaluateAdapterResult>\n\n protected generateId(): string {\n return `${this.name}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n }\n}\n"],"mappings":";;;;;;;AA4LA,IAAsB,sBAAtB,MAGuD;CACrD,OAAgB;CAEhB;CAOA;CAEA,YAAY,SAAgC,CAAC,GAAG,OAAe;EAC7D,KAAK,SAAS;EACd,KAAK,QAAQ;CACf;CAMA,aAA+B;EAC7B,OAAO,GAAG,KAAK,KAAK,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;CAC5E;AACF"}
|
|
@@ -51,6 +51,10 @@ export interface EvaluateResultMeta {
|
|
|
51
51
|
/** Resolved model id from the provider. */
|
|
52
52
|
model: string;
|
|
53
53
|
usage: TokenUsage;
|
|
54
|
+
/** Provider response id, when the adapter returns one. */
|
|
55
|
+
id?: string;
|
|
56
|
+
/** Upstream provider that served the request, when the adapter returns one. */
|
|
57
|
+
provider?: string;
|
|
54
58
|
}
|
|
55
59
|
/**
|
|
56
60
|
* Map a helper question (or wire question) to its public answer type.
|
|
@@ -292,7 +292,9 @@ async function decide(options) {
|
|
|
292
292
|
});
|
|
293
293
|
return withMeta(answers, {
|
|
294
294
|
model: result.model,
|
|
295
|
-
usage: result.usage
|
|
295
|
+
usage: result.usage,
|
|
296
|
+
...result.id !== void 0 && { id: result.id },
|
|
297
|
+
...result.provider !== void 0 && { provider: result.provider }
|
|
296
298
|
});
|
|
297
299
|
} catch (error) {
|
|
298
300
|
const duration = Date.now() - startTime;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/evaluate/index.ts"],"sourcesContent":["/**\n * Evaluate Activity\n *\n * Asks typed questions about a shared state and returns values your code can\n * branch on. This is a self-contained module with implementation, types, and\n * JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport { isAbortShapedError } from '../error-payload'\nimport {\n createGenerationContext,\n runGenerationAbort,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { TokenUsage } from '../../types'\nimport type { GenerationMiddleware } from '../middleware/types'\nimport type {\n EvaluateAdapter,\n EvaluateInstructions,\n EvaluateState,\n WireAnswer,\n WireChoiceAnswer,\n WireNoulAnswer,\n WireQuestion,\n WireScoreAnswer,\n WireScoreQuestion,\n} from './adapter'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'evaluate' as const\n\n/** Question key reserved for `result.meta`. */\nconst RESERVED_QUESTION_KEY = 'meta' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/** Extract provider options from an EvaluateAdapter via ~types */\nexport type EvaluateProviderOptions<TAdapter> = TAdapter extends {\n '~types': { providerOptions: infer P extends object }\n}\n ? P\n : object\n\n// ===========================\n// Unified answers\n// ===========================\n\n/**\n * Public choice answer. `.value` is the selected option key.\n */\nexport interface ChoiceAnswer<TValue extends string = string> {\n type: 'choice'\n value: TValue\n /** P(selected option). */\n probability: number\n confidence: number\n probabilities: Record<TValue, number>\n}\n\n/**\n * Public score answer. `.value` is the nearest level label.\n * `.score` is the raw TypeSafe fraction.\n */\nexport interface ScoreAnswer<TLevel extends string = string> {\n type: 'score'\n value: TLevel\n /** P(nearest level). */\n probability: number\n confidence: number\n score: number\n legend: Record<string, string>\n probabilities: Record<string, number>\n}\n\n/**\n * Public yes/no answer. `.value` is `true` when P(true) is 0.5 or more.\n * There is no `.confidence`.\n */\nexport interface BooleanAnswer {\n type: 'boolean'\n value: boolean\n /** P(true), from the wire `noul` field. */\n probability: number\n}\n\nexport interface EvaluateResultMeta {\n /** Resolved model id from the provider. */\n model: string\n usage: TokenUsage\n}\n\n/**\n * Map a helper question (or wire question) to its public answer type.\n */\nexport type InferEvaluateAnswer<TQuestion> = TQuestion extends {\n type: 'choice'\n criteria: infer TCriteria\n}\n ? TCriteria extends Record<string, string | null>\n ? ChoiceAnswer<Extract<keyof TCriteria, string>>\n : ChoiceAnswer\n : TQuestion extends { type: 'score'; criteria: infer TLevels }\n ? TLevels extends ReadonlyArray<string>\n ? ScoreAnswer<TLevels[number] & string>\n : ScoreAnswer\n : TQuestion extends { type: 'noul' }\n ? BooleanAnswer\n : never\n\n/**\n * Result of `decide()`. Each question key is a top-level answer.\n * `meta` holds the resolved model id and usage.\n */\nexport type EvaluateResult<TQuestions extends Record<string, WireQuestion>> = {\n [K in keyof TQuestions as K extends typeof RESERVED_QUESTION_KEY\n ? never\n : K]: InferEvaluateAnswer<TQuestions[K]>\n} & {\n meta: EvaluateResultMeta\n}\n\n// ===========================\n// Activity Options Types\n// ===========================\n\n/**\n * Options for the evaluate activity. The model is extracted from the\n * adapter's model property.\n *\n * @template TAdapter - The evaluate adapter type\n * @template TQuestions - The questions object passed to `decide`\n */\nexport interface EvaluateActivityOptions<\n TAdapter extends EvaluateAdapter<string, EvaluateProviderOptions<TAdapter>>,\n TQuestions extends Record<string, WireQuestion>,\n> {\n /** The evaluate adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** Shared state every question judges. A JSON array is one state, not a batch. */\n state: EvaluateState\n /**\n * Questions built with `choice`, `score`, and `boolean`.\n * The key `meta` is reserved.\n */\n questions: TQuestions\n /** Provider-specific options */\n modelOptions?: EvaluateProviderOptions<TAdapter>\n /** Forwarded to the provider request for cancellation. */\n abortSignal?: AbortSignal\n /**\n * Observe-only middleware notified on start, usage, success, abort, and\n * error. Pass `otelMiddleware()` to emit OpenTelemetry spans, or implement\n * the `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n}\n\n// ===========================\n// Helper Functions\n// ===========================\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\nfunction isAbortError(error: unknown, signal?: AbortSignal): boolean {\n // Prefer the error's own identity over the signal state. A genuine\n // cancellation throws an abort-shaped error (DOM `AbortError`, the OpenRouter\n // SDK's `RequestAbortedError`, ...). Classifying on `signal.aborted` alone\n // would misroute a real failure to the abort hook whenever a shared signal\n // happens to already be aborted, hiding it from `onError` observers.\n if (isAbortShapedError(error)) return true\n // Fall back to signal state only for non-Error throws we can't otherwise\n // identify; a real Error with a non-abort name is never an abort.\n return error instanceof Error ? false : signal?.aborted === true\n}\n\nfunction questionKeys(questions: Record<string, WireQuestion>) {\n return Object.keys(questions)\n}\n\nfunction assertQuestions(questions: Record<string, WireQuestion>) {\n const keys = questionKeys(questions)\n if (keys.length === 0) {\n throw new Error('decide() requires at least one question')\n }\n if (Object.hasOwn(questions, RESERVED_QUESTION_KEY)) {\n throw new Error('decide() reserves the question key \"meta\"')\n }\n return keys\n}\n\nfunction mapChoiceAnswer(wire: WireChoiceAnswer, key: string) {\n const probability = wire.probabilities[wire.choice]\n if (typeof probability !== 'number') {\n throw new Error(\n `decide(): missing probability for choice \"${wire.choice}\" on \"${key}\"`,\n )\n }\n return {\n type: 'choice' as const,\n value: wire.choice,\n probability,\n confidence: wire.confidence,\n probabilities: wire.probabilities,\n }\n}\n\nfunction mapScoreAnswer(\n question: WireScoreQuestion,\n wire: WireScoreAnswer,\n key: string,\n) {\n const levels = question.criteria\n if (levels.length < 2) {\n throw new Error(\n `decide(): score question \"${key}\" needs at least two levels`,\n )\n }\n const lastIndex = levels.length - 1\n const rounded = Math.round(wire.score)\n const nearestIndex =\n rounded < 0 ? 0 : rounded > lastIndex ? lastIndex : rounded\n const value = levels[nearestIndex]\n if (value === undefined) {\n throw new Error(\n `decide(): score question \"${key}\" has no level at index ${nearestIndex}`,\n )\n }\n const probability = wire.probabilities[String(nearestIndex)]\n if (typeof probability !== 'number') {\n throw new Error(\n `decide(): missing probability for score level ${nearestIndex} on \"${key}\"`,\n )\n }\n return {\n type: 'score' as const,\n value,\n probability,\n confidence: wire.confidence,\n score: wire.score,\n legend: wire.legend,\n probabilities: wire.probabilities,\n }\n}\n\nfunction mapBooleanAnswer(wire: WireNoulAnswer) {\n return {\n type: 'boolean' as const,\n value: wire.noul >= 0.5,\n probability: wire.noul,\n }\n}\n\nfunction mapWireAnswer(question: WireQuestion, wire: WireAnswer, key: string) {\n switch (question.type) {\n case 'choice': {\n if (wire.type !== 'choice') {\n throw new Error(\n `decide(): expected choice answer for \"${key}\", got ${wire.type}`,\n )\n }\n return mapChoiceAnswer(wire, key)\n }\n case 'score': {\n if (wire.type !== 'score') {\n throw new Error(\n `decide(): expected score answer for \"${key}\", got ${wire.type}`,\n )\n }\n return mapScoreAnswer(question, wire, key)\n }\n case 'noul': {\n if (wire.type !== 'noul') {\n throw new Error(\n `decide(): expected noul answer for \"${key}\", got ${wire.type}`,\n )\n }\n return mapBooleanAnswer(wire)\n }\n }\n}\n\nfunction mapAnswers<TQuestions extends Record<string, WireQuestion>>(\n questions: TQuestions,\n wireAnswers: Record<string, WireAnswer>,\n) {\n const answers = {} as {\n [K in keyof TQuestions]: InferEvaluateAnswer<TQuestions[K]>\n }\n const keys = Object.keys(questions) as Array<keyof TQuestions>\n for (const key of keys) {\n const question = questions[key]\n const wire = wireAnswers[String(key)]\n if (question === undefined) {\n throw new Error(`decide(): missing question \"${String(key)}\"`)\n }\n if (wire === undefined) {\n throw new Error(`decide(): missing answer for question \"${String(key)}\"`)\n }\n answers[key] = mapWireAnswer(\n question,\n wire,\n String(key),\n ) as InferEvaluateAnswer<TQuestions[typeof key]>\n }\n return answers\n}\n\nfunction withMeta<TAnswers extends object>(\n answers: TAnswers,\n meta: EvaluateResultMeta,\n) {\n return { ...answers, meta }\n}\n\n// ===========================\n// Question helpers\n// ===========================\n\n/**\n * Build a choice question. The model picks one key from `options`.\n *\n * Option keys become the union on `.value`. Use `null` when a key needs no\n * extra description. On the wire, `options` is sent as TypeSafe `criteria`.\n *\n * @param options.instructions What the model should decide.\n * @param options.options Map of option key to description, or `null`.\n *\n * @example\n * ```ts\n * const queue = choice({\n * instructions: 'Which team should handle this ticket?',\n * options: {\n * billing: 'Payments, invoices, refunds',\n * tech: 'Bugs, outages, integrations',\n * sales: 'Pricing, upgrades, new accounts',\n * },\n * })\n * ```\n */\nexport function choice<\n const TOptions extends Record<string, string | null>,\n>(options: { instructions: EvaluateInstructions; options: TOptions }) {\n return {\n type: 'choice' as const,\n instructions: options.instructions,\n criteria: options.options,\n }\n}\n\n/**\n * Build a score question. The model rates `state` on ordered `levels`.\n *\n * You must pass at least two levels. `.value` is the nearest level label.\n * The raw fraction stays on `.score`. On the wire, `levels` is sent as\n * TypeSafe `criteria`.\n *\n * @param options.instructions What the model should rate.\n * @param options.levels Ordered labels, lowest first. At least two.\n *\n * @example\n * ```ts\n * const urgency = score({\n * instructions: 'How urgent is this ticket?',\n * levels: ['low', 'medium', 'high'],\n * })\n * ```\n */\nexport function score<const TLevels extends ReadonlyArray<string>>(options: {\n instructions: EvaluateInstructions\n levels: TLevels\n}) {\n if (options.levels.length < 2) {\n throw new Error('score() requires at least two levels')\n }\n return {\n type: 'score' as const,\n instructions: options.instructions,\n criteria: options.levels,\n }\n}\n\n/**\n * Build a yes/no question.\n *\n * `.value` is `true` when P(true) is 0.5 or more. There is no `.confidence`.\n * On the wire, the type is TypeSafe `noul`.\n *\n * @param options.instructions The yes/no question to judge.\n * @param options.criteria Optional descriptions of yes and no.\n *\n * @example\n * ```ts\n * const refund = boolean({\n * instructions: 'Is the customer asking for a refund?',\n * })\n * ```\n */\nexport function boolean(options: {\n instructions: EvaluateInstructions\n criteria?: {\n true?: string\n false?: string\n }\n}) {\n if (options.criteria === undefined) {\n return {\n type: 'noul' as const,\n instructions: options.instructions,\n }\n }\n return {\n type: 'noul' as const,\n instructions: options.instructions,\n criteria: options.criteria,\n }\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Ask typed questions about `state` and get answers your code can branch on.\n *\n * You have state (a ticket, a record, a log) and you need typed answers, not\n * prose. Pass questions built with `choice`, `score`, and `boolean`. Then\n * branch on `result.queue.value` in ordinary TypeScript.\n *\n * The question key `meta` is reserved. Throws if `questions` is empty or uses\n * that key.\n *\n * @param options.adapter Evaluate adapter created with a model.\n * @param options.state Shared state every question judges.\n * @param options.questions Questions built with `choice`, `score`, `boolean`.\n * @param options.modelOptions Provider-specific options.\n * @param options.abortSignal Cancels the in-flight request.\n * @param options.middleware Observe-only generation middleware.\n * @param options.debug Debug logging option.\n *\n * @example Route a support ticket\n * ```ts\n * import { decide, choice, score, boolean } from '@tanstack/ai'\n * import { typesafeDecider } from '@tanstack/ai-typesafe'\n *\n * const result = await decide({\n * adapter: typesafeDecider('jev-latest'),\n * state: ticket,\n * questions: {\n * queue: choice({\n * instructions: 'Which team should handle this ticket?',\n * options: {\n * billing: 'Payments, invoices, refunds',\n * tech: 'Bugs, outages, integrations',\n * sales: 'Pricing, upgrades, new accounts',\n * },\n * }),\n * urgency: score({\n * instructions: 'How urgent is this ticket?',\n * levels: ['low', 'medium', 'high'],\n * }),\n * refund: boolean({\n * instructions: 'Is the customer asking for a refund?',\n * }),\n * },\n * })\n *\n * result.queue.value\n * result.meta.model\n * result.meta.usage\n * ```\n */\nexport async function decide<\n TAdapter extends EvaluateAdapter<string, EvaluateProviderOptions<TAdapter>>,\n TQuestions extends Record<string, WireQuestion>,\n>(options: EvaluateActivityOptions<TAdapter, TQuestions>) {\n const {\n adapter,\n state,\n questions,\n modelOptions,\n abortSignal,\n middleware,\n debug,\n } = options\n const model = adapter.model\n const keys = assertQuestions(questions)\n const requestId = createId('evaluate')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(debug)\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'evaluate',\n provider: adapter.name,\n model,\n modelOptions,\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n aiEventClient.emit('evaluate:request:started', {\n requestId,\n provider: adapter.name,\n model,\n questionCount: keys.length,\n timestamp: startTime,\n })\n\n logger.request(`activity=evaluate provider=${adapter.name}`, {\n provider: adapter.name,\n model,\n questionCount: keys.length,\n })\n\n try {\n const result = await adapter.evaluate({\n model,\n state,\n questions,\n modelOptions,\n abortSignal,\n logger,\n })\n\n const answers = mapAnswers(questions, result.answers)\n const duration = Date.now() - startTime\n\n aiEventClient.emit('evaluate:request:completed', {\n requestId,\n provider: adapter.name,\n model: result.model,\n questionCount: keys.length,\n duration,\n timestamp: Date.now(),\n })\n\n aiEventClient.emit('evaluate:usage', {\n requestId,\n model: result.model,\n usage: result.usage,\n timestamp: Date.now(),\n })\n\n logger.output(`activity=evaluate answers=${keys.length}`, {\n answerCount: keys.length,\n })\n\n await runGenerationUsage(middleware, mwCtx, result.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration,\n usage: result.usage,\n })\n\n return withMeta(answers, {\n model: result.model,\n usage: result.usage,\n })\n } catch (error) {\n const duration = Date.now() - startTime\n if (isAbortError(error, abortSignal)) {\n await runGenerationAbort(middleware, mwCtx, {\n reason: error instanceof Error ? error.message : undefined,\n duration,\n })\n } else {\n await runGenerationError(middleware, mwCtx, { error, duration })\n }\n logger.errors('evaluate activity failed', { error, source: 'evaluate' })\n throw error\n }\n}\n\n// Re-export adapter types\nexport type {\n EvaluateAdapter,\n EvaluateAdapterConfig,\n AnyEvaluateAdapter,\n EvaluateOptions,\n EvaluateAdapterResult,\n EvaluateState,\n EvaluateInstructions,\n EvaluateJsonValue,\n WireQuestion,\n WireAnswer,\n WireChoiceQuestion,\n WireScoreQuestion,\n WireNoulQuestion,\n WireChoiceAnswer,\n WireScoreAnswer,\n WireNoulAnswer,\n} from './adapter'\nexport { BaseEvaluateAdapter } from './adapter'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/evaluate/index.ts"],"sourcesContent":["/**\n * Evaluate Activity\n *\n * Asks typed questions about a shared state and returns values your code can\n * branch on. This is a self-contained module with implementation, types, and\n * JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport { isAbortShapedError } from '../error-payload'\nimport {\n createGenerationContext,\n runGenerationAbort,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { TokenUsage } from '../../types'\nimport type { GenerationMiddleware } from '../middleware/types'\nimport type {\n EvaluateAdapter,\n EvaluateInstructions,\n EvaluateState,\n WireAnswer,\n WireChoiceAnswer,\n WireNoulAnswer,\n WireQuestion,\n WireScoreAnswer,\n WireScoreQuestion,\n} from './adapter'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'evaluate' as const\n\n/** Question key reserved for `result.meta`. */\nconst RESERVED_QUESTION_KEY = 'meta' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/** Extract provider options from an EvaluateAdapter via ~types */\nexport type EvaluateProviderOptions<TAdapter> = TAdapter extends {\n '~types': { providerOptions: infer P extends object }\n}\n ? P\n : object\n\n// ===========================\n// Unified answers\n// ===========================\n\n/**\n * Public choice answer. `.value` is the selected option key.\n */\nexport interface ChoiceAnswer<TValue extends string = string> {\n type: 'choice'\n value: TValue\n /** P(selected option). */\n probability: number\n confidence: number\n probabilities: Record<TValue, number>\n}\n\n/**\n * Public score answer. `.value` is the nearest level label.\n * `.score` is the raw TypeSafe fraction.\n */\nexport interface ScoreAnswer<TLevel extends string = string> {\n type: 'score'\n value: TLevel\n /** P(nearest level). */\n probability: number\n confidence: number\n score: number\n legend: Record<string, string>\n probabilities: Record<string, number>\n}\n\n/**\n * Public yes/no answer. `.value` is `true` when P(true) is 0.5 or more.\n * There is no `.confidence`.\n */\nexport interface BooleanAnswer {\n type: 'boolean'\n value: boolean\n /** P(true), from the wire `noul` field. */\n probability: number\n}\n\nexport interface EvaluateResultMeta {\n /** Resolved model id from the provider. */\n model: string\n usage: TokenUsage\n /** Provider response id, when the adapter returns one. */\n id?: string\n /** Upstream provider that served the request, when the adapter returns one. */\n provider?: string\n}\n\n/**\n * Map a helper question (or wire question) to its public answer type.\n */\nexport type InferEvaluateAnswer<TQuestion> = TQuestion extends {\n type: 'choice'\n criteria: infer TCriteria\n}\n ? TCriteria extends Record<string, string | null>\n ? ChoiceAnswer<Extract<keyof TCriteria, string>>\n : ChoiceAnswer\n : TQuestion extends { type: 'score'; criteria: infer TLevels }\n ? TLevels extends ReadonlyArray<string>\n ? ScoreAnswer<TLevels[number] & string>\n : ScoreAnswer\n : TQuestion extends { type: 'noul' }\n ? BooleanAnswer\n : never\n\n/**\n * Result of `decide()`. Each question key is a top-level answer.\n * `meta` holds the resolved model id and usage.\n */\nexport type EvaluateResult<TQuestions extends Record<string, WireQuestion>> = {\n [K in keyof TQuestions as K extends typeof RESERVED_QUESTION_KEY\n ? never\n : K]: InferEvaluateAnswer<TQuestions[K]>\n} & {\n meta: EvaluateResultMeta\n}\n\n// ===========================\n// Activity Options Types\n// ===========================\n\n/**\n * Options for the evaluate activity. The model is extracted from the\n * adapter's model property.\n *\n * @template TAdapter - The evaluate adapter type\n * @template TQuestions - The questions object passed to `decide`\n */\nexport interface EvaluateActivityOptions<\n TAdapter extends EvaluateAdapter<string, EvaluateProviderOptions<TAdapter>>,\n TQuestions extends Record<string, WireQuestion>,\n> {\n /** The evaluate adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** Shared state every question judges. A JSON array is one state, not a batch. */\n state: EvaluateState\n /**\n * Questions built with `choice`, `score`, and `boolean`.\n * The key `meta` is reserved.\n */\n questions: TQuestions\n /** Provider-specific options */\n modelOptions?: EvaluateProviderOptions<TAdapter>\n /** Forwarded to the provider request for cancellation. */\n abortSignal?: AbortSignal\n /**\n * Observe-only middleware notified on start, usage, success, abort, and\n * error. Pass `otelMiddleware()` to emit OpenTelemetry spans, or implement\n * the `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n}\n\n// ===========================\n// Helper Functions\n// ===========================\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\nfunction isAbortError(error: unknown, signal?: AbortSignal): boolean {\n // Prefer the error's own identity over the signal state. A genuine\n // cancellation throws an abort-shaped error (DOM `AbortError`, the OpenRouter\n // SDK's `RequestAbortedError`, ...). Classifying on `signal.aborted` alone\n // would misroute a real failure to the abort hook whenever a shared signal\n // happens to already be aborted, hiding it from `onError` observers.\n if (isAbortShapedError(error)) return true\n // Fall back to signal state only for non-Error throws we can't otherwise\n // identify; a real Error with a non-abort name is never an abort.\n return error instanceof Error ? false : signal?.aborted === true\n}\n\nfunction questionKeys(questions: Record<string, WireQuestion>) {\n return Object.keys(questions)\n}\n\nfunction assertQuestions(questions: Record<string, WireQuestion>) {\n const keys = questionKeys(questions)\n if (keys.length === 0) {\n throw new Error('decide() requires at least one question')\n }\n if (Object.hasOwn(questions, RESERVED_QUESTION_KEY)) {\n throw new Error('decide() reserves the question key \"meta\"')\n }\n return keys\n}\n\nfunction mapChoiceAnswer(wire: WireChoiceAnswer, key: string) {\n const probability = wire.probabilities[wire.choice]\n if (typeof probability !== 'number') {\n throw new Error(\n `decide(): missing probability for choice \"${wire.choice}\" on \"${key}\"`,\n )\n }\n return {\n type: 'choice' as const,\n value: wire.choice,\n probability,\n confidence: wire.confidence,\n probabilities: wire.probabilities,\n }\n}\n\nfunction mapScoreAnswer(\n question: WireScoreQuestion,\n wire: WireScoreAnswer,\n key: string,\n) {\n const levels = question.criteria\n if (levels.length < 2) {\n throw new Error(\n `decide(): score question \"${key}\" needs at least two levels`,\n )\n }\n const lastIndex = levels.length - 1\n const rounded = Math.round(wire.score)\n const nearestIndex =\n rounded < 0 ? 0 : rounded > lastIndex ? lastIndex : rounded\n const value = levels[nearestIndex]\n if (value === undefined) {\n throw new Error(\n `decide(): score question \"${key}\" has no level at index ${nearestIndex}`,\n )\n }\n const probability = wire.probabilities[String(nearestIndex)]\n if (typeof probability !== 'number') {\n throw new Error(\n `decide(): missing probability for score level ${nearestIndex} on \"${key}\"`,\n )\n }\n return {\n type: 'score' as const,\n value,\n probability,\n confidence: wire.confidence,\n score: wire.score,\n legend: wire.legend,\n probabilities: wire.probabilities,\n }\n}\n\nfunction mapBooleanAnswer(wire: WireNoulAnswer) {\n return {\n type: 'boolean' as const,\n value: wire.noul >= 0.5,\n probability: wire.noul,\n }\n}\n\nfunction mapWireAnswer(question: WireQuestion, wire: WireAnswer, key: string) {\n switch (question.type) {\n case 'choice': {\n if (wire.type !== 'choice') {\n throw new Error(\n `decide(): expected choice answer for \"${key}\", got ${wire.type}`,\n )\n }\n return mapChoiceAnswer(wire, key)\n }\n case 'score': {\n if (wire.type !== 'score') {\n throw new Error(\n `decide(): expected score answer for \"${key}\", got ${wire.type}`,\n )\n }\n return mapScoreAnswer(question, wire, key)\n }\n case 'noul': {\n if (wire.type !== 'noul') {\n throw new Error(\n `decide(): expected noul answer for \"${key}\", got ${wire.type}`,\n )\n }\n return mapBooleanAnswer(wire)\n }\n }\n}\n\nfunction mapAnswers<TQuestions extends Record<string, WireQuestion>>(\n questions: TQuestions,\n wireAnswers: Record<string, WireAnswer>,\n) {\n const answers = {} as {\n [K in keyof TQuestions]: InferEvaluateAnswer<TQuestions[K]>\n }\n const keys = Object.keys(questions) as Array<keyof TQuestions>\n for (const key of keys) {\n const question = questions[key]\n const wire = wireAnswers[String(key)]\n if (question === undefined) {\n throw new Error(`decide(): missing question \"${String(key)}\"`)\n }\n if (wire === undefined) {\n throw new Error(`decide(): missing answer for question \"${String(key)}\"`)\n }\n answers[key] = mapWireAnswer(\n question,\n wire,\n String(key),\n ) as InferEvaluateAnswer<TQuestions[typeof key]>\n }\n return answers\n}\n\nfunction withMeta<TAnswers extends object>(\n answers: TAnswers,\n meta: EvaluateResultMeta,\n) {\n return { ...answers, meta }\n}\n\n// ===========================\n// Question helpers\n// ===========================\n\n/**\n * Build a choice question. The model picks one key from `options`.\n *\n * Option keys become the union on `.value`. Use `null` when a key needs no\n * extra description. On the wire, `options` is sent as TypeSafe `criteria`.\n *\n * @param options.instructions What the model should decide.\n * @param options.options Map of option key to description, or `null`.\n *\n * @example\n * ```ts\n * const queue = choice({\n * instructions: 'Which team should handle this ticket?',\n * options: {\n * billing: 'Payments, invoices, refunds',\n * tech: 'Bugs, outages, integrations',\n * sales: 'Pricing, upgrades, new accounts',\n * },\n * })\n * ```\n */\nexport function choice<\n const TOptions extends Record<string, string | null>,\n>(options: { instructions: EvaluateInstructions; options: TOptions }) {\n return {\n type: 'choice' as const,\n instructions: options.instructions,\n criteria: options.options,\n }\n}\n\n/**\n * Build a score question. The model rates `state` on ordered `levels`.\n *\n * You must pass at least two levels. `.value` is the nearest level label.\n * The raw fraction stays on `.score`. On the wire, `levels` is sent as\n * TypeSafe `criteria`.\n *\n * @param options.instructions What the model should rate.\n * @param options.levels Ordered labels, lowest first. At least two.\n *\n * @example\n * ```ts\n * const urgency = score({\n * instructions: 'How urgent is this ticket?',\n * levels: ['low', 'medium', 'high'],\n * })\n * ```\n */\nexport function score<const TLevels extends ReadonlyArray<string>>(options: {\n instructions: EvaluateInstructions\n levels: TLevels\n}) {\n if (options.levels.length < 2) {\n throw new Error('score() requires at least two levels')\n }\n return {\n type: 'score' as const,\n instructions: options.instructions,\n criteria: options.levels,\n }\n}\n\n/**\n * Build a yes/no question.\n *\n * `.value` is `true` when P(true) is 0.5 or more. There is no `.confidence`.\n * On the wire, the type is TypeSafe `noul`.\n *\n * @param options.instructions The yes/no question to judge.\n * @param options.criteria Optional descriptions of yes and no.\n *\n * @example\n * ```ts\n * const refund = boolean({\n * instructions: 'Is the customer asking for a refund?',\n * })\n * ```\n */\nexport function boolean(options: {\n instructions: EvaluateInstructions\n criteria?: {\n true?: string\n false?: string\n }\n}) {\n if (options.criteria === undefined) {\n return {\n type: 'noul' as const,\n instructions: options.instructions,\n }\n }\n return {\n type: 'noul' as const,\n instructions: options.instructions,\n criteria: options.criteria,\n }\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Ask typed questions about `state` and get answers your code can branch on.\n *\n * You have state (a ticket, a record, a log) and you need typed answers, not\n * prose. Pass questions built with `choice`, `score`, and `boolean`. Then\n * branch on `result.queue.value` in ordinary TypeScript.\n *\n * The question key `meta` is reserved. Throws if `questions` is empty or uses\n * that key.\n *\n * @param options.adapter Evaluate adapter created with a model.\n * @param options.state Shared state every question judges.\n * @param options.questions Questions built with `choice`, `score`, `boolean`.\n * @param options.modelOptions Provider-specific options.\n * @param options.abortSignal Cancels the in-flight request.\n * @param options.middleware Observe-only generation middleware.\n * @param options.debug Debug logging option.\n *\n * @example Route a support ticket\n * ```ts\n * import { decide, choice, score, boolean } from '@tanstack/ai'\n * import { typesafeDecider } from '@tanstack/ai-typesafe'\n *\n * const result = await decide({\n * adapter: typesafeDecider('jev-latest'),\n * state: ticket,\n * questions: {\n * queue: choice({\n * instructions: 'Which team should handle this ticket?',\n * options: {\n * billing: 'Payments, invoices, refunds',\n * tech: 'Bugs, outages, integrations',\n * sales: 'Pricing, upgrades, new accounts',\n * },\n * }),\n * urgency: score({\n * instructions: 'How urgent is this ticket?',\n * levels: ['low', 'medium', 'high'],\n * }),\n * refund: boolean({\n * instructions: 'Is the customer asking for a refund?',\n * }),\n * },\n * })\n *\n * result.queue.value\n * result.meta.model\n * result.meta.usage\n * ```\n */\nexport async function decide<\n TAdapter extends EvaluateAdapter<string, EvaluateProviderOptions<TAdapter>>,\n TQuestions extends Record<string, WireQuestion>,\n>(options: EvaluateActivityOptions<TAdapter, TQuestions>) {\n const {\n adapter,\n state,\n questions,\n modelOptions,\n abortSignal,\n middleware,\n debug,\n } = options\n const model = adapter.model\n const keys = assertQuestions(questions)\n const requestId = createId('evaluate')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(debug)\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'evaluate',\n provider: adapter.name,\n model,\n modelOptions,\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n aiEventClient.emit('evaluate:request:started', {\n requestId,\n provider: adapter.name,\n model,\n questionCount: keys.length,\n timestamp: startTime,\n })\n\n logger.request(`activity=evaluate provider=${adapter.name}`, {\n provider: adapter.name,\n model,\n questionCount: keys.length,\n })\n\n try {\n const result = await adapter.evaluate({\n model,\n state,\n questions,\n modelOptions,\n abortSignal,\n logger,\n })\n\n const answers = mapAnswers(questions, result.answers)\n const duration = Date.now() - startTime\n\n aiEventClient.emit('evaluate:request:completed', {\n requestId,\n provider: adapter.name,\n model: result.model,\n questionCount: keys.length,\n duration,\n timestamp: Date.now(),\n })\n\n aiEventClient.emit('evaluate:usage', {\n requestId,\n model: result.model,\n usage: result.usage,\n timestamp: Date.now(),\n })\n\n logger.output(`activity=evaluate answers=${keys.length}`, {\n answerCount: keys.length,\n })\n\n await runGenerationUsage(middleware, mwCtx, result.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration,\n usage: result.usage,\n })\n\n return withMeta(answers, {\n model: result.model,\n usage: result.usage,\n ...(result.id !== undefined && { id: result.id }),\n ...(result.provider !== undefined && { provider: result.provider }),\n })\n } catch (error) {\n const duration = Date.now() - startTime\n if (isAbortError(error, abortSignal)) {\n await runGenerationAbort(middleware, mwCtx, {\n reason: error instanceof Error ? error.message : undefined,\n duration,\n })\n } else {\n await runGenerationError(middleware, mwCtx, { error, duration })\n }\n logger.errors('evaluate activity failed', { error, source: 'evaluate' })\n throw error\n }\n}\n\n// Re-export adapter types\nexport type {\n EvaluateAdapter,\n EvaluateAdapterConfig,\n AnyEvaluateAdapter,\n EvaluateOptions,\n EvaluateAdapterResult,\n EvaluateState,\n EvaluateInstructions,\n EvaluateJsonValue,\n WireQuestion,\n WireAnswer,\n WireChoiceQuestion,\n WireScoreQuestion,\n WireNoulQuestion,\n WireChoiceAnswer,\n WireScoreAnswer,\n WireNoulAnswer,\n} from './adapter'\nexport { BaseEvaluateAdapter } from './adapter'\n"],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files Adapter
|
|
3
|
+
*
|
|
4
|
+
* Base class and interface for the `files` activity — a provider's native Files
|
|
5
|
+
* API (upload a media asset once, reference it later by the returned handle
|
|
6
|
+
* instead of re-sending base64 or a public URL each request).
|
|
7
|
+
*
|
|
8
|
+
* Providers with a native surface expose a factory (`openaiFiles()`,
|
|
9
|
+
* `anthropicFiles()`, `geminiFiles()`, `falFiles()`). `upload` is required;
|
|
10
|
+
* `get`/`delete` are optional because not every provider has a lifecycle API
|
|
11
|
+
* (fal's storage is upload-only).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Input to {@link FilesAdapter.upload}. Either a `Blob` (memory-efficient,
|
|
15
|
+
* preferred for large assets) or base64 `data` plus its `mimeType`.
|
|
16
|
+
*/
|
|
17
|
+
export type FileUploadInput = Blob | {
|
|
18
|
+
/** Base64-encoded file bytes. */
|
|
19
|
+
data: string;
|
|
20
|
+
/** MIME type of the bytes (e.g. `'image/png'`, `'application/pdf'`). */
|
|
21
|
+
mimeType: string;
|
|
22
|
+
/** Optional filename hint sent to providers that accept one. */
|
|
23
|
+
filename?: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A provider-issued file handle returned by {@link FilesAdapter.upload} /
|
|
27
|
+
* {@link FilesAdapter.get}. Reference it in a message via a `{ type: 'file' }`
|
|
28
|
+
* content source — use `fileSourceFromHandle` to build one.
|
|
29
|
+
*
|
|
30
|
+
* `TProvider` carries the issuing provider's name as a literal (`'openai'`,
|
|
31
|
+
* `'gemini'`, ...) when the handle came from a concrete files adapter, so
|
|
32
|
+
* cross-provider lifecycle calls (`deleteFile` with a foreign handle) fail at
|
|
33
|
+
* compile time. It defaults to `string` so wire-deserialized handles still fit.
|
|
34
|
+
*/
|
|
35
|
+
export interface FileHandle<TProvider extends string = string> {
|
|
36
|
+
/**
|
|
37
|
+
* Provider handle used for lifecycle operations (`get`/`delete`): the
|
|
38
|
+
* OpenAI/Anthropic `file_id`, the Gemini file resource name (`files/...`), or
|
|
39
|
+
* the fal storage URL (fal itself has no lifecycle API — the URL doubles as
|
|
40
|
+
* the wire reference).
|
|
41
|
+
*/
|
|
42
|
+
id: string;
|
|
43
|
+
/** The provider that issued the handle (`'openai'`, `'gemini'`, ...). */
|
|
44
|
+
provider: TProvider;
|
|
45
|
+
/**
|
|
46
|
+
* The handle's URL form when the provider exposes one (Gemini file URI, fal
|
|
47
|
+
* storage URL). For providers whose handle is an opaque id (OpenAI,
|
|
48
|
+
* Anthropic) this is `undefined`.
|
|
49
|
+
*/
|
|
50
|
+
uri?: string;
|
|
51
|
+
/** MIME type reported by the provider (or echoed from the upload input). */
|
|
52
|
+
mimeType?: string;
|
|
53
|
+
/** File size in bytes when the provider reports it. */
|
|
54
|
+
sizeBytes?: number;
|
|
55
|
+
/** Expiry as epoch milliseconds when the handle is scheduled to expire. */
|
|
56
|
+
expiresAt?: number;
|
|
57
|
+
/** Original filename when the provider reports it. */
|
|
58
|
+
filename?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The `files` adapter contract. `upload` is required; `get`/`delete` are
|
|
62
|
+
* optional and present only when the provider has a lifecycle API.
|
|
63
|
+
*
|
|
64
|
+
* `TName` is the provider name literal (`'openai'`, `'gemini'`, ...); concrete
|
|
65
|
+
* adapters bind it so the handles they issue carry their provenance in the
|
|
66
|
+
* type system.
|
|
67
|
+
*/
|
|
68
|
+
export interface FilesAdapter<TName extends string = string> {
|
|
69
|
+
readonly kind: 'files';
|
|
70
|
+
readonly name: TName;
|
|
71
|
+
upload: (input: FileUploadInput) => Promise<FileHandle<TName>>;
|
|
72
|
+
get?: (id: string) => Promise<FileHandle<TName>>;
|
|
73
|
+
delete?: (id: string) => Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
export type AnyFilesAdapter = FilesAdapter<string>;
|
|
76
|
+
/**
|
|
77
|
+
* Normalize a {@link FileUploadInput} to a `Blob` (plus best-effort MIME /
|
|
78
|
+
* filename) so provider adapters can hand it straight to their SDK. A `Blob`
|
|
79
|
+
* input passes through; base64 `{ data }` is decoded to bytes. Shared so
|
|
80
|
+
* provider files adapters don't each re-implement the decode.
|
|
81
|
+
*/
|
|
82
|
+
export declare function normalizeFileUploadInput(input: FileUploadInput): {
|
|
83
|
+
blob: Blob;
|
|
84
|
+
mimeType?: string;
|
|
85
|
+
filename?: string;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Abstract base for provider files adapters. Subclasses bind `TName` to their
|
|
89
|
+
* provider literal, set `name`, implement `upload`, and may add `get`/`delete`
|
|
90
|
+
* (declared on {@link FilesAdapter}, not here, since not every provider has a
|
|
91
|
+
* lifecycle API).
|
|
92
|
+
*/
|
|
93
|
+
export declare abstract class BaseFilesAdapter<TName extends string = string> implements FilesAdapter<TName> {
|
|
94
|
+
readonly kind: "files";
|
|
95
|
+
abstract readonly name: TName;
|
|
96
|
+
abstract upload(input: FileUploadInput): Promise<FileHandle<TName>>;
|
|
97
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { base64ToArrayBuffer } from "@tanstack/ai-utils";
|
|
2
|
+
//#region src/activities/files/adapter.ts
|
|
3
|
+
/**
|
|
4
|
+
* Files Adapter
|
|
5
|
+
*
|
|
6
|
+
* Base class and interface for the `files` activity — a provider's native Files
|
|
7
|
+
* API (upload a media asset once, reference it later by the returned handle
|
|
8
|
+
* instead of re-sending base64 or a public URL each request).
|
|
9
|
+
*
|
|
10
|
+
* Providers with a native surface expose a factory (`openaiFiles()`,
|
|
11
|
+
* `anthropicFiles()`, `geminiFiles()`, `falFiles()`). `upload` is required;
|
|
12
|
+
* `get`/`delete` are optional because not every provider has a lifecycle API
|
|
13
|
+
* (fal's storage is upload-only).
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a {@link FileUploadInput} to a `Blob` (plus best-effort MIME /
|
|
17
|
+
* filename) so provider adapters can hand it straight to their SDK. A `Blob`
|
|
18
|
+
* input passes through; base64 `{ data }` is decoded to bytes. Shared so
|
|
19
|
+
* provider files adapters don't each re-implement the decode.
|
|
20
|
+
*/
|
|
21
|
+
function normalizeFileUploadInput(input) {
|
|
22
|
+
if (input instanceof Blob) return {
|
|
23
|
+
blob: input,
|
|
24
|
+
mimeType: input.type || void 0
|
|
25
|
+
};
|
|
26
|
+
const bytes = base64ToArrayBuffer(input.data);
|
|
27
|
+
return {
|
|
28
|
+
blob: new Blob([bytes], { type: input.mimeType }),
|
|
29
|
+
mimeType: input.mimeType,
|
|
30
|
+
filename: input.filename
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Abstract base for provider files adapters. Subclasses bind `TName` to their
|
|
35
|
+
* provider literal, set `name`, implement `upload`, and may add `get`/`delete`
|
|
36
|
+
* (declared on {@link FilesAdapter}, not here, since not every provider has a
|
|
37
|
+
* lifecycle API).
|
|
38
|
+
*/
|
|
39
|
+
var BaseFilesAdapter = class {
|
|
40
|
+
kind = "files";
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { BaseFilesAdapter, normalizeFileUploadInput };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","names":[],"sources":["../../../../src/activities/files/adapter.ts"],"sourcesContent":["/**\n * Files Adapter\n *\n * Base class and interface for the `files` activity — a provider's native Files\n * API (upload a media asset once, reference it later by the returned handle\n * instead of re-sending base64 or a public URL each request).\n *\n * Providers with a native surface expose a factory (`openaiFiles()`,\n * `anthropicFiles()`, `geminiFiles()`, `falFiles()`). `upload` is required;\n * `get`/`delete` are optional because not every provider has a lifecycle API\n * (fal's storage is upload-only).\n */\n\nimport { base64ToArrayBuffer } from '@tanstack/ai-utils'\n\n/**\n * Input to {@link FilesAdapter.upload}. Either a `Blob` (memory-efficient,\n * preferred for large assets) or base64 `data` plus its `mimeType`.\n */\nexport type FileUploadInput =\n | Blob\n | {\n /** Base64-encoded file bytes. */\n data: string\n /** MIME type of the bytes (e.g. `'image/png'`, `'application/pdf'`). */\n mimeType: string\n /** Optional filename hint sent to providers that accept one. */\n filename?: string\n }\n\n/**\n * A provider-issued file handle returned by {@link FilesAdapter.upload} /\n * {@link FilesAdapter.get}. Reference it in a message via a `{ type: 'file' }`\n * content source — use `fileSourceFromHandle` to build one.\n *\n * `TProvider` carries the issuing provider's name as a literal (`'openai'`,\n * `'gemini'`, ...) when the handle came from a concrete files adapter, so\n * cross-provider lifecycle calls (`deleteFile` with a foreign handle) fail at\n * compile time. It defaults to `string` so wire-deserialized handles still fit.\n */\nexport interface FileHandle<TProvider extends string = string> {\n /**\n * Provider handle used for lifecycle operations (`get`/`delete`): the\n * OpenAI/Anthropic `file_id`, the Gemini file resource name (`files/...`), or\n * the fal storage URL (fal itself has no lifecycle API — the URL doubles as\n * the wire reference).\n */\n id: string\n /** The provider that issued the handle (`'openai'`, `'gemini'`, ...). */\n provider: TProvider\n /**\n * The handle's URL form when the provider exposes one (Gemini file URI, fal\n * storage URL). For providers whose handle is an opaque id (OpenAI,\n * Anthropic) this is `undefined`.\n */\n uri?: string\n /** MIME type reported by the provider (or echoed from the upload input). */\n mimeType?: string\n /** File size in bytes when the provider reports it. */\n sizeBytes?: number\n /** Expiry as epoch milliseconds when the handle is scheduled to expire. */\n expiresAt?: number\n /** Original filename when the provider reports it. */\n filename?: string\n}\n\n/**\n * The `files` adapter contract. `upload` is required; `get`/`delete` are\n * optional and present only when the provider has a lifecycle API.\n *\n * `TName` is the provider name literal (`'openai'`, `'gemini'`, ...); concrete\n * adapters bind it so the handles they issue carry their provenance in the\n * type system.\n */\nexport interface FilesAdapter<TName extends string = string> {\n readonly kind: 'files'\n readonly name: TName\n upload: (input: FileUploadInput) => Promise<FileHandle<TName>>\n get?: (id: string) => Promise<FileHandle<TName>>\n delete?: (id: string) => Promise<void>\n}\n\nexport type AnyFilesAdapter = FilesAdapter<string>\n\n/**\n * Normalize a {@link FileUploadInput} to a `Blob` (plus best-effort MIME /\n * filename) so provider adapters can hand it straight to their SDK. A `Blob`\n * input passes through; base64 `{ data }` is decoded to bytes. Shared so\n * provider files adapters don't each re-implement the decode.\n */\nexport function normalizeFileUploadInput(input: FileUploadInput): {\n blob: Blob\n mimeType?: string\n filename?: string\n} {\n if (input instanceof Blob) {\n return { blob: input, mimeType: input.type || undefined }\n }\n const bytes = base64ToArrayBuffer(input.data)\n return {\n blob: new Blob([bytes], { type: input.mimeType }),\n mimeType: input.mimeType,\n filename: input.filename,\n }\n}\n\n/**\n * Abstract base for provider files adapters. Subclasses bind `TName` to their\n * provider literal, set `name`, implement `upload`, and may add `get`/`delete`\n * (declared on {@link FilesAdapter}, not here, since not every provider has a\n * lifecycle API).\n */\nexport abstract class BaseFilesAdapter<\n TName extends string = string,\n> implements FilesAdapter<TName> {\n readonly kind = 'files' as const\n abstract readonly name: TName\n\n abstract upload(input: FileUploadInput): Promise<FileHandle<TName>>\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0FA,SAAgB,yBAAyB,OAIvC;CACA,IAAI,iBAAiB,MACnB,OAAO;EAAE,MAAM;EAAO,UAAU,MAAM,QAAQ,KAAA;CAAU;CAE1D,MAAM,QAAQ,oBAAoB,MAAM,IAAI;CAC5C,OAAO;EACL,MAAM,IAAI,KAAK,CAAC,KAAK,GAAG,EAAE,MAAM,MAAM,SAAS,CAAC;EAChD,UAAU,MAAM;EAChB,UAAU,MAAM;CAClB;AACF;;;;;;;AAQA,IAAsB,mBAAtB,MAEiC;CAC/B,OAAgB;AAIlB"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ContentPartFileSource } from '../../types.js';
|
|
2
|
+
import { FileHandle, FileUploadInput, FilesAdapter } from './adapter.js';
|
|
3
|
+
/** The adapter kind this activity handles */
|
|
4
|
+
export declare const kind: "files";
|
|
5
|
+
/**
|
|
6
|
+
* Upload a file to a provider's Files API and return its handle. The handle
|
|
7
|
+
* carries the provider name as a literal type, so passing it to another
|
|
8
|
+
* provider's lifecycle call is a compile error.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const files = openaiFiles()
|
|
13
|
+
* const handle = await uploadFile({ adapter: files, input: { data, mimeType: 'image/png' } })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function uploadFile<TName extends string>(options: {
|
|
17
|
+
adapter: FilesAdapter<TName> & {
|
|
18
|
+
kind: typeof kind;
|
|
19
|
+
};
|
|
20
|
+
input: FileUploadInput;
|
|
21
|
+
}): Promise<FileHandle<TName>>;
|
|
22
|
+
/**
|
|
23
|
+
* Fetch metadata for a previously uploaded file. Accepts the handle itself
|
|
24
|
+
* (preferred — the provider-literal type rejects a foreign provider's handle
|
|
25
|
+
* at compile time) or its raw lifecycle id.
|
|
26
|
+
*
|
|
27
|
+
* @throws if the provider's files adapter has no `get` (e.g. fal storage).
|
|
28
|
+
*/
|
|
29
|
+
export declare function getFile<TName extends string>(options: {
|
|
30
|
+
adapter: FilesAdapter<TName> & {
|
|
31
|
+
kind: typeof kind;
|
|
32
|
+
};
|
|
33
|
+
id: string | FileHandle<NoInfer<TName>>;
|
|
34
|
+
}): Promise<FileHandle<TName>>;
|
|
35
|
+
/**
|
|
36
|
+
* Delete a previously uploaded file. Accepts the handle itself (preferred —
|
|
37
|
+
* the provider-literal type rejects a foreign provider's handle at compile
|
|
38
|
+
* time) or its raw lifecycle id.
|
|
39
|
+
*
|
|
40
|
+
* @throws if the provider's files adapter has no `delete` (e.g. fal storage).
|
|
41
|
+
*/
|
|
42
|
+
export declare function deleteFile<TName extends string>(options: {
|
|
43
|
+
adapter: FilesAdapter<TName> & {
|
|
44
|
+
kind: typeof kind;
|
|
45
|
+
};
|
|
46
|
+
id: string | FileHandle<NoInfer<TName>>;
|
|
47
|
+
}): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Build a `{ type: 'file' }` content source from an uploaded
|
|
50
|
+
* {@link FileHandle}, for use in a chat message (image/audio/document part
|
|
51
|
+
* `source`).
|
|
52
|
+
*
|
|
53
|
+
* The source's `value` is the handle's wire form: the handle URL when the
|
|
54
|
+
* provider exposes one (Gemini, fal, Grok), otherwise the opaque id (OpenAI,
|
|
55
|
+
* Anthropic). `provider` records the issuer, so an adapter for a different
|
|
56
|
+
* provider rejects the source rather than sending a handle it cannot resolve.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const handle = await uploadFile({ adapter: openaiFiles(), input })
|
|
61
|
+
* messages.push({ role: 'user', content: [
|
|
62
|
+
* { type: 'image', source: fileSourceFromHandle(handle) },
|
|
63
|
+
* ] })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function fileSourceFromHandle<TProvider extends string>(handle: FileHandle<TProvider>): ContentPartFileSource<TProvider>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region src/activities/files/index.ts
|
|
2
|
+
/** The adapter kind this activity handles */
|
|
3
|
+
var kind = "files";
|
|
4
|
+
/**
|
|
5
|
+
* Upload a file to a provider's Files API and return its handle. The handle
|
|
6
|
+
* carries the provider name as a literal type, so passing it to another
|
|
7
|
+
* provider's lifecycle call is a compile error.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const files = openaiFiles()
|
|
12
|
+
* const handle = await uploadFile({ adapter: files, input: { data, mimeType: 'image/png' } })
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
async function uploadFile(options) {
|
|
16
|
+
return options.adapter.upload(options.input);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a lifecycle id from either a raw id string or a {@link FileHandle}
|
|
20
|
+
* (whose `id` — not its `uri`/wire value — is the lifecycle currency).
|
|
21
|
+
*/
|
|
22
|
+
function toLifecycleId(id) {
|
|
23
|
+
return typeof id === "string" ? id : id.id;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fetch metadata for a previously uploaded file. Accepts the handle itself
|
|
27
|
+
* (preferred — the provider-literal type rejects a foreign provider's handle
|
|
28
|
+
* at compile time) or its raw lifecycle id.
|
|
29
|
+
*
|
|
30
|
+
* @throws if the provider's files adapter has no `get` (e.g. fal storage).
|
|
31
|
+
*/
|
|
32
|
+
async function getFile(options) {
|
|
33
|
+
const { adapter } = options;
|
|
34
|
+
if (!adapter.get) throw new Error(`${adapter.name}: files adapter does not support get() — this provider has no file-retrieval API.`);
|
|
35
|
+
return adapter.get(toLifecycleId(options.id));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Delete a previously uploaded file. Accepts the handle itself (preferred —
|
|
39
|
+
* the provider-literal type rejects a foreign provider's handle at compile
|
|
40
|
+
* time) or its raw lifecycle id.
|
|
41
|
+
*
|
|
42
|
+
* @throws if the provider's files adapter has no `delete` (e.g. fal storage).
|
|
43
|
+
*/
|
|
44
|
+
async function deleteFile(options) {
|
|
45
|
+
const { adapter } = options;
|
|
46
|
+
if (!adapter.delete) throw new Error(`${adapter.name}: files adapter does not support delete() — this provider has no file-deletion API.`);
|
|
47
|
+
return adapter.delete(toLifecycleId(options.id));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build a `{ type: 'file' }` content source from an uploaded
|
|
51
|
+
* {@link FileHandle}, for use in a chat message (image/audio/document part
|
|
52
|
+
* `source`).
|
|
53
|
+
*
|
|
54
|
+
* The source's `value` is the handle's wire form: the handle URL when the
|
|
55
|
+
* provider exposes one (Gemini, fal, Grok), otherwise the opaque id (OpenAI,
|
|
56
|
+
* Anthropic). `provider` records the issuer, so an adapter for a different
|
|
57
|
+
* provider rejects the source rather than sending a handle it cannot resolve.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const handle = await uploadFile({ adapter: openaiFiles(), input })
|
|
62
|
+
* messages.push({ role: 'user', content: [
|
|
63
|
+
* { type: 'image', source: fileSourceFromHandle(handle) },
|
|
64
|
+
* ] })
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
function fileSourceFromHandle(handle) {
|
|
68
|
+
return {
|
|
69
|
+
type: "file",
|
|
70
|
+
value: handle.uri ?? handle.id,
|
|
71
|
+
provider: handle.provider,
|
|
72
|
+
...handle.mimeType ? { mimeType: handle.mimeType } : {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { deleteFile, fileSourceFromHandle, getFile, kind, uploadFile };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=index.js.map
|