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

# SSE 事件

> 流式响应的完整事件词表：20 个公开事件名、载荷字段与消费约定

# SSE 事件

平台 API 的流式端点返回 `text/event-stream`。这一页是**公开事件词表的完整清单**——不在这张表里的事件名，说明是内部事件原样透传，不构成契约，随时可能变。

## 传输格式

每个事件两行加一个空行：

```
event: output.text.delta
data: {"delta":"你好","event_id":12,"type":"output.text.delta"}

```

三个约定：

* `data` 是单行 JSON
* 每个 payload 都带**单调递增**的 `event_id`（从 1 开始），可用于去重与断点判断
* `type` 字段与 `event:` 行的值相同，所以只解析 `data` 也够用

流的结束标记是：

```
data: [DONE]
```

<Warning>
  `[DONE]` 不是 JSON。直接 `JSON.parse(line.slice(6))` 会抛 `SyntaxError: Unexpected token D`。这是接入时最常见的第一个坑——解析循环必须先判断这一行再解析。
</Warning>

## 事件全表

内部事件名到公开事件名是一张固定映射表。**公开名是契约，内部名不是**——内部改名不影响你，但公开名改动会走版本流程。

### 运行生命周期

| 公开事件名             | 内部名          | 含义                         |
| ----------------- | ------------ | -------------------------- |
| `run.created`     | `preparing`  | 请求已受理，正在准备执行环境（沙箱、上下文、工具集） |
| `run.in_progress` | `turn_start` | 一轮推理开始                     |
| `run.completed`   | `complete`   | 运行正常结束                     |
| `run.failed`      | `error`      | 运行失败，载荷含错误信息               |

`run.in_progress` 每轮都会发一次。一次运行里模型可能调工具、看结果、再推理，每一轮都是一个 turn——**所以收到多个 `run.in_progress` 是正常的**，不要当成重复请求。

### 输出内容

| 公开事件名                    | 内部名               | 含义              |
| ------------------------ | ----------------- | --------------- |
| `output.text.delta`      | `text`            | 正文文本增量          |
| `output.thinking.delta`  | `reasoning`       | 思考过程增量          |
| `output.thinking.status` | `thinking_status` | 思考阶段状态（如"正在分析"） |

<Warning>
  `text` 与 `reasoning` 这两类事件的内容字段，在映射时**从 `content` 改名成了 `delta`**。这是刻意的（对齐 OpenAI Responses API），但意味着任何消费方都必须读 `delta`。

  稳妥的写法是 `event.delta ?? event.content`——两个名字都吃，这样内部事件透传或跨版本时都不会哑掉。
</Warning>

拼接文本就是把所有 `output.text.delta` 的 `delta` 顺序拼起来。不要按 `event_id` 重排——事件本来就是顺序到达的，重排只会引入额外的失败模式。

### 工具调用

工具调用被拆成五个事件，前四个由内部 `tool_call` 事件按 `status` 字段分流：

| 公开事件名                       | 触发条件                  | 含义        |
| --------------------------- | --------------------- | --------- |
| `tool_call.created`         | `status=pending`      | 模型决定调用某工具 |
| `tool_call.in_progress`     | `status=running`      | 工具开始执行    |
| `tool_call.completed`       | `status=completed`    | 工具执行成功    |
| `tool_call.failed`          | `status=failed`       | 工具执行失败    |
| `tool_call.arguments.delta` | 内部 `tool_call_chunk`  | 工具参数的流式增量 |
| `tool_call.result`          | 内部 `tool_call_result` | 工具返回的结果载荷 |
| `tool_call.denied`          | 内部 `tool_denied`      | 工具被权限策略拒绝 |

<Note>
  出现未列出的 `tool_call.<something>` 是可能的：`status` 若是映射表以外的值，会按 `tool_call.${status}` 原样生成事件名。所以 UI 对 `tool_call.*` 前缀要有兜底渲染，不要写死四种状态的 switch 然后让 default 分支什么都不做。
</Note>

`tool_call.denied` 与 `tool_call.failed` 是两回事：

* `failed` = 工具跑了但出错（网络超时、参数不合法、目标不存在）
* `denied` = 工具根本没跑，被权限门拦下

把两者合并成"工具出错"会让用户看到"重试"按钮却永远重试不成功。

### 审批与委托

| 公开事件名                  | 内部名                    | 含义         |
| ---------------------- | ---------------------- | ---------- |
| `approval.required`    | `Ask`                  | 需要人工确认才能继续 |
| `delegation.started`   | `delegation_started`   | 开始委托给另一个专家 |
| `delegation.completed` | `delegation_completed` | 委托执行完成     |

`approval.required` 是**阻塞事件**：流会停在这里等你回应。不处理它，运行就一直挂着直到超时。

### 用量与配额

| 公开事件名                    | 内部名                | 含义         |
| ------------------------ | ------------------ | ---------- |
| `usage.token`            | `token_usage`      | token 用量上报 |
| `usage.budget_exhausted` | `budget_exhausted` | 预算耗尽，运行终止  |
| `usage.trial_exhausted`  | `trial_exhausted`  | 试用轮次耗尽     |

<Warning>
  `usage.trial_exhausted` 必须单独处理。它不是错误——是"这个用户的试用额度用完了，接下来需要购买"。

  把它落进通用错误分支的后果是用户看到"当前操作被拒绝"这种毫无信息量的提示，然后以为账号出问题了。IM 渠道曾经就是这么翻译的，导致试用结束的用户全部误以为故障。正确做法是命中这个事件时给出购买入口。
</Warning>

## 消费示例

<CodeGroup>
  ```typescript TypeScript theme={null}
  const res = await fetch(url, { method: "POST", headers, body });
  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let text = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";

    for (const line of lines) {
      if (!line.startsWith("data: ")) continue;

      const payload = line.slice(6);
      if (payload === "[DONE]") return text;   // 必须先判断，否则 JSON.parse 抛错

      const evt = JSON.parse(payload);
      switch (evt.type) {
        case "output.text.delta":
          text += evt.delta ?? evt.content ?? "";
          break;
        case "usage.trial_exhausted":
          showPurchasePrompt();               // 不要落进错误分支
          break;
        case "run.failed":
          throw new Error(evt.message ?? "run failed");
        default:
          if (evt.type?.startsWith("tool_call.")) renderToolCall(evt);
      }
    }
  }
  ```

  ```python Python theme={null}
  import json

  async for line in response.aiter_lines():
      if not line.startswith("data: "):
          continue

      payload = line[6:]
      if payload == "[DONE]":
          break

      evt = json.loads(payload)
      t = evt.get("type")

      if t == "output.text.delta":
          print(evt.get("delta") or evt.get("content") or "", end="", flush=True)
      elif t == "usage.trial_exhausted":
          handle_trial_exhausted()
      elif t == "run.failed":
          raise RuntimeError(evt.get("message", "run failed"))
  ```
</CodeGroup>

## 边界与失败态

| 症状                                | 原因                       | 处理                     |
| --------------------------------- | ------------------------ | ---------------------- |
| `SyntaxError: Unexpected token D` | 把 `[DONE]` 当 JSON 解析     | 解析前先判断这一行              |
| 文本全空但事件在来                         | 只读了 `content`，没读 `delta` | 改成 `delta ?? content`  |
| 收到不认识的事件名                         | 内部事件原样透传                 | 忽略即可，不要抛错              |
| 流中断但没有 `[DONE]`                   | 网络断开或服务端异常               | 按未完成处理；不要当作正常结束        |
| 事件名带未知 `tool_call.` 后缀            | `status` 不在四值映射表内        | 按 `tool_call.*` 前缀兜底渲染 |
| 收到多个 `run.in_progress`            | 多轮推理，每轮一次                | 正常，不要去重成一次             |

<Note>
  无法识别的事件类型是**原样透传**而不是丢弃。这是刻意的：新增内部事件不会因为映射表没更新就在你这边消失。代价是你必须容忍未知事件名——所以 `default` 分支应该是"忽略"，不是"抛错"。
</Note>

## 验证接入

最小验证：

```bash theme={null}
curl -N https://api.profy.cn/v1/agents/run \
  -H "Authorization: Bearer $PROFY_API_KEY" \
  -H "X-End-User-Id: verify-1" \
  -H "Content-Type: application/json" \
  -d '{"agent":"your-expert","message":"说一句话"}' \
  | head -40
```

应当看到：

1. `event: run.created` 打头
2. 若干 `event: output.text.delta`
3. `event: run.completed`
4. `data: [DONE]` 收尾

若第一个事件就是 `run.failed`，看载荷里的错误码，对照 [错误码](/zh/developers/error-codes)。

## 相关页面

<CardGroup cols={2}>
  <Card title="端点速查" icon="list" href="/zh/developers/reference/endpoints">
    哪些端点返回流
  </Card>

  <Card title="错误码" icon="triangle-exclamation" href="/zh/developers/error-codes">
    `run.failed` 载荷里的码怎么读
  </Card>
</CardGroup>

<Note>
  核对日期 2026-08-11。来源：`services/core/src/lib/sse-event-mapper.ts`（`EVENT_MAP` 16 条 + `TOOL_CALL_STATUS_MAP` 4 条 = 20 个公开事件名，该文件是词表的唯一真相源）。
</Note>
