> ## Documentation Index
> Fetch the complete documentation index at: https://docs-agents.fpt.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Chuẩn bị kỹ thuật cho External Agent

> Những gì đội kỹ thuật của đối tác phải dựng xong trước khi kết nối vào FPT AI EOS

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.

> [!NOTE]
>
> 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.

| Việc | Ai lo |
| - | - |
| Giao diện chat, lịch sử hội thoại, publish | Console |
| Gọi `POST /runs` mỗi lượt, hiển thị frame nhận được | Console |
| Logic nghiệp vụ, prompt, gọi model, chạy tool | External Agent |
| **Context và state của hội thoại** | External Agent |
| Host, scale, uptime, log, monitoring | External Agent |
| Host file do Nhân sự số sinh ra và cấp link tải được | External Agent |

> [!WARNING]
>
> 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](https://mintcdn.com/fpt-62e894b4/3vi0Hyj1ddtKI8oH/images/ext_run_vi.png?fit=max\&auto=format\&n=3vi0Hyj1ddtKI8oH\&q=85\&s=3cbef85ad72d7137b24cf98584740e74)

## Hai endpoint phải có

| Method | Path | Trả về | Dùng để |
| - | - | - | - |
| `POST` | `{base_url}/runs` | `text/event-stream` | Một request là một lượt chat |
| `GET` | `{base_url}/health` | `application/json` | Kiểm tra sống và phiên bản protocol |

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ị.

| Cách dùng | Ví dụ |
| - | - |
| Bearer token | `Authorization: Bearer <token>` |
| API key tên tùy ý | `X-API-Key: <key>` |
| Nhiều header cùng lúc | `X-API-Key: <key>` và `X-Tenant: <mã>` |

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`.

> [!NOTE]
>
> 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ộ.

> [!NOTE]
>
> 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

| Field | Có mặt | Ý nghĩa |
| - | - | - |
| `threadId` | Luôn có | Id hội thoại. **Là khóa kho context của bên bạn** |
| `runId` | Luôn có | Id lượt này. Dùng làm khóa idempotency |
| `agentId` | Luôn có | Nhân sự số nào đang bị gọi. Một `base_url` phục vụ được nhiều Nhân sự số |
| `workspaceId` | Luôn có | Tổ chức đang gọi. Dùng cho quota, rate limit, cấu hình theo khách |
| `userId` | Có thể vắng | Người đang chat. Vắng khi lượt không có người thật phía sau |

> [!WARNING]
>
> 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`

| Field | Kiểu | Bắt buộc | Giá trị |
| - | - | - | - |
| `protocolVersion` | string | Có | Luôn là `"1"` |
| `runId` | string | Có | Id lượt, mới ở mỗi lượt |
| `threadId` | string | Có | Id hội thoại |
| `agentId` | string | Có | Nhân sự số đang bị gọi |
| `workspaceId` | string | Có | Tổ chức đang gọi |
| `input` | string | Có | Câu người dùng gõ. **Có thể là chuỗi rỗng** |
| `userId` | string | Không | Người đang chat |
| `attachments` | array | Không | File người dùng gửi kèm lượt này |
| `resume` | object | Không | Chỉ có ở lượt trả lời một câu hỏi lại |
| `history` | array | Không | **Thực tế gần như luôn vắng** |

Một lượt chat bình thường chỉ có bấy nhiêu:

```json theme={null}
{
  "protocolVersion": "1",
  "runId": "01K5BH5PSX926W6RZ6YR1P8WWV",
  "threadId": "918273645500123456",
  "agentId": "918273688800000200",
  "workspaceId": "918273600000000001",
  "userId": "918273644400000042",
  "input": "Đơn DH-99217 giờ tới đâu rồi?"
}
```

> [!NOTE]
>
> `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.

> [!WARNING]
>
> 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

```text theme={null}
data: {"type":"TEXT_MESSAGE_START","messageId":"a1","role":"assistant"}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"a1","delta":"Hôm qua vàng "}

data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"a1","delta":"giảm 0,5%."}

data: {"type":"TEXT_MESSAGE_END","messageId":"a1"}

data: {"type":"RUN_FINISHED","outcome":{"type":"success"}}
```

### Envelope chung của mọi frame

| Field | Bắt buộc | Giá trị |
| - | - | - |
| `type` | Có | Tên frame. Không nhận ra thì frame bị bỏ, không làm hỏng lượt |
| `event_id` | Không | Id frame. Gửi lại đúng frame thì dùng lại đúng id để trình duyệt khử trùng lặp |
| `run_id` | Không | Có thì phải bằng `runId` trong request, lệch thì frame bị loại |
| `threadId` | Không | Có thì phải bằng `threadId` trong request |
| `messageId` | Không | Ở các frame có message |

`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ý

| Frame | Mức | Field bắt buộc |
| - | - | - |
| `TEXT_MESSAGE_START` | Bắt buộc | `messageId` |
| `TEXT_MESSAGE_CONTENT` | Bắt buộc | `messageId`, `delta` |
| `TEXT_MESSAGE_END` | Bắt buộc | `messageId` |
| `RUN_FINISHED` | Bắt buộc | `outcome` |
| `RUN_ERROR` | Bắt buộc | `message` |
| `TOOL_CALL_START` | Nên có | `toolCallId`, `toolCallName` |
| `TOOL_CALL_ARGS` | Nên có | `toolCallId`, `delta` dạng **chuỗi** |
| `TOOL_CALL_END` | Nên có | `toolCallId` |
| `TOOL_CALL_RESULT` | Nên có | `toolCallId`, `content` |
| `STEP_STARTED` và `STEP_FINISHED` | Nên có | `stepName` |
| `ACTIVITY_SNAPSHOT` | Nên có | `messageId`, `activityType`, `content` |
| `ACTIVITY_DELTA` | Tùy chọn | `messageId`, `patch` |
| `STATE_SNAPSHOT` | Tùy chọn | `snapshot` |
| `STATE_DELTA` | Tùy chọn | `delta` |
| `REASONING_MESSAGE_START`, `_CONTENT`, `_END` | Tùy chọn | `messageId` |
| `CUSTOM` | Tùy chọn | `name` |

Lượt phải kết thúc bằng `RUN_FINISHED` **hoặc** `RUN_ERROR`.

> [!NOTE]
>
> **Đừ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.

> [!WARNING]
>
> Đó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 | Mặc định |
| - | - |
| Kết nối TCP và TLS | 5 giây |
| **Frame đầu tiên** | **10 giây** kể từ lúc response bắt đầu |
| Khoảng lặng giữa hai chunk | 30 giây, một dòng comment reset đồng hồ này |
| Tổng một lượt | 5 phút, đặt riêng cho từng Nhân sự số lúc onboard |
| Kích thước một frame | 256 KiB |
| Kích thước cả luồng | 8 MiB |
| Số frame một lượt | 20.000 |
| Hiệu lực link file đính kèm | 7 ngày |

> [!WARNING]
>
> **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.

> [!NOTE]
>
> 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`.

> [!WARNING]
>
> **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.

| `kind` | Hiển thị | Field nội dung |
| - | - | - |
| `approval` | Hai nút đồng ý và từ chối | Chỉ `prompt` |
| `choice` | Câu hỏi kèm danh sách lựa chọn, luôn có thêm ô tự gõ | `options` |
| `input` | Câu hỏi chỉ có ô tự gõ | Chỉ `prompt` |
| `review` | Một hay nhiều tool call đề xuất, kèm tham số xem và sửa được | `items` |
| `authorization` | Thẻ kết nối tài khoản, mở popup tới `authorizeUrl` | `provider`, `authorizeUrl` |

```json theme={null}
{ "type": "RUN_FINISHED",
  "outcome": { "type": "interrupt",
    "interrupts": [ { "kind": "approval",
                      "prompt": "Xác nhận đặt vé HAN đi SGN ngày 20/8?" } ] } }
```

Ở lượt kế tiếp, Console gửi lại quyết định trong `resume`:

| `kind` của interrupt | `decision` nhận được | Nội dung nằm ở |
| - | - | - |
| `approval` | `approve` hoặc `reject` | Không có, `input` rỗng |
| `choice` và `input` | `respond` | `message`, `answers`, và `input` |
| `review` | `approve`, `reject`, `edit`, `respond` | `editedAction` khi `edit` |
| `authorization` | `authorized` | Không có |

> [!WARNING]
>
> 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.

> [!NOTE]
>
> `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`

```json theme={null}
{ "status": "ok", "protocolVersions": ["1"], "name": "Flight Assistant", "version": "2.3.1" }
```

| Field | Bắt buộc | Giá trị |
| - | - | - |
| `status` | Có | `ok`, `degraded` hoặc `down` |
| `protocolVersions` | Có | **Phải chứa `"1"`** |
| `name` | Không | Tên hiển thị |
| `version` | Không | Phiên bản của bên bạn |

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`.

> [!WARNING]
>
> `/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.

> [!NOTE]
>
> 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ố.

| `code` | Xảy ra khi | Sửa ở đâu |
| - | - | - |
| `ENDPOINT_UNAVAILABLE` | Endpoint đang bị đánh dấu không khả dụng, request không được gửi đi | Phía FPT, chạy lại kiểm tra |
| `PROTOCOL_MISMATCH` | Lần kiểm tra gần nhất thấy `protocolVersions` không có `"1"` | External Agent |
| `EGRESS_REFUSED` | `base_url` trỏ vào địa chỉ Console không kết nối tới được | Hạ tầng |
| `CONNECT_TIMEOUT` | Không bắt tay được trong 5 giây | Hạ tầng |
| `UPSTREAM_REJECTED` | External Agent trả HTTP 3xx hoặc 4xx cho `POST /runs`, thường là `401` | External Agent |
| `UPSTREAM_UNAVAILABLE` | External Agent trả HTTP 5xx | External Agent |
| `TTFB_TIMEOUT` | Luồng mở nhưng không có frame nào trong 10 giây | External Agent |
| `IDLE_TIMEOUT` | Im lặng quá 30 giây giữa luồng, thiếu nhịp giữ kết nối | External Agent |
| `STREAM_TRUNCATED` | Luồng đứt giữa chừng, phần đã nhận vẫn được lưu | Hạ tầng |
| `LIMIT_EXCEEDED` | Vượt trần kích thước frame, tổng byte hoặc số frame | External Agent |
| `INTERRUPT_REJECTED` | Không dựng được thẻ hỏi lại người dùng | External Agent |

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

| Đừng dựng | Vì sao |
| - | - |
| `GET /tools`, `POST /credentials`, `POST /credentials/revoke` | Console không gọi tới ở phiên bản hiện tại |
| Kiểm chữ ký `X-FPT-Signature` | Đã bỏ khỏi contract |
| Logic dựa trên `history` | Gần như luôn vắng, xem mục Tự giữ context |
| Endpoint hủy hay resume | Không có trong contract |

## 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.

> [!NOTE]
>
> 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.