惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
U
Unit 42
MyScale Blog
MyScale Blog
B
Blog
阮一峰的网络日志
阮一峰的网络日志
量子位
IT之家
IT之家
The GitHub Blog
The GitHub Blog
F
Fortinet All Blogs
Recent Announcements
Recent Announcements
V
Visual Studio Blog
G
Google Developers Blog
Last Week in AI
Last Week in AI
雷峰网
雷峰网
博客园 - 聂微东
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
J
Java Code Geeks
博客园 - 司徒正美
Y
Y Combinator Blog
T
The Blog of Author Tim Ferriss
月光博客
月光博客
aimingoo的专栏
aimingoo的专栏

Chat SDK Documentation

History | Chat SDK History | Chat SDK List a vendor-official adapter | Chat SDK Approvals | Chat SDK Vercel Connect | Chat SDK Teams Low-Level APIs | Chat SDK CLI | Chat SDK Platform Adapters | Chat SDK State Adapters | Chat SDK Cards | Chat SDK Getting Started | Chat SDK Introduction | Chat SDK Modals | Chat SDK Slack Low-Level APIs | Chat SDK Testing | Chat SDK Overview | Chat SDK toAiMessages | Chat SDK Cards | Chat SDK Overview | Chat SDK Markdown | Chat SDK Modals | Chat SDK AI SDK Tools | Chat SDK Types | Chat SDK Message Subject | Chat SDK Conversation History | Chat SDK Transcripts | Chat SDK Slack bot with Next.js and Redis Actions | Chat SDK Direct Messages | Chat SDK Emoji | Chat SDK
Streaming | Chat SDK
Vercel · 2026-05-29 · via Chat SDK Documentation

Stream real-time text responses from AI models and other async sources to chat platforms.

Chat SDK accepts any AsyncIterable<string> as a message, enabling real-time streaming of AI responses and other incremental content to chat platforms. For platforms with native or structured streaming support, you can also stream StreamChunk objects for rich content like task progress cards and plan updates.

Pass an AI SDK fullStream or textStream directly to thread.post():

import { ToolLoopAgent } from "ai";

const agent = new ToolLoopAgent({
  model: "anthropic/claude-4.5-sonnet",
  instructions: "You are a helpful assistant.",
});

bot.onNewMention(async (thread, message) => {
  const result = await agent.stream({ prompt: message.text });
  await thread.post(result.fullStream);
});

Why fullStream over textStream?

When AI SDK agents make tool calls between text steps, textStream concatenates all text without separators — "hello.how are you?" instead of "hello.\n\nhow are you?". The fullStream contains explicit finish-step events that Chat SDK uses to inject paragraph breaks between steps automatically.

Both stream types are auto-detected:

// Recommended: fullStream preserves step boundaries
await thread.post(result.fullStream);

// Also works: textStream for single-step generation
await thread.post(result.textStream);

Any async iterable works:

const stream = (async function* () {
  yield "Processing";
  yield "...";
  yield " done!";
})();

await thread.post(stream);
PlatformMethodDescription
SlackNative streaming APIUses Slack's chatStream for smooth, real-time updates
TelegramPrivate chat draft previewsUses Telegram's sendMessageDraft in private chats and falls back to post + edit elsewhere
TeamsNative (DMs) / Buffered (group chats)Uses the Teams SDK's native stream.emit() for direct messages; accumulates chunks and posts one final message when no native streamer is active
Google ChatPost + EditPosts a message then edits it as chunks arrive
DiscordPost + EditPosts a message then edits it as chunks arrive
GitHubBufferedAccumulates chunks and posts one final comment
LinearAgent sessions / Post + EditUses agent session activities in agent-session threads; falls back to post+edit comments in issue threads
WhatsAppBufferedAccumulates chunks and sends one final message
MessengerBufferedAccumulates chunks and sends one final message

The post+edit fallback throttles edits to avoid rate limits. Configure the update interval when creating your Chat instance:

const bot = new Chat({
  // ...
  streamingUpdateIntervalMs: 500, // Default: 500ms
});

Disabling the placeholder message

By default, post+edit adapters send an initial "..." placeholder message before the first chunk arrives. You can disable this to wait for real content before posting:

const bot = new Chat({
  // ...
  fallbackStreamingPlaceholderText: null,
});

You can also customize the placeholder text:

const bot = new Chat({
  // ...
  fallbackStreamingPlaceholderText: "Thinking...",
});

During streaming, chunks often arrive mid-word or mid-syntax — for example, **bold before the closing ** arrives. The SDK automatically heals incomplete markdown in intermediate renders using remend, so messages always display with correct formatting while streaming.

The final message uses the raw accumulated text without healing, so the original markdown is preserved.

When streaming content that contains GFM tables (e.g. from an LLM), the SDK automatically buffers potential table headers until a separator line (|---|---|) confirms them. This prevents tables from briefly flashing as raw pipe-delimited text before the table structure is complete.

This happens transparently — no configuration needed.

For Slack native streams and Linear agent-session streams, you can yield StreamChunk objects alongside plain text for rich progress updates:

import type { StreamChunk } from "chat";

const stream = (async function* () {
  yield { type: "markdown_text", text: "Searching..." } satisfies StreamChunk;

  yield {
    type: "task_update",
    id: "search-1",
    title: "Searching documents",
    details: "Querying internal docs and ranking the best matches",
    status: "in_progress",
  } satisfies StreamChunk;

  // ... do work ...

  yield {
    type: "task_update",
    id: "search-1",
    title: "Searching documents",
    details: "Ranked 3 relevant results",
    status: "complete",
    output: "Found 3 results",
  } satisfies StreamChunk;

  yield { type: "markdown_text", text: "Here are your results..." } satisfies StreamChunk;
})();

await thread.post(stream);

Chunk types

TypeFieldsDescription
markdown_texttextStreamed text content
task_updateid, title, status, details?, output?Tool/step progress updates (pending, in_progress, complete, error) with optional extra task context
plan_updatetitlePlan title updates on supported platforms

Streaming with options

Wrap a stream in a StreamingPlan to pass platform-specific options through thread.post() without dropping down to adapter.stream() directly:

import { StreamingPlan } from "chat";

const planned = new StreamingPlan(stream, {
  groupTasks: "plan",         // Slack: render task cards as a single grouped block
  endWith: [feedbackBlock],   // Slack: Block Kit elements appended after stream stops
  updateIntervalMs: 750,      // Post+edit cadence on supported adapters
});

await thread.post(planned);
OptionPlatformDescription
groupTasksSlack"timeline" (default) renders task cards inline; "plan" groups them into one plan block
endWithSlackBlock Kit elements attached when the stream stops (e.g. retry / feedback buttons)
updateIntervalMsPost+edit adaptersMinimum interval between post+edit cycles in ms (default 500)

Adapters without structured chunk support extract text from markdown_text chunks and ignore other types. Slack-only options are silently ignored on other platforms.

Use endWith on StreamingPlan to attach Block Kit elements to the final message. This is useful for adding action buttons after a streamed response completes:

import { StreamingPlan } from "chat";

const planned = new StreamingPlan(textStream, {
  endWith: [
    {
      type: "actions",
      elements: [{
        type: "button",
        text: { type: "plain_text", text: "Retry" },
        action_id: "retry",
      }],
    },
  ],
});

await thread.post(planned);

For step-by-step task progress that lives outside an LLM stream, post a Plan directly. Plan is a PostableObject you can mutate after posting — every mutation re-renders the block in place.

import { Plan } from "chat";

const plan = new Plan({ initialMessage: "Researching options..." });
await thread.post(plan);

const lookup = await plan.addTask({ title: "Look up customer record" });
// ...do work...
await plan.updateTask("Found 3 matches");

await plan.addTask({ title: "Summarize findings" });
await plan.complete({ completeMessage: "Done!" });

By default updateTask() mutates the most recent in_progress task. Pass { id } to target a specific task — useful when work runs in parallel or out of order:

const fetchTask = await plan.addTask({ title: "Fetch data" });
const transformTask = await plan.addTask({ title: "Transform" });

// Update a specific task by id, even if it isn't the most recent in_progress one.
await plan.updateTask({ id: fetchTask.id, output: "Got 42 rows" });
await plan.updateTask({ id: transformTask.id, status: "complete" });

Adapters that don't support PostableObject editing (e.g. WhatsApp) render the plan as a fallback emoji-list message; the plan still posts, but mutations are no-ops.

MethodDescription
addTask({ title, children? })Append a new task. The previous in-progress task is auto-completed
updateTask(input)Mutate the current (or { id }-targeted) task's output, status, or title
complete({ completeMessage })Mark all in-progress tasks complete and update the plan title
reset({ initialMessage })Discard all tasks and start fresh with a new initial message — useful when re-using a plan handle for a new run

Streaming with conversation history

Combine message history with streaming for multi-turn AI conversations. Use toAiMessages() to convert chat messages into the { role, content } format expected by AI SDKs:

import { toAiMessages } from "chat/ai";

bot.onSubscribedMessage(async (thread, message) => {
  // Fetch recent messages for context
  const result = await thread.adapter.fetchMessages(thread.id, { limit: 20 });

  const history = await toAiMessages(result.messages);

  const response = await agent.stream({ prompt: history });
  await thread.post(response.fullStream);
});

See the toAiMessages reference for all options including includeNames, transformMessage, and attachment handling.