UNPKG

@mastra/core

Version:

Mastra is a framework for building AI-powered applications and agents with a modern TypeScript stack.

439 lines 18.5 kB
import type { Chat, Adapter, CardElement, ChatConfig, Message, StateAdapter, Thread } from 'chat'; import type { Agent } from '../agent/agent.js'; import type { IMastraLogger } from '../logger/logger.js'; import type { Mastra } from '../mastra/index.js'; import type { InputProcessor, InputProcessorOrWorkflow } from '../processors/index.js'; import type { ApiRoute, CorsOptions } from '../server/types.js'; /** Message content that can be posted to a channel. */ export type PostableMessage = string | CardElement; /** Per-adapter configuration. */ export interface ChannelAdapterConfig { adapter: Adapter; /** * CORS configuration for the generated webhook route for this adapter. */ cors?: CorsOptions; /** * Start a persistent Gateway WebSocket listener for this adapter * (default: `true`). * * Only relevant for adapters that support it (e.g. Discord). * Required for receiving DMs, @mentions, and reactions. Set to `false` for * serverless deployments that only need slash commands via HTTP Interactions. */ gateway?: boolean; /** * Use rich card formatting for tool calls, approvals, and results. * Set to `false` to use plain text formatting instead. * * Some platforms (e.g. Discord) may have rendering issues with cards. * @default true */ cards?: boolean; /** * Override how tool calls are rendered in the chat. * Called once per tool invocation after the result is available. * Return `null` to suppress the message entirely. * * @default - A Card showing the function-call signature and result. */ formatToolCall?: (info: { toolName: string; args: Record<string, unknown>; result: unknown; isError?: boolean; }) => PostableMessage | null; /** * Override how errors are rendered in the chat. * Return a user-friendly message instead of exposing the raw error. * * @default `"❌ Error: <error.message>"` */ formatError?: (error: Error) => PostableMessage; } /** * Handler function for channel events. * Receives the thread, message, and the default handler implementation. * Call `defaultHandler` to run the built-in behavior, or ignore it to fully replace. */ export type ChannelHandler = (thread: Thread, message: Message, defaultHandler: (thread: Thread, message: Message) => Promise<void>) => Promise<void>; /** * Handler configuration for channel events. * - `undefined` or omitted → use default handler * - `false` → disable handler entirely * - function → custom handler (receives defaultHandler as 3rd arg to wrap/extend) */ export type ChannelHandlerConfig = ChannelHandler | false | undefined; /** Handler overrides for built-in channel event handlers. */ export interface ChannelHandlers { /** * Handler for direct messages to the bot. * Default: Routes to agent.stream and posts the response. */ onDirectMessage?: ChannelHandlerConfig; /** * Handler for @mentions of the bot in channels. * Default: Routes to agent.stream and posts the response. */ onMention?: ChannelHandlerConfig; /** * Handler for messages in subscribed threads. * Default: Routes to agent.stream and posts the response. */ onSubscribedMessage?: ChannelHandlerConfig; } /** Configuration for agent chat channels. */ export interface ChannelConfig { /** Platform adapters keyed by name (e.g. 'slack', 'discord'). */ adapters: Record<string, Adapter | ChannelAdapterConfig>; /** * Override built-in event handlers. * Use this to customize how the agent responds to DMs, mentions, etc. * * @example * ```ts * handlers: { * // Wrap the default handler with logging * onDirectMessage: async (thread, message, defaultHandler) => { * console.log('Received DM:', message.text); * await defaultHandler(thread, message); * }, * // Disable mention handling entirely * onMention: false, * } * ``` */ handlers?: ChannelHandlers; /** * Which media types to send inline to the model (as file parts). * Everything else is described as text metadata so the agent knows about the * file without crashing models that reject unsupported types. * * - **Array of globs** — e.g. `['image/png', 'image/jpeg', 'image/webp', 'application/pdf']` (default), `['image/*', 'video/*']` * - **Function** — `(mimeType: string) => boolean` * * @default `['image/png', 'image/jpeg', 'image/webp', 'application/pdf']` * * @example * ```ts * // Gemini supports video/audio natively * inlineMedia: ['image/*', 'video/*', 'audio/*'] * * // Send everything inline * inlineMedia: () => true * ``` */ inlineMedia?: string[] | ((mimeType: string) => boolean); /** * Promote URLs found in message text to file parts so the model can "see" linked * content (images, videos, PDFs, etc.) instead of just the raw URL text. * * Each entry matches a domain. When a URL in the message matches, it's added as * a `file` part alongside the text. Use a string for domains where a HEAD request * determines the Content-Type, or an object to force a specific mime type (useful * for sites like YouTube where HEAD returns `text/html` but the model treats the * URL as video). * * - **String** — domain to match; HEAD determines the mime type * - **Object** `{ match, mimeType }` — domain + forced mime type (skips HEAD) * - `'*'` — match all URLs (HEAD each one) * - `undefined` (default) — disabled, no URLs are promoted * * For string entries (or `'*'`), the resolved Content-Type is checked against * `inlineMedia` — only matching types become file parts. For object entries with * a forced `mimeType`, the file part is always added. * * @example * ```ts * // Gemini can process YouTube URLs natively as video * inlineLinks: [ * { match: 'youtube.com', mimeType: 'video/*' }, * { match: 'youtu.be', mimeType: 'video/*' }, * ] * * // HEAD-check linked images from any domain * inlineLinks: ['*'] * * // Mix: force YouTube, HEAD-check everything else * inlineLinks: [ * { match: 'youtube.com', mimeType: 'video/*' }, * 'imgur.com', * 'i.redd.it', * ] * ``` */ inlineLinks?: InlineLinkEntry[]; /** State adapter for deduplication, locking, and subscriptions. Defaults to in-memory. */ state?: StateAdapter; /** The bot's display name (default: agent's name, or `'Mastra'`). */ userName?: string; /** * Fetch recent thread messages from the platform to provide context when the agent * is mentioned mid-conversation. Only fetches on the first mention in a thread — * once subscribed, the agent has full history via Mastra's memory system. * * @example * ```ts * threadContext: { maxMessages: 15 } // Fetch more context * threadContext: { maxMessages: 0 } // Disable (opt-out) * ``` */ threadContext?: { /** * Maximum number of recent platform messages to fetch (default: 10). * Only applies to non-DM threads where the agent isn't already subscribed. * Set to 0 to disable. */ maxMessages?: number; }; /** * Whether to include channel tools (add_reaction, remove_reaction). * Set to `false` for models that don't support function calling. * * @default true */ tools?: boolean; /** * Additional options passed directly to the Chat SDK. * Use this for advanced configuration not exposed by Mastra. * * @see https://github.com/vercel/chat * @example * ```ts * chatOptions: { * dedupeTtlMs: 600000, // 10 minute deduplication window * fallbackStreamingPlaceholderText: '⏳', * } * ``` */ chatOptions?: Omit<ChatConfig, 'adapters' | 'state' | 'userName'>; } /** A single entry in the `inlineLinks` config. */ export type InlineLinkEntry = string | { match: string; mimeType: string; }; /** Check if a URL's hostname matches a domain pattern. @internal */ export declare function matchesDomain(url: string, pattern: string): boolean; export declare function extractUrls(text: string): string[]; /** * Manages a single Chat SDK instance for an agent, wiring all adapters * to the Mastra pipeline (thread mapping → agent.stream → thread.post). * * One AgentChannels = one bot identity across multiple platforms. * * @internal Created automatically by the Agent when `channels` config is provided. */ export declare class AgentChannels { readonly adapters: Record<string, Adapter>; private chat; /** Stored initialization promise so webhook handlers can await readiness on serverless cold starts. */ private initPromise; private agent; private logger?; private customState; private stateAdapter; private userName; /** Normalized per-adapter configs (gateway flags, hooks, etc.). */ private adapterConfigs; /** Handler overrides from config. */ private handlerOverrides; /** Additional Chat SDK options. */ private chatOptions; /** Thread context config for fetching prior messages. */ private threadContext; /** Determines whether a mime type should be sent inline to the model. */ private shouldInline; /** Inline-link rules for promoting URLs in message text to file parts. */ private inlineLinkRules; /** Whether channel tools (reactions, etc.) are enabled. */ private toolsEnabled; /** * The original `ChannelConfig` passed to the constructor. * * Useful for rebuilding `AgentChannels` while preserving existing adapters/handlers, * e.g. when a `ChannelProvider` wants to inject its own adapter without clobbering * adapters configured by the agent author: * * @example * ```ts * const existing = agent.getChannels(); * existing?.close(); * const next = new AgentChannels({ * ...existing?.channelConfig, * adapters: { ...existing?.channelConfig.adapters, slack: slackAdapter }, * }); * agent.setChannels(next); * ``` */ readonly channelConfig: ChannelConfig; /** Channel tool names whose effects are already visible on the platform (skip rendering cards). */ private channelToolNames; /** Platforms whose routes are managed externally (e.g., by SlackProvider). */ private externallyManagedPlatforms; /** * Per-Mastra-thread subscriptions. We lazily open one `agent.subscribeToThread()` per channel * thread on the first message we route through it, so any signals we send (and any signals * other callers send to the same thread) are rendered exactly once to the platform. The * subscription stays open until `close()` is called or the consumer errors out — we don't * eagerly subscribe at startup because the per-thread chunk consumer needs the `sdkThread` * handle, which only exists after a platform event arrives. */ private threadSubscriptions; /** * Tool-approval cards that have been clicked and are about to be resumed via `approveToolCall` / * `declineToolCall`. The resumed run's `tool-result` chunks arrive through the thread * subscription consumer rather than the click handler, so we stash the approval card's * platform `messageId` (plus the tool's display metadata) here for the consumer to pick up * when it renders the result. Entries are removed as soon as the consumer consumes them. */ private pendingApprovalCards; constructor(config: ChannelConfig); /** * Bind this AgentChannels to its owning agent. Called by Agent constructor. * @internal */ __setAgent(agent: Agent<any, any, any, any>): void; /** * Set the logger. Called by Mastra.addAgent. * @internal */ __setLogger(logger: IMastraLogger): void; /** * Register an adapter dynamically. * When `managesRoutes` is true, AgentChannels will NOT create webhook routes for this platform * (the ChannelProvider handles routing and calls handleWebhookEvent directly). * @internal */ __registerAdapter(platform: string, adapter: Adapter, config?: ChannelAdapterConfig, options?: { managesRoutes?: boolean; }): void; /** * Check if an adapter is registered for the given platform. */ hasAdapter(platform: string): boolean; /** * Get the underlying Chat SDK instance. * Available after Mastra initialization. Use this to register additional * event handlers or access adapter-specific methods. * * @example * ```ts * agent.channels.sdk.onReaction((thread, reaction) => { * console.log('Reaction received:', reaction); * }); * ``` */ get sdk(): Chat | null; /** * Initialize the Chat SDK, register handlers, and start gateway listeners. * Called by Mastra.addAgent after the server is ready. */ initialize(mastra: Mastra): Promise<void>; /** * Returns API routes for receiving webhook events from each adapter. * One POST route per adapter at `/api/agents/{agentId}/channels/{platform}/webhook`. * Skips platforms that are externally managed (e.g., by SlackProvider). */ getWebhookRoutes(): ApiRoute[]; /** * Handle a webhook event from an external source (e.g., SlackProvider). * Use this when a ChannelProvider manages its own routes but wants AgentChannels * to process the actual message handling (threading, agent responses, etc.). * * @param platform - The platform name (e.g., 'slack') * @param request - The raw HTTP request * @param options - Optional execution context for serverless environments * @returns The response from the Chat SDK webhook handler */ handleWebhookEvent(platform: string, request: Request, options?: { waitUntil?: (p: Promise<unknown>) => void; }): Promise<Response>; /** * Returns channel input processors (e.g. system prompt injection). * Skips if the user already added a processor with the same id. */ getInputProcessors(configuredProcessors?: InputProcessorOrWorkflow[]): InputProcessor[]; /** * Returns generic channel tools (send_message, add_reaction, etc.) * that resolve the target adapter from the current request context. */ getTools(): Record<string, unknown>; /** * Tear down all live thread subscriptions opened by this AgentChannels. Safe to call * multiple times. Useful for tests and for graceful shutdown of long-lived processes — * each cached subscription holds a handler in the agent's thread-stream runtime that * would otherwise stay registered for the lifetime of the process. */ close(): void; /** * Resolve the adapter for the current conversation from request context. */ private getAdapterFromContext; /** * Derive the three per-event shapes we hand off to downstream systems from one set of * inputs. Keeping this in one place ensures the LLM (`attributes`), input processors * (`requestContext`), and memory (`metadata`) all see consistent author / thread facts. * * - `channelContext` — goes on `requestContext` under the 'channel' key, consumed by * `ChatChannelProcessor` and other input processors. * - `attributes` — serialized as XML on the signal element the LLM sees (e.g. on * `<user-message messageId=... authorId=... />`). Strings only. * - `providerOptions` — written to the stored message's `content.providerMetadata` * under `mastra.channels.<platform>` so UI/query callers can read author/channel * facts off the message (e.g. show a Slack icon + author name) without unpacking * the signal envelope. The LLM ignores `providerOptions.mastra.*` since only * provider-keyed entries (openai, anthropic, …) are forwarded to the model. */ private buildEventContext; /** * Core handler wired to Chat SDK's onDirectMessage, onNewMention, * and onSubscribedMessage. Streams the Mastra agent response and * updates the channel message in real-time via edits. */ private handleChatMessage; private processChatMessage; /** * Fetch recent messages from the platform thread to provide context. * Returns messages in chronological order (oldest first), excluding the * current triggering message. */ private fetchThreadHistory; /** * Consume the agent stream and render all chunks to the chat platform. * * Iterates the outer `fullStream` to handle all chunk types: * - `text-delta`: Accumulates text and posts when flushed. * - `tool-call`: Posts a "Running…" card eagerly. * - `tool-result`: Edits the "Running…" card with the result. * - `tool-call-approval`: Edits the card to show Approve/Deny buttons. * - `step-finish` / `finish`: Flushes accumulated text. */ private editOrPost; /** * Lazily open (and cache) an `agent.subscribeToThread()` for a Mastra thread, attaching a * background chunk consumer that renders run output to the originating chat platform. We * cache by `mastraThreadId` so multiple incoming messages on the same thread share one * subscription and run output is never rendered twice. * * If the underlying consumer throws (e.g. the platform `sdkThread` becomes unusable), we * tear down the cache entry so the next message can reopen a fresh subscription. */ private ensureThreadSubscription; private consumeAgentStream; /** * Resolves an existing Mastra thread for the given external IDs, or creates one. */ private getOrCreateThread; /** * Generate generic channel tools that resolve the adapter from request context. * Tool names are platform-agnostic (e.g. `send_message`, not `discord_send_message`). */ private makeChannelTools; /** * Persistent reconnection loop for Gateway-based adapters (e.g. Discord). */ private startGatewayLoop; private log; } //# sourceMappingURL=agent-channels.d.ts.map