メインコンテンツへスキップ

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.

エージェント実行フロー

エージェントはユーザーの入力を分析し、自動的にツールを選択して実行します。一部のツール(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
直接回答ツールを使わずテキストで回答なし
searchWeb検索(SERP + スクレイピング + 要約)なし
x_searchX(Twitter)投稿検索なし
browseブラウザ操作による調査条件付き
source_summaryURLのスクレイピング・ファイル要約なし
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に回答します。
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 を使います。
{
  "runId": "cmo...",
  "sessionId": "browser-session-id",
  "modelName": "gpt-5-mini-2025-08-07",
  "input": "このPDFの内容を踏まえて回答してください",
  "fileUrls": ["https://example.com/file.pdf"]
}
質問待ちではないが、実行中のブラウザへ追加指示を出したい場合は /browser/spontaneous-input を使います。例: 「次は価格ページを開いて」「そのフォームには入力しないで」。
GET /agent/run/{runId}/statusbrowseState.askHumanQuestion でも質問待ちかどうかは確認できます。ただし回答には sessionId が必要です。APIクライアントは browse-start.payload.websocketInfo.session_id を受け取った時点で保存しておいてください。

パターン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
{
  "status": "pending",
  "pendingPlanDraft": true
}

ステップ2: 確認操作(いずれか1つ)

質問に回答してドラフトを修正:
POST /api/v1/agent/run/{runId}/plan/answer
{
  "runId": "cmo...",
  "answer": "追加で〇〇も調べて",
  "modelName": "gpt-5-mini-2025-08-07"
}
regenerated_plan イベントが返り、再度確認待ちになります。 プランを確定:
POST /api/v1/agent/run/{runId}/plan/confirm
{ "runId": "cmo..." }
plan_confirmed が返ります。 質問をスキップして確定:
POST /api/v1/agent/run/{runId}/plan/skip
{ "runId": "cmo..." }

ステップ3: レジューム

POST /api/v1/agent/run/stream/{runId}
{ "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
{
  "status": "pending",
  "pendingReportDraft": true
}

ステップ2: 確認操作

質問に回答:
POST /api/v1/agent/run/{runId}/report/answer
{
  "runId": "cmo...",
  "answer": "第3章を追加して",
  "modelName": "gpt-5-mini-2025-08-07"
}
確定:
POST /api/v1/agent/run/{runId}/report/confirm
{ "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
{
  "status": "pending",
  "pendingMatrixDraft": true
}

ステップ2: 確認操作

質問に回答:
POST /api/v1/agent/run/{runId}/matrix/answer
{
  "runId": "cmo...",
  "answer": "価格の列を追加して",
  "modelName": "gpt-5-mini-2025-08-07"
}
確定:
POST /api/v1/agent/run/{runId}/matrix/confirm
{ "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: truebrowse 実行中必要なら 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}/statusskillState.pendingSecretKeys に不足キーが表示されます。
{
  "skillState": {
    "isRunningSkill": true,
    "skillName": "patent-search",
    "pendingSecretKeys": ["PATENT_API_KEY"]
  }
}
不足しているキーごとに /secret へ登録します。
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}/statuspendingSecretKeys が空になることを確認します。

plan / report / matrix の回答API

pendingPlanDraftpendingReportDraftpendingMatrixDraft は、どれも「ドラフトに対する確認待ち」です。修正したい場合は /answer に回答を送り、返ってきた再生成ドラフトを確認します。問題なければ /confirm で確定し、最後に /agent/run/stream/{runId} で実行を再開します。
状態修正・回答確定レジューム
pendingPlanDraft: truePOST /agent/run/{runId}/plan/answerPOST /agent/run/{runId}/plan/confirm または POST /agent/run/{runId}/plan/skipPOST /agent/run/stream/{runId}
pendingReportDraft: truePOST /agent/run/{runId}/report/answerPOST /agent/run/{runId}/report/confirmPOST /agent/run/stream/{runId}
pendingMatrixDraft: truePOST /agent/run/{runId}/matrix/answerPOST /agent/run/{runId}/matrix/confirmPOST /agent/run/stream/{runId}

/answer のリクエストボディ

Plan / Report / Matrix の /answer は共通の形です。
{
  "runId": "cmo...",
  "answer": "このドラフトに追加してほしい内容、直してほしい観点、削ってほしい範囲を書く",
  "modelName": "gpt-5-mini-2025-08-07",
  "fileUrls": ["https://example.com/reference.pdf"]
}
フィールド必須説明
runIdはい対象の AgentRun ID。パスの {runId} と同じ値
answerはいドラフトへの回答・修正指示
modelNameはい再生成に使うモデル
fileUrlsいいえ回答時に参照させるファイルURL
例:
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 だけを送ります。
{
  "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 を繰り返す
plan / report / matrix は「エージェント実行全体が一度止まる」ため、確認後に /agent/run/stream/{runId} でレジュームします。browse の質問は「ブラウザセッションへの入力待ち」なので、/browser/answer-question に回答し、同じ実行をそのまま続けます。

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

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

シンプルな実行(HITLなし)

ツールなし or search/browse/skill など、HITLが発生しないケースです。
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確認が発生するケースです。
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確認に人間の判断を挟む場合

自動確定ではなく、質問内容を表示して人間が判断するパターンです。
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 + ポーリングで進行する方式です。
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実装例

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) ?? "";
}