FPT AI EOS が クライアント、External Agent が サーバー です。FPT AI EOS が呼び、External Agent が答えます。逆方向はありません。開いているストリームの外に、プラットフォームへデータを送り込む経路はありません。
役割の分担
プラットフォームは External Agent を実行しません。FPT AI EOS が行うのは外側だけです。Workspace のチャット画面、会話履歴、Console での登録と公開、そして 1 ターンにつき 1 回のPOST /runs 呼び出しです。それ以外はすべてパートナー側の責任です。
1 ターンのチャットが External Agent を通る流れ

必須の 2 エンドポイント
この 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 はヘッダー名もスキームも指定せず、値に何も付け足しません。
正しく実装すべき点:
- 値はそのまま送られます。 運用担当者が Console で
Bearer abc123という文字列全体を入力し、プラットフォームはそれをそのまま送ります。接頭辞を付け足すことはありません。 - 組に含まれるヘッダーをすべて検査してください。 2 つ必須なら、片方しかないリクエストは拒否する必要があります。
hmac.compare_digestやcrypto.timingSafeEqualのような定数時間比較を使ってください。==は最初に異なるバイトで止まるため、応答時間から正しい値が少しずつ漏れます。- 誤りや不足があれば
401を返します。
認証なしという選択も有効です。その場合プラットフォームはヘッダーを一切送りません。IP 許可リスト、mTLS、閉域網など別の手段で既にアクセスを制限している場合にだけ選んでください。
HMAC 署名はありません。
X-FPT-Signature ヘッダーは契約から削除済みなので待たないでください。ターンのリプレイ対策は runId で引き続き可能です。プラットフォームが送る識別子
ユーザーが過去のターンを編集して再実行すると、
threadId にブランチの接尾辞が付きます。直線的な会話では 918273645500123456、フォーク後は 918273645500123456:<branch> です。パートナー側から見れば 別の会話 2 つ であり、それが正しい挙動です。片方のブランチがもう片方のコンテキストを見てはいけません。
POST /runs のボディ
通常のチャットターンはこれだけです:
input が空なのは正常で、意味があります。ユーザーが問い返しに対して 承認 または 拒否 を押しただけのとき、input に入れる文字はなく、判断は resume.decision に入ります。threadId をキーにコンテキストを自分で持つ
現行バージョンでは、history が埋まるのはユーザーが会話をフォークしたときだけです。直線的な会話、つまり通常のチャットすべてにおいて、プラットフォームは 過去のターンを 1 つも送りません。
その結果として、External Agent は 必ず 自分でコンテキストを保存し、threadId をキーにし、TTL を自分で決め、デプロイをまたいで残るようにする必要があります。ほかに情報源はありません。
コンテキストと同じ系統の話として 冪等性 があります。完了済みの 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 つの回答の最小ストリーム
すべてのフレーム共通のエンベロープ
messageId と toolCallId は 1 ターン内で 一意であれば十分なので、毎ターン "a1" を使い回しても安全です。
プラットフォームが処理するフレーム
ターンは
RUN_FINISHED または RUN_ERROR で終わる必要があります。
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 回送ってください。
時間制限と上限
以下は既定値であり、契約上の定数ではありません。実際に動かす環境の数値が必要なら運用担当者に確認してください。
ターンのライフサイクルについてもう 2 点:
- 途中キャンセルはありません。 ユーザーが停止を押すとプラットフォームは接続を閉じ、それがキャンセル信号のすべてです。クライアントが切断したら処理を止めてください。
- リトライもありません。 失敗したターンは失敗したままです。ユーザーが送り直せば、それは新しい
runIdです。
ファイル
ユーザーがアップロードしたファイル
attachments[] の各要素は id、url、name、mime、sizeBytes を持ちます。リンクは https で、認証ヘッダーなしでダウンロードでき、期限があります。
- 重複判定は
idで行ってください。urlやnameではありません。同じファイルならidはターンをまたいで変わりません。 - 後のターンで使うなら、有効なうちにダウンロードして自分で保存してください。 リンクは失効する前提で考えます。
- リンクに認証はないので、持っている人は誰でもダウンロードできます。ログに残さず、自社システムの外へ転送しないでください。
- メタデータを信用しないでください。
nameをパスとして使わず、mimeはプラットフォームの申告なので中身を自分で検査します。
プラットフォームがリンクを発行できなかったファイルは
attachments から外れ、システム通知の 1 行として input の中で名前だけ伝えられます。それはプラットフォームの文であってユーザーの文ではないので、そのまま復唱しないでください。External Agent が生成したファイル
External Agent が自分でファイルをホストし、ユーザーのブラウザから到達できる URL を用意したうえで、artifacts キーの STATE_SNAPSHOT、または name が file の CUSTOM で送ります。
STATE_SNAPSHOT と STATE_DELTA で受け付けられるキーは todos、artifacts、files の 3 つだけです。それ以外は破棄されます。
ユーザーへの問い返し
先に進むのにユーザーの判断が必要なときは、outcome.type を interrupt にした RUN_FINISHED でターンを終えます。1 回の停止が運べる interrupt は ちょうど 1 つ です。
resume に入れて返します:
authorizeUrl は 4 点が検査されます。存在すること、https であること、認証情報を埋め込んでいないこと、ホストが IP アドレスでないことです。ドメイン許可リストはないので、事前登録は不要です。
decision が authorized でも、権限が付与された証拠にはなりません。先に進む前に自前のトークン保存を確認してください。GET /health
このエンドポイントも
/runs と同じ認証ヘッダーの組を受け取ります。軽量であること、モデルやデータベースを呼ばないこと、リダイレクトしないことが条件です。
プラットフォームは結果を 3 つの状態にまとめます。健全なら、チャットターンはそのまま通ります。degraded でも同じです。遅い External Agent のほうが、完全に止められた External Agent よりましだからです。それ以外は unavailable で、接続できないとき、401 や 403 が返るとき、ボディが JSON でないとき、protocolVersions に "1" が無いとき、あるいは自分で down と申告したときに起こります。
定期的な自動ヘルスチェックはまだありません。3 回連続で失敗するとエンドポイントは
unavailable になり、チャットターンは即座に拒否されます。障害が解消しても基盤は 自動では気づかない ため、運用担当者に Console でのテスト再実行を依頼してください。ログ突き合わせ用のエラーコード
code はユーザーには表示されませんが、障害時に運用担当者が最初に読む情報です。
失敗したターンも履歴に残ります。送信できた分のテキストはエラー表示付きで会話に出ます。
作らなくてよいもの
オンボーディング前のチェックリスト
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 には再実行せず以前の結果を返すこと。問題が起きたら、運用担当者に 4 点を伝えてください。
runId、threadId、発生時刻、そして上の表のエラーコードがあればそれです。双方のログを突き合わせるにはこれで足ります。