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. Nó 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.
Console là client, External Agent là server. Console 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 sang Console.

Ai lo gì

Console không chạy External Agent. Console publish Nhân sự số, cấp giao diện chat, lưu lịch sử hội thoại, và gọi POST /runs một lần cho mỗi lượt. Toàn bộ phần còn lại thuộc về bên đối tác.
Console 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ì Console 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. Console từ chối http.
  • Không được nhúng credential trong URL dạng user:pass@.
  • Console 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

Console 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ì: Console 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, Console 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ệ: Console 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 Console 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, Console 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 Nhân sự số 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. Console 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 Console 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 Console xử lý

Lượt phải kết thúc bằng RUN_FINISHED hoặc RUN_ERROR.
Đừng gửi RUN_STARTED, Console đã tự phát frame đó khi mở lượt. Đừng gửi REASONING_END, Console 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. Console có đóng hộ nhưng không bịa ra TOOL_CALL_RESULT.
  • Đừng phát một bóng thoại 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ó. Console đọc response như một luồng nên thứ tự là ràng buộc thật.
  • Khi rảnh, 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: Console ghi lượt đó là thành công với đúng phần đã nhận được. Một Nhân sự số 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. Đó là lý do RUN_ERROR quan trọng hơn vẻ ngoài của 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ù Nhân sự số 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ì Console đó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 Console khai nên tự kiểm nội dung.
File Console 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 Console, không phải của người dùng, nên đừng lặp lại nguyên văn.

File Nhân sự số 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ột lần dừng chỉ chở đúng một interrupt.
Ở lượt kế tiếp, Console 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. Console xếp kết quả thành ba trạng thái. degraded vẫn cho lượt chat đi ra bình thường, vì một Nhân sự số chậm vẫn hơn một Nhân sự số bị gỡ khỏi sóng. 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, Console 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.

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 luôn kết thúc tử tế

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.