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

# External Agent の技術要件

> FPT AI EOS に接続する前にパートナーの技術チームが用意しておくもの

このページは External Agent を運用する技術チーム向けです。オンボーディング前に用意しておくものを一通り挙げます。2 つのエンドポイント、認証、レスポンスストリームの形式、各種の時間制限、そして External Agent 側で自前で持つ必要があるものです。Console 側での登録手順は [外部エージェントを接続する](https://docs-agents.fpt.ai/ja/connect-external-agent.md) にあります。

このページには 3 つの名前が出てきます。**Console** は運用担当者が External Agent を登録し、設定し、公開する場所です。**Workspace** はエンドユーザーがチャットする場所です。**FPT AI EOS** はその裏で動くプラットフォームで、パートナー側のサーバーへ接続を開くのはこれです。

> [!NOTE]
>
> FPT AI EOS が **クライアント**、External Agent が **サーバー** です。FPT AI EOS が呼び、External Agent が答えます。逆方向はありません。開いているストリームの外に、プラットフォームへデータを送り込む経路はありません。

登録が済むと、あなたの External Agent は Workspace の中でふつうの Digital Worker として現れ、利用者はほかの Digital Worker と同じようにチャットします。このページでは、パートナー側のインフラで動く部分を引き続き External Agent と呼びます。プラットフォームが自分で動かす Digital Worker と区別するためです。

## 役割の分担

プラットフォームは External Agent を実行しません。FPT AI EOS が行うのは外側だけです。Workspace のチャット画面、会話履歴、Console での登録と公開、そして 1 ターンにつき 1 回の `POST /runs` 呼び出しです。それ以外はすべてパートナー側の責任です。

| 担当範囲 | 担当 |
| - | - |
| チャット画面と会話履歴 | Workspace |
| External Agent の登録、設定、テスト、公開 | Console |
| 毎ターンの `POST /runs` 呼び出し、受け取ったフレームの表示 | FPT AI EOS |
| 業務ロジック、プロンプト、モデル呼び出し、ツール実行 | External Agent |
| **会話のコンテキストと状態** | External Agent |
| ホスティング、スケール、稼働率、ログ、監視 | External Agent |
| External Agent が生成したファイルのホスティングとダウンロード用リンクの提供 | External Agent |

> [!WARNING]
>
> FPT AI EOS は **会話のコンテキストを代わりに保持しません**。読み戻すためのエンドポイントもありません。このページで最も重要な点です。下の「コンテキストを自分で持つ」を参照してください。

## 1 ターンのチャットが External Agent を通る流れ

![External Agent を通る 1 ターンのシーケンス図](https://mintcdn.com/fpt-62e894b4/Ni9tqJzj8ysW4-TM/images/ext_run_ja_v2.png?fit=max\&auto=format\&n=Ni9tqJzj8ysW4-TM\&q=85\&s=d1a794e6f53ca048abc7ebb92c9c6a3e)

## 必須の 2 エンドポイント

| メソッド | パス | 返すもの | 用途 |
| - | - | - | - |
| `POST` | `{base_url}/runs` | `text/event-stream` | 1 リクエストが 1 ターンのチャット |
| `GET` | `{base_url}/health` | `application/json` | 死活確認とプロトコルバージョン |

この 2 つのパスは `base_url` の下に固定で、個別に設定することはできません。`base_url` が `https://agent.example.com/fpt` なら、プラットフォームは `POST https://agent.example.com/fpt/runs` と `GET https://agent.example.com/fpt/health` を呼びます。

`base_url` の制約:

* **https** が必須です。FPT AI EOS は `http` を拒否します。
* `user:pass@` の形で URL に認証情報を埋め込むことはできません。
* プラットフォームは **接続のたびに** DNS を解決してアドレスを検査します。オンボーディング時の 1 回だけではありません。プライベート、ループバック、リンクローカル、予約済み、マルチキャストのアドレスはすべて拒否されます。内部アドレスに向くよう DNS を変えたドメインは、次のターンで即座に遮断されます。
* **リダイレクトは追いません。** `301` や `302` はエラー扱いです。場所を変えるときは `base_url` を変更してください。

## 認証

プラットフォームは、オンボーディング時に双方で合意したヘッダーの組を、両方のエンドポイントに同じ内容で送ります。その中身を決めるのはパートナー側です。FPT AI EOS はヘッダー名もスキームも指定せず、値に何も付け足しません。

| 使い方 | 例 |
| - | - |
| Bearer トークン | `Authorization: Bearer <token>` |
| 任意の名前の API キー | `X-API-Key: <key>` |
| 複数ヘッダーの同時利用 | `X-API-Key: <key>` と `X-Tenant: <id>` |

正しく実装すべき点:

* **値はそのまま送られます。** 運用担当者が Console で `Bearer abc123` という文字列全体を入力し、プラットフォームはそれをそのまま送ります。接頭辞を付け足すことはありません。
* **組に含まれるヘッダーをすべて検査してください。** 2 つ必須なら、片方しかないリクエストは拒否する必要があります。
* **`hmac.compare_digest` や `crypto.timingSafeEqual` のような定数時間比較を使ってください。** `==` は最初に異なるバイトで止まるため、応答時間から正しい値が少しずつ漏れます。
* 誤りや不足があれば `401` を返します。

> [!NOTE]
>
> 認証なしという選択も有効です。その場合プラットフォームはヘッダーを一切送りません。IP 許可リスト、mTLS、閉域網など別の手段で既にアクセスを制限している場合にだけ選んでください。

> [!NOTE]
>
> HMAC 署名はありません。`X-FPT-Signature` ヘッダーは契約から削除済みなので待たないでください。ターンのリプレイ対策は `runId` で引き続き可能です。

## プラットフォームが送る識別子

| フィールド | 有無 | 意味 |
| - | - | - |
| `threadId` | 常にあり | 会話 ID。**自前のコンテキスト保存のキー** |
| `runId` | 常にあり | このターンの ID。冪等キーとして使用 |
| `agentId` | 常にあり | 呼ばれている External Agent。1 つの `base_url` で複数の External Agent を扱えます |
| `workspaceId` | 常にあり | 呼び出し元の組織。クォータ、レート制限、顧客別設定に使用 |
| `userId` | 無い場合あり | 会話している人。人が介在しないターンでは欠けます |

> [!WARNING]
>
> ID はすべて **文字列** で送られ、63 ビット整数です。数値としてパースしないでください。JavaScript の `JSON.parse` は 2^53 を超えると丸め誤差が出て、別々の ID が同一になることがあります。

ユーザーが過去のターンを編集して再実行すると、`threadId` にブランチの接尾辞が付きます。直線的な会話では `918273645500123456`、フォーク後は `918273645500123456:<branch>` です。パートナー側から見れば **別の会話 2 つ** であり、それが正しい挙動です。片方のブランチがもう片方のコンテキストを見てはいけません。

## `POST /runs` のボディ

| フィールド | 型 | 必須 | 値 |
| - | - | - | - |
| `protocolVersion` | string | はい | 常に `"1"` |
| `runId` | string | はい | ターン ID。毎ターン新しくなります |
| `threadId` | string | はい | 会話 ID |
| `agentId` | string | はい | 呼ばれている External Agent |
| `workspaceId` | string | はい | 呼び出し元の組織 |
| `input` | string | はい | ユーザーが入力した文。**空文字列もあり得ます** |
| `userId` | string | いいえ | 会話している人 |
| `attachments` | array | いいえ | このターンで添付されたファイル |
| `resume` | object | いいえ | 問い返しへの回答ターンにのみ存在 |
| `history` | array | いいえ | **実際にはほぼ常に欠けています** |

通常のチャットターンはこれだけです:

```json theme={null}
{
  "protocolVersion": "1",
  "runId": "01K5BH5PSX926W6RZ6YR1P8WWV",
  "threadId": "918273645500123456",
  "agentId": "918273688800000200",
  "workspaceId": "918273600000000001",
  "userId": "918273644400000042",
  "input": "注文 DH-99217 は今どこですか?"
}
```

> [!NOTE]
>
> `input` が空なのは正常で、意味があります。ユーザーが問い返しに対して *承認* または *拒否* を押しただけのとき、`input` に入れる文字はなく、判断は `resume.decision` に入ります。

## `threadId` をキーにコンテキストを自分で持つ

現行バージョンでは、`history` が埋まるのはユーザーが会話をフォークしたときだけです。直線的な会話、つまり通常のチャットすべてにおいて、プラットフォームは **過去のターンを 1 つも送りません**。

その結果として、External Agent は **必ず** 自分でコンテキストを保存し、`threadId` をキーにし、TTL を自分で決め、デプロイをまたいで残るようにする必要があります。ほかに情報源はありません。

> [!WARNING]
>
> `history` に頼って前のターンを覚えようとする External Agent は、どのメッセージも最初の 1 通であるかのように答えます。この症状はモデルのせいにされがちなので、回答品質を疑う前にコンテキストのキーを確認してください。

コンテキストと同じ系統の話として **冪等性** があります。完了済みの `runId` に対しては、再実行せず以前の結果を返してください。プラットフォームに業務レベルのリトライはありませんが、このキーはターンのリプレイを防ぎます。

## SSE ストリームで返す

`Content-Type: text/event-stream` を返し、`Cache-Control: no-cache` と `X-Accel-Buffering: no` も付けてください。各フレームは JSON オブジェクト 1 つを載せた `data:` 行で、フレームどうしは空行で区切ります。

プラットフォームのリーダーは最小構成の SSE リーダーです。`event:`、`id:`、`retry:` は無視され、内容はすべて `data:` 行の JSON に入ります。コロンで始まる行はコメントで、接続維持のハートビートとして使われます。

### 1 つの回答の最小ストリーム

```text theme={null}
data: {"type":"TEXT_MESSAGE_START","messageId":"a1","role":"assistant"}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"a1","delta":"昨日の金は "}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"a1","delta":"0.5% 下げました。"}

data: {"type":"TEXT_MESSAGE_END","messageId":"a1"}

data: {"type":"RUN_FINISHED","outcome":{"type":"success"}}
```

### すべてのフレーム共通のエンベロープ

| フィールド | 必須 | 値 |
| - | - | - |
| `type` | はい | フレーム名。未知のものは破棄され、ターンは壊れません |
| `event_id` | いいえ | フレーム ID。同じフレームを送り直すときは同じ ID を使うとブラウザ側で重複排除されます |
| `run_id` | いいえ | ある場合はリクエストの `runId` と一致必須。違えばそのフレームは破棄されます |
| `threadId` | いいえ | ある場合はリクエストの `threadId` と一致必須 |
| `messageId` | いいえ | メッセージを伴うフレームで使用 |

`messageId` と `toolCallId` は **1 ターン内で** 一意であれば十分なので、毎ターン `"a1"` を使い回しても安全です。

### プラットフォームが処理するフレーム

| フレーム | 位置づけ | 必須フィールド |
| - | - | - |
| `TEXT_MESSAGE_START` | 必須 | `messageId` |
| `TEXT_MESSAGE_CONTENT` | 必須 | `messageId`, `delta` |
| `TEXT_MESSAGE_END` | 必須 | `messageId` |
| `RUN_FINISHED` | 必須 | `outcome` |
| `RUN_ERROR` | 必須 | `message` |
| `TOOL_CALL_START` | 推奨 | `toolCallId`, `toolCallName` |
| `TOOL_CALL_ARGS` | 推奨 | `toolCallId`, **文字列** の `delta` |
| `TOOL_CALL_END` | 推奨 | `toolCallId` |
| `TOOL_CALL_RESULT` | 推奨 | `toolCallId`, `content` |
| `STEP_STARTED` と `STEP_FINISHED` | 推奨 | `stepName` |
| `ACTIVITY_SNAPSHOT` | 推奨 | `messageId`, `activityType`, `content` |
| `ACTIVITY_DELTA` | 任意 | `messageId`, `patch` |
| `STATE_SNAPSHOT` | 任意 | `snapshot` |
| `STATE_DELTA` | 任意 | `delta` |
| `REASONING_MESSAGE_START`, `_CONTENT`, `_END` | 任意 | `messageId` |
| `CUSTOM` | 任意 | `name` |

ターンは `RUN_FINISHED` **または** `RUN_ERROR` で終わる必要があります。

> [!NOTE]
>
> **`RUN_STARTED` は送らないでください。** プラットフォームがターンを開くときに自分で出しています。**`REASONING_END` も送らないでください。** プラットフォームが `REASONING_MESSAGE_END` と併せて出します。

### テキストとツール呼び出しが交互に来る場合

連続したテキストのかたまりはそれぞれ独立したメッセージで、固有の `messageId` と `START` / `END` の組を持ちます。かたまりは次のツール呼び出しの前に閉じ、ツール呼び出しより後に来たテキストは新しいメッセージに属し、**回答はそのターンの最後のメッセージ** です。

### ターンの正しい終わらせ方

* ストリームを閉じる **前に** `RUN_FINISHED` か `RUN_ERROR` を送ってください。
* 最後のフレームの前に、開いたものをすべて閉じてください。`TEXT_MESSAGE_START`、`TOOL_CALL_START`、`STEP_STARTED`、`REASONING_MESSAGE_START` です。プラットフォームも代わりに閉じますが、**`TOOL_CALL_RESULT` を勝手に作ることはありません**。
* 空の吹き出しを出さないでください。言うことがなければ `RUN_ERROR` を送ります。
* ツール呼び出しは、それを参照する回答より **前** に来る必要があります。プラットフォームはレスポンスをストリームとして読むので、順序は実際の制約です。
* 処理中は接続維持のコメント行を、少なくとも 15 秒に 1 回送ってください。

> [!WARNING]
>
> ストリームをきれいに閉じたが終了フレームを忘れた場合、**エラーにはなりません**。プラットフォームは受け取った分をそのまま成功として記録します。文の途中で落ちてソケットを閉じただけの External Agent は、途中で切れた回答を完全な回答として残します。小さく見えますが、それを止められるのは `RUN_ERROR` だけです。

## 時間制限と上限

以下は既定値であり、契約上の定数ではありません。実際に動かす環境の数値が必要なら運用担当者に確認してください。

| 項目 | 既定値 |
| - | - |
| TCP と TLS の接続 | 5 秒 |
| **最初のフレーム** | レスポンス開始から **10 秒** |
| チャンク間の無音 | 30 秒。コメント行でこのタイマーはリセットされます |
| ターン全体 | 5 分。オンボーディング時に External Agent ごとに設定 |
| 1 フレームのサイズ | 256 KiB |
| ストリーム全体 | 8 MiB |
| 1 ターンのフレーム数 | 20,000 |
| 添付リンクの有効期間 | 7 日 |

> [!WARNING]
>
> **最初のフレームの 10 秒という制限は、ほぼすべての連携がつまずく箇所です。** これは回答全体の期限ではなく、**最初のフレーム** の期限です。ターン全体を処理し終えてから最初のバイトを返す構成、たとえばノーコードのワークフローやレスポンスをバッファするサーバーレス関数では、External Agent が健全でもタイムアウトします。
>
> 正しい対処は、**モデルを呼ぶ前にフレームを 1 つ即座に送り出す** ことです。`STEP_STARTED` 1 つかコメント行 1 行でストリームが開き、はるかに余裕のある 30 秒の無音タイマーに移ります。

ターンのライフサイクルについてもう 2 点:

* **途中キャンセルはありません。** ユーザーが停止を押すとプラットフォームは接続を閉じ、それがキャンセル信号のすべてです。クライアントが切断したら処理を止めてください。
* **リトライもありません。** 失敗したターンは失敗したままです。ユーザーが送り直せば、それは新しい `runId` です。

## ファイル

### ユーザーがアップロードしたファイル

`attachments[]` の各要素は `id`、`url`、`name`、`mime`、`sizeBytes` を持ちます。リンクは https で、認証ヘッダーなしでダウンロードでき、**期限があります**。

* **重複判定は `id` で行ってください。** `url` や `name` ではありません。同じファイルなら `id` はターンをまたいで変わりません。
* 後のターンで使うなら、**有効なうちにダウンロードして自分で保存してください。** リンクは失効する前提で考えます。
* リンクに認証はないので、持っている人は誰でもダウンロードできます。ログに残さず、自社システムの外へ転送しないでください。
* メタデータを信用しないでください。`name` をパスとして使わず、`mime` はプラットフォームの申告なので中身を自分で検査します。

> [!NOTE]
>
> プラットフォームがリンクを発行できなかったファイルは `attachments` から外れ、システム通知の 1 行として `input` の中で名前だけ伝えられます。それはプラットフォームの文であってユーザーの文ではないので、そのまま復唱しないでください。

### External Agent が生成したファイル

External Agent が自分でファイルをホストし、ユーザーのブラウザから到達できる URL を用意したうえで、`artifacts` キーの `STATE_SNAPSHOT`、または `name` が `file` の `CUSTOM` で送ります。

> [!WARNING]
>
> **1 ファイルにつき 1 経路。** この 2 つは独立した経路で、互いに重複排除されません。同じファイルを両方で送ると、ユーザーには **カードが 2 枚** 表示されます。本文中に markdown 画像でも同じファイルを埋め込めば 3 枚になります。

`STATE_SNAPSHOT` と `STATE_DELTA` で受け付けられるキーは `todos`、`artifacts`、`files` の 3 つだけです。それ以外は破棄されます。

## ユーザーへの問い返し

先に進むのにユーザーの判断が必要なときは、`outcome.type` を `interrupt` にした `RUN_FINISHED` でターンを終えます。1 回の停止が運べる interrupt は **ちょうど 1 つ** です。

| `kind` | 表示 | 内容フィールド |
| - | - | - |
| `approval` | 承認と拒否の 2 ボタン | `prompt` のみ |
| `choice` | 選択肢付きの質問。常に自由入力欄も付きます | `options` |
| `input` | 自由入力欄だけの質問 | `prompt` のみ |
| `review` | 提案されたツール呼び出し 1 つ以上。引数は閲覧と編集が可能 | `items` |
| `authorization` | アカウント連携カード。`authorizeUrl` をポップアップで開きます | `provider`, `authorizeUrl` |

```json theme={null}
{ "type": "RUN_FINISHED",
  "outcome": { "type": "interrupt",
    "interrupts": [ { "kind": "approval",
                      "prompt": "8 月 20 日の HAN 発 SGN 行きで確定しますか?" } ] } }
```

次のターンで、プラットフォームは判断を `resume` に入れて返します:

| interrupt の `kind` | 受け取る `decision` | 内容の在り処 |
| - | - | - |
| `approval` | `approve` または `reject` | なし。`input` は空 |
| `choice` と `input` | `respond` | `message`、`answers`、`input` |
| `review` | `approve`, `reject`, `edit`, `respond` | `edit` のときは `editedAction` |
| `authorization` | `authorized` | なし |

> [!WARNING]
>
> 上記 5 つ以外の `kind`、`name` を欠いた `review`、不正な `authorizeUrl` は **ターン全体を失敗させます**。壊れたフレームが無視されずにターンを壊す唯一のケースです。

`authorizeUrl` は 4 点が検査されます。存在すること、https であること、認証情報を埋め込んでいないこと、ホストが IP アドレスでないことです。ドメイン許可リストはないので、事前登録は不要です。

> [!NOTE]
>
> `decision` が `authorized` でも、権限が付与された証拠にはなりません。先に進む前に自前のトークン保存を確認してください。

## `GET /health`

```json theme={null}
{ "status": "ok", "protocolVersions": ["1"], "name": "Flight Assistant", "version": "2.3.1" }
```

| フィールド | 必須 | 値 |
| - | - | - |
| `status` | はい | `ok`、`degraded`、`down` のいずれか |
| `protocolVersions` | はい | **`"1"` を含む必要があります** |
| `name` | いいえ | 表示名 |
| `version` | いいえ | パートナー側のバージョン |

このエンドポイントも `/runs` と同じ認証ヘッダーの組を受け取ります。軽量であること、モデルやデータベースを呼ばないこと、リダイレクトしないことが条件です。

プラットフォームは結果を 3 つの状態にまとめます。健全なら、チャットターンはそのまま通ります。`degraded` でも同じです。遅い External Agent のほうが、完全に止められた External Agent よりましだからです。それ以外は `unavailable` で、接続できないとき、`401` や `403` が返るとき、ボディが JSON でないとき、`protocolVersions` に `"1"` が無いとき、あるいは自分で `down` と申告したときに起こります。

> [!WARNING]
>
> `/health` が `ok` を返すだけでは **公開には足りません**。運用担当者が Console でテストを押すと、プラットフォームは `input` を `Hello` にした実際の `POST /runs` ターンも送ります。`threadId` は `validate-<16 桁の 16 進数>` という使い捨ての形で、最初の正常なフレームで打ち切られます。このターンは履歴に保存されません。見慣れない `threadId` で処理が壊れないようにしてください。

> [!NOTE]
>
> 定期的な自動ヘルスチェックはまだありません。3 回連続で失敗するとエンドポイントは `unavailable` になり、チャットターンは即座に拒否されます。障害が解消しても基盤は **自動では気づかない** ため、運用担当者に Console でのテスト再実行を依頼してください。

## ログ突き合わせ用のエラーコード

`code` はユーザーには表示されませんが、障害時に運用担当者が最初に読む情報です。

| `code` | 発生条件 | 対処する側 |
| - | - | - |
| `ENDPOINT_UNAVAILABLE` | エンドポイントが利用不可と記録され、リクエストが送られていない | FPT 側。テストを再実行 |
| `PROTOCOL_MISMATCH` | 直近のテストで `protocolVersions` に `"1"` が無かった | External Agent |
| `EGRESS_REFUSED` | `base_url` がプラットフォームから接続できないアドレスに解決される | インフラ |
| `CONNECT_TIMEOUT` | 5 秒以内にハンドシェイクできない | インフラ |
| `UPSTREAM_REJECTED` | `POST /runs` に対し External Agent が HTTP 3xx か 4xx を返した。多くは `401` | External Agent |
| `UPSTREAM_UNAVAILABLE` | External Agent が HTTP 5xx を返した | External Agent |
| `TTFB_TIMEOUT` | ストリームは開いたが 10 秒間フレームが来ない | External Agent |
| `IDLE_TIMEOUT` | ストリーム中に 30 秒を超える無音。ハートビート不足 | External Agent |
| `STREAM_TRUNCATED` | ストリームが途中で切れた。受け取った分は保存されます | インフラ |
| `LIMIT_EXCEEDED` | フレームサイズ、総バイト数、フレーム数の上限超過 | External Agent |
| `INTERRUPT_REJECTED` | 問い返しのカードを構築できなかった | External Agent |

失敗したターンも履歴に残ります。送信できた分のテキストはエラー表示付きで会話に出ます。

## 作らなくてよいもの

| 不要なもの | 理由 |
| - | - |
| `GET /tools`、`POST /credentials`、`POST /credentials/revoke` | 現行バージョンのプラットフォームは呼びません |
| `X-FPT-Signature` の署名検証 | 契約から削除済み |
| `history` を前提としたロジック | ほぼ常に欠けています。「コンテキストを自分で持つ」を参照 |
| キャンセルや再開のエンドポイント | 契約にありません |

## オンボーディング前のチェックリスト

**1. 2 つのエンドポイントが動く**

`GET /health` が `status` を `ok` で返し、`protocolVersions` に `"1"` を含むこと。`POST /runs` が上記のボディを受け取り `text/event-stream` を返すこと。

**2. 認証が漏れなく、安全に比較されている**

合意した組のヘッダーをすべて検査し、定数時間比較を使い、失敗時は `401` を返すこと。

**3. 最初のフレームが 10 秒以内**

モデルを呼ぶ前にフレームを 1 つ送り出すこと。最も外しやすい制限です。

**4. threadId をキーにコンテキストを保存**

自前の保存、自前の TTL、デプロイをまたいで残ること。互いに参照し合う 2 ターンを続けて質問して確認します。

**5. ターンが必ずきちんと終わる**

ストリームを閉じる前に `RUN_FINISHED` か `RUN_ERROR` を送り、開いたものをすべて閉じること。

**6. 接続維持のハートビート**

処理に時間がかかっている間、少なくとも 15 秒に 1 回コメント行を送ること。

**7. runId による冪等性**

完了済みの `runId` には再実行せず以前の結果を返すこと。

> [!NOTE]
>
> 問題が起きたら、運用担当者に 4 点を伝えてください。`runId`、`threadId`、発生時刻、そして上の表のエラーコードがあればそれです。双方のログを突き合わせるにはこれで足ります。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.