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ọiPOST /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.
Một lượt chat đi 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
301hay302bị tính là lỗi. Cần đổi vị trí thì đổibase_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_digesthoặccrypto.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
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.
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_FINISHEDhoặcRUN_ERRORrồ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 raTOOL_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.
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.
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
runIdmới.
File
File người dùng gửi lên
Mỗi phần tử củaattachments[] 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ằngurlhayname.idkhô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:
namekhông dùng làm đường dẫn,mimedo 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ằngSTATE_SNAPSHOT với khóa artifacts, hoặc bằng CUSTOM với name là file.
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ằngRUN_FINISHED với outcome.type là interrupt. Một lần dừng chỉ chở đúng một interrupt.
resume:
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.
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.