UNPKG

ai-functions

Version:

Core AI primitives for building intelligent applications

718 lines (664 loc) 21.6 kB
/** * Core types for AI functions */ import type { SandboxEnv } from 'ai-evaluate' /** * Host Workers environment for the ai-evaluate sandbox. * * Re-exported from ai-evaluate so consumers can type the optional `env` * argument threaded through `DefinedFunction.call` / `generateAndRunCode` * without importing ai-evaluate directly. */ export type { SandboxEnv } from 'ai-evaluate' // ============================================================================ // Human Function Pending Types // ============================================================================ /** * Symbol used to identify pending human function results */ export const PENDING_HUMAN_RESULT_SYMBOL = Symbol.for('HumanFunctionPending') /** * Human interaction channels * * - chat: Real-time messaging (WebSocket, in-app) * - email: Async email communication * - phone: Voice calls * - sms: SMS text messages * - workspace: Team collaboration platforms (Slack, Teams, Discord) * - web: Web-based interface (browser, web app) */ export type HumanChannel = 'chat' | 'email' | 'phone' | 'sms' | 'workspace' | 'web' /** * Pending human function result returned when human input is not yet available. * * This is returned by human functions that need actual human input but are running * in a placeholder mode (e.g., during testing or when channel integrations are not configured). * * Use `isPendingHumanResult()` to check if a result is pending before using it. * * @example * ```ts * const result = await approveRefund({ amount: 500, reason: 'Duplicate charge' }) * * if (isPendingHumanResult(result)) { * console.log('Waiting for human approval via:', result.channel) * // Handle pending state - queue for later, show UI, etc. * } else { * // result is the actual TOutput type * console.log('Approved:', result.approved) * } * ``` */ export interface HumanFunctionPending<TExpected = unknown> { /** Symbol marker for type identification */ readonly [PENDING_HUMAN_RESULT_SYMBOL]: true /** Indicates this is a pending placeholder, not actual human response */ readonly _pending: true /** The channel where human input was requested */ readonly channel: HumanChannel /** Generated UI/content artifacts for the channel */ readonly artifacts: unknown /** The expected response type schema */ readonly expectedResponseType: TExpected } /** * Type guard to check if a result is a pending human function result. * * Use this to safely handle cases where human input is not yet available. * * @param value - The value to check * @returns True if the value is a HumanFunctionPending object * * @example * ```ts * const result = await reviewDocument({ docId: '123' }) * * if (isPendingHumanResult(result)) { * // result is HumanFunctionPending - human input needed * console.warn('Human review pending:', result.channel) * return { status: 'pending', channel: result.channel } * } * * // result is ReviewResult - actual human response * return { status: 'reviewed', approved: result.approved } * ``` */ export function isPendingHumanResult<T>( value: T | HumanFunctionPending ): value is HumanFunctionPending { return ( value !== null && typeof value === 'object' && '_pending' in value && value._pending === true && PENDING_HUMAN_RESULT_SYMBOL in value ) } // ============================================================================ // AI Function Types // ============================================================================ /** * A function definition that can be called by AI * * @typeParam TOutput - The return type of the function handler * @typeParam TInput - The input type accepted by the function handler */ export interface AIFunctionDefinition<TOutput = unknown, TInput = unknown> { /** Unique name for the function */ name: string /** Human-readable description for the AI */ description: string /** JSON Schema for the input parameters */ parameters: JSONSchema /** The implementation */ handler: (input: TInput) => TOutput | Promise<TOutput> } /** * JSON Schema type (simplified) */ export interface JSONSchema { type?: string properties?: Record<string, JSONSchema> items?: JSONSchema required?: string[] description?: string enum?: unknown[] default?: unknown [key: string]: unknown } /** * Options for AI generation */ export interface AIGenerateOptions { /** The prompt or input */ prompt?: string /** System message */ system?: string /** Model to use */ model?: string /** Temperature (0-1) */ temperature?: number /** Maximum tokens to generate */ maxTokens?: number /** Stop sequences */ stop?: string[] /** Structured output schema */ schema?: JSONSchema /** Available functions */ functions?: AIFunctionDefinition[] } /** * Result of AI generation */ export interface AIGenerateResult { /** Generated text */ text: string /** Structured output (if schema was provided) */ object?: unknown /** Function calls (if functions were provided) */ functionCalls?: AIFunctionCall[] /** Token usage */ usage?: { promptTokens: number completionTokens: number totalTokens: number } } /** * A function call made by the AI */ export interface AIFunctionCall { /** Name of the function to call */ name: string /** Arguments as JSON */ arguments: unknown } /** * AI client interface - all methods return Promise for pipelining */ export interface AIClient { /** Generate text or structured output */ generate(options: AIGenerateOptions): Promise<AIGenerateResult> /** Execute an action */ do(action: string, context?: unknown): Promise<unknown> /** Type checking / validation */ is(value: unknown, type: string | JSONSchema): Promise<boolean> /** Generate code */ code(prompt: string, language?: string): Promise<string> /** Make a decision */ decide<T extends string>(options: T[], context?: string): Promise<T> /** Generate a diagram */ diagram(description: string, format?: 'mermaid' | 'svg' | 'ascii'): Promise<string> /** Generate an image */ image(prompt: string, options?: ImageOptions): Promise<ImageResult> /** Generate a video */ video(prompt: string, options?: VideoOptions): Promise<VideoResult> /** Write/generate text content */ write(prompt: string, options?: WriteOptions): Promise<string> /** Generate a list of items with names and descriptions */ list(prompt: string): Promise<ListResult> /** Generate multiple named lists of items */ lists(prompt: string): Promise<ListsResult> } export interface ImageOptions { width?: number height?: number style?: string model?: string } export interface ImageResult { url: string base64?: string width: number height: number } export interface VideoOptions { duration?: number width?: number height?: number fps?: number model?: string } export interface VideoResult { url: string duration: number width: number height: number } export interface WriteOptions { tone?: string length?: 'short' | 'medium' | 'long' format?: 'text' | 'markdown' | 'html' } /** * Type for functions that support both regular calls and tagged template literals */ export type TemplateFunction<TArgs extends unknown[], TReturn> = (( prompt: string, ...args: TArgs ) => TReturn) & ((strings: TemplateStringsArray, ...values: unknown[]) => TReturn) /** * A single item in a list */ export interface ListItem { name: string description: string } /** * Result of the list() function */ export interface ListResult { items: ListItem[] } /** * A named list containing items */ export interface NamedList { name: string items: ListItem[] } /** * Result of the lists() function */ export interface ListsResult { lists: NamedList[] } // ============================================================================ // Function Definition Types // ============================================================================ /** * Supported programming languages for code generation */ export type CodeLanguage = 'typescript' | 'javascript' | 'python' | 'go' | 'rust' /** * Output types for generative functions */ export type GenerativeOutputType = 'string' | 'object' | 'image' | 'video' /** * Legacy channel type for backwards compatibility * @deprecated Use HumanChannel instead */ export type LegacyHumanChannel = 'slack' | 'email' | 'web' | 'sms' | 'custom' /** * Schema limitations that apply across providers * These constraints ensure compatibility with OpenAI, Anthropic, and Google */ export interface SchemaLimitations { /** OpenAI requires additionalProperties: false on all objects */ noAdditionalProperties: true /** OpenAI requires all properties in 'required' - no optional keys */ allPropertiesRequired: true /** OpenAI doesn't support default values */ noDefaultValues: true /** No recursive schema definitions (all providers) */ noRecursiveSchemas: true /** No external $ref URLs (all providers) */ noExternalRefs: true /** Anthropic doesn't support min/max on numbers */ noNumericBounds: true /** Anthropic doesn't support minLength/maxLength on strings */ noStringLengthBounds: true /** Anthropic only supports minItems of 0 or 1 */ limitedArrayMinItems: true /** Anthropic doesn't support complex types in enums */ simpleEnumsOnly: true /** Anthropic doesn't support lookahead/lookbehind in regex */ simpleRegexOnly: true } /** * Base definition shared by all function types * * @typeParam TOutput - The return type schema * @typeParam TInput - The arguments schema */ export interface BaseFunctionDefinition<TOutput = unknown, TInput = unknown> { /** Function name (used as the callable identifier) */ name: string /** Human-readable description of what this function does */ description?: string /** Arguments schema - SimpleSchema or Zod schema */ args: TInput /** Return type schema - SimpleSchema or Zod schema (optional) */ returnType?: TOutput } /** * Code function - a **deterministic** handler. * * `Code` is the deterministic kind: a fetch/transform/rule handler that runs * the **same logic every time** with no model in the execution path. When * called, it invokes the supplied {@link CodeFunctionDefinition.handler} * (or evaluates the inline {@link CodeFunctionDefinition.code} body) and * returns the result directly — no LLM is consulted at call time. * * This is the contract consumers bind to (e.g. ADR-0033's * "Code = deterministic"). If you want a model to *author* code, that is a * generation task: use {@link generateCode} / the `generate('code', …)` * primitive, or define a `Generative` function whose output is source text. * See the migration note in the package README. * * Exactly one of `handler` or `code` should be supplied. `handler` is the * canonical form (a native function reference); `code` is an inline source * body, deterministically compiled by the consumer's runtime — never sent to * a model. * * @example * ```ts * defineFunction({ * type: 'code', * name: 'calculateTax', * args: { * amount: 'The amount to calculate tax on (number)', * rate: 'Tax rate as decimal (number)', * }, * returnType: 'The calculated tax amount (number)', * handler: ({ amount, rate }) => amount * rate, * }) * ``` * * @typeParam TOutput - The return type schema * @typeParam TInput - The arguments schema */ export interface CodeFunctionDefinition<TOutput = unknown, TInput = unknown> extends BaseFunctionDefinition<TOutput, TInput> { type: 'code' /** * The deterministic handler invoked on every call. Receives the parsed * `args` and returns (or resolves to) the result. No LLM is involved. * * Either `handler` or {@link CodeFunctionDefinition.code} must be provided; * `handler` takes precedence when both are present. */ handler?: (input: TInput) => TOutput | Promise<TOutput> /** * Inline source body for the handler, as a string. Deterministically * compiled by the consumer's runtime (it is **not** generated by a model). * Used when a handler cannot be passed by reference (e.g. a definition * loaded from storage). The body receives the `args` in scope and should * `return` the result. * * Either `code` or {@link CodeFunctionDefinition.handler} must be provided. */ code?: string /** Target programming language of `code` (default: `'typescript'`). Metadata only. */ language?: CodeLanguage /** Optional human-readable note about what the handler does. Metadata only. */ instructions?: string } /** * Definition for a code-**authoring** request — the explicit, opt-in path for * having a model *write* code. This is the behavior `type: 'code'` used to * have implicitly; it has been split out so that `Code` can mean * "deterministic handler" without a silent change of meaning. * * This is **not** part of the {@link FunctionDefinition} union and never * executes deterministically — calling it consults a model. Drive it via * {@link generateCode}. * * @typeParam TInput - The arguments schema describing the code to author */ export interface CodeGenerationDefinition<TInput = unknown> { /** Name of the function/query to author */ name: string /** Human-readable description of what the code should do */ description?: string /** Arguments/spec schema describing the code to generate */ args: TInput /** Return type schema the authored code should produce (optional) */ returnType?: unknown /** Target programming language */ language?: CodeLanguage /** Additional context or requirements for code generation */ instructions?: string /** Whether to include vitest tests (default: true) */ includeTests?: boolean /** Whether to include example usage (default: true) */ includeExamples?: boolean /** Model to use for authoring (defaults to 'sonnet') */ model?: string } /** * Code function result with generated artifacts */ export interface CodeFunctionResult { /** The generated implementation code */ code: string /** Generated vitest test code */ tests?: string /** Generated example usage code */ examples?: string /** The language the code was generated in */ language: CodeLanguage /** JSDoc or equivalent documentation */ documentation: string } /** * Generative function - uses AI to generate text, objects, or media * * @example * ```ts * defineFunction({ * type: 'generative', * name: 'summarize', * args: { text: 'The text to summarize' }, * output: 'string', * system: 'You are an expert summarizer.', * promptTemplate: 'Summarize the following text:\n\n{{text}}', * }) * ``` * * @typeParam TOutput - The return type schema * @typeParam TInput - The arguments schema */ export interface GenerativeFunctionDefinition<TOutput = unknown, TInput = unknown> extends BaseFunctionDefinition<TOutput, TInput> { type: 'generative' /** What type of output this function produces */ output: GenerativeOutputType /** System prompt for the AI */ system?: string /** Prompt template with {{arg}} placeholders */ promptTemplate?: string /** Model to use (defaults to 'sonnet') */ model?: string /** Temperature for generation (0-2) */ temperature?: number } /** * Generative function result */ export interface GenerativeFunctionResult<T = unknown> { /** Generated text (if output is 'string') */ text?: string /** Generated object (if output is 'object') */ object?: T /** Generated image (if output is 'image') */ image?: ImageResult /** Generated video (if output is 'video') */ video?: VideoResult } /** * Agentic function - runs in a loop with tools until completion * * @example * ```ts * defineFunction({ * type: 'agentic', * name: 'researchTopic', * args: { topic: 'The topic to research' }, * returnType: { * summary: 'Research summary', * sources: ['List of sources'], * }, * instructions: 'Research the topic thoroughly using available tools.', * tools: [searchTool, fetchTool], * maxIterations: 10, * }) * ``` * * @typeParam TOutput - The return type schema * @typeParam TInput - The arguments schema */ export interface AgenticFunctionDefinition<TOutput = unknown, TInput = unknown> extends BaseFunctionDefinition<TOutput, TInput> { type: 'agentic' /** Instructions for the agent on how to accomplish the task */ instructions: string /** Prompt template with {{arg}} placeholders */ promptTemplate?: string /** Tools available to the agent */ tools?: AIFunctionDefinition[] /** Maximum number of iterations before stopping (default: 10) */ maxIterations?: number /** Model to use for the agent (defaults to 'sonnet') */ model?: string /** Whether to stream intermediate results */ stream?: boolean } /** * Agentic function execution state */ export interface AgenticExecutionState { /** Current iteration number */ iteration: number /** Tool calls made so far */ toolCalls: AIFunctionCall[] /** Intermediate results */ intermediateResults: unknown[] /** Whether the agent has completed */ completed: boolean /** Final result (if completed) */ result?: unknown } /** * Human function - requires human input/approval * * Generates appropriate UI/interaction for the specified channel: * - slack: Generates Slack BlockKit JSON * - email: Generates email template * - web: Generates form/UI component * - sms: Generates SMS-friendly text * - custom: Provides structured data for custom implementation * * @example * ```ts * defineFunction({ * type: 'human', * name: 'approveExpense', * args: { * amount: 'Expense amount (number)', * description: 'What the expense is for', * submitter: 'Who submitted the expense', * }, * returnType: { * approved: 'Whether the expense was approved (boolean)', * notes: 'Any notes from the approver', * }, * channel: 'workspace', * instructions: 'Review the expense request and approve or reject it.', * }) * ``` * * @typeParam TOutput - The return type schema * @typeParam TInput - The arguments schema */ export interface HumanFunctionDefinition<TOutput = unknown, TInput = unknown> extends BaseFunctionDefinition<TOutput, TInput> { type: 'human' /** How to interact with the human */ channel: HumanChannel /** Instructions shown to the human */ instructions: string /** Prompt template with {{arg}} placeholders for the request */ promptTemplate?: string /** Timeout in milliseconds (default: none - wait indefinitely) */ timeout?: number /** Who should handle this (e.g., user ID, email, channel ID) */ assignee?: string } /** * Human function result with channel-specific artifacts */ export interface HumanFunctionResult<T = unknown> { /** The human's response */ response: T /** Who responded */ respondedBy?: string /** When they responded */ respondedAt?: Date /** Channel-specific artifacts */ artifacts?: { /** Slack BlockKit JSON */ slackBlocks?: unknown[] /** Email HTML template */ emailHtml?: string /** Web form component */ webComponent?: string /** SMS message text */ smsText?: string } } /** * Union of all function definition types * * @typeParam TOutput - The return type schema * @typeParam TInput - The arguments schema */ export type FunctionDefinition<TOutput = unknown, TInput = unknown> = | CodeFunctionDefinition<TOutput, TInput> | GenerativeFunctionDefinition<TOutput, TInput> | AgenticFunctionDefinition<TOutput, TInput> | HumanFunctionDefinition<TOutput, TInput> /** * Result of defineFunction - a callable with metadata * * @typeParam TOutput - The return type of the function * @typeParam TInput - The arguments type accepted by the function */ export interface DefinedFunction<TOutput = unknown, TInput = unknown> { /** The original definition */ definition: FunctionDefinition<TOutput, TInput> /** * Call the function. * * @param args - The function arguments. * @param env - Optional host Workers env (carrying a `LOADER` worker-loader * binding, and `TEST` for the test path) used by `type: 'code'` functions * to run inline `code` bodies in ai-evaluate's sandbox. When omitted, the * inline-code path falls back to the Miniflare-backed Node runtime. Has no * effect on `handler`-based code functions or other function types. */ call: (args: TInput, env?: SandboxEnv) => Promise<TOutput> /** Get the function as a tool definition for AI */ asTool: () => AIFunctionDefinition<TOutput, TInput> } /** * Function registry for storing and retrieving defined functions * * Note: This is in-memory only. For persistence, use mdxai or mdxdb packages. */ export interface FunctionRegistry { /** Get a function by name */ get(name: string): DefinedFunction | undefined /** Set/store a function */ set(name: string, fn: DefinedFunction): void /** Check if a function exists */ has(name: string): boolean /** List all function names */ list(): string[] /** Delete a function */ delete(name: string): boolean /** Clear all functions */ clear(): void } /** * Result of auto-defining a function */ export interface AutoDefineResult { /** The determined function type */ type: 'code' | 'generative' | 'agentic' | 'human' /** Reasoning for why this type was chosen */ reasoning: string /** The complete function definition */ definition: FunctionDefinition }