# SSE 流式读取智能体运行事件

`GET` `/api/agent/run/stream`

通过 requestId 订阅异步运行产生的 SSE 事件流，支持断线重连；事件带序号，保证不丢失、不重复。以下为完整事件手册。

## 一、SSE 协议约定

响应为标准 Server-Sent Events（`Content-Type: text/event-stream`），每条消息包含三个字段：

| SSE 字段 | 说明 |
|---|---|
| `event` | 事件类型名，即下文事件目录中的枚举名（如 `TEXT_DELTA`、`TOOL_START`） |
| `id` | 事件序号，从 0 开始递增，是断线重连的凭据；**HEARTBEAT 事件不携带 id** |
| `data` | 事件体 JSON，结构见下 |

`data` 的统一结构（AgentEvent）：

```json
{
  "eventType": "TEXT_DELTA",
  "data": { "type": "text", "text": "你好" },
  "timestamp": "2026-07-24T10:00:00",
  "step": 1,
  "totalTokens": 1234
}
```

| 字段 | 类型 | 说明 |
|---|---|---|
| `eventType` | string | 事件类型，与 SSE `event` 字段一致 |
| `data` | object | 事件数据体，**以 `data.type` 字段判别具体结构**（见第三节） |
| `timestamp` | string | 事件产生时间 |
| `step` | integer | Agent 执行步骤号（部分事件为空） |
| `totalTokens` | integer | 截至当前累计消耗的 token 数（部分事件为空） |

回答结束时，`MESSAGE_COMPLETED`、`MESSAGE_FAILED` 的 `data.usage` 可携带回答用量快照：
`promptTokens`、`completionTokens`、
`cachedTokens`、`reasoningTokens`、`totalTokens` 为当前助手消息累计 Token，不含压缩。
缓存命中率由 `cachedTokens / promptTokens` 计算，输入为 0 时不计算。
旧事件或无法取得统计时 `usage` 可为空。顶层 `step` 与 `totalTokens` 保持兼容。


## 二、事件目录（共 19 种）

**会话事件**

| 事件 | data.type | 触发时机 | 客户端处理建议 |
|---|---|---|---|
| `SESSION_CREATED` | session | 任务开始执行时的首个事件 | 记录 sessionId、userMessageId、assistantMessageId，用于后续消息定位与重试 |

**文本事件**（最终回答正文）

| 事件 | data.type | 触发时机 | 客户端处理建议 |
|---|---|---|---|
| `TEXT_START` | text | 一段正文开始，`text` 为初始文本；`synthetic=true` 表示系统合成文本 | 新建一个正文气泡 |
| `TEXT_DELTA` | text | 正文增量，`text` 为**增量片段** | 按到达顺序追加拼接 |
| `TEXT_END` | text | 一段正文结束（data 无有效字段） | 结束当前正文气泡 |

**推理事件**（思考过程，非正文）

| 事件 | data.type | 触发时机 | 客户端处理建议 |
|---|---|---|---|
| `REASONING_START` | reasoning | 一段推理开始 | 新建"思考中"折叠区 |
| `REASONING_DELTA` | reasoning | 推理增量，`text` 为增量片段 | 追加拼接，与正文分开展示 |
| `REASONING_END` | reasoning | 一段推理结束 | 折叠或收起思考区 |

**工具事件**

| 事件 | data.type | 触发时机 | 客户端处理建议 |
|---|---|---|---|
| `TOOL_CALL_GENERATING` | tool | 模型正在生成工具调用参数（逐帧，均早于 TOOL_START）。首帧携带 `toolCallId`/`toolName`，后续帧 `arguments` 为**增量片段** | 首帧即可展示"正在调用 XXX 工具"，按 `toolCallId` 拼接 arguments 实现参数逐字流式展示 |
| `TOOL_START` | tool | 参数生成完毕、工具开始执行，携带**完整** `arguments` | 作为同 `toolCallId` 的 TOOL_CALL_GENERATING 的隐含结束信号，用完整参数覆盖拼接结果 |
| `TOOL_RUNNING` | tool | 工具执行中，`title` 为可展示的进度标题 | 更新工具卡片进度文案 |
| `TOOL_COMPLETED` | tool | 工具执行成功，`output` 为执行结果 | 渲染执行结果，结束工具卡片 |
| `TOOL_FAILED` | tool | 工具执行失败，`errorMessage` 为失败原因 | 展示失败态；智能体通常会自行调整策略继续执行，无需人工干预 |
| `TOOL_EVENT` | tool_side | 工具执行过程中的侧信道结构化信息（如授权缺失、流式数据） | 按 `category` 分发处理，未识别的 category 忽略 |

**权限确认事件**

| 事件 | data.type | 触发时机 | 客户端处理建议 |
|---|---|---|---|
| `PERMISSION_ASK` | permission_ask | 工具触发权限 ASK 规则，等待用户决策；随后会收到 `MESSAGE_SUSPENDED`（suspendReason.type=PERMISSION_ASK） | 弹出授权确认 UI，用户决策后调用 /api/agent/run/resumeTask（传 approved） |
| `PERMISSION_ASK_RESOLVED` | permission_ask_resolved | 预留事件：当前版本内核不主动发射，确认结果以 resumeTask 响应及恢复后的事件流为准 | 预留兼容处理即可 |

**消息生命周期事件**（终态，三选一收尾）

| 事件 | data.type | 触发时机 | 客户端处理建议 |
|---|---|---|---|
| `MESSAGE_COMPLETED` | message | 运行结束（正常完成或达到轮数上限） | 关闭 SSE 连接，结束会话轮次 |
| `MESSAGE_SUSPENDED` | message | 任务挂起（权限确认、手动挂起、异步工具等待、节点停机），`suspendReason` 携带原因 | 关闭连接，按 suspendReason.type 展示对应 UI；恢复后用**相同 requestId** 重新连接本接口 |
| `MESSAGE_FAILED` | message | 任务失败，携带 `errorCode`/`recoverable`/`networkError` | 关闭连接；`recoverable=true` 时可引导用户调用 /api/agent/run/retryTask 从失败点续跑 |

**附件事件**

| 事件 | data.type | 触发时机 | 客户端处理建议 |
|---|---|---|---|
| `ATTACHMENT_CHANGED` | attachment | 智能体将产出文件转存为会话附件（新建或更新，按文件粒度逐个发射） | 实时刷新附件列表，凭 `virtualPath` 调用 /api/agent/attachment/download 下载，无需等消息结束后轮询 |
| `ATTACHMENT_SENT` | attachment | 附件被智能体主动发送给用户（如外发渠道推送） | 复用附件渲染逻辑，展示"已发送"提示 |

**补充输入事件**

| 事件 | data.type | 触发时机 | 客户端处理建议 |
|---|---|---|---|
| `SUPPLEMENTARY_INPUT_CONSUMED` | supplementary_input | 运行中提交的补充输入（/addSupplementaryInput）在步骤边界被注入上下文 | 将对应的补充输入气泡标记为"已送达" |

**心跳事件**

| 事件 | data.type | 触发时机 | 客户端处理建议 |
|---|---|---|---|
| `HEARTBEAT` | - | 连接保活定时发送，**不携带 id** | 直接忽略，不参与断点序号记录 |

**运行期状态事件**

| 事件 | data.type | 触发时机 | 客户端处理建议 |
|---|---|---|---|
| `AGENT_STATUS` | agent_status | 运行期瞬时信号（限流重试、上下文压缩等），状态码见第四节 | 渲染为 toast 或行内瞬时提示，**不要拼入正文**；只发"进入"信号，下一个正文/工具事件到达即隐含恢复 |

## 三、数据体字段说明（按 data.type）

**session**：`sessionId`（会话ID）、`userMessageId`（本轮用户消息ID）、`assistantMessageId`（本轮助手消息ID）。

**text**：`text`（文本内容，START 为初始文本、DELTA 为增量片段）、`synthetic`（是否系统合成文本，仅 TEXT_START 有值）。

**reasoning**：`text`（推理内容，语义同 text）。

**tool**：`toolCallId`（工具调用唯一标识，同一次调用的全部事件共享）、`toolName`（工具名）、`title`（进度标题，仅 TOOL_RUNNING）、`arguments`（调用参数 JSON：GENERATING 为增量片段、START 为完整参数）、`output`（执行结果，仅 TOOL_COMPLETED）、`errorMessage`（失败原因，仅 TOOL_FAILED）。

**tool_side**：`toolCallId`、`toolName`、`category`（事件分类，已知值：`auth_required` 用户授权缺失、`tool_stream_data` 工具流式数据；后续可能扩展）、`payload`（结构化数据，随 category 变化）、`userMessage`（用户友好提示，可选）。

**permission_ask**：`askId`（确认请求唯一标识，resumeTask 时需要）、`principalId`（身份主体ID）、`toolName`、`toolCallId`、`arguments`（待执行的工具参数 JSON）、`ruleDescription`（命中的权限规则描述）、`rulePattern`（规则匹配模式）。

**permission_ask_resolved**（预留）：`askId`、`approved`（是否批准）、`reason`（结果原因）、`resolveType`（预留字段）。

**message**：`messageId`（助手消息ID）；MESSAGE_COMPLETED 可含 `finishReason`（`STOP` 正常完成、`MAX_STEPS` 达到本次运行轮数上限）和 `finishMessage`（结束原因文本）。旧事件可能缺少这两个字段；客户端应兼容未来原因值。达到上限的提示也通过既有 synthetic TEXT_START/TEXT_END 展示并保存，完成事件中的 finishMessage 不应重复追加到正文；仅 MESSAGE_FAILED 有值：`errorMessage`（错误信息）、`errorCode`（错误码，见第五节）、`recoverable`（是否可从失败点重试续跑）、`networkError`（是否网络类瞬态错误）；仅 MESSAGE_SUSPENDED 有值：`suspendReason`（挂起原因对象，`suspendReason.type` 可选值：`PERMISSION_ASK` 权限确认等待、`MANUAL_SUSPEND` 外部主动挂起、`SHUTDOWN_INTERRUPT` 系统停机中断、`ASYNC_TOOL_PENDING` 异步工具等待完成）。

**attachment**：`attachmentId`（附件ID）、`sessionId`、`messageId`（可为空）、`fileName`（文件名）、`fileExtension`（扩展名如 pdf、xlsx）、`contentType`（MIME 类型）、`fileSize`（字节数）、`virtualPath`（虚拟文件路径，用于构造下载URL）、`source`（来源：`USER_UPLOAD` 用户上传 / `AGENT_GENERATED` 智能体生成）、`changeType`（变更类型：`CREATED` 新建 / `UPDATED` 内容覆盖更新）。

**supplementary_input**：`inputId`（补充输入ID）、`content`（文本内容）、`userMessageId`（注入后生成的用户消息ID）、`assistantMessageId`、`attachmentIds`（随补充输入注入的附件ID列表）。

**agent_status**：`code`（状态码，见第四节）、`level`（严重等级：`INFO`/`WARN`/`ERROR`）、`message`（用户可读文案，可直接展示）、`attributes`（结构化扩展属性，随 code 变化）。

## 四、AGENT_STATUS 状态码

| 状态码 | level | 含义 | attributes 结构 |
|---|---|---|---|
| `IMPORTING_ATTACHMENTS` | INFO | 正在将 URL 附件导入为平台附件（仅进度提示，导入失败经由后续错误事件或任务状态返回） | `count`（导入文件数） |
| `LLM_RATE_LIMITED` | WARN | 模型限流，正在重试 | `attempt`（当前重试次数）、`maxRetries`（最大重试次数）、`backoffMs`（本次退避毫秒数） |
| `LLM_NETWORK_RETRYING` | WARN | 模型网络抖动，正在重连 | 同上：`attempt`、`maxRetries`、`backoffMs` |
| `LLM_AUTO_RECOVERING` | WARN | 网络类失败已被拦截，正在从失败点自动恢复 | `attempt`（当前恢复尝试次数）、`maxAttempts`（最大尝试次数） |
| `LLM_EMPTY_RESPONSE_RETRYING` | WARN | 模型返回空回复，正在自动重试 | `attempt`、`maxRetries` |
| `CONTEXT_COMPACTING` | INFO | 正在压缩历史上下文以释放 token 空间（同步调用摘要模型，耗时较久） | `scope`（压缩范围：`in-loop` 循环内 / `turn-level` 轮次级）；scope=in-loop 时另有 `estInput`（预估输入token数）、`maxInput`（最大输入token上限） |
| `TOOL_CONSECUTIVE_FAILURES` | WARN | 模型连续多个步骤的工具调用全部失败，已注入引导提醒 | `failedSteps`（连续失败步骤数）、`maxFailedSteps`（触发阈值） |

处理约定：该事件不写入消息历史、不回灌模型上下文，任务结束后查询消息历史不可见；UI 不应持久化，建议渲染为瞬时提示。服务端未来可能新增状态码，客户端遇到未识别的 code 必须静默忽略（可兜底展示 `message` 文案）。

## 五、终态与错误处理

每次运行的事件流**必然**以 `MESSAGE_COMPLETED`、`MESSAGE_SUSPENDED`、`MESSAGE_FAILED` 三者之一收尾，随后 SSE 连接正常关闭；收到终态事件后客户端应主动断开，不要再重连。

`MESSAGE_FAILED` 的 `errorCode` 可选值：

| errorCode | 含义 | recoverable |
|---|---|---|
| `LLM_ERROR` | 模型调用失败（网络超时、限流等瞬态错误） | true，可调用 retryTask 从失败点续跑 |
| `LLM_INVALID_REQUEST` | 模型拒绝了请求内容（参数错误、内容审核等），原样重试必然再次失败 | false |
| `TOOL_FAILED` | 工具执行失败 | false |
| `TASK_TIMEOUT` | 任务执行超时 | false |
| `SYSTEM_CRASH` | 节点宕机等系统异常中断 | false |
| `USER_CANCELLED` | 用户主动取消（terminateTask） | false |
| `USER_NO_RESPONSE` | 挂起等待用户响应超时（兜底回收） | false |
| `RUN_SNAPSHOT_INVALID` | 运行请求快照无效 | false |
| `INTERNAL_ERROR` | 内部未分类错误 | false |

`MESSAGE_SUSPENDED` 后的恢复方式：调用 /api/agent/run/resumeTask（PERMISSION_ASK 场景需携带 approved 决策），恢复成功后使用**相同 requestId** 重新连接本接口（携带 lastEventId）继续消费。

**控制台兜底错误事件**：当流初始化失败（如 requestId 不存在或无权访问）时，本接口会发送一条 `event: error` 的 SSE 消息，其 data 为空、错误信息位于 SSE 注释行（`:` 开头的行），随后连接关闭。客户端收到 `error` 事件应终止重连并提示用户。

## 六、断线重连

1. 消费过程中持续记录最近一条**带 id** 事件的 id 值（HEARTBEAT 无 id，跳过）；
2. 连接断开且未收到终态事件时，携带 `lastEventId=<最近id>` 重新调用本接口；
3. 服务端从 id+1 位置续推历史事件并无缝衔接实时事件，事件按序号连续、不丢不重；
4. 若不传 lastEventId，则从事件流起点（id=0）完整重放。

## 七、典型事件时序示例

一次包含推理、工具调用与附件产出的完整运行，事件顺序如下：

```
SESSION_CREATED
REASONING_START → REASONING_DELTA × N → REASONING_END
TOOL_CALL_GENERATING × N → TOOL_START → TOOL_RUNNING → TOOL_COMPLETED
ATTACHMENT_CHANGED
TEXT_START → TEXT_DELTA × N → TEXT_END
MESSAGE_COMPLETED
```

（期间可能穿插 HEARTBEAT、AGENT_STATUS、SUPPLEMENTARY_INPUT_CONSUMED，位置不固定）

关键事件的完整 SSE 报文示例：

```
id: 0
event: SESSION_CREATED
data: {"eventType":"SESSION_CREATED","data":{"type":"session","sessionId":9001,"userMessageId":10001,"assistantMessageId":10002},"timestamp":"2026-07-24T10:00:00"}

id: 5
event: TOOL_START
data: {"eventType":"TOOL_START","data":{"type":"tool","toolCallId":"call_abc123","toolName":"web_search","arguments":"{\"query\":\"沪深300走势\"}"},"timestamp":"2026-07-24T10:00:03","step":1,"totalTokens":856}

id: 12
event: TEXT_DELTA
data: {"eventType":"TEXT_DELTA","data":{"type":"text","text":"根据检索结果，"},"timestamp":"2026-07-24T10:00:06","step":2,"totalTokens":1420}

id: 20
event: MESSAGE_COMPLETED
data: {"eventType":"MESSAGE_COMPLETED","data":{"type":"message","messageId":10002,"finishReason":"STOP"},"timestamp":"2026-07-24T10:00:09","step":2,"totalTokens":2310}
```

## 八、客户端展示工具（virtualTools）的渲染指令消费

若在 `/async` 请求中声明了 `virtualTools`，模型对这些工具的调用**不产生新的事件类型**，完全复用上文的 `TOOL_*` 事件：

1. `TOOL_CALL_GENERATING`：按 `toolCallId` 拼接 `arguments` 增量，可用于展示"正在准备图表"之类的过渡态；
2. `TOOL_START`：`arguments` 为完整的渲染参数 JSON，是**开始渲染的时机**；
3. `TOOL_COMPLETED`：参数校验通过后，`output` 原样返回工具定义中的 `mockResponse` 字符串（包括空字符串）；省略或传入 null 时返回默认回执 `{"status":"success"}`。预设内容是模拟结果，不代表客户端已完成渲染或外部业务实际执行。

消费要点：

- 按 `toolName` 与自己声明的工具名匹配，即可把渲染指令与平台内置工具的调用区分开；未匹配上的工具名按普通工具处理；
- 服务端不执行调用方代码，渲染完全在客户端完成，**渲染结果无需回传**，也没有对应的回传接口；
- 渲染失败属于客户端内部问题，不影响运行主流程，事件流仍会照常推进到终态。

## 九、迁移进来的历史运行没有事件流

通过定时任务「迁移历史运行记录」接口登记的运行（`triggerSource` 为 `imported`）只是历史留痕，
没有真实的执行过程，因此**不存在可订阅的事件流**：用这类运行的 requestId 调用本接口会返回 409 冲突，
而不是返回一个空流。

需要展示这类历史对话时，请改用会话轨迹查询接口读取已落库的消息内容。原生运行的事件流行为不受影响。

## 十、兼容性约定

- 服务端事件类型、data.type、AGENT_STATUS 状态码均**只增不删**；
- 客户端遇到未识别的事件类型、data.type、状态码或新增字段时，必须静默忽略，不得报错中断消费；
- 字段可能为 null 或缺省，解析时需做好空值兼容。


## 请求参数

### 请求头参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| X-Principal-Id | integer | 否 | 身份主体ID（Long类型）。通过API访问时必须传递以标识操作主体；通过页面访问时无需传递。可通过「身份主体」模块的创建接口获取。 |

### Query 参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| requestId | string | 是 | 异步请求ID，由 /async 接口返回 |
| lastEventId | integer | 否 | 上次收到的事件ID，断线重连时传入 |

## 响应

### 400

| 参数名 | 类型 | 说明 |
|--------|------|------|
| httpStatusCode | integer | HTTP状态码 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| success | boolean | 是否成功 |
| requestId | string | 上游请求ID，用于跨系统链路追踪排查问题 |
| errCode | string | 上游错误码，前端可据此做特殊UI处理 |

### 500

| 参数名 | 类型 | 说明 |
|--------|------|------|
| httpStatusCode | integer | HTTP状态码 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| success | boolean | 是否成功 |
| requestId | string | 上游请求ID，用于跨系统链路追踪排查问题 |
| errCode | string | 上游错误码，前端可据此做特殊UI处理 |

