Slack のエージェントボットで、処理中にボット名をキラキラさせてステータスを表示する
Claude の Slack 連携や、Nous Research の Hermes Agent を Slack で動かすと、応答を作っている間だけボット名の横がキラキラと光り、「is thinking...」のような文言が表示される。同じ表示を自作のエージェントボットで出す方法をまとめる。
Slack アプリを「App」ではなく「Agent」として設定する手順から、ステータス表示の実装までを扱う。コードは Bolt for JavaScript を前提とする。
実際に動かすとこうなる。ボット名の横のステータス行が「考え中…」から「調べています…」へ変わり、応答が返ると消える。
TL;DR
- あの表示は
assistant.threads.setStatusが出すステータス行を Slack クライアントがアニメーション描画したもの。API から指定するのは表示する文言。 - 表示するには Slack アプリを Agents & AI Apps (エージェントモード) として設定し、
assistant:writeまたはchat:writeスコープを付ける。 - 新しい UI (
agent_view) では DM がmessage.imイベントとして届くため、client.assistant.threads.setStatus()を直接呼ぶ。 - ステータスは最後の更新から約2分で自動的に消えるので、実行中は定期的に送り直して維持する。
- API 呼び出しは直列化して、クリアが必ず最後の書き込みになるようにする。
- 実行中のツール名に応じて文言を差し替えると、「コマンドを実行しています…」のような実況になる。
1. ステータス行の仕組み
assistant.threads.setStatus でステータス文字列をセットすると、Slack は会話の中に <アプリ名> <ステータス文言> という行を描画する。この行にアニメーションが付いているため、アプリ名の部分が光って見える。API から制御するのは文言で、視覚効果は Slack クライアント側が担当する。
オープンソースの Hermes Agent の Slack アダプタ (plugins/platforms/slack/adapter.py) にも、同じ API を使っていることが明記されている。
async def send_typing(self, chat_id: str, metadata=None) -> None:
"""Show a typing/status indicator using assistant.threads.setStatus.
Displays "is thinking..." next to the bot name in a thread, ...
なお、2025年10月に追加されたメッセージのストリーミング API (chat.startStream / chat.appendStream / chat.stopStream) は本文を少しずつ配信するための別機能で、ステータス行とは独立している。
2. Slack アプリを「App」ではなく「Agent」として設定する
ステータス行を出すには、アプリをエージェントとして設定する必要がある。https://api.slack.com/apps でアプリを開いて以下を設定する。
2-1. Socket Mode を有効化 (任意)
Socket Mode は wss://wss-primary.slack.com への outbound WebSocket で通信する方式。インバウンドポートを公開しなくてよいので、自宅 LAN 内やローカルマシンでもそのまま動く。
- Settings → Socket Mode → Enable Socket Mode
- App-Level Token を作成 (scope:
connections:write) →xapp-...を控える
有効にすると、イベントは Request URL (HTTP) ではなく WebSocket 側に配送される。HTTP エンドポイントを公開できる環境なら不要。
2-2. エージェントモードを有効化
ここが「App ではなく Agent にする」本体。
- Features → Agents & AI Apps (以前は Assistant という名前) → Enable
- App Home → Messages Tab (Chat Tab) を有効化
- App Home → 「Allow users to send Slash commands and messages from the messages tab」を ON
3つ目を ON にすると、ユーザーが Messages タブから DM を送れるようになる。
2-3. Bot Token Scopes
OAuth & Permissions で付与する。
| スコープ | 用途 |
|---|---|
assistant:write |
エージェントモード (ステータス行の表示) |
chat:write |
メッセージ投稿。ステータス行にも使える |
app_mentions:read |
チャンネルでのメンション受信 |
im:history |
DM の受信 |
channels:history / groups:history |
スレッド返信の継続応答 |
reactions:write |
処理中のリアクション表示 (任意) |
assistant.threads.setStatus の必要スコープは、2026年3月5日の変更で assistant:write または chat:write のどちらでも通るようになった。今後は chat:write に寄せる方針とアナウンスされている。
スコープを追加したらワークスペースへの再インストールを行う。
2-4. Event Subscriptions
Socket Mode を使う場合も、購読するイベントの設定は必要。
message.im— DMapp_mention— チャンネルでのメンションmessage.channels/message.groups— スレッド返信の継続assistant_thread_started/assistant_thread_context_changed—assistant_viewの場合
2-5. assistant_view と agent_view
エージェントの UI には2種類あり、DM の届き方が異なる。
assistant_view |
agent_view |
|
|---|---|---|
| UI | Chat / History タブが分離した専用 UI | 通常の DM と同じ Messages タブで完結 |
| 位置づけ | 従来からのもの | 2026年6月30日以降、新規アプリの既定 |
| DM の届き方 | assistant_thread_started 等の専用イベント |
通常の message.im イベント |
自分のアプリがどちらかは、App Manifest の features.assistant_view / features.agent_view で確認できる。新規に作成したアプリは agent_view になる。
Bolt for JS の Assistant クラスは assistant_thread_started などの assistant_view 専用イベントを前提とした設計で、agent_view の DM は app.message() 側に message.im として届く。どちらの UI でもステータスを出すなら、次章のように client から直接呼ぶ。
3. 実装 — client から直接 setStatus を呼ぶ
client から直接叩けば、agent_view の DM でも、チャンネルのスレッドでも表示される。Hermes Agent も同じ方針を取っている。
await client.assistant.threads.setStatus({
channel_id: channelId,
thread_ts: threadTs,
status: "考え中…",
});
クリアするときは空文字を送る。
await client.assistant.threads.setStatus({
channel_id: channelId,
thread_ts: threadTs,
status: "",
});
3-1. thread_ts の指定
thread_ts は必須パラメータ。agent_view ではユーザーの発言がトップレベルのメッセージとして届くため、その発言自身の ts を渡す。Slack 側でそのメッセージを起点にスレッドが開く。Hermes Agent も同じ扱いをしている (thread_ts = event.thread_ts or ts)。
const threadTs = message.thread_ts ?? message.ts;
3-2. 定期的に送り直して維持する
ステータスは最後の更新から約2分で自動的にクリアされる。エージェントの処理はそれより長くなることがあるため、実行中は一定間隔で同じ文言を送り直す。8秒間隔なら1スレッドあたり毎分7.5リクエストで、既定のレート上限 (600 req/min/app/team) に対して余裕がある。
3-3. API 呼び出しを直列化する
setStatus を並行に投げると、ネットワークの応答順序によってクリアより後に古い文言が届くことがある。Promise チェーンで直列化して、クリアが必ず最後の書き込みになるようにする。
let chain: Promise<void> = Promise.resolve();
const enqueuePost = (text: string): void => {
chain = chain.then(async () => {
await client.assistant.threads.setStatus({
channel_id: channelId,
thread_ts: threadTs,
status: text,
});
});
};
3-4. クリアは必ず送る
スコープ不足やネットワークエラーで setStatus が失敗した場合、定期再送は止めてよいが、クリアだけは一度送るようにしておく。処理終了時に確実にステータス行が消える。
3-5. loading_messages で文言をローテーションさせる
assistant.threads.setStatus には loading_messages という任意パラメータがあり、最大10件の文言を渡すとローテーション表示される。
await client.assistant.threads.setStatus({
channel_id: channelId,
thread_ts: threadTs,
status: "考え中…",
loading_messages: ["考え中…", "調べています…", "整理しています…", "もう少し待って…"],
});
ツールを使い始めるまでの表示として使うとちょうどいい。
4. 実行中のツールに応じて文言を変える
「考え中…」を出し続けるより、エージェントが今何をしているかを出すと待ち時間の体感が変わる。Hermes Agent も「is running pytest…」のような実況を出している。
エージェント SDK 側でツールの実行開始を拾い、ステータス文言に反映する。Claude Agent SDK なら、ストリームに流れる assistant メッセージの content から tool_use ブロックを拾う。
for await (const message of query(options)) {
// サブエージェント内のツール実行は表に出さない
if (message.type === "assistant" && !message.parent_tool_use_id) {
for (const block of message.message.content) {
if (block.type === "tool_use") {
onToolUse(block.name);
}
}
}
}
parent_tool_use_id が入っているものはサブエージェント内部の実行なので、除外すると表示が落ち着く。
あとはツール名を文言にマッピングする。
function statusTextForTool(toolName: string): string {
if (toolName === "Bash") return "コマンドを実行しています…";
if (toolName === "Task") return "サブエージェントに調べさせています…";
if (["Read", "Glob", "Grep"].includes(toolName)) return "ファイルを調べています…";
if (["Write", "Edit"].includes(toolName)) return "ファイルを編集しています…";
if (["WebSearch", "WebFetch"].includes(toolName)) return "Web で調べています…";
if (toolName.startsWith("mcp__")) {
const server = toolName.split("__")[1];
return server ? `${server} を使っています…` : "外部ツールを使っています…";
}
return "作業しています…";
}
MCP のツールは mcp__<サーバー名>__<ツール名> という命名なので、サーバー名を抜き出して表示すると分かりやすい。
5. スレッドとセッションの扱い
setStatus を呼ぶと、agent_view ではそのメッセージを起点にスレッドが開く。返信も同じスレッドに投稿すると、ステータス行と回答の位置が揃う。
await client.chat.postMessage({
channel: message.channel,
thread_ts: threadTs,
text: reply,
});
また Bolt の Assistant ミドルウェアは、channel_type=im かつ thread_ts ありのメッセージを message.im のハンドラより先に受け取る。DM をスレッド化すると、続きの発言は Assistant ハンドラ側に流れる。
DM を「会話全体で1つの続き」として扱いたい場合は、セッションキーをスレッドで分割しない。一方 assistant_view でユーザーが自分で立てたスレッドは別の相談なので、そちらはスレッド単位で分けたい。既存セッションの有無で判別すると、どちらの UI でも意図どおりになる。
const threadKey = sessionKeyOf(teamId, channel, threadTs);
const dmKey = dmSessionKeyOf(teamId, channel);
const sessionKey = hasSession(threadKey) || !hasSession(dmKey) ? threadKey : dmKey;
この hasSession() は、保存済みのセッション ID に加えて実行中のものも見る。セッション ID の永続化は実行完了後に行われるのが一般的なので、初回の実行中に同じスレッドへ追加のメッセージが来たときも、実行キューに載っているものを「セッションあり」として扱うと会話が継続する。
まとめ
Slack でエージェントボットのステータスを表示するのに使う API は assistant.threads.setStatus の一本。
- Slack アプリを Agents & AI Apps として設定し、
assistant:writeまたはchat:writeを付ける clientから直接呼ぶと、agent_viewの DM でもチャンネルのスレッドでも表示されるthread_tsには、スレッドでなければ発言自身の ts を渡す- 約2分で消えるので、実行中は定期的に送り直す
- API 呼び出しは直列化して、クリアを最後の書き込みにする
- 実行中のツール名に応じて文言を差し替えると実況になる
参考
- assistant.threads.setStatus | Slack Developer Docs
- Developing an agent | Slack Developer Docs
- Introducing the Agent messaging experience (changelog)
- assistant.threads.setStatus のスコープ変更 (changelog)
- NousResearch/hermes-agent —
plugins/platforms/slack/adapter.pyのsend_typing()
開発相談をお待ちしています。