@mastra/core
Version:
Mastra is a framework for building AI-powered applications and agents with a modern TypeScript stack.
439 lines • 18.5 kB
TypeScript
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