メインコンテンツへスキップ
このページは External Agent を運用する技術チーム向けです。オンボーディング前に用意しておくものを一通り挙げます。2 つのエンドポイント、認証、レスポンスストリームの形式、各種の時間制限、そして External Agent 側で自前で持つ必要があるものです。Console 側での登録手順は 外部エージェントを接続する にあります。 このページには 3 つの名前が出てきます。Console は運用担当者が External Agent を登録し、設定し、公開する場所です。Workspace はエンドユーザーがチャットする場所です。FPT AI EOS はその裏で動くプラットフォームで、パートナー側のサーバーへ接続を開くのはこれです。
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 呼び出しです。それ以外はすべてパートナー側の責任です。
FPT AI EOS は 会話のコンテキストを代わりに保持しません。読み戻すためのエンドポイントもありません。このページで最も重要な点です。下の「コンテキストを自分で持つ」を参照してください。

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

External Agent を通る 1 ターンのシーケンス図

必須の 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 で引き続き可能です。

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

ID はすべて 文字列 で送られ、63 ビット整数です。数値としてパースしないでください。JavaScript の JSON.parse は 2^53 を超えると丸め誤差が出て、別々の ID が同一になることがあります。
ユーザーが過去のターンを編集して再実行すると、threadId にブランチの接尾辞が付きます。直線的な会話では 918273645500123456、フォーク後は 918273645500123456:<branch> です。パートナー側から見れば 別の会話 2 つ であり、それが正しい挙動です。片方のブランチがもう片方のコンテキストを見てはいけません。

POST /runs のボディ

通常のチャットターンはこれだけです:
input が空なのは正常で、意味があります。ユーザーが問い返しに対して 承認 または 拒否 を押しただけのとき、input に入れる文字はなく、判断は resume.decision に入ります。

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

現行バージョンでは、history が埋まるのはユーザーが会話をフォークしたときだけです。直線的な会話、つまり通常のチャットすべてにおいて、プラットフォームは 過去のターンを 1 つも送りません。 その結果として、External Agent は 必ず 自分でコンテキストを保存し、threadId をキーにし、TTL を自分で決め、デプロイをまたいで残るようにする必要があります。ほかに情報源はありません。
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 つの回答の最小ストリーム

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

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

時間制限と上限

以下は既定値であり、契約上の定数ではありません。実際に動かす環境の数値が必要なら運用担当者に確認してください。
最初のフレームの 10 秒という制限は、ほぼすべての連携がつまずく箇所です。 これは回答全体の期限ではなく、最初のフレーム の期限です。ターン全体を処理し終えてから最初のバイトを返す構成、たとえばノーコードのワークフローやレスポンスをバッファするサーバーレス関数では、External Agent が健全でもタイムアウトします。正しい対処は、モデルを呼ぶ前にフレームを 1 つ即座に送り出す ことです。STEP_STARTED 1 つかコメント行 1 行でストリームが開き、はるかに余裕のある 30 秒の無音タイマーに移ります。
ターンのライフサイクルについてもう 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 で送ります。
1 ファイルにつき 1 経路。 この 2 つは独立した経路で、互いに重複排除されません。同じファイルを両方で送ると、ユーザーには カードが 2 枚 表示されます。本文中に markdown 画像でも同じファイルを埋め込めば 3 枚になります。
STATE_SNAPSHOT と STATE_DELTA で受け付けられるキーは todos、artifacts、files の 3 つだけです。それ以外は破棄されます。

ユーザーへの問い返し

先に進むのにユーザーの判断が必要なときは、outcome.type を interrupt にした RUN_FINISHED でターンを終えます。1 回の停止が運べる interrupt は ちょうど 1 つ です。
次のターンで、プラットフォームは判断を resume に入れて返します:
上記 5 つ以外の kind、name を欠いた review、不正な authorizeUrl は ターン全体を失敗させます。壊れたフレームが無視されずにターンを壊す唯一のケースです。
authorizeUrl は 4 点が検査されます。存在すること、https であること、認証情報を埋め込んでいないこと、ホストが IP アドレスでないことです。ドメイン許可リストはないので、事前登録は不要です。
decision が authorized でも、権限が付与された証拠にはなりません。先に進む前に自前のトークン保存を確認してください。

GET /health

このエンドポイントも /runs と同じ認証ヘッダーの組を受け取ります。軽量であること、モデルやデータベースを呼ばないこと、リダイレクトしないことが条件です。 プラットフォームは結果を 3 つの状態にまとめます。健全なら、チャットターンはそのまま通ります。degraded でも同じです。遅い External Agent のほうが、完全に止められた External Agent よりましだからです。それ以外は unavailable で、接続できないとき、401 や 403 が返るとき、ボディが JSON でないとき、protocolVersions に "1" が無いとき、あるいは自分で down と申告したときに起こります。
/health が ok を返すだけでは 公開には足りません。運用担当者が Console でテストを押すと、プラットフォームは input を Hello にした実際の POST /runs ターンも送ります。threadId は validate-<16 桁の 16 進数> という使い捨ての形で、最初の正常なフレームで打ち切られます。このターンは履歴に保存されません。見慣れない threadId で処理が壊れないようにしてください。
定期的な自動ヘルスチェックはまだありません。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、発生時刻、そして上の表のエラーコードがあればそれです。双方のログを突き合わせるにはこれで足ります。