Skip to content

PATTERN Cited by 2 sources

Multi-platform chat adapter with single shared agent

Definition

A factoring of chat-bot architecture in which one agent pipeline is shared across many messaging platforms (Slack, Microsoft Teams, Google Chat, Discord, Telegram, GitHub, Linear, WhatsApp, web chat), with per-platform adapter packages handling platform-specific concerns — authentication, event formats, messaging APIs, streaming semantics, formatting dialects, UI primitives, name resolution, rate limits — while the agent's handler code stays unchanged when the deployment target changes.

The anti-pattern this addresses is one bot per platform: each with its own codebase, auth flow, event loop, message rendering, and agent glue. That's the path that Vercel's 2026-04-21 Chat SDK launch post narrated as the motivation — "each of those introduced a new integration adventure for every agent." (Source: sources/2026-04-21-vercel-chat-sdk-brings-agents-to-your-users.)

Structure

Three layers:

  1. Agent pipeline — platform-agnostic. Consumes a normalised event ("new mention in thread X by user Y with text Z"), runs the model call, emits a result (text stream, structured content, or component tree).
  2. Adapter layer — one package per platform (@chat-adapter/slack, @chat-adapter/discord, @chat-adapter/whatsapp, ...). Each adapter:
  3. normalises inbound events (raw platform payload → common event shape) including bidirectional clear-text name resolution;
  4. renders outbound content (common content shape → platform-native rendering) via platform-adaptive component rendering and streaming markdown-to-native conversion;
  5. handles platform constraints (e.g. WhatsApp's [[concepts/messaging-platform-24-hour-response-window|24-hour messaging window]]);
  6. auto-detects credentials from environment variables.
  7. State layer — a pluggable adapter (Redis, ioredis, Postgres) via patterns/pluggable-state-backend providing thread subscriptions, distributed locks, and key-value cache with TTL.

Canonical shape (Vercel Chat SDK)

import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createDiscordAdapter } from "@chat-adapter/discord";
import { createRedisState } from "@chat-adapter/state-redis";

const bot = new Chat({
  adapters: {
    slack: createSlackAdapter(),
    discord: createDiscordAdapter(),
  },
  state: createRedisState(),
});

bot.onNewMention(async (thread) => {
  await thread.subscribe();
  await thread.post("Hello! I'm listening to this thread now.");
});

bot.onSubscribedMessage(async (thread, message) => {
  await thread.post(`You said: ${message.text}`);
});

The contract: event-handler callbacks (onNewMention, onSubscribedMessage) and thread-scoped helpers (thread.subscribe(), thread.post()) are defined once; adapter set is changed via constructor. "Switching from Slack to Discord means swapping the adapter, not rewriting the bot."

The streaming-composition contract

The pattern's keystone integration is that thread.post() accepts either: - a string; - an AI-SDK streamText text streamresult.textStream pipes a live LLM response directly into the adapter layer, which renders it using the platform-appropriate streaming strategy (native streaming on Slack; markdown-to-native conversion at each intermediate edit on other platforms).

const result = await streamText({
  model: "anthropic/claude-sonnet-4",
  prompt: "Summarize what's happening in this thread.",
});
await thread.post(result.textStream);

This is the join point between the AI SDK (model abstraction) and the Chat SDK (platform abstraction): two write-once-run-anywhere abstractions composed end-to-end with one line of code.

Why this factoring, not just "target one platform"

Even for single-platform deployments (agent only lives in Slack), the pattern pays off because the adapter layer normalises: - bidirectional name resolution — raw IDs in events → clear text for prompt context; clear text in agent output → raw IDs for notification firing. Without this, an agent either confuses itself with U07ABCDEF in prompts or fails to ping users on output. - context enrichment — Chat SDK "automatically includes link preview content, referenced posts, and images directly in agent prompts." - markdown-dialect conversion"models generate standard markdown, Slack does not natively support it. Chat SDK converts standard markdown to the Slack variant automatically. This conversion happens in real time, even when using Slack's native append-only streaming API."

Trade-offs

  • Lowest-common-denominator risk on UI primitives. JSX components (Table, Card, Modal, Button) fall back gracefully "if a platform doesn't support a given element", but the agent developer writes once and can't tailor for a single platform's premium affordances without ejecting.
  • Adapter lag. A new platform (e.g. a regional messenger) requires an adapter package, which may not exist. The pattern is strongest when a public adapter directory exists with community coverage.
  • Platform-constraint leakage. Not all platform constraints can be hidden. WhatsApp's 24-hour messaging window is a product-level rule; the adapter can't paper over it, so it surfaces as an SDK-level caveat (see concepts/messaging-platform-24-hour-response-window).
  • Streaming-semantics mismatch tax. Platforms without native streaming require the markdown-to-native conversion at each intermediate edit pipeline; that's more complex than Slack's native path and has different latency characteristics.
  • State-adapter correctness is now your problem. Distributed locks across bot instances (for e.g. thread-subscription-uniqueness) need a correct backend; if the backend changes from Redis to Postgres, lock semantics may differ.

Seen in

Last updated · 476 distilled / 1,218 read