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

# エージェント実行フロー

> ツール分岐・HITL（Human-in-the-Loop）・状態遷移の全パターン

# エージェント実行フロー

エージェントはユーザーの入力を分析し、自動的にツールを選択して実行します。一部のツール（plan / report / matrix）は、実行前に人間の確認が必要な Human-in-the-Loop（HITL）ポイントを含んでいます。

このページでは、すべての実行パターンのイベントフローと、APIユーザーが各状態でどう操作すべきかを説明します。

***

## 共通フロー

すべての実行は以下の形式で開始されます。

```
POST /api/v1/agent/run/stream
```

```
config → delta(複数) → [ツール分岐] → complete
```

| SSE イベント   | タイミング              |
| ---------- | ------------------ |
| `config`   | ストリーム開始時に1回        |
| `delta`    | エージェントがテキストを生成するたび |
| `step`     | 各実行ステップの完了時        |
| `complete` | 実行完了時（ストリームの最後）    |

***

## ツール分岐一覧

エージェントは `chat-routing` で入力を分析し、以下のツールから自動的に選択します。

| ツール                    | 説明                         | HITL   |
| ---------------------- | -------------------------- | ------ |
| 直接回答                   | ツールを使わずテキストで回答             | なし     |
| `search`               | Web検索（SERP + スクレイピング + 要約） | なし     |
| `x_search`             | X（Twitter）投稿検索             | なし     |
| `browse`               | ブラウザ操作による調査                | 条件付き   |
| `source_summary`       | URLのスクレイピング・ファイル要約         | なし     |
| `recall`               | 過去の調査結果・記憶の検索              | なし     |
| `plan`                 | 複数ステップの調査計画                | **あり** |
| `report`               | レポート・長文ドキュメントの生成           | **あり** |
| `matrix`               | 表・マトリクスの作成                 | **あり** |
| `skill`                | サンドボックスでのコード実行             | なし     |
| `extract_related_urls` | ウェブサイト内のリンク発見              | なし     |

***

## パターン1: 直接回答

ツールを使わず、エージェントが直接テキストで回答します。

```
config → delta → step → complete
```

API操作: 不要

***

## パターン2: search（Web検索）

```
config
→ delta（ツール選択）
→ step（tool-calls）
→ search-query-generation-start
→ search-query-generated（複数クエリ）
→ search-results → search-scraping
→ search-summary-start → search-summary-delta → search-summary-complete
→ delta（検索結果を基に回答）
→ step → complete
```

API操作: 不要（全自動）

***

## パターン3: browse（ブラウザ操作）

```
config
→ browse-start（VNC接続情報付き）
→ browse-step（複数: スクリーンショット・操作）
→ browse-final（結果）
→ browse-end
→ delta → complete
```

API操作: 不要。`maxBrowsingSteps` パラメータで最大ステップ数を制御できます。

### browse 中に人間の入力が必要な場合

browse は通常は全自動ですが、ログイン画面・Cookie確認・判断が必要な画面などで人間に質問することがあります。この場合は plan/report/matrix の HITL とは扱いが違います。

```
browse-start（websocketInfo.session_id を保持）
→ browse-step
→ browse-ask-human（question 付き）
→ ★ ブラウザ操作が回答待ち ★
```

この状態では `/agent/run/{runId}/plan/answer` などは使いません。`browse-start` イベントで受け取った `websocketInfo.session_id` を使って、ブラウザ操作APIに回答します。

```bash theme={null}
curl -X POST "https://app.snorbe.deskrex.ai/api/v1/browser/answer-question" \
  -H "Authorization: Bearer snorbe_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "runId": "cmo...",
    "sessionId": "browser-session-id",
    "input": "左側メニューの「資料」を開いてください"
  }'
```

ファイルを添付して回答する場合は `/browser/answer-question-with-files` を使います。

```json theme={null}
{
  "runId": "cmo...",
  "sessionId": "browser-session-id",
  "modelName": "gpt-5-mini-2025-08-07",
  "input": "このPDFの内容を踏まえて回答してください",
  "fileUrls": ["https://example.com/file.pdf"]
}
```

質問待ちではないが、実行中のブラウザへ追加指示を出したい場合は `/browser/spontaneous-input` を使います。例: 「次は価格ページを開いて」「そのフォームには入力しないで」。

<Note>
  `GET /agent/run/{runId}/status` の `browseState.askHumanQuestion` でも質問待ちかどうかは確認できます。ただし回答には `sessionId` が必要です。APIクライアントは `browse-start.payload.websocketInfo.session_id` を受け取った時点で保存しておいてください。
</Note>

***

## パターン4: skill（サンドボックス実行）

```
config
→ skill-session-start
→ skill-delta（複数: stdoutストリーミング）
→ skill-complete（outputFiles付き）
→ delta → complete
```

API操作: 不要（全自動）

***

## パターン5: plan（調査計画） — HITLあり

```
config
→ delta
→ first_plan（goal, steps, question付き）
→ ★ ストリーム終了 ★
```

### ステップ1: 状態確認

```
GET /api/v1/agent/run/{runId}/status
```

```json theme={null}
{
  "status": "pending",
  "pendingPlanDraft": true
}
```

### ステップ2: 確認操作（いずれか1つ）

**質問に回答してドラフトを修正:**

```
POST /api/v1/agent/run/{runId}/plan/answer
```

```json theme={null}
{
  "runId": "cmo...",
  "answer": "追加で〇〇も調べて",
  "modelName": "gpt-5-mini-2025-08-07"
}
```

→ `regenerated_plan` イベントが返り、再度確認待ちになります。

**プランを確定:**

```
POST /api/v1/agent/run/{runId}/plan/confirm
```

```json theme={null}
{ "runId": "cmo..." }
```

→ `plan_confirmed` が返ります。

**質問をスキップして確定:**

```
POST /api/v1/agent/run/{runId}/plan/skip
```

```json theme={null}
{ "runId": "cmo..." }
```

### ステップ3: レジューム

```
POST /api/v1/agent/run/stream/{runId}
```

```json theme={null}
{ "modelName": "gpt-5-mini-2025-08-07" }
```

→ 調査が開始され、search/browse等のツールイベントがストリーミングされます。

***

## パターン6: report（レポート生成） — HITLあり

```
config
→ delta
→ first_report_structure（title, sections, question付き）
→ ★ ストリーム終了 ★
```

### ステップ1: 状態確認

```
GET /api/v1/agent/run/{runId}/status
```

```json theme={null}
{
  "status": "pending",
  "pendingReportDraft": true
}
```

### ステップ2: 確認操作

**質問に回答:**

```
POST /api/v1/agent/run/{runId}/report/answer
```

```json theme={null}
{
  "runId": "cmo...",
  "answer": "第3章を追加して",
  "modelName": "gpt-5-mini-2025-08-07"
}
```

**確定:**

```
POST /api/v1/agent/run/{runId}/report/confirm
```

```json theme={null}
{ "runId": "cmo..." }
```

### ステップ3: レジューム → セクション自動生成

```
POST /api/v1/agent/run/stream/{runId}
```

```
→ report_section_start
→ report_section_delta（複数）→ report_section_complete
→ report_section_delta → report_section_complete（各セクション繰り返し）
→ report_complete（全文）
→ complete
```

***

## パターン7: matrix（マトリクス生成） — HITLあり

```
config
→ delta
→ first_matrix_structure（title, columns, question付き）
→ ★ ストリーム終了 ★
```

### ステップ1: 状態確認

```
GET /api/v1/agent/run/{runId}/status
```

```json theme={null}
{
  "status": "pending",
  "pendingMatrixDraft": true
}
```

### ステップ2: 確認操作

**質問に回答:**

```
POST /api/v1/agent/run/{runId}/matrix/answer
```

```json theme={null}
{
  "runId": "cmo...",
  "answer": "価格の列を追加して",
  "modelName": "gpt-5-mini-2025-08-07"
}
```

**確定:**

```
POST /api/v1/agent/run/{runId}/matrix/confirm
```

```json theme={null}
{ "runId": "cmo..." }
```

### ステップ3: レジューム → データ自動抽出

```
POST /api/v1/agent/run/stream/{runId}
```

```
→ matrix-structure-draft-delta → matrix-structure-draft-complete
→ matrix-data-preview（リアルタイム）
→ matrix-data-completed
→ complete
```

***

## 複合パターン: plan → report

1つの実行で複数の HITL が発生する場合があります。

```
first_plan → [confirm] → レジューム
  → [search/browse等で調査]
  → first_report_structure → ★停止★
    → [confirm] → レジューム
    → report_section_delta(複数) → report_complete → complete
```

この場合、レジューム → `pendingReportDraft: true` を検出 → confirm → 再レジューム、という流れを繰り返します。

***

## 状態判定早見表

| `getStatus` レスポンス                   | 意味                  | 次のAPI操作                                                     |
| ----------------------------------- | ------------------- | ----------------------------------------------------------- |
| `pendingPlanDraft: true`            | プラン確認待ち             | `plan/answer` or `plan/confirm` or `plan/skip` → レジューム      |
| `pendingReportDraft: true`          | レポート構成確認待ち          | `report/answer` or `report/confirm` → レジューム                 |
| `pendingMatrixDraft: true`          | マトリクス構成確認待ち         | `matrix/answer` or `matrix/confirm` → レジューム                 |
| `browseState.askHumanQuestion` あり   | browse 中の質問待ち       | `browser/answer-question`。`/agent/run/stream/{runId}` は呼ばない |
| `browseState.isBrowsing: true`      | browse 実行中          | 必要なら `browser/spontaneous-input`。通常は待機                      |
| `skillState.pendingSecretKeys` あり   | skill の secret 登録待ち | 不足キーを `/secret` に登録。通常はレジューム不要                              |
| すべて `false` + `status: "completed"` | 完了                  | 不要                                                          |
| すべて `false` + `status: "running"`   | 実行中                 | ポーリングで待機                                                    |

## skill の secret 登録待ち

skill が外部APIキーなどを必要とする場合、SSE に `skill-ask-secret` が流れ、`GET /agent/run/{runId}/status` の `skillState.pendingSecretKeys` に不足キーが表示されます。

```json theme={null}
{
  "skillState": {
    "isRunningSkill": true,
    "skillName": "patent-search",
    "pendingSecretKeys": ["PATENT_API_KEY"]
  }
}
```

不足しているキーごとに `/secret` へ登録します。

```bash theme={null}
curl -X POST "https://app.snorbe.deskrex.ai/api/v1/secret" \
  -H "Authorization: Bearer snorbe_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "PATENT_API_KEY",
    "value": "your-secret-value"
  }'
```

secret 登録は待機中の skill に通知されるため、通常は `/agent/run/stream/{runId}` を呼び直す必要はありません。クライアントは同じSSEを読み続けるか、`GET /agent/run/{runId}/status` で `pendingSecretKeys` が空になることを確認します。

## plan / report / matrix の回答API

`pendingPlanDraft`、`pendingReportDraft`、`pendingMatrixDraft` は、どれも「ドラフトに対する確認待ち」です。修正したい場合は `/answer` に回答を送り、返ってきた再生成ドラフトを確認します。問題なければ `/confirm` で確定し、最後に `/agent/run/stream/{runId}` で実行を再開します。

| 状態                         | 修正・回答                                   | 確定                                                                             | レジューム                            |
| -------------------------- | --------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------- |
| `pendingPlanDraft: true`   | `POST /agent/run/{runId}/plan/answer`   | `POST /agent/run/{runId}/plan/confirm` または `POST /agent/run/{runId}/plan/skip` | `POST /agent/run/stream/{runId}` |
| `pendingReportDraft: true` | `POST /agent/run/{runId}/report/answer` | `POST /agent/run/{runId}/report/confirm`                                       | `POST /agent/run/stream/{runId}` |
| `pendingMatrixDraft: true` | `POST /agent/run/{runId}/matrix/answer` | `POST /agent/run/{runId}/matrix/confirm`                                       | `POST /agent/run/stream/{runId}` |

### `/answer` のリクエストボディ

Plan / Report / Matrix の `/answer` は共通の形です。

```json theme={null}
{
  "runId": "cmo...",
  "answer": "このドラフトに追加してほしい内容、直してほしい観点、削ってほしい範囲を書く",
  "modelName": "gpt-5-mini-2025-08-07",
  "fileUrls": ["https://example.com/reference.pdf"]
}
```

| フィールド       | 必須  | 説明                                 |
| ----------- | --- | ---------------------------------- |
| `runId`     | はい  | 対象の AgentRun ID。パスの `{runId}` と同じ値 |
| `answer`    | はい  | ドラフトへの回答・修正指示                      |
| `modelName` | はい  | 再生成に使うモデル                          |
| `fileUrls`  | いいえ | 回答時に参照させるファイルURL                   |

例:

```bash theme={null}
curl -X POST "https://app.snorbe.deskrex.ai/api/v1/agent/run/{runId}/matrix/answer" \
  -H "Authorization: Bearer snorbe_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "runId": "{runId}",
    "answer": "列に価格、導入企業、主要ユースケースを追加してください",
    "modelName": "gpt-5-mini-2025-08-07"
  }'
```

### `/confirm` と `/skip` のリクエストボディ

確定系APIは `runId` だけを送ります。

```json theme={null}
{
  "runId": "{runId}"
}
```

`plan/skip` は「追加回答なしでプランを確定する」操作です。Report と Matrix には skip はありません。

## APIユーザーの基本ループ

```
1. POST /agent/run/stream → SSEイベントを受信
2. browse-start が来たら session_id を保存
3. browse-ask-human が来たら /browser/answer-question で回答
4. complete イベントでストリーム終了
5. GET /agent/run/{runId}/status で pending*Draft / browseState / skillState を確認
6. plan/report/matrix の pending があれば → answer or confirm → POST /agent/run/stream/{runId} でレジューム
7. skillState.pendingSecretKeys があれば → /secret に登録
8. complete まで 4-7 を繰り返す
```

<Note>
  `plan` / `report` / `matrix` は「エージェント実行全体が一度止まる」ため、確認後に `/agent/run/stream/{runId}` でレジュームします。`browse` の質問は「ブラウザセッションへの入力待ち」なので、`/browser/answer-question` に回答し、同じ実行をそのまま続けます。
</Note>

***

## 実装例: 完全なエージェント実行クライアント

以下は、SSEストリーミング・HITL確認・レジュームをすべて自動で処理するPython実装例です。APIをどう組み合わせて進行させるかの参考にしてください。

### シンプルな実行（HITLなし）

ツールなし or search/browse/skill など、HITLが発生しないケースです。

```python theme={null}
import requests
import json

API_KEY = "snorbe_your_api_key_here"
BASE = "https://app.snorbe.deskrex.ai/api/v1"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Accept": "text/event-stream",
    "Content-Type": "application/json",
}

def run_agent(prompt: str) -> str:
    """エージェントを実行し、最終テキストを返す（HITLなしの場合）"""
    resp = requests.post(
        f"{BASE}/agent/run/stream",
        headers=HEADERS,
        json={
            "modelName": "gpt-5-mini-2025-08-07",
            "promptKey": "chat-routing",
            "inputText": prompt,
            "locale": "ja",
        },
        stream=True,
        timeout=300,
    )

    result = {}
    for line in resp.iter_lines(decode_unicode=True):
        if not line or not line.startswith("data: "):
            continue
        event = json.loads(line[6:])

        if event["type"] == "delta":
            # リアルタイム表示
            print(event["payload"]["deltaText"], end="", flush=True)
        elif event["type"] == "complete":
            result = event["payload"]
            print()  # 改行

    return result.get("text", "")

# 使い方
text = run_agent("最新のAIニュースを3つ教えて")
```

### HITL対応の完全ループ

plan → report → matrix など、複数回のHITL確認が発生するケースです。

```python theme={null}
import requests
import json
import time

API_KEY = "snorbe_your_api_key_here"
BASE = "https://app.snorbe.deskrex.ai/api/v1"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Accept": "text/event-stream",
    "Content-Type": "application/json",
}

def stream_until_complete(run_url: str, body: dict) -> dict:
    """SSEストリームを読み取り、completeイベントのpayloadを返す"""
    resp = requests.post(run_url, headers=HEADERS, json=body, stream=True, timeout=600)
    result = {}
    for line in resp.iter_lines(decode_unicode=True):
        if not line or not line.startswith("data: "):
            continue
        event = json.loads(line[6:])

        if event["type"] == "delta":
            print(event["payload"].get("deltaText", ""), end="", flush=True)
        elif event["type"] == "first-plan":
            print(f"\n[Plan] {event['payload']['plan']['goal'][:80]}...")
        elif event["type"] == "first_report_structure":
            print(f"\n[Report] {event['payload']['title']}")
        elif event["type"] == "first_matrix_structure":
            print(f"\n[Matrix] {event['payload']['title']}")
        elif event["type"] == "complete":
            result = event["payload"]
            print()
        elif event["type"] == "error":
            raise RuntimeError(event["payload"]["message"])

    return result

def get_status(run_id: str) -> dict:
    """実行ステータスを取得"""
    resp = requests.get(
        f"{BASE}/agent/run/{run_id}/status",
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    return resp.json()

def confirm_draft(run_id: str, draft_type: str) -> None:
    """HITL確認（plan/report/matrix）をスキップして確定"""
    resp = requests.post(
        f"{BASE}/agent/run/{run_id}/{draft_type}/skip",
        headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
        json={"runId": run_id},
    )
    data = resp.json()
    if data.get("status") != "confirmed":
        raise RuntimeError(f"Confirm failed: {data}")

def run_agent_with_hitl(prompt: str) -> str:
    """HITLを含む完全なエージェント実行ループ"""
    # Step 1: 初回実行
    result = stream_until_complete(
        f"{BASE}/agent/run/stream",
        {
            "modelName": "gpt-5-mini-2025-08-07",
            "promptKey": "chat-routing",
            "inputText": prompt,
            "locale": "ja",
        },
    )
    run_id = result.get("runId")
    if not run_id:
        return result.get("text", "")

    # Step 2: HITLループ
    max_iterations = 10
    for _ in range(max_iterations):
        status = get_status(run_id)

        # 完了チェック
        if status["status"] == "completed":
            break

        # HITL確認が必要かチェック
        hitl_type = None
        if status.get("pendingPlanDraft"):
            hitl_type = "plan"
        elif status.get("pendingReportDraft"):
            hitl_type = "report"
        elif status.get("pendingMatrixDraft"):
            hitl_type = "matrix"

        if not hitl_type:
            # 実行中ならポーリング
            time.sleep(5)
            continue

        # Step 3: 確認 → レジューム
        print(f"\n[HITL] {hitl_type} confirmation required, auto-confirming...")
        confirm_draft(run_id, hitl_type)

        result = stream_until_complete(
            f"{BASE}/agent/run/stream/{run_id}",
            {"modelName": "gpt-5-mini-2025-08-07"},
        )

    return result.get("text", "")

# 使い方: plan → 調査 → report まで自動進行
text = run_agent_with_hitl(
    "plan modeでAIエージェントの最新動向を調査して、レポートを作成して"
)
print(f"\nFinal result: {text[:200]}...")
```

### HITL確認に人間の判断を挟む場合

自動確定ではなく、質問内容を表示して人間が判断するパターンです。

```python theme={null}
def confirm_draft_with_human(run_id: str, draft_type: str) -> None:
    """質問を表示し、人間の入力に基づいて回答または確定"""

    # 初回ストリームで first_plan 等のイベントから question を抽出済みと仮定
    # （上記 stream_until_complete で event を保存しておく）

    answer = input(f"\n[{draft_type}] 質問に回答（空Enterでスキップ）: ").strip()

    if answer:
        # 回答を送信 → ドラフトが再生成される
        resp = requests.post(
            f"{BASE}/agent/run/{run_id}/{draft_type}/answer",
            headers={
                "Authorization": f"Bearer {API_KEY}",
                "Content-Type": "application/json",
            },
            json={
                "runId": run_id,
                "answer": answer,
                "modelName": "gpt-5-mini-2025-08-07",
            },
        )
        print(f"回答送信 → {resp.json().get('status')}")
        # 再生成されたドラフトに対して再度確認が必要
    else:
        # スキップして確定
        confirm_draft(run_id, draft_type)
```

### 非ストリーミング版（ポーリング方式）

SSEではなく、非ストリーミングAPI + ポーリングで進行する方式です。

```python theme={null}
def run_agent_polling(prompt: str) -> str:
    """非ストリーミングで実行し、ポーリングで完了を待つ"""
    # Step 1: 実行開始
    resp = requests.post(
        f"{BASE}/agent/run",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "modelName": "gpt-5-mini-2025-08-07",
            "promptKey": "chat-routing",
            "inputText": prompt,
            "locale": "ja",
        },
        timeout=300,
    )
    data = resp.json()
    run_id = data["runId"]

    # Step 2: 完了 or HITL をポーリング
    while True:
        status = get_status(run_id)

        if status["status"] == "completed":
            return data.get("text", "")

        # HITL確認
        for draft_type in ["plan", "report", "matrix"]:
            if status.get(f"pending{draft_type.capitalize()}Draft"):
                print(f"[HITL] {draft_type} 確認待ち、自動スキップ...")
                confirm_draft(run_id, draft_type)

                # レジューム（非ストリーミング）
                resp = requests.post(
                    f"{BASE}/agent/run/{run_id}/resume",
                    headers={
                        "Authorization": f"Bearer {API_KEY}",
                        "Content-Type": "application/json",
                    },
                    json={
                        "runId": run_id,
                        "modelName": "gpt-5-mini-2025-08-07",
                    },
                    timeout=300,
                )
                data = resp.json()
                break
        else:
            time.sleep(5)

# 使い方
text = run_agent_polling("半導体業界の最新トレンドを調べて")
```

### TypeScript実装例

```typescript theme={null}
async function* consumeSSE(
  url: string,
  body: Record<string, unknown>,
  apiKey: string,
): AsyncGenerator<{ type: string; payload: Record<string, unknown> }> {
  const resp = await fetch(url, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      Accept: "text/event-stream",
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  const reader = resp.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    const parts = buffer.split("\n\n");
    buffer = parts.pop() ?? "";

    for (const part of parts) {
      for (const line of part.split("\n")) {
        if (line.startsWith("data: ")) {
          yield JSON.parse(line.slice(6));
        }
      }
    }
  }
}

async function runAgent(prompt: string, apiKey: string): Promise<string> {
  const BASE = "https://app.snorbe.deskrex.ai/api/v1";
  let result: Record<string, unknown> = {};

  // Step 1: 初回ストリーミング
  let runId = "";
  for await (const event of consumeSSE(
    `${BASE}/agent/run/stream`,
    {
      modelName: "gpt-5-mini-2025-08-07",
      promptKey: "chat-routing",
      inputText: prompt,
      locale: "ja",
    },
    apiKey,
  )) {
    if (event.type === "delta") {
      process.stdout.write(event.payload.deltaText ?? "");
    } else if (event.type === "complete") {
      result = event.payload;
      runId = event.payload.runId as string;
    }
  }

  // Step 2: HITLループ
  for (let i = 0; i < 10; i++) {
    const statusResp = await fetch(`${BASE}/agent/run/${runId}/status`, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    const status = await statusResp.json();

    if (status.status === "completed") break;

    const draftType = status.pendingPlanDraft
      ? "plan"
      : status.pendingReportDraft
        ? "report"
        : status.pendingMatrixDraft
          ? "matrix"
          : null;

    if (!draftType) {
      await new Promise((r) => setTimeout(r, 5000));
      continue;
    }

    // 確認 → レジューム
    await fetch(`${BASE}/agent/run/${runId}/${draftType}/skip`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ runId }),
    });

    for await (const event of consumeSSE(
      `${BASE}/agent/run/stream/${runId}`,
      { modelName: "gpt-5-mini-2025-08-07" },
      apiKey,
    )) {
      if (event.type === "delta") {
        process.stdout.write(event.payload.deltaText ?? "");
      } else if (event.type === "complete") {
        result = event.payload;
      }
    }
  }

  console.log();
  return (result.text as string) ?? "";
}
```
