コンテンツにスキップ

Slack MCP Agent — エラーハンドリング設計

v1.0 / 2026-04-18

パイプライン各層の失敗パターン、却下再生成、再実行、中止のフローを定義する。
docs/architecture.md Section 5・9・10、docs/data-model.md Section 3 と整合。


1. 設計原則

# 原則 説明
E-1 ユーザーに常にフィードバック エラー発生時は Slack カードを失敗状態に更新し、何が起きたかを表示
E-2 リカバリ可能にする 失敗 → [再実行]、却下 → 再生成。ユーザーが行き詰まらない設計
E-3 エラーは握りつぶさない 全エラーを audit_logs に記録。Agent にはテキストでエラーを返す
E-4 リトライは自動、リカバリは手動 一時的エラー (429, 5xx) は自動リトライ。業務エラーはユーザー判断
E-5 タイムアウトは明示的 L4 は 360 秒上限。超過は failed として処理

2. エラー分類

flowchart TD
    E["エラー発生"] --> T{"一時的?"}
    T -->|Yes| R["自動リトライ\n(最大 3 回, backoff)"]
    R --> S{"成功?"}
    S -->|Yes| OK["正常続行"]
    S -->|No| F["失敗処理"]
    T -->|No| C{"業務エラー?"}
    C -->|Yes| U["ユーザーに判断委譲\n(却下/再実行)"]
    C -->|No| F
    F --> L["audit_log 記録\nSlack カード更新\nDB ステータス更新"]
分類 対処
一時的エラー API 429/5xx、ネットワーク断、DB ロック 自動リトライ (exponential backoff)
認証エラー freee 401、Slack トークン無効 トークンリフレッシュ → リトライ (1 回)
業務エラー LLM 出力パース失敗、不正な JSON ユーザーに通知、再実行可能
致命的エラー DB マイグレーション失敗、設定不備 起動時に即失敗、ログ出力
タイムアウト L4 ポーリング上限到達 failed → [再実行] ボタン
ユーザー起因 却下、中止 正常フローとして処理

3. パイプライン各層のエラーハンドリング

3.1 L1: Message → Task

エラー 検知方法 対処 Slack 表示
Anthropic API エラー anthropic.APIError リトライ (3 回) → 失敗 スレッドにエラーメッセージを投稿
JSON パースエラー json.JSONDecodeError リトライ (1 回) → 失敗 同上
必須キー欠落 KeyError リトライ (1 回) → 失敗 同上
レートリミット anthropic.RateLimitError backoff リトライ → 失敗 同上
async def run_l1_with_retry(client, user_message, model, max_retries=3):
    for attempt in range(max_retries):
        try:
            result = await run_l1(client, user_message, model)
            _validate_task_json(result)
            return result
        except (anthropic.RateLimitError, anthropic.InternalServerError):
            if attempt == max_retries - 1:
                raise
            await asyncio.sleep(2 ** attempt)
        except json.JSONDecodeError:
            if attempt == max_retries - 1:
                raise

3.2 L2: Task → Prompt

エラー 対処
Anthropic API エラー L1 と同様のリトライ
空レスポンス リトライ (1 回) → 失敗 → Prompt カードを失敗表示

L2 の出力は自然言語テキストのため、パースエラーは発生しにくい。

3.3 L3: Prompt → Process

エラー 検知方法 対処
JSON パースエラー json.JSONDecodeError マークダウンフェンス除去後にリトライ
steps キー欠落 KeyError リトライ → 失敗
不正な tool 名 バリデーション エラーを含めてリトライ → 失敗
steps が空配列 バリデーション リトライ → 失敗
def _validate_steps(steps: list[dict], valid_tools: set[str]) -> None:
    if not steps:
        raise ValueError("steps array is empty")
    for step in steps:
        required = {"stepId", "order", "tool", "toolInput"}
        missing = required - set(step.keys())
        if missing:
            raise ValueError(f"Step {step.get('stepId', '?')} missing keys: {missing}")
        if step["tool"] not in valid_tools and step["tool"] != "agent_decide":
            raise ValueError(f"Unknown tool: {step['tool']}")

3.4 L4: Managed Agent Session

エラー 検知方法 対処 Slack 表示
Session 作成失敗 anthropic.APIError リトライ (2 回) → failed Execution カード: 失敗 + [再実行]
ポーリングタイムアウト MAX_POLL_ATTEMPTS 到達 failed 同上
Session エラーステータス session.status == "error" failed 同上
ツール実行エラー ツール結果にエラー Agent に返却、Agent が判断 Execution カード: 進捗更新
MCP Server 到達不可 Agent イベントにエラー failed Execution カード: 失敗
ngrok 切断 MCP 接続タイムアウト failed 同上

4. 却下 → 再生成フロー

4.1 Prompt 却下

sequenceDiagram
    actor User
    participant Slack
    participant Backend
    participant DB
    participant LLM as Anthropic API

    User->>Slack: [却下] クリック
    Slack->>Backend: action: reject_prompt
    Backend->>Slack: views.open (理由入力モーダル)
    User->>Slack: 理由を入力して送信
    Slack->>Backend: view_submission

    Backend->>DB: prompts UPDATE (status: rejected, rejection_reason)
    Backend->>DB: audit_logs INSERT (prompt.rejected)

    Backend->>LLM: L2 再実行 (前 Prompt + 却下理由)
    LLM-->>Backend: 新 Prompt テキスト

    Backend->>DB: prompts INSERT (version +1, status: generating)
    Backend->>DB: prompts UPDATE (status: pending_approval)
    Backend->>Slack: chat.update (Prompt カード: 新バージョン承認待ち)

4.2 Process 却下

Prompt 却下と同一パターン。L3 を再実行し、新バージョンの Process を生成。

4.3 再生成時のガードレール

ルール 説明
バージョン上限 最大 5 回再生成。超過時は「最大試行回数に達しました」と表示
却下理由必須 空文字の却下理由は受け付けない (モーダルでバリデーション)
前バージョン参照 再生成プロンプトに前バージョンの内容 + 却下理由を必ず含める

5. 再実行フロー

5.1 失敗後の再実行

sequenceDiagram
    actor User
    participant Slack
    participant Backend
    participant DB
    participant Agent as Managed Agent

    Note over Slack: Execution カード: 失敗 + [再実行] ボタン

    User->>Slack: [再実行] クリック
    Slack->>Backend: action: retry_execution

    Backend->>DB: executions INSERT (新レコード, status: pending)
    Backend->>DB: tasks UPDATE (status: running)
    Backend->>DB: audit_logs INSERT (execution.started)

    Backend->>Slack: chat.update (Execution カード: 実行中)

    Backend->>Agent: sessions.create + events.send(steps)
    loop ポーリング (3 秒間隔)
        Backend->>Agent: sessions.retrieve
        Agent-->>Backend: status + events
        Backend->>Slack: chat.update (進捗更新)
    end

    Agent-->>Backend: status: idle (完了)
    Backend->>DB: executions UPDATE (status: completed)
    Backend->>DB: tasks UPDATE (status: completed)
    Backend->>DB: audit_logs INSERT (execution.completed)
    Backend->>Slack: chat.update (Execution カード: 完了)

5.2 再実行のルール

ルール 説明
同一 Process 再実行は同じ承認済み Process steps を使用
新 Execution 新しい Execution レコードを作成 (履歴保持)
新 Session Managed Agent Session は新規作成
再実行上限 最大 3 回。超過時はメッセージ表示

6. 中止フロー

6.1 実行中の中止

sequenceDiagram
    actor User
    participant Slack
    participant Backend
    participant DB
    participant Agent as Managed Agent

    Note over Slack: Execution カード: 実行中 + [中止] ボタン

    User->>Slack: [中止] クリック
    Slack->>Backend: action: cancel_execution

    Backend->>Agent: sessions.cancel(session_id)
    Backend->>DB: executions UPDATE (status: cancelled, cancelled_by, cancelled_at)
    Backend->>DB: tasks UPDATE (status: cancelled)
    Backend->>DB: audit_logs INSERT (execution.cancelled)
    Backend->>Slack: chat.update (Execution カード: 中止)

6.2 中止のルール

ルール 説明
実行中のみ running ステータスの Execution のみ中止可能
即時反映 ボタンクリック後、カードを即座に「中止中...」に更新
Session 停止 sessions.cancel が失敗しても DB は cancelled に更新
中止後の再実行 中止後は新しい Execution で再実行可能

7. 外部 API エラーハンドリング

7.1 freee API

HTTP エラー名 対処 リトライ
401 Unauthorized トークンリフレッシュ → リトライ 1 回
403 Forbidden スコープ不足。エラーテキストを Agent に返す しない
404 Not Found 従業員/レコード不存在。エラーテキストを Agent に返す しない
429 Rate Limited Retry-After ヘッダに従い待機 最大 3 回
500-599 Server Error Exponential backoff 最大 3 回
async def call_freee_api(method, url, token_manager, tenant_id, conn, **kwargs):
    for attempt in range(4):
        token = await token_manager.get_access_token(tenant_id, conn)
        headers = {"Authorization": f"Bearer {token}"}
        resp = await httpx.request(method, url, headers=headers, **kwargs)

        if resp.status_code == 401 and attempt == 0:
            await token_manager.force_refresh(tenant_id, conn)
            continue
        if resp.status_code == 429:
            wait = int(resp.headers.get("Retry-After", 2 ** attempt))
            await asyncio.sleep(wait)
            continue
        if resp.status_code >= 500:
            await asyncio.sleep(2 ** attempt)
            continue

        resp.raise_for_status()
        return resp.json()

    raise FreeeAPIError(f"freee API failed after retries: {url}", resp.status_code)

7.2 Anthropic API

エラー 対処 リトライ
RateLimitError Exponential backoff 最大 3 回
InternalServerError Exponential backoff 最大 3 回
AuthenticationError 起動時にフェイル、API Key 確認 しない
BadRequestError プロンプト修正が必要。ログ記録 しない
APITimeoutError リトライ 最大 2 回

7.3 Slack API

エラー 対処
slack_api_error ログ記録。ユーザーへの通知はベストエフォート
Rate Limited Bolt が自動リトライ
Socket Mode 切断 Bolt が自動再接続

8. DB エラーハンドリング

エラー 対処
OperationalError (ロック) WAL モード + PRAGMA busy_timeout=5000 で軽減
IntegrityError (FK 違反) バグ。ログ記録 + 500
IntegrityError (UNIQUE 違反) 冪等性チェック (既存レコード返却)
マイグレーション失敗 起動時に即失敗。手動対応

9. ユーザー向けエラーメッセージ

9.1 Slack エラーメッセージ

全エラーメッセージは日本語で、技術詳細は含めない。

シーン メッセージ
L1 失敗 「タスクの解析に失敗しました。もう一度お試しください。」
L2 失敗 「実行方針の生成に失敗しました。もう一度お試しください。」
L3 失敗 「実行ステップの生成に失敗しました。もう一度お試しください。」
L4 タイムアウト 「実行がタイムアウトしました。[再実行] で再試行できます。」
L4 エラー 「実行中にエラーが発生しました。[再実行] で再試行できます。」
freee 接続エラー 「freee への接続に失敗しました。管理画面で接続状態を確認してください。」
再生成上限 「最大試行回数(5 回)に達しました。タスクを新しく作成してください。」
再実行上限 「最大再実行回数(3 回)に達しました。新しいタスクを作成してください。」

9.2 Web 管理画面エラー

シーン 表示
DB 接続エラー error.tsx: 「データベースに接続できません」
認証失敗 /login にリダイレクト
404 not-found.tsx: 「ページが見つかりません」
設定保存失敗 Toast: 「設定の保存に失敗しました」
OAuth 失敗 Toast: 「freee の認証に失敗しました」

10. ログ戦略

10.1 構造化ログ

import logging
import json

logger = logging.getLogger("slack-mcp-agent")

def log_error(layer: str, error: Exception, context: dict):
    logger.error(json.dumps({
        "layer": layer,
        "error_type": type(error).__name__,
        "error_message": str(error),
        **context,
    }, ensure_ascii=False))

10.2 ログレベル

レベル 用途
ERROR API エラー、パースエラー、DB エラー
WARNING リトライ発生、タイムアウト接近
INFO パイプライン進行 (L1 完了, 承認, 実行開始 等)
DEBUG API リクエスト/レスポンス詳細

変更履歴

日付 バージョン 変更内容
2026-04-18 v1.0 初版作成