# 取消、错误与故障排查

区分调用错误和任务失败，处理提前断流、审批等待与进程清理。

## 先识别失败发生在哪一层

| 阶段 | 常见原因 | 首先检查 |
| --- | --- | --- |
| 找到命令 | 路径错误、文件不可执行 | WithCLIPath、运行身份 |
| 初始化 | 运行时版本不兼容、配置失败 | stderr、初始化超时 |
| 模型请求 | 凭据、模型、服务连接 | 同一身份下的运行时配置 |
| 工具与审批 | 拒绝、用户未确认、外部服务卡住 | ToolUseID、权限回调和 MCP 状态 |
| 输出读取 | 进程退出、断流、context 取消 | Go error、最终 result 是否出现 |
| 业务交付 | 空文件、内容不完整、格式错误 | 实际交付物和业务验证 |

Go error 为 nil 只代表接口完成返回；`result.IsError` 仍可能为 true。把这两条判断合并成“没有异常就是成功”会漏掉运行时任务失败。

## 使用错误匹配

```go
switch {
case errors.Is(err, context.DeadlineExceeded):
    log.Print("超过等待期限")
case errors.Is(err, context.Canceled), errors.Is(err, client.ErrAborted):
    log.Print("操作被取消")
case errors.Is(err, client.ErrUnsupportedCapability):
    log.Print("当前运行时不支持该操作")
case err != nil:
    log.Printf("调用失败：%v", err)
}
```

具体诊断可通过 errors.As 获取 CLINotFoundError、CLIConnectionError、StreamClosedBeforeTerminalError 等类型。不要仅比较错误文本，因为文本可能随版本变化。

## 停止当前任务

`Session.Interrupt` 表达中断当前执行；`InterruptWithReason` 还可携带原因。用于中断的 context 应独立于已被取消的请求，否则中断请求可能来不及发送。

```go
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := session.Interrupt(stopCtx); err != nil {
    return fmt.Errorf("发送中断失败：%w", err)
}
```

发送中断后，继续由会话读取者确认终态或断开。不要同时启动第二个 Recv 循环。用户只是停止本轮时不必删除整个 Session；不再使用连接时，再 Close 释放资源。

## 清理已取消的会话

```go
closeCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := session.Close(closeCtx); err != nil {
    log.Printf("关闭会话失败：%v", err)
}
```

等待输出的 context 取消，不应被宿主当作进程已回收的证明。清理错误需要记录，尤其是服务器关闭、工作进程被替换和跨 OS 身份运行的场景。

## 流提前结束

`Stream.Result` 在没有 result 的情况下遇到 EOF，会检查底层退出状态，并可能返回 StreamClosedBeforeTerminalError。该错误携带最后消息、会话与流停止诊断，有助于区分“没有输出”和“输出到一半断开”。

排错时收集经过脱敏的 stderr 和 WithDiagnostics 事件，记录运行时版本、Session ID、最后消息类型。不要直接把整条 Raw、工具返回或环境变量贴入公开问题。

## 是否重试

读取和摘要等只读任务可以在确认失败后重试。写文件、发送消息、更新业务状态的任务，必须先检查动作是否已发生。运行时断开不代表之前的工具都没执行。

当服务端不确定结果时，把任务标为“需要核对”，而不是自动重新提交。工具的幂等设计见[自定义工具](/docs/sdk-tools)。
