Slack のエージェントボットで、処理中にボット名をキラキラさせてステータスを表示する

2026-07-20 09:26 (20 hours ago)

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 — DM
  • app_mention — チャンネルでのメンション
  • message.channels / message.groups — スレッド返信の継続
  • assistant_thread_started / assistant_thread_context_changedassistant_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 の一本。

  1. Slack アプリを Agents & AI Apps として設定し、assistant:write または chat:write を付ける
  2. client から直接呼ぶと、agent_view の DM でもチャンネルのスレッドでも表示される
  3. thread_ts には、スレッドでなければ発言自身の ts を渡す
  4. 約2分で消えるので、実行中は定期的に送り直す
  5. API 呼び出しは直列化して、クリアを最後の書き込みにする
  6. 実行中のツール名に応じて文言を差し替えると実況になる

参考

評価をお願いします (会員登録・ログイン不要)
まだ評価がありません
著者は、アプリケーション開発会社 Cyberneura を運営しています。
開発相談をお待ちしています。

アーカイブ