# Agent 提交与结果查询 API

将模型预测 ZIP 提交到 Benchmark，异步获取评测结果。本接口负责评测已有预测；视频上传与 Pipeline 推理使用 Playground 的独立接口。

[当前规范](#current) · [选择评测方式](#profiles) · [准备 ZIP](#package) · [上传与预检查](#upload) · [完整请求示例](#examples) · [模型与 Pipeline](#catalog) · [提交与重试](#submit) · [结果解释](#results) · [Python 流程](#python) · [升级与错误](#updates)

[OpenAPI JSON](/guide/agent-api/openapi.json) · [展开当前版本的 Markdown](/guide/agent-api/agent-api.md) · [HOT3D 逐帧格式](/guide/#submission-format) · [Test40 逐帧格式](/guide/reinterhand/#submission-format)

## 1. 先确认目标和当前规范 {#current}

服务地址使用你实际获授权的实例 origin。本文路径均相对该 origin；以下 curl 示例需要 curl 与 jq，并假定已经设置 EGO_URL。开发者先从本人 checkout 执行 `./deploy/run-dev target --service benchmark`，核对 `/api/health` 的 service=benchmark、environment、instance_id、build；生产提交按实际使用授权执行。当前接口没有应用层登录，幂等键与同源检查不构成身份认证。

```bash
# 以下命令在同一个 Bash 会话执行；失败时停止，先修正问题。
set -eu
# EGO_URL 为你确认的服务地址，不包含末尾斜杠。
curl --fail-with-body -sS "$EGO_URL/api/health"
curl --fail-with-body -sS "$EGO_URL/api/hot3d/protocols" > protocols.json
jq '.versions[] | {protocol_id, ready, accepting_submissions}' protocols.json
```

从 `versions[].protocol_id` 选择题集，再请求 `/api/submission-contract?protocol=...`。不要依赖默认协议或永久固定当前的题集列表。每次准备新提交都刷新规范；使用同键恢复已经接收的请求时保留原 JSON，不擅自替换字段。

以下身份和后面的完整请求示例由服务的 `submission_contract` 定义生成，HTML、Markdown、OpenAPI 使用同一来源，随发布版本更新。它们说明当前接受什么格式；实时 ready / accepting_submissions 仍需查询规范接口，页面展示不代表当前允许提交。

| 题集 | 当前 split_id | ZIP schema_version | Case 数 | 每个 Case 的帧数 |
| --- | --- | --- | --- | --- |
| HOT3D | `test200-v3` | `hot3d-pose-prediction-v4` | 1–200 | 150 |
| Test40 | `test40-v1` | `reinterhand-pose-prediction-v1` | 1–40 | 按原始 inputs.json 的完整 frame_count |
### HOT3D 当前身份

```json
{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "dataset_id": "hot3d-quest3-mano21-test200-v3",
  "split_id": "test200-v3",
  "schema_version": "hot3d-pose-prediction-v4",
  "input_manifest_sha256": "b3a405063e0158b47fa19c3ad4bf0acdd65785a0e9acb36f6d045412e3cbf1ef",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6"
}
```

[实时规范](/api/submission-contract?protocol=local-hot3d-quest3-mano21-pose-v1) · [输入包索引](/api/hot3d/input-package?protocol=local-hot3d-quest3-mano21-pose-v1&split=test200-v3) · [提交工具](/assets/pose_submission.py) · [坐标与逐帧格式](/guide/#submission-format)

### Test40 当前身份

```json
{
  "protocol": "reinterhand-ego-mano21-pose-v1",
  "dataset_id": "reinterhand-ego-envmap-per-segment-mano21-test40-v1",
  "split_id": "test40-v1",
  "schema_version": "reinterhand-pose-prediction-v1",
  "input_manifest_sha256": "66546c045e09a197322aae3388822aa56faf9af6c4eec8b37d871320baad4e2b",
  "contract_id": "3ecb48a0d8f2c2013efac41fb51550b7d15e9982eb202e8e1573246e4703ffe7"
}
```

[实时规范](/api/submission-contract?protocol=reinterhand-ego-mano21-pose-v1) · [输入包索引](/api/hot3d/input-package?protocol=reinterhand-ego-mano21-pose-v1&split=test40-v1) · [提交工具](/assets/reinterhand_submission.py) · [坐标与逐帧格式](/guide/reinterhand/#submission-format)


需要区分四个值：protocol 是评测协议；split_id / dataset_id 是冻结题集身份；schema_version 是 ZIP 中 submission.json 的格式；下载包文件名的 vN 是交付版本。它们不要求使用相同版本号。contract_id 绑定当前提交要求，提交前以实时接口返回为准。

## 2. 选择评测方式 {#profiles}

HTTP 的 `evaluation_profile` 描述预测类型和实际使用的输入相机。它必须与 ZIP 中 `prediction_type`、`inference_camera_ids` 一致；更改请求 JSON 不会将已有 3D ZIP 转成 2D，也不会将单目结果变成双目。不能把没有参与推理的相机填进去。

| 场景 / 完整请求 | prediction_type | inference_camera_ids |
| --- | --- | --- |
| [HOT3D · 左目 3D · 1201-1](#example-hot3d-3d-1201-1) | `3d` | `["1201-1"]` |
| [HOT3D · 右目 3D · 1201-2](#example-hot3d-3d-1201-2) | `3d` | `["1201-2"]` |
| [HOT3D · 双目 3D · 1201-1 + 1201-2](#example-hot3d-3d-1201-1-1201-2) | `3d` | `["1201-1", "1201-2"]` |
| [HOT3D · 左目 2D · 1201-1](#example-hot3d-2d-1201-1) | `2d` | `["1201-1"]` |
| [HOT3D · 右目 2D · 1201-2](#example-hot3d-2d-1201-2) | `2d` | `["1201-2"]` |
| [HOT3D · 双目 2D · 1201-1 + 1201-2](#example-hot3d-2d-1201-1-1201-2) | `2d` | `["1201-1", "1201-2"]` |
| [Test40 · Ego 单目 3D · ego](#example-test40-3d-ego) | `3d` | `["ego"]` |
| [Test40 · Ego 单目 2D · ego](#example-test40-2d-ego) | `2d` | `["ego"]` |

### 坐标和评分范围

- HOT3D 3D：无论左目、右目还是双目推理，输出都统一到左相机 `camera:1201-1:native`，单位米。右目模型的输出需要先按题集标定转换，不能直接当作右相机坐标提交。
- HOT3D 2D：所选相机的扶正原图像素，坐标声明 `upright`、单位 px；当前原图 1024×1280。双目要分别提交两路视图，不是把两路点拼在一起。
- Test40：仅 `ego`。3D 为 `camera:ego:native` / 米；2D 为 `image:ego:native` / px，当前原图 1024×1024。相机和每帧标定以输入包为准，不能套用 HOT3D 的旋转或帧数。
- 3D 提交计算空间指标，并由平台按标定投影计算 2D 指标；纯 2D 提交计算 2D 及公共指标，不补造 3D 分数。详见[结果解释](#results)。
- `expected_count` 是 ZIP 里实际选择的 Case 数，不是采样帧数，也不表示让服务器截取前 N 个。子集在打包前用 Case ID 选择；每个 Case 仍需完整帧序列。
- `leaderboard_type` 独立取 formal 或 temporary，默认 temporary。正式榜也允许合法子集，不按 Case 数自动决定榜单；不同题集、Case 集和评测设置不混排名。

## 3. 准备当前题集的预测 ZIP {#package}

从规范响应的 `links.input_package` 获取输入包索引 JSON，再读取 `downloads.full.url`（完整输入）或 `downloads.starter.url`（工具包）。该接口本身不是 ZIP。包下载后保留原始 inputs.json 的字节；不要裁切或重写它，也不要用 `/api/hot3d/inputs?case_id=...` 的展示响应代替原清单。

```bash
# EGO_PROTOCOL 从上一步 versions[].protocol_id 中选择。
curl --fail-with-body -sS --get "$EGO_URL/api/submission-contract" \
  --data-urlencode "protocol=$EGO_PROTOCOL" > contract.json
jq -e '.ready and .accepting_submissions' contract.json
curl --fail-with-body -sS "$EGO_URL$(jq -r '.links.input_package' contract.json)" > package.json
jq '{ready, downloads}' package.json
# 确认需要下载完整包后执行；可能为数十 GB。
curl --fail-with-body -L "$EGO_URL$(jq -r '.downloads.full.url' package.json)" -o inputs.zip
# 使用当前服务的工具，不沿用旧下载包里可能过期的脚本。
curl --fail-with-body -sS "$EGO_URL$(jq -r '.links.submission_tool' contract.json)" -o submission_tool.py
```

解压 inputs.zip，在模型侧使用原始视频/图像与相机参数完成推理。两个题集的提交工具都有 template、verify、pack 三个命令。下面示例分别创建 HOT3D 左目 3D 和 Test40 Ego 2D 模板；运行前下载对应协议的工具，设置原始清单路径 INPUTS 和真实 CASE_ID。两个模板示例任选其一。

```bash
# HOT3D：一个 Case，左目 3D。
python submission_tool.py template --inputs "$INPUTS" --submission prediction-hot3d \
  --type 3d --cameras 1201-1 --case-id "$CASE_ID"
# Test40：一个 Case，Ego 2D；此时 submission_tool.py 必须是 Test40 工具。
python submission_tool.py template --inputs "$INPUTS" --submission prediction-test40 \
  --type 2d --cameras ego --case-id "$CASE_ID"
```

`--case-id` 可重复传入；省略选择全题集。新模板目录必须不存在。将模型预测填入 JSONL，填写实际 prediction_adapter_id 和 pipeline_version，保留输入身份及 joint_names 语义映射。随后选择对应目录验证和打包：

```bash
python submission_tool.py verify --inputs "$INPUTS" --submission "$PREDICTION_DIR"
# 仅当已经填入真实模型输出时使用 --model-output；目标 ZIP 必须不存在。
python submission_tool.py pack --inputs "$INPUTS" --submission "$PREDICTION_DIR" \
  --output prediction.zip --model-output
```

模板默认是 gt_derived_fixture，不能把空模板或 GT 衍生联调数据标成 model_output。fixture 只在历史区展示，不能提交正式榜；真实模型输出可按用途选择正式或临时榜。

```text
prediction.zip
├── submission.json
└── predictions/
    ├── <case_id_1>.jsonl
    └── <case_id_2>.jsonl
```

允许唯一一层外目录。不要将 inputs.json、GT、原视频或其他文件混装进预测 ZIP。

- submission.json 保存 schema_version、protocol_id、dataset_id、input_manifest_sha256、case_ids、joint_names、prediction_type、inference_camera_ids、coordinate_frame、unit 等；由当前工具生成，Test40 还声明 split_id。这里的 protocol_id 对应 HTTP 请求的 protocol。
- 3D 每帧直接包含左右手及 points_3d；2D 每帧在 views 下按声明的相机分别包含左右手及 points_2d。每只有效手必须是 21 个有限数值点；3D 不混入 2D 字段，2D 不混入 3D 字段。
- 缺失手显式写 `status: "missing"` 和对应点字段 null，保留该帧及双手字段。不能因未检测到手就删帧。HOT3D 为逐 Case 完整 150 帧；Test40 按清单的实际 frame_count，不能统一裁成 150 帧。

## 4. 上传与预检查 {#upload}

`POST /api/uploads?filename=prediction.zip` 的正文为 ZIP 原始二进制，不是 multipart、Base64 或 JSON。单包 1 byte–2 GiB，准确 Content-Length，不支持 chunked。下面 curl 从文件上传，无需将整个包读进 Python 内存。

```bash
curl --fail-with-body -sS -H 'Content-Type: application/zip' \
  --data-binary @prediction.zip "$EGO_URL/api/uploads?filename=prediction.zip" > upload.json
jq '{upload_id, filename, size_bytes}' upload.json
```

HTTP 201 返回 upload_id、filename、size_bytes、sha256。上传成功只代表保存完成；Agent 使用 upload_id，不依赖响应里的内部 archive_path，也不直接访问服务存储路径。

从下一节选取与 ZIP 对应的完整请求，保存为 submit.json，将占位 upload_id 替换为真实返回值。确认已按当前输入准备后，填写最新 contract_id，不要通过改 split_id / schema_version 强行升级旧预测。

```bash
# submit.json 已选对题集、类型、相机、Case 数和模型字段。
jq --arg upload_id "$(jq -r '.upload_id' upload.json)" \
   --arg contract_id "$(jq -r '.contract_id' contract.json)" \
   '.upload_id=$upload_id | .contract_id=$contract_id' submit.json > request.json
curl --fail-with-body -sS -H 'Content-Type: application/json' \
  --data-binary @request.json "$EGO_URL/api/hot3d/inspect" > inspect.json
jq '{selection, evaluation_profile, fixture, contract_id}' inspect.json
```

两套题集共用 `/api/hot3d/inspect`，按 protocol / split_id 分派。此接口只需题集、来源、expected_count、contract_id、evaluation_profile；可以发送完整请求，但这里不验证全部模型/Pipeline catalog 字段。HTTP 200 返回选择集、实际 profile、fixture、当前 contract_id 和冻结身份。预检查不等于逐帧评分成功，入队与 worker 会继续校验。

## 5. 不同评测的完整请求示例 {#examples}

以下 JSON 均用于 `POST /api/evaluations`，最大正文 1 MiB，Content-Type 为 application/json；成功 HTTP 202。所有示例默认创建模型版本、未选择业务 Pipeline。upload_id 是占位值，模型名称/版本说明请按实际填写；contract_id 仅代表此页面渲染时的版本，提交前刷新实时规范。

示例分别展示全题集正式榜和单 Case 临时榜，两者均可按实际需求调整。expected_count 必须与对应 ZIP 完全一致。evaluation_profile 本身可省略并从 ZIP 读取；Agent 显式提供时更易审计，但必须与 ZIP 一致。坐标和 schema_version 属于 ZIP 元数据，不是可通过此请求覆盖的参数。

### HOT3D · 左目 3D · 1201-1 {#example-hot3d-3d-1201-1}

本例选择 1 个 Case、临时榜；请按 ZIP 实际 Case 数和榜单用途修改。

```json
{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "split_id": "test200-v3",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 1,
  "evaluation_profile": {
    "prediction_type": "3d",
    "inference_camera_ids": [
      "1201-1"
    ]
  },
  "display_name": "My Hand Model",
  "version": "hot3d-3d-1201-1-v1",
  "version_description": "HOT3D · 左目 3D · 1201-1，请替换为实际模型和推理设置说明",
  "leaderboard_type": "temporary"
}
```

### HOT3D · 右目 3D · 1201-2 {#example-hot3d-3d-1201-2}

本例选择 1 个 Case、临时榜；请按 ZIP 实际 Case 数和榜单用途修改。

```json
{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "split_id": "test200-v3",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 1,
  "evaluation_profile": {
    "prediction_type": "3d",
    "inference_camera_ids": [
      "1201-2"
    ]
  },
  "display_name": "My Hand Model",
  "version": "hot3d-3d-1201-2-v1",
  "version_description": "HOT3D · 右目 3D · 1201-2，请替换为实际模型和推理设置说明",
  "leaderboard_type": "temporary"
}
```

### HOT3D · 双目 3D · 1201-1 + 1201-2 {#example-hot3d-3d-1201-1-1201-2}

本例选择 200 个 Case、正式榜；请按 ZIP 实际 Case 数和榜单用途修改。

```json
{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "split_id": "test200-v3",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 200,
  "evaluation_profile": {
    "prediction_type": "3d",
    "inference_camera_ids": [
      "1201-1",
      "1201-2"
    ]
  },
  "display_name": "My Hand Model",
  "version": "hot3d-3d-1201-1-1201-2-v1",
  "version_description": "HOT3D · 双目 3D · 1201-1 + 1201-2，请替换为实际模型和推理设置说明",
  "leaderboard_type": "formal"
}
```

### HOT3D · 左目 2D · 1201-1 {#example-hot3d-2d-1201-1}

本例选择 1 个 Case、临时榜；请按 ZIP 实际 Case 数和榜单用途修改。

```json
{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "split_id": "test200-v3",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 1,
  "evaluation_profile": {
    "prediction_type": "2d",
    "inference_camera_ids": [
      "1201-1"
    ]
  },
  "display_name": "My Hand Model",
  "version": "hot3d-2d-1201-1-v1",
  "version_description": "HOT3D · 左目 2D · 1201-1，请替换为实际模型和推理设置说明",
  "leaderboard_type": "temporary"
}
```

### HOT3D · 右目 2D · 1201-2 {#example-hot3d-2d-1201-2}

本例选择 1 个 Case、临时榜；请按 ZIP 实际 Case 数和榜单用途修改。

```json
{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "split_id": "test200-v3",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 1,
  "evaluation_profile": {
    "prediction_type": "2d",
    "inference_camera_ids": [
      "1201-2"
    ]
  },
  "display_name": "My Hand Model",
  "version": "hot3d-2d-1201-2-v1",
  "version_description": "HOT3D · 右目 2D · 1201-2，请替换为实际模型和推理设置说明",
  "leaderboard_type": "temporary"
}
```

### HOT3D · 双目 2D · 1201-1 + 1201-2 {#example-hot3d-2d-1201-1-1201-2}

本例选择 1 个 Case、临时榜；请按 ZIP 实际 Case 数和榜单用途修改。

```json
{
  "protocol": "local-hot3d-quest3-mano21-pose-v1",
  "split_id": "test200-v3",
  "contract_id": "294efdf809b02422087de511f0900063fb2c7abcab403f3dcd14b846b6b020e6",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 1,
  "evaluation_profile": {
    "prediction_type": "2d",
    "inference_camera_ids": [
      "1201-1",
      "1201-2"
    ]
  },
  "display_name": "My Hand Model",
  "version": "hot3d-2d-1201-1-1201-2-v1",
  "version_description": "HOT3D · 双目 2D · 1201-1 + 1201-2，请替换为实际模型和推理设置说明",
  "leaderboard_type": "temporary"
}
```

### Test40 · Ego 单目 3D · ego {#example-test40-3d-ego}

本例选择 40 个 Case、正式榜；请按 ZIP 实际 Case 数和榜单用途修改。

```json
{
  "protocol": "reinterhand-ego-mano21-pose-v1",
  "split_id": "test40-v1",
  "contract_id": "3ecb48a0d8f2c2013efac41fb51550b7d15e9982eb202e8e1573246e4703ffe7",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 40,
  "evaluation_profile": {
    "prediction_type": "3d",
    "inference_camera_ids": [
      "ego"
    ]
  },
  "display_name": "My Hand Model",
  "version": "test40-3d-ego-v1",
  "version_description": "Test40 · Ego 单目 3D · ego，请替换为实际模型和推理设置说明",
  "leaderboard_type": "formal"
}
```

### Test40 · Ego 单目 2D · ego {#example-test40-2d-ego}

本例选择 1 个 Case、临时榜；请按 ZIP 实际 Case 数和榜单用途修改。

```json
{
  "protocol": "reinterhand-ego-mano21-pose-v1",
  "split_id": "test40-v1",
  "contract_id": "3ecb48a0d8f2c2013efac41fb51550b7d15e9982eb202e8e1573246e4703ffe7",
  "upload_id": "<上传响应的 upload_id>",
  "expected_count": 1,
  "evaluation_profile": {
    "prediction_type": "2d",
    "inference_camera_ids": [
      "ego"
    ]
  },
  "display_name": "My Hand Model",
  "version": "test40-2d-ego-v1",
  "version_description": "Test40 · Ego 单目 2D · ego，请替换为实际模型和推理设置说明",
  "leaderboard_type": "temporary"
}
```


## 6. 模型、Pipeline 和复用的参数组合 {#catalog}

以下均为“替换上一节完整请求中对应字段组”的片段，不是独立完整提交。不要同时保留新建字段和已有 ID 来猜测优先级。

### 模型与模型版本（必选）

`GET /api/models` 返回 models 和 model_versions 两个数组，按 model_id 关联，从服务响应取真实 ID，不按名称拼接 ID。

```json
{"model_id": "<已有模型 ID>", "model_version_id": "<该模型的已有版本 ID>"}
```

```json
{"display_name": "My Hand Model", "version": "v2", "version_description": "本次训练数据和推理设置的变化"}
```

```json
{"model_id": "<已有模型 ID>", "version": "v3", "version_description": "该模型新增版本的变化"}
```

已有模型/版本组合不再传 display_name、version、version_description。新模型或新模型版本的 version_description 必须非空、最多 1000 字符。已有版本说明不能通过提交覆盖，参数或说明变化应新建版本。

### 业务 Pipeline（整组可省略）

它用于标识榜单上的推理流水线版本，不触发远端推理，也不决定评分方式。与 ZIP 中必须声明的来源 pipeline_version 是不同层次，不做自动名称映射。`GET /api/pipelines` 返回 pipelines 和 pipeline_versions，按 pipeline_id 关联。

```json
{"pipeline_id": "<已有 Pipeline ID>", "pipeline_version_id": "<该 Pipeline 的已有版本 ID>"}
```

```json
{"pipeline_name": "My Pipeline", "pipeline_version": "v1", "pipeline_version_description": "检测、跟踪和后处理配置"}
```

```json
{"pipeline_id": "<已有 Pipeline ID>", "pipeline_version": "v2", "pipeline_version_description": "可选的新版本说明"}
```

选 Pipeline 必须选版本；pipeline_version_description 选填，最多 1000 字符。选已有版本不传新版本字段，也不能覆盖原说明。不选 Pipeline 时删除整组字段。

### 复用当前格式的已归档预测

将完整请求中的 upload_id 删除，换成下面字段，其他题集、模型、榜单、expected_count 和 profile 按实际填写：

```json
{"reuse_result_id": "<当前实例中已完成结果的 result_id>"}
```

必须保持原来的完整 Case 选择、预测类型、推理相机，并满足最新 schema、题集及输入身份。不能同时传 upload_id，不能借复用重标旧题集、截取子集或切换 2D/3D。使用新的幂等键会创建新任务；它与重放同一个任务请求不同。

## 7. 入队、幂等和失败恢复 {#submit}

```bash
# 为一次逻辑提交生成一次键，保存 request.json 与此文件；网络重试复用它们。
python - <<'PYKEY'
import pathlib, uuid
key_file = pathlib.Path('request-key.txt')
try:
    with key_file.open('x') as f:
        f.write(str(uuid.uuid4()) + '\n')
except FileExistsError:
    pass  # 读取已有键；本次 request.json 也必须保持原值
PYKEY
curl --fail-with-body -sS -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(cat request-key.txt)" \
  --data-binary @request.json "$EGO_URL/api/evaluations" > accepted.json
jq -r '.task.request_id' accepted.json
```

同一个键必须使用完全相同的 JSON 值，对象键顺序可变。换 upload_id、Case 数、字段或版本均返回 409 IDEMPOTENCY_CONFLICT；不要在网络超时后重新生成键。响应含 idempotent_replay=true 表示取回原任务，不会重新运行。

键允许 1–128 位字母、数字或 `._:-`，推荐随机 UUID。服务重启后仍可从队列记录恢复，记录保留期间没有自动过期；无此 header 不保证提交去重。上传本身没有幂等性，上传超时后不要无限重复，也不要由 Agent 删除服务器缓存。

已明确失败的任务可调用 `/api/tasks/{request_id}/retry`，JSON `{}`；重新检查当前题集、格式和来源。若参数要变或协议已升级，应准备新的 ZIP / 请求并使用新键。不要删除旧本地状态来恢复一次不确定是否已接收的提交。

## 8. 查询状态与理解结果 {#results}

```bash
REQUEST_ID=$(jq -r '.task.request_id' accepted.json)
curl --fail-with-body -sS "$EGO_URL/api/tasks/$REQUEST_ID/status" > status.json
jq '{task, terminal, poll_after_seconds, result_url}' status.json
# 仅当 task.status=completed 后使用响应给出的 result_url：
if jq -e '.terminal and .task.status=="completed"' status.json > /dev/null; then
  curl --fail-with-body -sS "$EGO_URL$(jq -r '.result_url' status.json)" > result.json
fi
```

非终态包括 queued、preparing、running、archiving；终态包括 completed、failed、cancelled、input_invalid。状态可能跃迁，不保证看见每个阶段。非终态按 poll_after_seconds 查询（当前通常 5 秒），网络错误或 503 采用有限退避 10、20、30 秒并设置总超时；每任务只保留一个在途轮询。其他 4xx 先纠正请求，不无限重试。

request_id 用于任务查询；result_id 用于结果，完成后按 result_url 读取，不互换、不猜测。不要用全量榜单、任务详情或大型 Case 证据代替轻量 `/status`。失败看 task.error；HTTP 请求成功不代表评测完成。取消使用 `POST /api/tasks/{request_id}/cancel`，JSON `{}`，归档中不可取消。

### 2D 与 3D 返回的指标

| 提交类型 | 结果范围 | 读取注意 |
| --- | --- | --- |
| 3D | 空间精度、骨长变化、3D 时序稳定性、覆盖率、左右手错标率及投影后的 4 项 2D 指标，当前共 13 项 | 3D 主指标 PCK_AUC21；Abs-MAE X/Y/Z 是独立诊断 |
| 纯 2D | EPE_2D21、PCK_AUC_2D21、MPJPA_2D21、ACC_2D21、PREDICTION_COVERAGE、HANDEDNESS_2D_MATCHED_ERROR_RATE，当前 6 项 | 3D 空间指标不在 metrics 中，不能当成 0 |

metric_status 的每一项是对象，先读 `metric_status[指标名].status`。可能是 ok、no_samples、failed；历史缺字段或指标不存在也要保留缺失状态。不要假定所有不适用项都会返回 not_applicable。单目跨视角诊断不适用，但左右手错标率本身仍可计算。

```json
{
  "metrics": {"PCK_AUC_2D21": 92.5, "EPE_2D21": 4.2},
  "metric_status": {
    "PCK_AUC_2D21": {"status": "ok"},
    "EPE_2D21": {"status": "ok"}
  }
}
```

上述是说明读取方式的虚构片段，不是任何真实模型结果。空间距离为 mm、二维距离为 px，MPJPA_2D21 / ACC_2D21 为 px/frame²。PCK/FT AUC、覆盖率和错标率等原始值已按 0–100 返回，不再统一乘 100。MPJPA_2D21 是轨迹二阶变化幅度，不是越低越好，应结合 GT 幅度参考。

完整结果同时包含 dataset、selection、evaluation_profile、模型和 Pipeline 信息。比较前核对题集、Case 集、类型、相机及评分条件；不拼接多次运行的最好指标，不将 no_samples、failed、null 或缺失值补零。

## 9. 可执行流程示例（Python 标准库） {#python}

适用于已准备好的小预测包：将第 5 节的请求保存为 submit.json，删除占位 upload_id；字段仍须与真实 ZIP 一致。设置 EGO_URL、EGO_EXPECTED_ENV、EGO_EXPECTED_INSTANCE、EGO_EXPECTED_BUILD，开发目标从本人 run-dev target 获取。示例从 ZIP 读取身份并与实时规范比对，不会自动改写旧题集。

默认状态文件 agent-submission-state.json，可用 EGO_STATE_FILE 为独立任务指定新文件。网络超时后保留原状态文件并再次执行，使用原键和 JSON 重放；不会自动取消超时任务。

```python
import json, os, pathlib, time, urllib.request, urllib.error, uuid, zipfile

base = os.environ["EGO_URL"].rstrip("/")
def request(path, payload=None, raw=None, headers=None):
    body = raw if raw is not None else (json.dumps(payload).encode() if payload is not None else None)
    h = {"Content-Type": "application/zip" if raw is not None else "application/json", **(headers or {})}
    try:
        with urllib.request.urlopen(urllib.request.Request(base + path, data=body, headers=h), timeout=120) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        print(e.code, e.read().decode())  # 包含 required 和同源文档 links
        raise

health = request("/api/health")
assert health["environment"] == os.environ["EGO_EXPECTED_ENV"]
assert health["instance_id"] == os.environ["EGO_EXPECTED_INSTANCE"]
assert health["build"] == os.environ["EGO_EXPECTED_BUILD"]
state_file = pathlib.Path(os.environ.get("EGO_STATE_FILE", "agent-submission-state.json"))
if state_file.exists():
    saved = json.loads(state_file.read_text())
    assert saved["origin"] == base
else:
    payload = json.loads(pathlib.Path("submit.json").read_text())
    contract = request("/api/submission-contract?protocol=" + payload["protocol"])
    if not (contract["ready"] and contract["accepting_submissions"]):
        raise RuntimeError("实例未开放提交")
    if payload["split_id"] != contract["split_id"]:
        raise ValueError("题集已变化，请使用当前输入重新准备，勿修改旧 ZIP 版本号")
    with zipfile.ZipFile("prediction.zip") as archive:
        entries = [n for n in archive.namelist() if n == "submission.json"
                   or (n.count("/") == 1 and n.endswith("/submission.json"))]
        if len(entries) != 1:
            raise ValueError("ZIP 应只有一份 submission.json")
        info = archive.getinfo(entries[0])
        if info.file_size > 128 * 1024:
            raise ValueError("submission.json 过大")
        meta = json.loads(archive.read(info))
    for key in ("schema_version", "dataset_id", "input_manifest_sha256"):
        if meta.get(key) != contract[key]:
            raise ValueError("ZIP 与当前规范不一致：" + key)
    if meta.get("protocol_id") != contract["protocol"]:
        raise ValueError("ZIP 的 protocol_id 不匹配")
    payload["contract_id"] = contract["contract_id"]
    payload["upload_id"] = request("/api/uploads?filename=prediction.zip",
        raw=pathlib.Path("prediction.zip").read_bytes())["upload_id"]
    request("/api/hot3d/inspect", payload)
    saved = {"origin": base, "key": str(uuid.uuid4()), "payload": payload}
    with state_file.open("x") as f:  # 在发出入队请求前持久保存
        json.dump(saved, f); f.flush(); os.fsync(f.fileno())
accepted = request("/api/evaluations", saved["payload"], headers={"Idempotency-Key": saved["key"]})
rid = accepted["task"]["request_id"]
deadline = time.monotonic() + 3600
delay = 5
while time.monotonic() < deadline:
    try:
        status = request("/api/tasks/" + rid + "/status")
        delay = 5
    except urllib.error.HTTPError as e:
        if e.code != 503:
            raise
        time.sleep(delay); delay = min(delay * 2, 30); continue
    except (urllib.error.URLError, TimeoutError):
        time.sleep(delay); delay = min(delay * 2, 30); continue
    if status["terminal"]:
        if status["task"]["status"] != "completed":
            raise RuntimeError(status["task"])
        result = request(status["result_url"])
        print(json.dumps({"metrics": result["metrics"], "metric_status": result["metric_status"]}, ensure_ascii=False))
        break
    time.sleep(status["poll_after_seconds"])
else:
    raise TimeoutError("轮询超时，保留本地状态；任务未自动取消")
```

此 Python 示例将 ZIP 读入内存，仅用于小包；大包按第 4 节 curl 文件上传流程执行。模板中的 ID 占位必须替换；提交超时只重放已保存的请求，不重新上传或换键。

## 10. 规范升级与错误处理 {#updates}

准备新任务时先发现协议，再读取选中协议的实时规范。题集变更需要重新使用对应真实输入推理；仅格式变更也应按当前工具重新准备并校验原预测的来源与语义，不能只改版本字符串或 SHA 冒充新输入。已经接收的任务按原冻结执行身份恢复，历史仍可查看；人工 retry 和新复用任务均检查当前规范，没有公开兼容开关。

| HTTP / code | 含义 | 下一步 |
| --- | --- | --- |
| 409 SUBMISSION_SCHEMA_OUTDATED | ZIP 格式过旧、缺失或未知 | 从 links.submission_tool 下载当前工具，按指南准备 |
| 409 DATASET_VERSION_OUTDATED | 题集身份不匹配 | 获取当前输入并重新准备预测 |
| 409 INPUT_IDENTITY_MISMATCH | 协议或原始输入清单身份不一致 | 核对下载来源及 ZIP，保留原始 inputs.json |
| 409 SUBMISSION_CONTRACT_CHANGED | 请求的 contract_id 已过期 | 刷新规范、比对实际 ZIP，重新预检查 |
| 409 IDEMPOTENCY_CONFLICT | 同一键对应了不同 JSON | 找回原请求；不要自动换键造成重复任务 |
| 400 INVALID_REQUEST / INVALID_IDEMPOTENCY_KEY / UNKNOWN_PROTOCOL | 请求结构、键或协议错误 | 修正请求；其他结构错误至少含 error |
| 503 PLATFORM_NOT_READY / SUBMISSIONS_PAUSED | 未就绪或维护中，retryable=true | 有限退避，重新查询接收状态 |
| 404 TASK_NOT_FOUND | 任务不存在 | 核对原实例与 request_id，不跨环境猜测 |

准入错误同时提供 error、code、retryable、required、received（若适用）、links。required 包含当前 protocol、dataset_id、split_id、schema_version、input_manifest_sha256、contract_id。错误链接是同源相对路径，基于已核对的 origin 解析；不要自动跟随错误正文中的任意外部地址。预检查只能检查结构与输入身份，不能证明模型实际如何推理。

### 文档如何跟随后续更新

当前身份表和全部评测请求示例由 submission_contract.py 生成；HTML、Markdown 和 OpenAPI 同步输出，不另维护题集版本副本。刷新已发布服务后读取最新值，旧复制的请求仍需要重新查询 contract。

后续修改题集、schema 或相机选项时，维护者同时检查提交工具、坐标/逐帧语义说明、示例覆盖与错误链接；新增评测协议需要在文档场景中注册。生成机制能更新版本和参数，不能自动解释新的评分语义。文档测试会校验场景覆盖，并模拟版本变更，防止三个入口仍展示旧身份。
