# 事件映射 ## 三层映射 Pi 发出的事件首先经过 `PiEventAdapter` 变成经过裁剪和脱敏的 `HarnessEvent`。Web 的 reducer 再把有序事件投影为 assistant-ui 消息、工具和运行状态。Pi 原始对象不会直接进入浏览器。 | Pi / Harness 来源 | HarnessEvent | Web / assistant-ui 状态 | | --- | --- | --- | | Session 创建 | `session.started` | 建立初始 timeline | | HTTP message input | `user.message` | user message,附件仅为 IDs | | Harness 开始 run | `run.started` | `isRunning=true`,显示停止按钮 | | `message_update.text_delta` | `assistant.delta` | 按稳定 messageId 追加文本 | | assistant `message_end` | `assistant.completed` | assistant message complete | | `tool_execution_start` | `tool.requested` | 创建工具调用 part | | `tool_execution_end` 成功 | `tool.completed` | 工具卡显示 result | | `tool_execution_end` 失败 | `tool.failed` | 工具卡显示安全错误 | | Broker 请求 | `approval.required` | 打开服务器审批 Dialog | | Broker 决策 / 超时 / 取消 | `approval.resolved` | 关闭 pending 状态 | | AttachmentStore 成功 | `attachment.uploaded` | 保存安全附件元数据 | | AttachmentStore 拒绝 | `attachment.rejected` | timeline / 错误状态 | | ModelStore 配置 | `model.configured` | 更新安全模型摘要 | | Harness 正常结束 | `run.completed` | run 和消息标为 complete | | Harness 取消 | `run.cancelled` | 消息 incomplete/cancelled | | Harness 失败 | `run.failed` | 消息 incomplete/error | | Session 删除 | `session.ended` | 连接回到 idle | ## Skill 推导事件 `skill.requested` 和 `skill.loaded` 不是 Pi SDK 的原生事件。Harness 按下面条件推导: ```mermaid flowchart LR A[Pi read start] --> B[realpath] B --> C{是否匹配 SkillIndex} C -->|否| D[只保留普通 tool 事件] C -->|是| E[skill.requested] E --> F{同一 toolCallId read 成功} F -->|是| G[skill.loaded] F -->|否| H[不发 loaded] ``` 真实路径匹配避免仅凭用户提供的字符串伪造 Skill 活动。事件由 Session 队列串行处理,因此异步 `realpath` 不会导致 read end 越过 read start。 ## 顺序、重放与去重 - 每个 Session 的 sequence 从 1 单调递增。 - SSE 初次连接可用 `afterSequence`,重连可用 `Last-Event-ID`。 - Server 先重放 cursor 之后的事件,再订阅新事件,并发送 heartbeat。 - Web 忽略 sequence 小于等于当前值的重复事件。 - sequence 出现缺口时,Web 不应用该事件,而是重置投影并从 0 重连。 - timeline 最多保留 500 条客户端状态,界面显示最近 100 条筛选结果。 ## 数据最小化 事件负载经过 `redactAndLimit`。API Key、authorization、token 等敏感字段被脱敏;工具参数和结果有字节上限。图片事件只含 ID、MIME、字节数和尺寸,不含 base64。错误映射只保留明确 code 和有限长度的安全消息。