UNPKG

chat

Version:

Unified chat abstraction for Slack, Teams, Google Chat, and Discord

1,231 lines (1,223 loc) 123 kB
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; import { Root } from 'mdast'; import { Z as ChatElement, C as CardElement, ac as ModalElement, ah as SelectOptionElement } from './jsx-runtime-CFq1K_Ve.js'; interface ThreadHistoryConfig { /** Maximum messages to keep per thread (default: 100) */ maxMessages?: number; /** TTL for cached history in milliseconds (default: 7 days) */ ttlMs?: number; } /** * Persistent per-thread history cache backed by the StateAdapter. * * Used by adapters that lack server-side message history APIs (e.g. WhatsApp, * Telegram). Messages are atomically appended via `state.appendToList()`, * which is safe without holding a thread lock. * * Distinct from the cross-platform per-user {@link TranscriptsApi} (see * `transcripts.ts`) — this cache is keyed by thread, not user. */ declare class ThreadHistoryCache { private readonly state; private readonly maxMessages; private readonly ttlMs; constructor(state: StateAdapter, config?: ThreadHistoryConfig); /** * Atomically append a message to the history for a thread. * Trims to maxMessages (keeps newest) and refreshes TTL. */ append(threadId: string, message: Message): Promise<void>; /** * Get messages for a thread in chronological order (oldest first). * * @param threadId - The thread ID * @param limit - Optional limit on number of messages to return (returns newest N) */ getMessages(threadId: string, limit?: number): Promise<Message[]>; } /** * Serialized channel data for passing to external systems (e.g., workflow engines). */ interface SerializedChannel { _type: "chat:Channel"; adapterName: string; channelVisibility?: ChannelVisibility; id: string; isDM: boolean; } /** * Config for creating a ChannelImpl with explicit adapter/state instances. */ interface ChannelImplConfigWithAdapter { adapter: Adapter; channelVisibility?: ChannelVisibility; id: string; isDM?: boolean; stateAdapter: StateAdapter; threadHistory?: ThreadHistoryCache; } /** * Config for creating a ChannelImpl with lazy adapter resolution. */ interface ChannelImplConfigLazy { adapterName: string; channelVisibility?: ChannelVisibility; id: string; isDM?: boolean; } type ChannelImplConfig = ChannelImplConfigWithAdapter | ChannelImplConfigLazy; declare class ChannelImpl<TState = Record<string, unknown>> implements Channel<TState> { readonly id: string; readonly isDM: boolean; readonly channelVisibility: ChannelVisibility; private _adapter?; private readonly _adapterName?; private _stateAdapterInstance?; private _name; private readonly _threadHistory?; constructor(config: ChannelImplConfig); get adapter(): Adapter; private get _stateAdapter(); get name(): string | null; get state(): Promise<TState | null>; setState(newState: Partial<TState>, options?: { replace?: boolean; }): Promise<void>; /** * Iterate messages newest first (backward from most recent). * Uses adapter.fetchChannelMessages if available, otherwise falls back * to adapter.fetchMessages with the channel ID. */ get messages(): AsyncIterable<Message>; /** * Iterate threads in this channel, most recently active first. */ threads(): AsyncIterable<ThreadSummary>; fetchMetadata(): Promise<ChannelInfo>; post<T extends PostableObject>(message: T): Promise<T>; post(message: string | AdapterPostableMessage | AsyncIterable<string> | ChatElement): Promise<SentMessage>; private handlePostableObject; private postSingleMessage; postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage | null>; schedule(message: AdapterPostableMessage | ChatElement, options: { postAt: Date; }): Promise<ScheduledMessage>; private processCallbackUrls; startTyping(status?: string): Promise<void>; mentionUser(userId: string): string; toJSON(): SerializedChannel; static fromJSON<TState = Record<string, unknown>>(json: SerializedChannel, adapter?: Adapter): ChannelImpl<TState>; static [WORKFLOW_SERIALIZE](instance: ChannelImpl): SerializedChannel; static [WORKFLOW_DESERIALIZE](data: SerializedChannel): ChannelImpl; private createSentMessage; } /** * Derive the channel ID from a thread ID. */ declare function deriveChannelId(adapter: Adapter, threadId: string): string; /** * Logger types and implementations for chat-sdk */ type LogLevel = "debug" | "info" | "warn" | "error" | "silent"; interface Logger { /** Create a sub-logger with a prefix */ child(prefix: string): Logger; debug(message: string, ...args: unknown[]): void; error(message: string, ...args: unknown[]): void; info(message: string, ...args: unknown[]): void; warn(message: string, ...args: unknown[]): void; } /** * Default console logger implementation. */ declare class ConsoleLogger implements Logger { private readonly prefix; private readonly level; constructor(level?: LogLevel, prefix?: string); private shouldLog; child(prefix: string): Logger; debug(message: string, ...args: unknown[]): void; info(message: string, ...args: unknown[]): void; warn(message: string, ...args: unknown[]): void; error(message: string, ...args: unknown[]): void; } /** * Context provided to a PostableObject after it has been posted. */ interface PostableObjectContext { adapter: Adapter; logger?: Logger; messageId: string; threadId: string; } /** * Base interface for objects that can be posted to threads/channels. * Examples: Plan, Poll, etc. * * @template TData - The data type returned by getPostData() */ interface PostableObject<TData = unknown> { /** Symbol identifying this as a postable object */ readonly $$typeof: symbol; /** * Get a fallback text representation for adapters that don't support this object type. * This should return a human-readable string representation. */ getFallbackText(): string; /** Get the data to send to the adapter */ getPostData(): TData; /** Check if the adapter supports this object type */ isSupported(adapter: Adapter): boolean; /** The kind of object - used by adapters to dispatch */ readonly kind: string; /** Called after successful posting to bind the object to the thread */ onPosted(context: PostableObjectContext): void; } /** * Type guard to check if a value is a PostableObject. */ declare function isPostableObject(value: unknown): value is PostableObject; /** * Serialized thread data for passing to external systems (e.g., workflow engines). */ interface SerializedThread { _type: "chat:Thread"; adapterName: string; channelId: string; channelVisibility?: ChannelVisibility; currentMessage?: SerializedMessage; id: string; isDM: boolean; } /** * Config for creating a ThreadImpl with explicit adapter/state instances. */ interface ThreadImplConfigWithAdapter { adapter: Adapter; channelId: string; channelVisibility?: ChannelVisibility; currentMessage?: Message; fallbackStreamingPlaceholderText?: string | null; id: string; initialMessage?: Message; isDM?: boolean; isSubscribedContext?: boolean; logger?: Logger; stateAdapter: StateAdapter; streamingUpdateIntervalMs?: number; threadHistory?: ThreadHistoryCache; } /** * Config for creating a ThreadImpl with lazy adapter resolution. * The adapter will be looked up from the Chat singleton on first access. */ interface ThreadImplConfigLazy { adapterName: string; channelId: string; channelVisibility?: ChannelVisibility; currentMessage?: Message; fallbackStreamingPlaceholderText?: string | null; id: string; initialMessage?: Message; isDM?: boolean; isSubscribedContext?: boolean; logger?: Logger; streamingUpdateIntervalMs?: number; } type ThreadImplConfig = ThreadImplConfigWithAdapter | ThreadImplConfigLazy; declare class ThreadImpl<TState = Record<string, unknown>> implements Thread<TState> { readonly id: string; readonly channelId: string; readonly isDM: boolean; readonly channelVisibility: ChannelVisibility; /** Direct adapter instance (if provided) */ private _adapter?; /** Adapter name for lazy resolution */ private readonly _adapterName?; /** Direct state adapter instance (if provided) */ private _stateAdapterInstance?; private _recentMessages; private readonly _isSubscribedContext; /** Current message context for streaming - provides userId/teamId */ private readonly _currentMessage?; /** Update interval for fallback streaming */ private readonly _streamingUpdateIntervalMs; /** Placeholder text for fallback streaming (post + edit) */ private readonly _fallbackStreamingPlaceholderText; /** Cached channel instance */ private _channel?; /** Thread history cache (set only for adapters with persistThreadHistory) */ private readonly _threadHistory?; private readonly _logger?; constructor(config: ThreadImplConfig); /** * Get the adapter for this thread. * If created with lazy config, resolves from Chat singleton on first access. */ get adapter(): Adapter; /** * Get the state adapter for this thread. * If created with lazy config, resolves from Chat singleton on first access. */ private get _stateAdapter(); get recentMessages(): Message[]; set recentMessages(messages: Message[]); /** * Get the current thread state. * Returns null if no state has been set. */ get state(): Promise<TState | null>; /** * Set the thread state. Merges with existing state by default. * State is persisted for 30 days. */ setState(newState: Partial<TState>, options?: { replace?: boolean; }): Promise<void>; /** * Get the Channel containing this thread. * Lazy-created and cached. */ get channel(): Channel<TState>; /** * Iterate messages newest first (backward from most recent). * Auto-paginates lazily. */ get messages(): AsyncIterable<Message>; get allMessages(): AsyncIterable<Message>; getParticipants(): Promise<Author[]>; isSubscribed(): Promise<boolean>; subscribe(): Promise<void>; unsubscribe(): Promise<void>; post<T extends PostableObject>(message: T): Promise<T>; post(message: string | AdapterPostableMessage | AsyncIterable<string> | ChatElement): Promise<SentMessage>; private handlePostableObject; postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage | null>; private processCallbackUrls; schedule(message: AdapterPostableMessage | ChatElement, options: { postAt: Date; }): Promise<ScheduledMessage>; /** * Handle streaming from an AsyncIterable. * Normalizes the stream (supports both textStream and fullStream from AI SDK), * then uses the adapter's stream implementation if available, otherwise falls back to post+edit. */ private handleStream; /** * Slack payloads carry the workspace ID in a few different shapes depending on * the webhook type: * - Message events: `team_id` or `team` as a string * - `block_actions` payloads: `team.id` (object), with `user.team_id` as a fallback */ private extractSlackRecipientTeamId; startTyping(status?: string): Promise<void>; /** * Fallback streaming implementation using post + edit. * Used when adapter doesn't support native streaming. * Uses recursive setTimeout to send updates every intervalMs (default 500ms). * Schedules next update only after current edit completes to avoid overwhelming slow services. */ private fallbackStream; refresh(): Promise<void>; mentionUser(userId: string): string; /** * Serialize the thread to a plain JSON object. * Use this to pass thread data to external systems like workflow engines. * * @example * ```typescript * // Pass to a workflow * await workflow.start("my-workflow", { * thread: thread.toJSON(), * message: serializeMessage(message), * }); * ``` */ toJSON(): SerializedThread; /** * Reconstruct a Thread from serialized JSON data. * * Reconstructs a ThreadImpl from serialized data. * Uses lazy resolution from Chat.getSingleton() for adapter and state. * * @param json - Serialized thread data * @requires Call `chat.registerSingleton()` before deserializing threads * * @example * ```typescript * const thread = ThreadImpl.fromJSON(serializedThread); * ``` */ static fromJSON<TState = Record<string, unknown>>(json: SerializedThread, adapter?: Adapter): ThreadImpl<TState>; /** * Serialize a ThreadImpl instance for @workflow/serde. * This static method is automatically called by workflow serialization. */ static [WORKFLOW_SERIALIZE](instance: ThreadImpl): SerializedThread; /** * Deserialize a ThreadImpl from @workflow/serde. * Uses lazy adapter resolution from Chat.getSingleton(). * Requires chat.registerSingleton() to have been called. */ static [WORKFLOW_DESERIALIZE](data: SerializedThread): ThreadImpl; private createSentMessage; createSentMessageFromMessage(message: Message): SentMessage; } /** * Represents the visibility scope of a channel. * * - `private`: Channel is only visible to invited members (e.g., private Slack channels) * - `workspace`: Channel is visible to all workspace members (e.g., public Slack channels) * - `external`: Channel is shared with external organizations (e.g., Slack Connect) * - `unknown`: Visibility cannot be determined */ type ChannelVisibility = "private" | "workspace" | "external" | "unknown"; /** * Chat configuration with type-safe adapter inference. * @template TAdapters - Record of adapter name to adapter instance */ interface ChatConfig<TAdapters extends Record<string, Adapter> = Record<string, Adapter>> { /** Map of adapter name to adapter instance */ adapters: TAdapters; /** * How to handle messages that arrive while a handler is already * processing on the same thread. * * - `'drop'` (default) — discard the message (throw `LockError`) * - `'queue'` — queue the message; when the current handler finishes, * process only the latest queued message with `context.skipped` containing * all intermediate messages * - `'debounce'` — messages inside the debounce window replace the pending * message; only the final message in that window is processed * - `'burst'` — wait once before the first handler, then process the * latest message with `context.skipped` containing earlier burst messages * - `'concurrent'` — no locking; all messages processed in parallel * - `ConcurrencyConfig` — fine-grained control over strategy and parameters */ concurrency?: ConcurrencyStrategy | ConcurrencyConfig; /** * TTL for message deduplication entries in milliseconds. * Defaults to 300000 (5 minutes). Increase if your webhook cold starts * cause platform retries that arrive after the default TTL expires. */ dedupeTtlMs?: number; /** * Placeholder text for fallback streaming (post + edit) adapters. * Defaults to `"..."`. * * Set to `null` to avoid posting an initial placeholder message and instead * wait until some real text has been streamed before creating the message. */ fallbackStreamingPlaceholderText?: string | null; /** * Resolves a stable cross-platform user key from inbound messages. * * Required when `transcripts` is configured. Called once per inbound * message during dispatch; the result is attached to the Message * instance as `message.userKey` for handlers to use. */ identity?: IdentityResolver; /** * Lock scope determines which messages contend for the same lock. * * - `'thread'`: lock per threadId (default for most adapters) * - `'channel'`: lock per channelId (default for WhatsApp, Telegram) * - function: resolve scope dynamically per message (async supported) * * When not set, falls back to the adapter's `lockScope` property, * then to `'thread'`. */ lockScope?: LockScope | ((context: LockScopeContext) => LockScope | Promise<LockScope>); /** * Logger instance or log level. * Pass "silent" to disable all logging. */ logger?: Logger | LogLevel; /** * @deprecated Renamed to {@link ChatConfig.threadHistory}. Both fields are * read for backwards compatibility; `threadHistory` takes precedence when * both are set. */ messageHistory?: { maxMessages?: number; ttlMs?: number; }; /** * @deprecated Use `concurrency` instead. * * Behavior when a thread lock cannot be acquired (another handler is processing). * - `'drop'` (default) — throw `LockError`, preserving current behavior * - `'force'` — force-release the existing lock and re-acquire * - callback — custom logic receiving `(threadId, message)`, return `'force'` or `'drop'` * * When `'force'` is used, the previous handler continues executing — only the lock * is released, not the handler itself. This means two handlers may run concurrently * on the same thread. The old handler's `releaseLock()` call becomes a no-op since * the token no longer matches. */ onLockConflict?: "force" | "drop" | ((threadId: string, message: Message) => "force" | "drop" | Promise<"force" | "drop">); /** State adapter for subscriptions and locking */ state: StateAdapter; /** * Update interval for fallback streaming (post + edit) in milliseconds. * Defaults to 500ms. Lower values provide smoother updates but may hit rate limits. */ streamingUpdateIntervalMs?: number; /** * Configuration for persistent per-thread message history backfill. * * Only used by adapters that set `persistThreadHistory: true` (e.g. * Telegram, WhatsApp). Distinct from `transcripts` (the cross-platform * per-user Transcripts API). */ threadHistory?: { /** Maximum messages to store per thread (default: 100) */ maxMessages?: number; /** TTL for cached history in milliseconds (default: 7 days) */ ttlMs?: number; }; /** * Cross-platform per-user message persistence. * * When set, `chat.transcripts` is available for append/list/count/delete * keyed by a resolved cross-platform user key. * * Requires `identity` to also be set; the constructor throws otherwise. */ transcripts?: TranscriptsConfig; /** Default bot username across all adapters */ userName: string; } /** * Options for webhook handling. */ interface WebhookOptions { /** * Override the default modal-opening behavior to handle it inline * within the current webhook response cycle. * When provided, called instead of adapter.openModal(). * Used by Teams to return modal content in the HTTP invoke response. * * The returned `viewId` is platform-specific (e.g. Slack's view ID). * Adapters that don't produce a view ID may return void. */ onOpenModal?: (modal: ModalElement, contextId: string) => Promise<{ viewId: string; } | undefined>; /** * Function to run message handling in the background. * Use this to ensure fast webhook responses while processing continues. * * @example * // Next.js App Router * import { after } from "next/server"; * chat.webhooks.slack(request, { waitUntil: (p) => after(() => p) }); * * @example * // Vercel Functions * import { waitUntil } from "@vercel/functions"; * chat.webhooks.slack(request, { waitUntil }); */ waitUntil?: (task: Promise<unknown>) => void; } /** * Adapter interface with generics for platform-specific types. * @template TThreadId - Platform-specific thread ID data type * @template TRawMessage - Platform-specific raw message type */ interface Adapter<TThreadId = unknown, TRawMessage = unknown> { /** Add a reaction to a message */ addReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>; /** Bot user ID for platforms that use IDs in mentions (e.g., Slack's <@U123>) */ readonly botUserId?: string; /** * Derive channel ID from a thread ID. * Default fallback: first two colon-separated parts (e.g., "slack:C123"). * Adapters with different structures should override this. */ channelIdFromThreadId(threadId: string): string; /** Decode thread ID string back to platform-specific data */ decodeThreadId(threadId: string): TThreadId; /** Delete a message */ deleteMessage(threadId: string, messageId: string): Promise<void>; /** Cleanup hook called when Chat instance is shutdown */ disconnect?(): Promise<void>; /** Edit an existing message */ editMessage(threadId: string, messageId: string, message: AdapterPostableMessage): Promise<RawMessage<TRawMessage>>; /** * Edit a previously posted object (Plan, Poll, etc.). * If not implemented, object updates will throw PlanNotSupportedError. * * @param threadId - The thread containing the message * @param messageId - The message ID to edit * @param kind - The object kind (e.g., "plan") * @param data - The object data (type depends on kind) */ editObject?(threadId: string, messageId: string, kind: string, data: unknown): Promise<RawMessage<TRawMessage>>; /** Encode platform-specific data into a thread ID string */ encodeThreadId(platformData: TThreadId): string; /** * Fetch channel info/metadata. */ fetchChannelInfo?(channelId: string): Promise<ChannelInfo>; /** * Fetch channel-level messages (top-level, not thread replies). * For example, Slack's conversations.history vs conversations.replies. */ fetchChannelMessages?(channelId: string, options?: FetchOptions): Promise<FetchResult<TRawMessage>>; /** * Fetch a single message by ID. * Optional - adapters that don't implement this will return null. * * @param threadId - The thread ID containing the message * @param messageId - The platform-specific message ID * @returns The message, or null if not found/not supported */ fetchMessage?(threadId: string, messageId: string): Promise<Message<TRawMessage> | null>; /** * Fetch messages from a thread. * * **Direction behavior:** * - `backward` (default): Fetches the most recent messages. Use this for loading * a chat view. The `nextCursor` points to older messages. * - `forward`: Fetches the oldest messages first. Use this for iterating through * message history. The `nextCursor` points to newer messages. * * **Message ordering:** * Messages within each page are always returned in chronological order (oldest first), * regardless of direction. This is the natural reading order for chat messages. * * @example * ```typescript * // Load most recent 50 messages for display * const recent = await adapter.fetchMessages(threadId, { limit: 50 }); * // recent.messages: [older, ..., newest] in chronological order * * // Paginate backward to load older messages * const older = await adapter.fetchMessages(threadId, { * limit: 50, * cursor: recent.nextCursor, * }); * * // Iterate through all history from the beginning * const history = await adapter.fetchMessages(threadId, { * limit: 100, * direction: 'forward', * }); * ``` */ fetchMessages(threadId: string, options?: FetchOptions): Promise<FetchResult<TRawMessage>>; fetchSubject?(raw: TRawMessage): Promise<MessageSubject | null>; /** Fetch thread metadata */ fetchThread(threadId: string): Promise<ThreadInfo>; /** * Get the visibility scope of a channel containing the thread. * * This distinguishes between private channels, workspace-visible channels, * and externally shared channels (e.g., Slack Connect). * * @param threadId - The thread ID to check * @returns The channel visibility scope */ getChannelVisibility?(threadId: string): ChannelVisibility; /** * Look up user information by user ID. * Optional — not all platforms support this. * * @param userId - Platform-specific user ID * @returns User info, or null if user not found */ getUser?(userId: string): Promise<UserInfo | null>; /** Handle incoming webhook request */ handleWebhook(request: Request, options?: WebhookOptions): Promise<Response>; /** Called when Chat instance is created (internal use) */ initialize(chat: ChatInstance): Promise<void>; /** * Check if a thread is a direct message conversation. * * @param threadId - The thread ID to check * @returns True if the thread is a DM, false otherwise */ isDM?(threadId: string): boolean; /** * List threads in a channel. */ listThreads?(channelId: string, options?: ListThreadsOptions): Promise<ListThreadsResult<TRawMessage>>; /** * Default lock scope for this adapter. * - `'thread'` (default): lock per threadId * - `'channel'`: lock per channelId (for channel-based platforms like WhatsApp, Telegram) * * Can be overridden by `ChatConfig.lockScope`. */ readonly lockScope?: LockScope; /** Unique name for this adapter (e.g., "slack", "teams") */ readonly name: string; /** * Optional hook called when a thread is subscribed to. * Adapters can use this to set up platform-specific subscriptions * (e.g., Google Chat Workspace Events). */ onThreadSubscribe?(threadId: string): Promise<void>; /** * Open a direct message conversation with a user. * * @param userId - The platform-specific user ID * @returns The thread ID for the DM conversation * * @example * ```typescript * const dmThreadId = await adapter.openDM("U123456"); * await adapter.postMessage(dmThreadId, "Hello!"); * ``` */ openDM?(userId: string): Promise<string>; /** * Open a modal/dialog form. * * @param triggerId - Platform-specific trigger ID from the action event * @param modal - The modal element to display * @param contextId - Optional context ID for server-side stored thread/message context * @returns The view/dialog ID */ openModal?(triggerId: string, modal: ModalElement, contextId?: string): Promise<{ viewId: string; }>; /** Parse platform message format to normalized format */ parseMessage(raw: TRawMessage): Message<TRawMessage>; /** * @deprecated Renamed to {@link Adapter.persistThreadHistory}. Both flags * are read for backwards compatibility; either being `true` enables * persistence. */ readonly persistMessageHistory?: boolean; /** * When true, the SDK persists per-thread message history in the state * adapter for this platform. Use for platforms that lack server-side * message history APIs (e.g. WhatsApp, Telegram). */ readonly persistThreadHistory?: boolean; /** * Post a message to channel top-level (not in a thread). */ postChannelMessage?(channelId: string, message: AdapterPostableMessage): Promise<RawMessage<TRawMessage>>; /** * Post an ephemeral message visible only to a specific user. * * This is optional - if not implemented, Thread.postEphemeral will * fall back to openDM + postMessage when fallbackToDM is true. * * @param threadId - The thread to post in * @param userId - The user who should see the message * @param message - The message content * @returns EphemeralMessage with usedFallback: false */ postEphemeral?(threadId: string, userId: string, message: AdapterPostableMessage): Promise<EphemeralMessage<TRawMessage>>; /** Post a message to a thread */ postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage<TRawMessage>>; /** * Post a special object (Plan, Poll, etc.) as a single message. * If not implemented, posting such objects will throw PlanNotSupportedError. * * @param threadId - The thread to post to * @param kind - The object kind (e.g., "plan") * @param data - The object data (type depends on kind) */ postObject?(threadId: string, kind: string, data: unknown): Promise<RawMessage<TRawMessage>>; /** * Reconstruct fetchData on an attachment after deserialization. * Called during message rehydration for queue/debounce strategies. * Uses fetchMetadata and adapter auth context to rebuild the download closure. */ rehydrateAttachment?(attachment: Attachment): Attachment; /** Remove a reaction from a message */ removeReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>; /** Render formatted content to platform-specific string */ renderFormatted(content: FormattedContent): string; /** * Schedule a message for future delivery. * * Optional — only supported by adapters with native scheduling APIs (e.g., Slack). * Thread.schedule() will throw NotImplementedError if this method is absent. * * @param threadId - The thread to post in * @param message - The message content * @param options - Scheduling options including the target delivery time * @returns A ScheduledMessage with cancel() capability */ scheduleMessage?(threadId: string, message: AdapterPostableMessage, options: { postAt: Date; }): Promise<ScheduledMessage<TRawMessage>>; /** Show typing indicator */ startTyping(threadId: string, status?: string): Promise<void>; /** * Stream a message using platform-native streaming APIs. * * The adapter consumes the async iterable and handles the entire streaming lifecycle. * Only available on platforms with native streaming support (e.g., Slack). * * The stream can yield plain strings (text chunks) or {@link StreamChunk} objects * for rich content like task progress cards. Adapters that don't support structured * chunks will extract text from `markdown_text` chunks and ignore other types. * * @param threadId - The thread to stream to * @param textStream - Async iterable of text chunks or structured StreamChunk objects * @param options - Platform-specific streaming options * @returns The raw message after streaming completes */ stream?(threadId: string, textStream: AsyncIterable<string | StreamChunk>, options?: StreamOptions): Promise<RawMessage<TRawMessage>>; /** Bot username (can override global userName) */ readonly userName: string; } /** * A structured streaming chunk for platform-native rich content. * * On Slack, these map directly to streaming chunk types: * - `markdown_text`: Streamed text content * - `task_update`: Tool/step progress cards (pending → in_progress → complete → error) * - `plan_update`: Plan title updates * * Adapters that don't support structured chunks will extract `text` from * `markdown_text` chunks and ignore other types gracefully. */ type StreamChunk = MarkdownTextChunk | TaskUpdateChunk | PlanUpdateChunk; interface MarkdownTextChunk { text: string; type: "markdown_text"; } interface TaskUpdateChunk { details?: string; id: string; output?: string; status: "pending" | "in_progress" | "complete" | "error"; title: string; type: "task_update"; } interface PlanUpdateChunk { title: string; type: "plan_update"; } /** * Options for streaming messages. * Platform-specific options are passed through to the adapter. */ interface StreamOptions { /** Slack: The team/workspace ID */ recipientTeamId?: string; /** Slack: The user ID to stream to (for AI assistant context) */ recipientUserId?: string; /** Block Kit elements to attach when stopping the stream (Slack only, via chat.stopStream) */ stopBlocks?: unknown[]; /** * Slack: Controls how task_update chunks are displayed. * - `"timeline"` — individual task cards shown inline with text (default) * - `"plan"` — all tasks grouped into a single plan block */ taskDisplayMode?: "timeline" | "plan"; /** Minimum interval between updates in ms (default: 1000). Used for fallback mode (GChat/Teams). */ updateIntervalMs?: number; } /** Internal interface for Chat instance passed to adapters */ interface ChatInstance { /** Get the configured logger, optionally with a child prefix */ getLogger(prefix?: string): Logger; getState(): StateAdapter; getUserName(): string; /** * @deprecated Use processMessage instead. This method is for internal use. */ handleIncomingMessage(adapter: Adapter, threadId: string, message: Message): Promise<void>; /** * Process an incoming action event (button click) from an adapter. * Handles waitUntil registration and error catching internally. * * @param event - The action event (without thread field, will be added) * @param options - Webhook options including waitUntil */ processAction(event: Omit<ActionEvent, "thread" | "openModal"> & { adapter: Adapter; }, options: WebhookOptions | undefined): Promise<void>; processAppHomeOpened(event: AppHomeOpenedEvent, options?: WebhookOptions): void; processAssistantContextChanged(event: AssistantContextChangedEvent, options?: WebhookOptions): void; processAssistantThreadStarted(event: AssistantThreadStartedEvent, options?: WebhookOptions): void; processMemberJoinedChannel(event: MemberJoinedChannelEvent, options?: WebhookOptions): void; /** * Process an incoming message from an adapter. * Handles waitUntil registration and error catching internally. * * @param adapter - The adapter that received the message * @param threadId - The thread ID * @param message - Either a parsed message, or a factory function for lazy async parsing * @param options - Webhook options including waitUntil */ processMessage(adapter: Adapter, threadId: string, message: Message | (() => Promise<Message>), options?: WebhookOptions): Promise<void>; /** * Process a modal close event from an adapter. * * @param event - The modal close event (without relatedThread/relatedMessage/relatedChannel) * @param contextId - Context ID for retrieving stored thread/message/channel context * @param options - Webhook options */ processModalClose(event: Omit<ModalCloseEvent, "relatedThread" | "relatedMessage" | "relatedChannel">, contextId?: string, options?: WebhookOptions): void; /** * Process a modal submit event from an adapter. * * @param event - The modal submit event (without relatedThread/relatedMessage/relatedChannel) * @param contextId - Context ID for retrieving stored thread/message/channel context * @param options - Webhook options */ processModalSubmit(event: Omit<ModalSubmitEvent, "relatedThread" | "relatedMessage" | "relatedChannel">, contextId?: string, options?: WebhookOptions): Promise<ModalResponse | undefined>; /** * Process an interactive options load event from an adapter. * Returns normalized select options for the adapter to render. */ processOptionsLoad(event: OptionsLoadEvent, options?: WebhookOptions): Promise<OptionsLoadResult | undefined>; /** * Process an incoming reaction event from an adapter. * Handles waitUntil registration and error catching internally. * * @param event - The reaction event (without adapter field, will be added) * @param options - Webhook options including waitUntil */ processReaction(event: Omit<ReactionEvent, "adapter" | "thread"> & { adapter?: Adapter; }, options?: WebhookOptions): void; /** * Process an incoming slash command from an adapter. * Handles waitUntil registration and error catching internally. * * @param event - The slash command event * @param options - Webhook options including waitUntil */ processSlashCommand(event: Omit<SlashCommandEvent, "channel" | "openModal"> & { adapter: Adapter; channelId: string; }, options: WebhookOptions | undefined): void; /** * Cross-platform per-user transcript store. Throws on access when * `transcripts` is not configured on the Chat instance — callers should * check `ChatConfig.transcripts` if they need a no-throw guard. */ readonly transcripts: TranscriptsApi; } /** Lock scope determines which messages contend for the same lock. */ type LockScope = "thread" | "channel"; /** Context provided to the lockScope resolver function. */ interface LockScopeContext { adapter: Adapter; channelId: string; isDM: boolean; threadId: string; } /** Concurrency strategy for overlapping messages on the same thread. */ type ConcurrencyStrategy = "drop" | "queue" | "debounce" | "burst" | "concurrent"; /** Fine-grained concurrency configuration. */ interface ConcurrencyConfig { /** Debounce window in milliseconds (debounce/burst strategies). Default: 1500. */ debounceMs?: number; /** Max concurrent handlers per thread (concurrent strategy). Default: Infinity. */ maxConcurrent?: number; /** Max queued messages per thread (queue/burst strategy). Default: 10. */ maxQueueSize?: number; /** What to do when queue is full. Default: 'drop-oldest'. */ onQueueFull?: "drop-oldest" | "drop-newest"; /** TTL for queued entries in milliseconds. Default: 90000 (90s). */ queueEntryTtlMs?: number; /** The concurrency strategy to use. */ strategy: ConcurrencyStrategy; } /** * An entry in the per-thread message queue. * Used by the `queue`, `debounce`, and `burst` concurrency strategies. */ interface QueueEntry { /** When this entry was enqueued (Unix ms). */ enqueuedAt: number; /** When this entry expires (Unix ms). Stale entries are discarded on dequeue. */ expiresAt: number; /** The queued message. */ message: Message; } /** * Context provided to message handlers when messages were queued * while a previous handler was running or while waiting for burst. */ interface MessageContext { /** * Messages that arrived while the previous handler was running, * in chronological order, excluding the current message (which is the latest). */ skipped: Message[]; /** Total messages received since last handler ran (skipped.length + 1). */ totalSinceLastHandler: number; } interface StateAdapter { /** Acquire a lock on a thread (returns null if already locked) */ acquireLock(threadId: string, ttlMs: number): Promise<Lock | null>; /** Atomically append a value to a list. Trims to maxLength (keeping newest). Refreshes TTL. */ appendToList(key: string, value: unknown, options?: { maxLength?: number; ttlMs?: number; }): Promise<void>; /** Connect to the state backend */ connect(): Promise<void>; /** Delete a cached value */ delete(key: string): Promise<void>; /** Pop the next message from the thread's queue. Returns null if empty. */ dequeue(threadId: string): Promise<QueueEntry | null>; /** Disconnect from the state backend */ disconnect(): Promise<void>; /** Atomically append a message to the thread's pending queue. Returns new queue depth. */ enqueue(threadId: string, entry: QueueEntry, maxSize: number): Promise<number>; /** Extend a lock's TTL */ extendLock(lock: Lock, ttlMs: number): Promise<boolean>; /** * Force-release a lock on a thread, regardless of ownership token. * The previous lock holder's handler continues running — only the lock is released. * The old handler's `releaseLock()` becomes a no-op (token mismatch). */ forceReleaseLock(threadId: string): Promise<void>; /** Get a cached value by key */ get<T = unknown>(key: string): Promise<T | null>; /** Read all values from a list in insertion order. Returns empty array if key does not exist. */ getList<T = unknown>(key: string): Promise<T[]>; /** Check if subscribed to a thread */ isSubscribed(threadId: string): Promise<boolean>; /** Get the current queue depth for a thread. */ queueDepth(threadId: string): Promise<number>; /** Release a lock */ releaseLock(lock: Lock): Promise<void>; /** Set a cached value with optional TTL in milliseconds */ set<T = unknown>(key: string, value: T, ttlMs?: number): Promise<void>; /** Atomically set a value only if the key does not already exist. Returns true if set, false if key existed. */ setIfNotExists(key: string, value: unknown, ttlMs?: number): Promise<boolean>; /** Subscribe to a thread (persists across restarts) */ subscribe(threadId: string): Promise<void>; /** Unsubscribe from a thread */ unsubscribe(threadId: string): Promise<void>; } interface Lock { expiresAt: number; threadId: string; token: string; } /** * Base interface for entities that can receive messages. * Both Thread and Channel extend this interface. * * @template TState - Custom state type stored per entity * @template TRawMessage - Platform-specific raw message type */ interface Postable<TState = Record<string, unknown>, TRawMessage = unknown> { /** The adapter this entity belongs to */ readonly adapter: Adapter; /** The visibility scope of this channel */ readonly channelVisibility: ChannelVisibility; /** Unique ID */ readonly id: string; /** Whether this is a direct message conversation */ readonly isDM: boolean; /** * Get a platform-specific mention string for a user. */ mentionUser(userId: string): string; /** * Iterate messages newest first (backward from most recent). * Auto-paginates lazily — only fetches pages as consumed. */ readonly messages: AsyncIterable<Message<TRawMessage>>; /** * Post a message. */ post<T extends PostableObject>(message: T): Promise<T>; post(message: string | PostableMessage | ChatElement): Promise<SentMessage<TRawMessage>>; /** * Post an ephemeral message visible only to a specific user. */ postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage<TRawMessage> | null>; /** * Schedule a message for future delivery. * * Currently only supported by the Slack adapter. Other adapters * will throw NotImplementedError. * * @param message - The message content (streaming not supported) * @param options - Scheduling options including the target delivery time * @returns A ScheduledMessage with cancel() capability * * @example * ```typescript * const scheduled = await thread.schedule("Reminder: standup!", { * postAt: new Date("2026-03-09T09:00:00Z"), * }); * * // Cancel before it's sent * await scheduled.cancel(); * ``` */ schedule(message: AdapterPostableMessage | ChatElement, options: { postAt: Date; }): Promise<ScheduledMessage<TRawMessage>>; /** * Set the state. Merges with existing state by default. */ setState(state: Partial<TState>, options?: { replace?: boolean; }): Promise<void>; /** Show typing indicator */ startTyping(status?: string): Promise<void>; /** * Get the current state. * Returns null if no state has been set. */ readonly state: Promise<TState | null>; } /** * Represents a channel/conversation container that holds threads. * Extends Postable for message posting capabilities. * * @template TState - Custom state type stored per channel * @template TRawMessage - Platform-specific raw message type */ interface Channel<TState = Record<string, unknown>, TRawMessage = unknown> extends Postable<TState, TRawMessage> { /** Fetch channel metadata from the platform */ fetchMetadata(): Promise<ChannelInfo>; /** Channel name (e.g., "#general"). Null until fetchInfo() is called. */ readonly name: string | null; /** * Iterate threads in this channel, most recently active first. * Returns ThreadSummary (lightweight) for efficiency. * Empty iterable on threadless platforms. */ threads(): AsyncIterable<ThreadSummary<TRawMessage>>; /** * Serialize the channel to a plain JSON object. * Use this to pass channel data to external systems like workflow engines. */ toJSON(): SerializedChannel; } /** * Lightweight summary of a thread within a channel. */ interface ThreadSummary<TRawMessage = unknown> { /** Full thread ID */ id: string; /** Timestamp of most recent reply */ lastReplyAt?: Date; /** Reply count (if available) */ replyCount?: number; /** Root/first message of the thread */ rootMessage: Message<TRawMessage>; } /** * Channel metadata returned by fetchInfo(). */ interface ChannelInfo { /** The visibility scope of this channel */ channelVisibility?: ChannelVisibility; id: string; isDM?: boolean; memberCount?: number; metadata: Record<string, unknown>; name?: string; } /** * Options for listing threads in a channel. */ interface ListThreadsOptions { cursor?: string; limit?: number; } /** * Result of listing threads in a channel. */ interface ListThreadsResult<TRawMessage = unknown> { nextCursor?: string; threads: ThreadSummary<TRawMessage>[]; } /** Default TTL for thread state (30 days in milliseconds) */ declare const THREAD_STATE_TTL_MS: number; /** * Thread interface with support for custom state. * Extends Postable for shared message posting capabilities. * * @template TState - Custom state type stored per-thread (default: Record<string, unknown>) * @template TRawMessage - Platform-specific raw message type */ interface Thread<TState = Record<string, unknown>, TRawMessage = unknown> extends Postable<TState, TRawMessage> { /** * Async iterator for all messages in the thread. * Messages are yielded in chronological order (oldest first). * Automatically handles pagination. */ allMessages: AsyncIterable<Message<TRawMessage>>; /** Get the Channel containing this thread */ readonly channel: Channel<TState, TRawMessage>; /** Channel/conversation ID */ readonly channelId: string; /** * Wrap a Message object as a SentMessage with edit/delete capabilities. * Used internally for reconstructing messages from serialized data. */ createSentMessageFromMessage(message: Message<TRawMessage>): SentMessage<TRawMessage>; /** * Get the unique human participants in this thread. * * Scans all messages in the thread and returns deduplicated authors, * excluding the bot itself. Useful for deciding whether to subscribe * based on how many humans are participating — subscribe when it's a * 1:1 conversation, unsubscribe when others join so humans can talk * without the bot replying to every message. * * @returns Array of unique non-bot authors * * @example * ```typescript * // Subscribe only when one person is talking to the bot * bot.onNewMention(async (thread, message) => { * const participants = await thread.getParticipants(); * if (participants.length === 1) { * await thread.subscribe(); * await thread.post("I'm here to help!"); * } * }); * * // Unsubscribe when the thread becomes a group conversation * bot.onSubscribedMessage(async (thread, message) => { * const participants = await thread.getParticipants(); * if (participants.length > 1) { * await thread.unsubscribe(); * return; * } * await thread.post("Still here to help!"); * }); * ``` */ getParticipants(): Promise<Author[]>; /** * Check if this thread is currently subscribed. * * In subscribed message handlers, this is optimized to return true immediately * without a state lookup, since we already know we're in a subscribed context. * * @returns Promise resolving to true if subscribed, false otherwise */ isSubscribed(): Promise<boolean>; /** * Get a platform-specific mention string for a user. * Use this to @-mention a user in a message. * @example * await thread.post(`Hey ${thread.mentionUser(userId)}, check this out!`); */ mentionUser(userId: string): string; /** * Post a message to this thread. * * Supports text, markdown, cards, and streaming from async iterables. * When posting a stream (e.g., from AI SDK), uses platform-native streaming * APIs when available (Slack), or falls back to post + edit with throttling. * * @param message - String, PostableMessage, JSX Card, or AsyncIterable<string> * @returns A SentMessage with methods to edit, delete, or add reactions * * @example