UNPKG

@tanstack/ai

Version:

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

472 lines (462 loc) • 17.2 kB
import { chat } from '../index' import { summarize } from '../../summarize/index' import { generateImage } from '../../generateImage/index' import { generateVideo } from '../../generateVideo/index' import { generateAudio } from '../../generateAudio/index' import { generateSpeech } from '../../generateSpeech/index' import { generateVoice } from '../../generateVoice/index' import { generateTranscription } from '../../generateTranscription/index' import { generateWorld } from '../../generateWorld/index' import { generateLiveVideo } from '../../generateLiveVideo/index' import { embed } from '../../embed/index' import { rerank } from '../../rerank/index' import { decide } from '../../evaluate/index' import type { GenerationMiddleware } from '../../middleware/types' import type { InterruptDefinition } from '../../../interrupt-definition' import type { AnyChatMiddleware } from '../middleware/types' import type { PromptCacheRetention, RunAgentResumeItem, SchemaInput, } from '../../../types' import type { DefinedAgent, SubagentContextExtension, SubagentRunContext, SubagentRunInput, } from './define-agent' import type { SubagentBudget } from './limits' import type { ProviderKeys } from '../../../byok/keyed' import type { BoundTextActivityOptions, TextActivityOptions, TextActivityResult, WithOptionalThreadId, } from '../index' import type { AnyTextAdapter } from '../adapter' import type { SummarizeActivityOptions, SummarizeActivityResult, } from '../../summarize/index' import type { SummarizeAdapter } from '../../summarize/adapter' import type { ImageActivityOptions, ImageActivityResult, } from '../../generateImage/index' import type { ImageAdapter } from '../../generateImage/adapter' import type { VideoActivityResult, VideoCreateOptions, } from '../../generateVideo/index' import type { VideoAdapter } from '../../generateVideo/adapter' import type { AudioActivityOptions, AudioActivityResult, AudioProviderOptions, } from '../../generateAudio/index' import type { AudioAdapter } from '../../generateAudio/adapter' import type { TTSActivityOptionsBase, TTSActivityResult, TTSProviderOptions, TTSTextOrTurns, } from '../../generateSpeech/index' import type { TTSAdapter } from '../../generateSpeech/adapter' import type { VoiceActivityOptions, VoiceActivityResult, VoiceProviderOptions, } from '../../generateVoice/index' import type { VoiceAdapter } from '../../generateVoice/adapter' import type { TranscriptionActivityOptions, TranscriptionActivityResult, TranscriptionProviderOptions, } from '../../generateTranscription/index' import type { TranscriptionAdapter } from '../../generateTranscription/adapter' import type { WorldActivityOptions, WorldActivityResult, WorldProviderOptions, } from '../../generateWorld/index' import type { WorldAdapter } from '../../generateWorld/adapter' import type { LiveVideoActivityOptions, LiveVideoActivityResult, LiveVideoProviderOptions, } from '../../generateLiveVideo/index' import type { LiveVideoAdapter } from '../../generateLiveVideo/adapter' import type { DebugOption } from '../../../logger/types' /** * The fields a child `chat()` needs, in one spread: * `chat({ adapter, messages, ...ctx.forward })`. */ export interface SubagentForward { threadId: string runId: string parentRunId: string subagentRunId: string /** The agent name. Middleware reads it as `ctx.subagentName`. */ subagentName: string /** The subagentRunId of the child that started this one, for a nested child. */ parentSubagentRunId?: string resume?: Array<RunAgentResumeItem> /** Aborts when the parent run or the subagent group stops. */ abortController: AbortController /** The prompt cache retention of the parent call, when it has one. */ promptCache?: PromptCacheRetention } /** * What a host adds to every activity call an agent makes through `ctx`. Apps * using plain `chat({ subagents })` do not set it. * * @internal For hosts that run agents, such as `@tanstack/ai-harness`. */ export interface SubagentBinding { /** * Added before the call's own middleware on every `ctx.chat` call. A * child's `ctx.chat({ subagents })` passes it down to its own children. */ chatMiddleware?: ReadonlyArray<AnyChatMiddleware> /** * Added after the call's own middleware on every `ctx.chat` call, so it * sees the final config and tool results (for example a host middleware * that scrubs secrets or logs results). A child's `ctx.chat({ subagents })` * passes it down to its own children, after their own. */ lastChatMiddleware?: ReadonlyArray<AnyChatMiddleware> /** * Added before the call's own middleware on every generation call. A * child's `ctx.chat({ subagents })` passes it down to its own children. */ generationMiddleware?: ReadonlyArray<GenerationMiddleware> /** * Interrupt definitions added to every `ctx.chat` call, so the host * middleware in `chatMiddleware` can stop the child with them. The call's * own `interrupts` (for example, the agent's own definitions) win: a host * definition with the same id is not added. A child's * `ctx.chat({ subagents })` passes them down to its own children. A plain * `chat()` call does not get them. */ interrupts?: ReadonlyArray<InterruptDefinition<any, any, any, any>> /** * The subagent tree budget. A child's `ctx.chat({ subagents })` passes it * down, so limits hold across the whole tree. */ budget?: SubagentBudget /** * The provider keys an agent reads as `ctx.keys`. `subagents.keys` sets * them. A child's `ctx.chat({ subagents })` passes them down to its own * children. Without them, `ctx.keys` reads each provider's `env` names. */ keys?: ProviderKeys /** * The prompt cache retention of the parent call. A child's `ctx.chat()` * uses it when the call gives no `promptCache`. */ promptCache?: PromptCacheRetention /** * The `debug` option for the model calls of a child agent that set no * `debug` of their own. A child's `ctx.chat({ subagents })` passes it down * to its own children. */ debug?: DebugOption /** * Adds fields to the `ctx` of every run. A host declares their types by * augmenting `SubagentContextExtension`. It passes down to nested children. */ extend?: (ctx: SubagentRunContext) => Partial<SubagentContextExtension> /** * Starts a child without waiting for it and returns its id at once. The * agent tools call it for `background: true`. With * `wake: true`, the host starts a new parent turn when the child ends. It * does not pass down to nested children. */ start?: ( call: { agent: string input?: unknown prompt?: string parentToolCallId?: string }, options: { wake: true }, ) => Promise<{ subagentRunId: string /** Settles when the child ends. `maxConcurrent` counts the child until then. */ done: Promise<unknown> }> } /** * Activity functions bound to one child run. Each takes the same options as * the plain function, but `threadId` is optional: the child run fills it. * Options you pass win over the bound defaults. */ export interface BoundActivities { chat: < TAdapter extends AnyTextAdapter, TSchema extends SchemaInput | undefined = undefined, TStream extends boolean = boolean, const TTools extends TextActivityOptions< TAdapter, TSchema, TStream, any >['tools'] = TextActivityOptions<TAdapter, TSchema, TStream, any>['tools'], const TInterrupts extends ReadonlyArray< InterruptDefinition<any, any, any, any> > = [], TContext = unknown, const TMiddleware extends Array<unknown> | undefined = undefined, const TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>, >( options: BoundTextActivityOptions< TAdapter, TSchema, TStream, TTools, TInterrupts, TContext, TMiddleware, TAgents >, ) => TextActivityResult<TSchema, TStream, TTools> summarize: < TAdapter extends SummarizeAdapter<string, object>, TStream extends boolean = false, >( options: WithOptionalThreadId<SummarizeActivityOptions<TAdapter, TStream>>, ) => SummarizeActivityResult<TStream> generateImage: < TAdapter extends ImageAdapter<string, any, any, any>, TStream extends boolean = false, >( options: WithOptionalThreadId<ImageActivityOptions<TAdapter, TStream>>, ) => ImageActivityResult<TStream> generateVideo: < TAdapter extends VideoAdapter<string, any, any, any, any, any>, TStream extends boolean = false, >( options: WithOptionalThreadId<VideoCreateOptions<TAdapter, TStream>>, ) => VideoActivityResult<'create', TStream> generateAudio: < TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>, TStream extends boolean = false, >( options: WithOptionalThreadId<AudioActivityOptions<TAdapter, TStream>>, ) => AudioActivityResult<TStream> generateSpeech: < TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>, TStream extends boolean = false, >( options: WithOptionalThreadId<TTSActivityOptionsBase<TAdapter, TStream>> & TTSTextOrTurns, ) => TTSActivityResult<TStream> generateVoice: < TAdapter extends VoiceAdapter<string, VoiceProviderOptions<TAdapter>>, TStream extends boolean = false, >( options: WithOptionalThreadId<VoiceActivityOptions<TAdapter, TStream>>, ) => VoiceActivityResult<TStream> generateTranscription: < TAdapter extends TranscriptionAdapter< string, TranscriptionProviderOptions<TAdapter> >, TStream extends boolean = false, >( options: WithOptionalThreadId< TranscriptionActivityOptions<TAdapter, TStream> >, ) => TranscriptionActivityResult<TStream> generateWorld: < TAdapter extends WorldAdapter<string, WorldProviderOptions<TAdapter>>, TStream extends boolean = false, >( options: WithOptionalThreadId<WorldActivityOptions<TAdapter, TStream>>, ) => WorldActivityResult<TStream> generateLiveVideo: < TAdapter extends LiveVideoAdapter< string, LiveVideoProviderOptions<TAdapter> >, TStream extends boolean = false, >( options: WithOptionalThreadId<LiveVideoActivityOptions<TAdapter, TStream>>, ) => LiveVideoActivityResult<TStream> embed: typeof embed rerank: typeof rerank decide: typeof decide } /** Options every bound call can receive. Typed loosely: each wrapper forwards * to a function whose own signature checks the caller. */ type LooseOptions = Record<string, any> & { middleware?: Array<unknown> } type AnyInterruptDefinition = InterruptDefinition<any, any, any, any> /** * The host definitions, then the call's own. The call's own definitions win: * a host definition with the same id is left out. */ function mergeInterrupts( host: ReadonlyArray<AnyInterruptDefinition>, own: ReadonlyArray<AnyInterruptDefinition> | undefined, ): Array<AnyInterruptDefinition> { const ids = new Set((own ?? []).map((definition) => definition.id)) return [ ...host.filter((definition) => !ids.has(definition.id)), ...(own ?? []), ] } export function createBoundActivities( agentName: string, input: SubagentRunInput, abortController: AbortController, binding?: SubagentBinding, ): BoundActivities { let calls = 0 let chatCalls = 0 const signal = abortController.signal const chatMiddleware = binding?.chatMiddleware ?? [] const lastChatMiddleware = binding?.lastChatMiddleware ?? [] const generationMiddleware = binding?.generationMiddleware ?? [] const interrupts = binding?.interrupts ?? [] // Every generation call gets its own run id under the child run. const ids = (activity: string) => { calls += 1 return { threadId: input.threadId, runId: `${input.runId}:${activity}-${calls}`, } } // Every generation call gets the host middleware first, and the host // `debug` when it sets no `debug` of its own. const withHost = (options: LooseOptions) => ({ ...options, debug: options.debug ?? binding?.debug, middleware: [...generationMiddleware, ...(options.middleware ?? [])], }) // Activities that accept thread and run ids plus an abort signal. const full = (activity: string, options: LooseOptions) => withHost({ ...ids(activity), abortSignal: signal, ...options, }) return { chat: ((options: LooseOptions) => chat({ messages: input.messages, threadId: input.threadId, // The first call is the child run. A later call gets its own run id. runId: chatCalls++ === 0 ? input.runId : `${input.runId}:chat-${chatCalls}`, parentRunId: input.parentRunId, subagentRunId: input.subagentRunId, subagentName: agentName, parentSubagentRunId: input.parentSubagentRunId, ...(input.resume ? { resume: input.resume } : {}), ...(binding?.promptCache ? { promptCache: binding.promptCache } : {}), abortController, ...options, // A call that sets no `debug` of its own gets the host one. debug: options.debug ?? binding?.debug, // Nested children get the host middleware before their own, share // the tree budget, and get the host keys and `debug` unless the call // sets its own. ...(options.subagents ? { subagents: { ...options.subagents, binding: { ...(binding?.keys ? { keys: binding.keys } : {}), ...(binding?.debug !== undefined ? { debug: binding.debug } : {}), ...options.subagents.binding, chatMiddleware: [ ...chatMiddleware, ...(options.subagents.binding?.chatMiddleware ?? []), ], lastChatMiddleware: [ ...(options.subagents.binding?.lastChatMiddleware ?? []), ...lastChatMiddleware, ], generationMiddleware: [ ...generationMiddleware, ...(options.subagents.binding?.generationMiddleware ?? []), ], ...(interrupts.length > 0 ? { interrupts: mergeInterrupts( interrupts, options.subagents.binding?.interrupts, ), } : {}), ...(binding?.budget ? { budget: binding.budget } : {}), ...(binding?.extend ? { extend: binding.extend } : {}), }, }, } : {}), // The host middleware can stop the call with the host interrupts. ...(interrupts.length > 0 ? { interrupts: mergeInterrupts(interrupts, options.interrupts) } : {}), middleware: [ ...chatMiddleware, ...(options.middleware ?? []), ...lastChatMiddleware, ], } as never)) as BoundActivities['chat'], summarize: ((options: LooseOptions) => summarize( full('summarize', options) as never, )) as BoundActivities['summarize'], generateImage: ((options: LooseOptions) => generateImage( full('image', options) as never, )) as BoundActivities['generateImage'], generateVideo: ((options: LooseOptions) => generateVideo( full('video', options) as never, )) as BoundActivities['generateVideo'], generateAudio: ((options: LooseOptions) => generateAudio( full('audio', options) as never, )) as BoundActivities['generateAudio'], generateSpeech: ((options: LooseOptions) => generateSpeech( full('speech', options) as never, )) as BoundActivities['generateSpeech'], generateVoice: ((options: LooseOptions) => generateVoice( full('voice', options) as never, )) as BoundActivities['generateVoice'], generateTranscription: ((options: LooseOptions) => generateTranscription( full('transcription', options) as never, )) as BoundActivities['generateTranscription'], generateWorld: ((options: LooseOptions) => generateWorld( full('world', options) as never, )) as BoundActivities['generateWorld'], generateLiveVideo: ((options: LooseOptions) => generateLiveVideo( full('liveVideo', options) as never, )) as BoundActivities['generateLiveVideo'], // `embed` takes neither ids nor an abort signal. embed: ((options: LooseOptions) => embed(withHost(options) as never)) as typeof embed, // `rerank` and `decide` take an abort signal but no ids. rerank: ((options: LooseOptions) => rerank( withHost({ abortSignal: signal, ...options }) as never, )) as typeof rerank, decide: ((options: LooseOptions) => decide( withHost({ abortSignal: signal, ...options }) as never, )) as typeof decide, } }