Chuyển đến nội dung chính
Trang này dành cho đội kỹ thuật vận hành một External Agent. Trang liệt kê đầy đủ những thứ phải dựng xong trước khi onboard: hai endpoint, cách xác thực, định dạng luồng trả về, các mốc thời gian và những việc External Agent phải tự lo. Phần thao tác khai báo trên Console nằm ở trang Kết nối Agent bên ngoài. Ba cái tên sẽ gặp suốt trang. Console là nơi người vận hành khai báo External Agent, cấu hình và publish. Workspace là nơi người dùng cuối chat. FPT AI EOS là nền tảng chạy phía sau, và đây mới là bên mở kết nối tới máy chủ của bên bạn.
FPT AI EOS là client, External Agent là server. FPT AI EOS gọi sang, External Agent trả lời. Không có chiều ngược lại: ngoài luồng đang mở, không có kênh nào để đẩy dữ liệu ngược lên nền tảng.
Sau khi khai báo xong, External Agent của bên bạn hiện ra trong Workspace như một Nhân sự số bình thường, người dùng chat với nó y hệt các Nhân sự số khác. Tài liệu này vẫn gọi phần chạy trên hạ tầng của đối tác là External Agent, để khỏi lẫn với những Nhân sự số do nền tảng tự chạy.

Phân chia trách nhiệm

Nền tảng không chạy External Agent. Phía FPT AI EOS chỉ lo phần vỏ: Giao diện chat trong Workspace, lịch sử hội thoại, việc khai báo và publish trong Console, và mỗi lượt thì gọi POST /runs đúng một lần. Toàn bộ phần còn lại thuộc về bên đối tác.
FPT AI EOS không giữ context hội thoại hộ bên bạn và không có endpoint nào để đọc lại. Đây là điều quan trọng nhất trong trang này, xem mục Tự giữ context bên dưới.

Một lượt chat đi qua External Agent

Sơ đồ trình tự một lượt chat qua External Agent

Hai endpoint phải có

Hai path này cố định dưới base_url, không cấu hình riêng từng path được. Ví dụ base_url là https://agent.example.com/fpt thì nền tảng gọi POST https://agent.example.com/fpt/runs và GET https://agent.example.com/fpt/health. Ràng buộc với base_url:
  • Bắt buộc https. FPT AI EOS từ chối http.
  • Không được nhúng credential trong URL dạng user:pass@.
  • Nền tảng phân giải DNS và kiểm địa chỉ ở mỗi lần kết nối, không phải một lần lúc onboard. Địa chỉ nội bộ, loopback, link-local, reserved, multicast đều bị từ chối. Một tên miền đổi DNS sang địa chỉ nội bộ sẽ bị chặn ngay ở lượt kế tiếp.
  • Redirect không được đi theo. Một 301 hay 302 bị tính là lỗi. Cần đổi vị trí thì đổi base_url.

Xác thực

Nền tảng gửi một tập header do hai bên thống nhất lúc onboard, y nguyên trên cả hai endpoint. Bên đối tác quyết định tập đó gồm những gì: FPT AI EOS không quy định tên header, không quy định scheme, và không tự thêm gì vào giá trị. Những điều phải làm đúng:
  • Giá trị gửi nguyên văn. Người vận hành nhập trọn chuỗi Bearer abc123 trong Console, nền tảng gửi đi đúng như vậy, không ghép thêm tiền tố.
  • Kiểm đủ mọi header trong tập. Yêu cầu hai header thì phải từ chối request chỉ có một.
  • So sánh bằng hàm thời gian hằng như hmac.compare_digest hoặc crypto.timingSafeEqual. Dùng == sẽ dừng ở byte lệch đầu tiên và thời gian trả lời lộ dần giá trị đúng.
  • Sai hoặc thiếu thì trả 401.
Không xác thực cũng là một lựa chọn hợp lệ: khi đó nền tảng không gửi header nào cả. Chỉ chọn khi bên bạn đã chặn truy cập bằng cách khác như IP allowlist, mTLS hay mạng nội bộ.
Không có chữ ký HMAC. Đừng chờ header X-FPT-Signature, nó đã được bỏ khỏi contract. Chống phát lại một lượt vẫn làm được bằng runId.

Định danh nền tảng gửi sang

Mọi id gửi dạng chuỗi và là số nguyên 63 bit. Đừng parse thành số: JSON.parse của JavaScript làm tròn sai ở mức trên 2^53 và hai id khác nhau có thể thành một.
threadId có hậu tố nhánh khi người dùng sửa rồi chạy lại một lượt cũ: 918273645500123456 ở hội thoại thẳng, 918273645500123456:<branch> sau khi fork. Với bên bạn đó là hai hội thoại khác nhau, và đúng là phải như vậy: một nhánh không được thấy context của nhánh kia.

Body của POST /runs

Một lượt chat bình thường chỉ có bấy nhiêu:
input rỗng là hợp lệ và có nghĩa. Khi người dùng chỉ bấm Đồng ý hay Từ chối cho một câu hỏi lại, không có chữ nào để nhấc lên input, và quyết định nằm ở resume.decision.

Tự giữ context theo threadId

Ở phiên bản đang chạy, history chỉ được điền khi người dùng fork một hội thoại. Trên hội thoại thẳng, tức là mọi cuộc chat bình thường, nền tảng gửi không lượt nào. Hệ quả: External Agent phải tự lưu context của mình, khóa theo threadId, tự đặt TTL, và dữ liệu phải sống qua deploy. Không có nguồn nào khác.
Một External Agent dựa vào history để nhớ lượt trước sẽ trả lời như thể mỗi tin nhắn là tin đầu tiên. Triệu chứng này rất dễ bị đổ nhầm cho model, nên kiểm tra khóa context trước khi nghi ngờ chất lượng trả lời.
Cùng nhóm với context là idempotency: runId đã chạy xong thì trả lại kết quả cũ, đừng chạy lại. Nền tảng không có cơ chế retry nghiệp vụ, nhưng khóa này chặn được việc một lượt bị phát lại.

Trả lời bằng luồng SSE

Trả Content-Type: text/event-stream, nên kèm Cache-Control: no-cache và X-Accel-Buffering: no. Mỗi frame là một dòng data: chứa một JSON object, các frame ngăn nhau bằng một dòng trắng. Bộ đọc của nền tảng là bộ đọc SSE tối giản: event:, id:, retry: bị bỏ qua, toàn bộ nội dung nằm trong JSON của dòng data:. Dòng bắt đầu bằng dấu hai chấm là comment và được dùng làm nhịp giữ kết nối.

Luồng tối thiểu của một câu trả lời

Envelope chung của mọi frame

messageId và toolCallId chỉ cần duy nhất trong một lượt, nên dùng lại "a1" ở mọi lượt là an toàn.

Danh sách frame nền tảng xử lý

Lượt phải kết thúc bằng RUN_FINISHED hoặc RUN_ERROR.
Đừng gửi RUN_STARTED, nền tảng đã tự phát frame đó khi mở lượt. Đừng gửi REASONING_END, nền tảng tự phát kèm REASONING_MESSAGE_END.

Text xen kẽ tool call

Mỗi đoạn text liền mạch là một message riêng, có messageId riêng và cặp START / END riêng. Đoạn text đóng lại trước tool call kế tiếp, text đến sau tool call thuộc về một message mới, và câu trả lời là message cuối cùng của lượt.

Kết thúc lượt cho đúng

  • Gửi RUN_FINISHED hoặc RUN_ERROR rồi mới đóng luồng.
  • Đóng mọi thứ đã mở trước frame cuối: TEXT_MESSAGE_START, TOOL_CALL_START, STEP_STARTED, REASONING_MESSAGE_START. Nền tảng có đóng hộ nhưng không bịa ra TOOL_CALL_RESULT.
  • Đừng gửi một tin nhắn rỗng. Không có gì để nói thì gửi RUN_ERROR.
  • Tool call phải đứng trước câu trả lời tham chiếu tới nó. Nền tảng đọc response như một luồng nên thứ tự là ràng buộc thật.
  • Trong lúc chưa có gì để gửi, cứ gửi một dòng comment làm nhịp giữ kết nối, tối thiểu 15 giây một lần.
Đóng luồng sạch mà quên frame kết thúc không bị coi là lỗi: Nền tảng ghi lượt đó là thành công với đúng phần đã nhận được. Một External Agent chết giữa câu rồi chỉ đóng socket sẽ để lại một câu trả lời cụt được lưu như một câu trả lời hoàn chỉnh. Nhìn thì nhỏ, nhưng RUN_ERROR chính là thứ chặn được chuyện đó.

Mốc thời gian và giới hạn

Đây là giá trị mặc định, không phải hằng số của contract. Hỏi người vận hành nếu bên bạn cần con số thật của môi trường sẽ chạy.
Mốc 10 giây cho frame đầu tiên là chỗ vấp của gần như mọi tích hợp. Nó không phải hạn cho cả câu trả lời, mà là hạn cho frame đầu. Nếu kiến trúc bên bạn chạy xong toàn bộ lượt rồi mới trả byte đầu, ví dụ một workflow no-code hay một hàm serverless buffer sẵn response, bên bạn sẽ timeout dù External Agent hoàn toàn khỏe.Cách đúng: Đẩy một frame ra ngay lập tức, trước khi gọi model. Một STEP_STARTED hay một dòng comment là đủ để mở luồng và chuyển sang đồng hồ khoảng lặng 30 giây, vốn rộng hơn nhiều.
Hai điều nữa về vòng đời một lượt:
  • Không có hủy giữa chừng. Người dùng bấm Dừng thì nền tảng đóng kết nối, và đó là toàn bộ tín hiệu hủy. Client ngắt kết nối thì dừng xử lý.
  • Không có retry. Một lượt hỏng là một lượt hỏng. Người dùng gửi lại thì đó là một runId mới.

File

File người dùng gửi lên

Mỗi phần tử của attachments[] có id, url, name, mime, sizeBytes. Link là https, tải được không cần header xác thực, và có hạn.
  • Chống trùng bằng id, không phải bằng url hay name. id không đổi qua các lượt cho cùng một file.
  • Cần dùng file ở lượt sau thì phải tải và tự lưu trong hạn. Cứ coi như link sẽ hết hạn.
  • Link không có auth nên ai có link đều tải được. Đừng log, đừng chuyển tiếp ra ngoài hệ thống của bên bạn.
  • Đừng tin metadata: name không dùng làm đường dẫn, mime do nền tảng khai nên tự kiểm nội dung.
File nền tảng không cấp được link sẽ bị loại khỏi attachments và được nêu tên trong input dưới dạng một dòng thông báo của hệ thống. Đó là câu của nền tảng, không phải của người dùng, nên đừng lặp lại nguyên văn.

File External Agent sinh ra

External Agent tự host file và cấp URL truy cập được từ trình duyệt người dùng, rồi gửi sang bằng STATE_SNAPSHOT với khóa artifacts, hoặc bằng CUSTOM với name là file.
Một file, một kênh. Hai cách trên là hai nguồn độc lập và không khử trùng lặp cho nhau. Gửi cả hai cho cùng một file thì người dùng thấy hai thẻ. Nếu văn xuôi của bên bạn còn chèn ảnh markdown trỏ tới file đó nữa thì thành ba.
Với STATE_SNAPSHOT và STATE_DELTA, chỉ ba khóa được chấp nhận: todos, artifacts, files. Khóa khác bị loại.

Hỏi lại người dùng

Cần người dùng quyết định mới đi tiếp thì kết thúc lượt bằng RUN_FINISHED với outcome.type là interrupt. Mỗi lần dừng chỉ mang theo đúng một interrupt.
Ở lượt kế tiếp, nền tảng gửi lại quyết định trong resume:
Một kind ngoài năm giá trị trên, hoặc một review thiếu name, hoặc authorizeUrl không hợp lệ sẽ làm hỏng cả lượt. Đây là trường hợp duy nhất một frame sai làm hỏng lượt thay vì bị bỏ qua.
authorizeUrl bị kiểm bốn điều: Phải có mặt, phải https, không nhúng credential, và host không được là địa chỉ IP. Không có allowlist domain, bên bạn không phải khai trước danh sách.
decision là authorized không phải bằng chứng đã cấp quyền. Tự kiểm kho token của mình trước khi đi tiếp.

GET /health

Endpoint này vẫn mang đúng tập header xác thực như /runs, phải nhẹ, không gọi model, không gọi database, và cũng không được redirect. Nền tảng xếp kết quả thành ba trạng thái. Khỏe thì lượt chat đi ra bình thường. degraded cũng vậy, vì một External Agent chậm vẫn hơn một External Agent bị ngắt hẳn. Còn lại là unavailable, xảy ra khi không kết nối được, khi trả 401 hay 403, khi body không phải JSON, khi protocolVersions không chứa "1", hoặc khi bên bạn tự khai down.
/health trả ok là chưa đủ để publish. Khi người vận hành bấm Kiểm tra trong Console, nền tảng còn gửi một lượt POST /runs thật với input là Hello và một threadId dùng một lần dạng validate-<16 ký tự hex>, rồi dừng ngay ở frame hợp lệ đầu tiên. Lượt này không được lưu vào lịch sử. Đừng để threadId lạ đó làm hỏng quy trình của bên bạn.
Chưa có kiểm tra sức khỏe tự động theo chu kỳ. Ba lần lỗi liên tiếp sẽ chuyển endpoint sang unavailable và mọi lượt chat bị từ chối ngay. Hết sự cố thì nền tảng không tự nhận ra, phải báo người vận hành chạy lại kiểm tra trong Console.

Mã lỗi để đối soát log

code không hiện cho người dùng, nhưng là thứ đầu tiên người vận hành đọc khi có sự cố. Lượt hỏng vẫn vào lịch sử: Phần văn bản đã kịp gửi vẫn hiện trong hội thoại kèm dấu lỗi.

Những thứ không phải làm

Checklist trước khi onboard

1

Hai endpoint chạy được

GET /health trả status là ok và protocolVersions chứa "1". POST /runs nhận body ở trên và trả text/event-stream.
2

Xác thực kiểm đủ và kiểm đúng cách

Kiểm mọi header trong tập đã thống nhất, so sánh bằng hàm thời gian hằng, sai thì trả 401.
3

Frame đầu ra trong 10 giây

Đẩy một frame ngay trước khi gọi model. Đây là mốc dễ trượt nhất.
4

Context lưu theo threadId

Tự lưu, tự đặt TTL, sống qua deploy. Kiểm tra bằng cách hỏi hai lượt liên tiếp có tham chiếu lẫn nhau.
5

Lượt nào cũng có frame kết thúc

RUN_FINISHED hoặc RUN_ERROR trước khi đóng luồng, và mọi thứ đã mở đều được đóng.
6

Nhịp giữ kết nối

Gửi một dòng comment tối thiểu 15 giây một lần khi đang xử lý lâu.
7

Idempotency theo runId

runId đã chạy xong thì trả kết quả cũ, không chạy lại.
Vướng ở đâu thì gửi cho người vận hành bốn thứ: runId, threadId, thời điểm, và mã lỗi ở bảng trên nếu có. Đó là bộ đủ để hai bên đối chiếu log.