> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fim.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# ReActエンジン

> FIM OneのReActエンジンの仕組み — デュアルモード実行、構造化されたLLM呼び出し、およびストリーミング回答合成。

## アーキテクチャ

ReActエンジンは2段階の実行モデルを実装しています。第1段階は反復的なツール使用ループです。エージェントはLLMに対してアクションを繰り返し要求し、要求されたツールを実行し、観察を追加し、LLMが「完了」を示すまで続行します。第2段階は回答の合成です。完全な実行トレースを読み取り、ユーザー向けの応答を生成する別のストリーミングLLM呼び出しです。

この分割は意図的なものです。ツール反復は速度のために最適化されています。ループ内のすべてのLLM呼び出しはストリーミングなしの`chat()`を使用します。これは、ユーザーが部分的なJSONアクションや中間推論トークンを見る必要がないためです。回答生成はUXのために最適化されています。`stream_chat()`を使用するため、ユーザーはトークンがリアルタイムで表示されるのを見ることができます。結果は両方の長所を備えています。高速なツール実行と応答性の高い回答配信です。

```mermaid theme={null}
flowchart TD
    A["User query"] --> B["Tool loop<br/>(non-streaming chat() x N)"]
    B -->|"tool_call"| E["Execute tool"]
    E --> F{"Cycle<br/>detected?"}
    F -->|"No"| B
    F -->|"Yes"| G["Inject warning:<br/>try different approach"]
    G --> B
    B -->|"done signal"| H{"tools ≥<br/>threshold?"}
    H -->|"Yes (once)"| I["Completion checklist:<br/>verify answer"]
    I --> B
    H -->|"No / already checked"| C["stream_answer()<br/>(streaming)"]
    C --> D["SSE token stream → frontend"]
```

ツールループは、完全な会話履歴（システムプロンプト、ユーザークエリ、すべてのアシスタントメッセージ、すべてのツール結果）を含む`AgentResult`を生成します。`stream_answer()`メソッドは、このトレースを簡潔で一貫性のある回答に蒸留します。ツール結果は合成コンテキストで各2,000文字に切り詰められ、複雑なマルチツールワークフロー後でもプロンプトを簡潔に保ちます。

**モデルバインディング。** LLMは`ReActAgent.__init__()`に注入され、`self._llm`として保存されます。単一の`run()`呼び出し内のすべての呼び出し（すべてのツールループ反復と最終的な回答合成）は、この同じインスタンスを使用します。モデルは反復間で変わりません。別のモデルを使用するには、新しい`ReActAgent`を構築する必要があります。DAGモードでは、`DAGExecutor._resolve_agent()`はこのパターンを活用します。ステップごとに新しいエージェントを作成し（`ModelRegistry`から`step.model_hint`に基づいてモデルを選択）、そのステップのReActループが開始される直前に作成します。詳細は[DAGエンジン — ステップごとのオーバーライド](/architecture/dag-engine#two-llm-architecture)を参照してください。

## デュアルモード実行

ReActエンジンは、ツールループ中にLLMと相互作用する2つの異なるモードをサポートしています。

**JSONモード** (`_run_json`) はツール説明をシステムプロンプトに直接埋め込み、LLMに`tool_call`アクション（ツール名と引数を含む）または`final_answer`シグナルのいずれかのJSONオブジェクトで応答するよう指示します。エージェントはレスポンスコンテンツからJSONを解析し、ツールを実行し、観察結果をユーザーメッセージとして追加します。

**ネイティブ関数呼び出し** (`_run_native`) はLLMプロバイダーの組み込みツール呼び出しAPIを使用します。ツール説明は`tools`パラメータを介して渡され、LLMはコンテンツ内でJSONを出力する代わりに、APIレスポンスで構造化された`tool_calls`を返します。これはそれをサポートするモデルの推奨モードです。

モード選択は自動的に行われます。`_native_mode_active`プロパティは、エージェントが`use_native_tools=True`（デフォルト）で作成され、かつLLMが`abilities["tool_call"] = True`をアドバタイズしている場合にのみ`True`を返します。いずれかの条件が失敗した場合、エンジンはJSONモードにフォールバックします。

| 側面         | JSONモード                | ネイティブ関数呼び出し            |
| ---------- | ---------------------- | ---------------------- |
| LLM出力      | メッセージコンテンツ内のJSONオブジェクト | APIレスポンス内の`tool_calls` |
| システムプロンプト  | テキスト内に完全なツール説明を埋め込む    | `tools`パラメータを介してツールを渡す |
| 並列ツール呼び出し  | 反復ごとに1つのツール            | `asyncio.gather`経由で複数  |
| 解析失敗処理     | 再フォーマットプロンプトで再試行       | 該当なし（APIによって構造化）       |
| ループLLM呼び出し | ストリーミングなし`chat()`      | ストリーミングなし`chat()`      |
| 最適な用途      | ツール呼び出しサポートなしのモデル      | GPT-4、Claude、および同様のモデル |

両モードは同じ回答合成フェーズを共有します — `stream_answer()`はツールループの実行方法に関わらず同じように機能します。

## structured\_llm\_call — 統一された出力抽出

LLMがJSONスキーマに準拠したデータを返す必要があるあらゆるコールサイトは、`structured_llm_call()`を使用します。これはフレームワーク全体における構造化出力の単一エントリーポイント — DAGプランナー、プラン分析器、ツール選択、およびLLMから解析されたJSONが必要な将来のコンポーネント。

この関数は3段階の劣化チェーンを実装し、LLMの公表された機能に基づいて各段階を順番に試みます:

**レベル1: ネイティブ関数呼び出し。** LLMの`tool_call` / `tool_choice` APIを使用して構造化応答を強制します。`abilities["tool_call"] = True`の場合に利用可能です。LLMが`tool_calls`を返す場合、引数は直接抽出されます。解析に失敗した場合、次のレベルにフォールスルーします。

**レベル2: JSONモード。** `response_format={"type": "json_object"}`を設定してLLMの出力形式を制限します。`abilities["json_mode"] = True`の場合に利用可能です。応答を解析できない場合、リフォーマットプロンプト（「前の応答は有効なJSONとして解析できませんでした...」）で1回再試行してから、次のレベルにフォールスルーします。

**レベル3: プレーンテキスト。** 形式制約なしでLLMを呼び出し、`extract_json()`を使用してフリーフォームテキストからJSONを抽出します。抽出に失敗した場合、オプションの`regex_fallback`関数が試されます。リフォーマットプロンプトで1回再試行してから、諦めます。

```mermaid theme={null}
flowchart TD
    START["structured_llm_call()"] --> CHECK1{"abilities.tool_call?"}
    CHECK1 -->|Yes| L1["Level 1: Native FC"]
    CHECK1 -->|No| CHECK2{"abilities.json_mode?"}
    L1 -->|Success| DONE["Return StructuredCallResult"]
    L1 -->|Fail| CHECK2
    CHECK2 -->|Yes| L2["Level 2: JSON Mode"]
    CHECK2 -->|No| L3["Level 3: Plain Text"]
    L2 -->|Success| DONE
    L2 -->|Fail| L2R["Retry with reformat prompt"]
    L2R -->|Success| DONE
    L2R -->|Fail| L3
    L3 -->|Success| DONE
    L3 -->|Fail| L3R["Retry with reformat prompt"]
    L3R -->|Success| DONE
    L3R -->|Fail| DEFAULT{"default_value?"}
    DEFAULT -->|Yes| DVAL["Return default"]
    DEFAULT -->|No| ERR["Raise StructuredOutputError"]
```

劣化チェーンは、フルツール呼び出しサポートを備えたGPT-4からプレーンテキストのみを生成できるローカルLLMまで、あらゆるモデルが構造化出力シナリオに参加できることを意味します。最悪のケースは5回のLLM呼び出し（1回のネイティブ + 1回のJSON + 1回のJSON再試行 + 1回のプレーン + 1回のプレーン再試行）ですが、実際にはほとんどの呼び出しはレベル1で1回の試行で解決されます。

| モデル機能                   | 取られるパス       | 最大LLM呼び出し |
| ----------------------- | ------------ | --------- |
| tool\_call + json\_mode | L1 → L2 → L3 | 5         |
| json\_mode のみ           | L2 → L3      | 4         |
| プレーンテキストのみ              | L3           | 2         |

結果は、解析された値、生のdict、成功したレベル、および累積トークン使用量を含む`StructuredCallResult`です。コールサイトは`parse_fn`を使用して生のdictをドメインオブジェクト（例：DAGプラン）に変換し、`default_value`を使用して完全な失敗が許容可能な場合のフォールバックを提供します。

`structured_llm_call`は以下によって使用されます: DAGプランナー（プランスキーマ）、プラン分析器（分析スキーマ）、ツール選択（ツールリストスキーマ）、および信頼性の高い構造化出力が必要なあらゆるコンポーネント。これは[Planning Landscape](/architecture/planning-landscape)でも説明されています。

## ツール選択

エージェントが多くのツールにアクセスできる場合（複数のコネクタがそれぞれ複数のアクションを公開するハブモードで一般的）、すべてのツールの完全なスキーマを会話コンテキストに注入することは無駄です。20個のツールを持つコネクタハブは、ツール説明だけで約5Kトークンを消費し、会話履歴とツール結果のスペースを圧迫します。

エンジンはこれを軽量な選択フェーズで対処します。登録されたツールの総数が `TOOL_SELECTION_THRESHOLD`（12）を超える場合、エージェントはメインループに入る前に予備的なLLM呼び出しを実行します。この呼び出しはコンパクトなカタログを受け取ります。ツールあたり約80文字で、名前と1行の説明のみを含み、パラメータスキーマは含みません。そして現在のクエリに最も関連するツールを選択し、最大 `_TOOL_SELECTION_MAX`（6）個までです。

選択は `structured_llm_call` を使用して、シンプルなスキーマ（`{"tools": ["tool_name_1", "tool_name_2"]}`）で実行されるため、同じ3レベルの低下から恩恵を受けます。選択されたツール名は、メインループがシステムプロンプト構築とツール実行の両方に使用するフィルタリングされた `ToolRegistry` を構築するために使用されます。

選択の失敗は意図的に致命的ではありません。LLMが解析不可能な出力を返す場合、選択されたすべての名前が無効な場合、または例外が発生した場合、エージェントは完全なツールセットにフォールバックします。これにより、不完全な選択がエージェントの機能を妨げることはなく、最適より多くのコンテキストを使用するだけです。

## 反復ループ

コアループはJSONモードとネイティブモードの両方を駆動し、メッセージ処理に若干の違いがあります。各反復は同じ高レベルパターンに従います：コンテキスト予算をチェック、LLMを呼び出し、レスポンスを処理し、ツールを実行するか中断するかのいずれかです。

```mermaid theme={null}
sequenceDiagram
    participant Agent as ReActAgent
    participant CG as ContextGuard
    participant LLM as LLM API
    participant Hooks as HookRegistry
    participant Tool as Tool Registry

    loop Each iteration (max 50)
        Agent->>CG: check_and_compact(messages)
        CG-->>Agent: messages (within budget)
        Agent->>LLM: chat(messages) [non-streaming]
        LLM-->>Agent: response
        alt tool_call action
            Agent->>Hooks: run_pre_tool_use_hooks(ctx)
            alt hook blocks
                Hooks-->>Agent: allow=False, error="..."
                Agent->>Agent: append block message, continue
            else hook allows (possibly rewrote args)
                Hooks-->>Agent: allow=True, modified_args?
                Agent->>Tool: execute(tool_name, args)
                Tool-->>Agent: observation
                Agent->>Hooks: run_post_tool_use_hooks(ctx, result)
                Hooks-->>Agent: modified_result?
                Agent->>Agent: cycle check (hash args)
                opt identical call ≥ threshold
                    Agent->>Agent: inject "try different approach" warning
                end
                Agent->>Agent: append observation, continue
            end
        else final_answer action
            opt tools used ≥ threshold AND not yet checked
                Agent->>Agent: inject completion checklist
                Agent->>Agent: continue (one more LLM call)
            end
            Agent->>Agent: break loop
        end
    end
    Agent->>LLM: stream_answer() [streaming]
    LLM-->>Agent: token stream → SSE to client
```

**JSONモードループ。** LLMのレスポンスは`_parse_action()`を介して解析され、`extract_json()`を使用してコンテンツ内のJSONオブジェクトを見つけます。解析に失敗した場合、エージェントは生のレスポンスと再フォーマットリクエストを追加してから続行します — これは`max_iterations`に対してカウントされ、無限再試行ループを防ぎます。成功時、アクションは`tool_call`（ツールを実行し、観察結果をユーザーメッセージとして追加）または`final_answer`（中断して合成に進む）のいずれかです。

**ネイティブモードループ。** LLMのレスポンスは1つ以上の`tool_calls`を含む場合があります。単一のレスポンス内のすべてのツール呼び出しは`asyncio.gather`を介して並列実行され、すべてのツール結果メッセージは他のメッセージの前に追加されます。この順序制約は重要です — OpenAI API（および互換性のあるプロバイダー）は、`tool`メッセージが`tool_calls`を生成した`assistant`メッセージの直後に続くことを要求します。それらの間に他のメッセージ（ユーザー割り込みなど）を挿入すると、プロトコルが破損します。`tool_calls`が存在しない場合、レスポンスは最終回答として扱われます。

**最大反復回数。** デフォルト制限は50反復です。ループがこの制限を使い果たして`final_answer`を生成しない場合、エージェントは累積されたステップ結果から フォールバックレスポンスを合成します — どのツールが呼び出されたか、および成功したか失敗したかの要約です。これは安全ネットであり、通常の終了パスではありません。

[コンテキスト管理](/architecture/context-management)は、ContextGuardが毎回の反復でトークン予算を強制する方法を説明しており、最近の推理チェーンを保持するようにコンパクション LLMに指示するヒントシステムが含まれています。

## ツール実行フック

反復ループ内のすべてのツール呼び出しは、2つのフックポイントでラップされます：

* **`PreToolUse`** — LLMがツールを選択した後、ツール実行前に発火します。フックは以下を実行できます：
  * **ブロック** `allow=False`を返すことで呼び出しをブロック（LLMは理由を説明する観測値を受け取ります。例：「人間の承認を待機中」または「組織が読み取り専用モード」）
  * **書き換え** 実行前にツール引数を書き換え
  * **副作用をトリガー** （Feishuカードを投稿、監査行を作成、メトリクスを発行）
* **`PostToolUse`** — ツール実行後、観測値がLLMに到達する前に発火します。フックは以下を実行できます：
  * **書き換え** 観測値を書き換え（例：200 KB SQLの結果を4 KBサマリーに切り詰め）
  * **副作用をトリガー** （`ConnectorCallLog`に書き込み、使用量カウンターを更新）

フックは**LLMループの外で実行**されます — LLMが言ったり実行したりすることは、フックをスキップできません。これはプロンプト指示に信頼できないポリシーの決定論的な強制レイヤーです：人間参加型の承認（`FeishuGateHook`）、監査ログ、レート制限、読み取り専用モードガード。詳細な設計については[フックシステム](/architecture/hook-system)を参照してください。

同じフックポイントが[DAGエンジン](/architecture/dag-engine)に存在します — 各DAGステップは独自のReActループを実行するため、どの実行エンジンがツール呼び出しを駆動しているかに関わらず、フックが均一に適用されます。

## ミッド・ループ自己反省

長い推論チェーン（10以上のツール呼び出し）は**目標ドリフト**のリスクがあります。エージェントが元の目的から徐々に焦点をずらし、ローカルな部分問題に集中したり、同様のアクションを繰り返したり、循環的な再試行ループに入ったりします。ミッド・ループ自己反省は軽量な対策です。

`_SELF_REFLECTION_INTERVAL` ツール呼び出し反復ごと（デフォルト：**6**）に、エージェントは会話にユーザーメッセージを挿入し、LLMに一時停止して反省するよう求めます：

* 元の目標に向けて依然としてオンコースですか？
* 同様のアクションを繰り返したり、ぐるぐる回ったりしていませんか？
* 完了するための最も直接的な次のステップは何ですか？
* 今すぐ最終的な回答を提供すべきですか？

カウンターは**実際のツール呼び出しのみ**を追跡します。JSONパース再試行、思考イベント、割り込み挿入はカウントされません。ネイティブモードでは、反省メッセージはtool\_use/tool\_resultペアリング制約を保持するため、すべての`tool_result`メッセージの直後に厳密に追加されます。

これは注入ごとに約100トークンのコスト（追加のLLM呼び出しなし）であり、短い実行（`< 6`ツール呼び出し）には影響がありません。ContextGuard（トークン予算を管理）とステップごとの検証（個別の結果を検証）を補完し、異なる障害モード（エージェントが多くの反復にわたって目標を見失うこと）に対処します。

## サイクル検出

自己反省はLLMに対してループしているかどうかを尋ねますが、LLMはしばしば「いいえ」と答えてループを続けます。サイクル検出は決定論的な代替手段であり、バイパスできません。

ツール実行後、エージェントは `(tool_name, arguments)` ペアをハッシュ化します。同じハッシュが `_CYCLE_DETECTION_THRESHOLD` 回（デフォルト: **3**、`REACT_CYCLE_DETECTION_THRESHOLD` で設定可能）表示されると、警告メッセージが会話に挿入されます:

> You have called `{tool}` with identical arguments N times and received the same result. Please try a different approach or tool.

警告はエージェントの内部履歴内の通常のユーザーメッセージです。エンドユーザーに対して個別のUIイベントとしてサーフェスされることはありません。エージェントは次のLLM呼び出しで警告を確認し、戦略を変更することを強制されます。トラッカーは各 `run()` 呼び出しの開始時にリセットされます。

DAGステップはReActAgentを介して実行されるため、サイクル検出は個別のDAGステップもツール呼び出しループから保護します。

## 完了チェックリスト

エージェントが少なくとも `_COMPLETION_CHECK_MIN_TOOLS` 個のツール（デフォルト: **3**、`REACT_COMPLETION_CHECK_MIN_TOOLS` で設定可能）を使用し、最終的な回答を生成することを決定した場合、回答が受け入れられる前に一度限りの検証プロンプトが挿入されます:

1. 回答は元の質問に完全に対応していますか?
2. ツール結果から重要な事実が検証されましたか?
3. 収集された情報に矛盾がありますか?

エージェントが確認すると、最終的な回答に進みます。何か不足していると判断した場合は、ツール呼び出しループを再開します。チェックリストは実行ごとに最大1回発火し、シンプルな会話応答（ツール未使用）ではトリガーされません。

このしきい値は、シンプルなタスクでの不要なレイテンシを防ぎます。単一のツール呼び出し（例: 簡単な計算の実行）は検証を必要としませんが、複数ツールを使用した調査は最終的なサニティチェックから利益を得ます。常時検証を行う場合は `REACT_COMPLETION_CHECK_MIN_TOOLS=1` に設定してください。

この機能はデフォルトで有効になっており（`ReActAgent` の `completion_check=True`）、エージェントインスタンスごとに無効にできます。DAG ステップはデフォルトを継承します。各ステップは独自の一度限りのチェックリストを取得します。

## 回答の合成 (stream\_answer)

ツールループと回答合成の分離は、コアアーキテクチャの決定です。ツール反復は生データを生成します — JSON アクション、ツール観察、エラーメッセージ。ユーザーは、エージェントの内部トレースのダンプではなく、一貫性のある、よくフォーマットされた回答が必要です。

`stream_answer()` は 2 つのコンポーネントから合成プロンプトを構築します。システムプロンプトは LLM に合成者として機能するよう指示します：結果を直接提示し、マークダウンフォーマットを使用し、メタコメンタリー（「ツール出力に基づいて...」）を避け、元のクエリの言語に一致させます。ユーザーメッセージには、元の質問とフォーマットされた実行トレースが含まれます — 各ツール呼び出しとその結果で、ツール結果は 2,000 文字に切り詰められます。

合成呼び出しは `stream_chat()` を使用し、トークンを段階的に生成します。ウェブレイヤーはこれらのトークンを SSE `answer` イベントで `delta` ステータスでラップするため、フロントエンドは到着時にそれらをレンダリングできます。

`stream_answer()` が失敗した場合 — ネットワークエラー、LLM タイムアウト、任意の例外 — ウェブレイヤーは `result.answer` にフォールバックします。これはツールループの最終反復からの簡潔なテキストです。これは低下した体験です（ストリーミングなし、潜在的にあまり洗練されていないプロース）が、ユーザーが常に応答を取得することを保証します。

## 割り込み処理

ユーザーはエージェントがまだ処理中に後続メッセージを送信できます。これらは`interrupt_queue`経由で配信されます。これは会話ごとに登録された`InterruptQueue`で、反復間でメッセージを蓄積します。

ドレイン タイミングはツール呼び出しの順序制約のため、モード間で異なります:

* **JSONモード**: キューは各アシスタント メッセージの直後にドレインされ、アクションが`final_answer`であるかどうかをチェックする前です。JSONモードは構造的なペアリング要件のない通常のユーザー/アシスタント メッセージを使用するため、これは安全です。

* **ネイティブFC モード**: キューはツール結果メッセージが追加された後にのみドレインされます。`tool`メッセージは`tool_calls`を含むアシスタント メッセージの直後に続く必要があります。その間にユーザー メッセージを挿入するとAPIプロトコルに違反し、エラーが発生します。

注入されたメッセージは`pinned=True`としてマークされ、その後のContextGuardによる圧縮で生き残ることが保証されます。ピン留めメカニズムが重要なメッセージの圧縮による破棄を防ぐ方法については、[ピン留めメッセージ](/architecture/context-management#pinned-messages)を参照してください。

`final_answer`が保留中だが注入されたメッセージが到着した場合、エージェントは最終回答を抑制し、ユーザーのフォローアップに対応できるようにループを続けます。同じドレインからの複数の注入は単一の`[USER INTERRUPT]`メッセージに結合されます。これにより、LLMが短いメッセージの断片化されたシーケンスを見ることを防ぎ、すべてのフォローアップに全体的に対応することを促進します。

## エラーハンドリングとフォールバック

エンジンは LLM またはツールの障害でクラッシュしないように設計されています。すべてのエラーパスは、サイレントに復旧するか、ユーザーに有用なメッセージを表示します。

```mermaid theme={null}
flowchart TD
    A["JSON parse failure"] -->|"retry once"| B["Append raw + reformat request"]
    B -->|"still fails"| C["Treat raw content as final_answer"]
    D["Tool not found"] --> E["Error observation → continue loop"]
    F["Tool execution exception"] --> G["Error observation → continue loop"]
    H["Context over budget"] --> I["ContextGuard compacts → continue"]
    J["Max iterations reached"] --> K["Summarize partial results"]
    L["stream_answer() failure"] --> M["Fall back to result.answer"]
```

**JSON パース失敗。** LLM が JSON モードで JSON 以外のコンテンツを返す場合、`_parse_action()` はそれを `final_answer` としてラップし、推論を `"(could not parse LLM output as JSON)"` とします。ループはこのセンチネルを検出し、生のコンテンツと再フォーマット指示を追加して続行します。再試行も失敗した場合、生のコンテンツが答えになります — 完璧ではありませんが、クラッシュしません。

**ツールエラー。** 「ツールが見つからない」と「ツール実行例外」の両方は、会話に追加されるエラー観測を生成します。LLM は次の反復でエラーを見て、異なる引数で再試行するか先に進むかを決定できます。これにより、エージェントは一時的なツール障害に対して自己修復可能になります。

**拡張思考。** DeepSeek R1 のようなモデルは、推論コンテンツを JSON ボディではなく、別の `reasoning_content` フィールドで返します。エンジンはこれをチェックし、JSON の `reasoning` フィールドが空の場合のフォールバックとして使用します。

**リッチコンテンツ。** ツールが HTML またはマークダウンアーティファクトを生成する場合、LLM に送信される観測は短いサマリーに置き換えられます（`"[Artifact generated: filename] The content is rendered as a preview in the UI..."`）。これにより、LLM が最終的な答えで大きな HTML ブロブをエコーバックするのを防ぎます — モデルが親切にツール出力全体を貼り付け直す一般的な障害モードです。

## SSE イベントプロトコル

ウェブレイヤーはエージェントの反復コールバックをServer-Sent Eventsに変換してフロントエンドに送信します。イベントは2つのSSEチャネルで発行されます：ツールループ用の `step` と合成フェーズ用の `answer` です。

```mermaid theme={null}
sequenceDiagram
    participant Agent as ReActAgent
    participant SSE as SSE Stream
    participant UI as Frontend

    Agent->>SSE: step {type: "thinking", status: "start"}
    Note over Agent: LLM call (non-streaming)
    Agent->>SSE: step {type: "thinking", status: "done", reasoning}
    Agent->>SSE: step {type: "iteration", status: "start", tool_name, tool_args}
    Note over Agent: Tool execution
    Agent->>SSE: step {type: "iteration", status: "done", observation, iter_elapsed}
    Note over Agent: ...more iterations...
    Agent->>SSE: step {type: "answer", status: "start"}
    Agent->>SSE: answer {status: "start"}
    loop Token stream
        Agent->>SSE: answer {status: "delta", content}
    end
    Agent->>SSE: answer {status: "done"}
    Agent->>SSE: done {answer, iterations, usage, elapsed}
```

| イベント   | チャネル      | ペイロード                                                                                         | タイミング              |
| ------ | --------- | --------------------------------------------------------------------------------------------- | ------------------ |
| 思考開始   | `step`    | `{type: "thinking", status: "start", iteration}`                                              | 各LLM呼び出しの前         |
| 思考完了   | `step`    | `{type: "thinking", status: "done", iteration, reasoning}`                                    | LLMが応答した後、ツール実行前   |
| 反復開始   | `step`    | `{type: "iteration", status: "start", iteration, tool_name, tool_args}`                       | ツール実行が開始される時       |
| 反復完了   | `step`    | `{type: "iteration", status: "done", iteration, tool_name, observation, error, iter_elapsed}` | ツール実行が完了した時        |
| 回答シグナル | `step`    | `{type: "answer", status: "start"}`                                                           | エージェントが最終回答を通知した時  |
| 回答開始   | `answer`  | `{status: "start"}`                                                                           | 合成ストリーミングが開始される時   |
| 回答デルタ  | `answer`  | `{status: "delta", content}`                                                                  | ストリーミングされた各トークン    |
| 回答完了   | `answer`  | `{status: "done"}`                                                                            | 合成ストリーミングが完了した時    |
| コンパクト  | `compact` | `{original_messages, kept_messages}`                                                          | ロード時にコンテキストが圧縮された時 |
| フェーズ   | `phase`   | `{phase: "selecting_tools", total_tools}`                                                     | ツール選択フェーズがアクティブな時  |
| インジェクト | `inject`  | `{type: "inject", content}`                                                                   | ユーザー割り込みを受け取った時    |
| 完了     | `done`    | `{answer, iterations, usage, elapsed}`                                                        | 最終結果ペイロード          |

フロントエンドは `step` イベントを使用して折りたたみ可能なツール呼び出しカード（実行中のツール、その引数、観測結果を表示）をレンダリングし、`answer` デルタを使用して応答テキストをストリーミングし、`compact` を使用してコンテキスト要約区切り線を表示します。`done` イベントは完全なメタデータ（総反復回数、トークン使用量、経過時間）を含み、応答フッターに使用されます。
