# Claude Agent SDK 语义消息标准化改造总结

## 背景

本次改造的目标是把 Claude Agent SDK 返回的原始消息，在 Forge 的 engine adapter 层统一识别成更稳定、更明确的语义事件。

改造前，消费方通常只能拿到较底层的事件，例如：

```text
tool_use
tool_result
tool_raw_result
agent_content
error
```

如果桌面端、H5 端、IM 端想要识别特殊语义，就需要各自写判断：

```ts
if (toolName === 'AskUserQuestion') { ... }
if (toolName === 'ExitPlanMode') { ... }
if (toolName === 'TodoWrite') { ... }
if (rawContent.type === 'web_search') { ... }
```

这会带来三个问题：

1. 多端重复实现同一套工具名判断。
2. 新增 Claude 工具语义时，需要多处修改。
3. 消费层需要理解 Claude SDK 的原始消息结构，耦合较重。

改造后，Forge 在 `MessageMapper` 中集中识别语义，然后通过 `AgentCallbacks` 暴露给不同消费方。

核心原则是：

```text
SDK 原始消息
  ↓
MessageMapper 集中识别语义
  ↓
AgentCallbacks 标准事件
  ↓
桌面端 / H5 端 / IM 端 / 未来自动化消费方按需处理
```

## 本次改动文件

本次主要修改 5 个文件：

```text
src/lib/engine/agent-types.ts
src/lib/engine/adapters/claude/message-mapper.ts
src/lib/engine/adapters/claude/stream-dispatcher.ts
src/lib/engine/adapters/claude/adapter.ts
src/lib/engine/consumers/sse-adapter.ts
```

其中：

- `agent-types.ts`：扩展标准事件类型、回调契约和最终结果元数据。
- `message-mapper.ts`：把 SDKMessage 映射成更丰富的语义事件。
- `stream-dispatcher.ts`：把语义事件分发到对应的 optional callback。
- `adapter.ts`：补充 `AgentResult` 元数据和权限风险分类。
- `sse-adapter.ts`：临时增加语义事件日志，并透传为 SSE 事件。

IM 聚合层暂未实现新增语义事件处理。

```text
src/lib/im/core/agent-callback-aggregator.ts 未改动新增语义事件逻辑
```

## 当前完整标准事件集合

### 文本与思考

```text
text_delta
thinking_start
thinking_delta
```

### 工具事件

```text
tool_use_start
tool_use
tool_result
tool_raw_result
tool_progress
tool_summary
```

### 特殊工具语义事件

```text
question_request
plan_review_request
todo_update
```

### Web / 搜索事件

```text
web_search_result
```

### 子 Agent 事件

```text
agent_start
agent_content
agent_done
agent_error
```

### 会话与状态事件

```text
session_init
api_retry
rate_limit
cancelled
error
```

### 最终结果元数据

`AgentResult` 现在包含：

```ts
{
  text,
  blocks,
  inputTokens,
  outputTokens,
  sdkSessionId,
  subtype,
  finishReason,
  totalCostUsd,
  durationMs,
  numTurns,
}
```

## 新增类型与回调

### `ErrorCategory`

用于给错误做机器可读分类：

```ts
type ErrorCategory =
  | 'auth'
  | 'rate_limit'
  | 'quota'
  | 'model'
  | 'network'
  | 'permission'
  | 'service'
  | 'token_limit'
  | 'unknown'
```

### `FinishReason`

用于区分真正错误、成功和用户主动停止：

```ts
type FinishReason =
  | 'success'
  | 'error'
  | 'user_abort'
  | 'cancelled'
  | 'timeout'
```

### 权限风险级别

```ts
type PermissionRiskLevel = 'low' | 'medium' | 'high'
```

`AgentPermissionRequest` 增加：

```ts
{
  riskLevel?: PermissionRiskLevel
  description?: string
}
```

### 新增 `AgentCallbacks`

```ts
onToolUseStart?(id: string, name: string): void | Promise<void>

onQuestionRequest?(req: QuestionRequestEvent): void | Promise<void>
onPlanReviewRequest?(req: PlanReviewRequestEvent): void | Promise<void>
onTodoUpdate?(req: TodoUpdateEvent): void | Promise<void>

onSessionInit?(event: SessionInitEvent): void | Promise<void>
onCancelled?(event: CancelledEvent): void | Promise<void>
onApiRetry?(event: ApiRetryEvent): void | Promise<void>
onRateLimit?(event: RateLimitEvent): void | Promise<void>

onWebSearchResult?(event: WebSearchResultEvent): void | Promise<void>
onToolSummary?(event: ToolSummaryEvent): void | Promise<void>

onAgentStart?(event: AgentStartEvent): void | Promise<void>
onAgentDone?(event: AgentDoneEvent): void | Promise<void>
onAgentError?(event: AgentErrorEvent): void | Promise<void>
```

这些 callback 都是 optional，因此未实现的消费方不会被破坏。

## 关键改造点

### 1. `tool_use_start`

改造前，`tool_use` 需要等 `input_json_delta` 完全累积并解析后才会发出。如果工具输入很长，消费方会长时间不知道模型正在准备调用工具。

改造后，`content_block_start/tool_use` 到达时立即发：

```ts
{
  type: 'tool_use_start',
  id: 'toolu_xxx',
  name: 'Edit'
}
```

后续 `content_block_stop` 时再发完整工具：

```ts
{
  type: 'tool_use',
  id: 'toolu_xxx',
  name: 'Edit',
  input: { ... }
}
```

收益：桌面端 / H5 端可以提前显示工具占位状态。

### 2. `AskUserQuestion` → `question_request`

改造前：

```ts
{
  type: 'tool_use',
  name: 'AskUserQuestion',
  input: { questions: [...] }
}
```

消费方必须自己判断工具名。

改造后额外输出：

```ts
{
  type: 'question_request',
  tool_use_id: 'toolu_question',
  input: { questions: [...] },
  questions: [...]
}
```

收益：

- 桌面端可渲染问题卡。
- H5 端可渲染轻量选项卡。
- IM 端未来可转成自然语言或交互按钮。

### 3. `ExitPlanMode` → `plan_review_request`

改造前：`ExitPlanMode` 只是普通工具调用。

改造后额外输出：

```ts
{
  type: 'plan_review_request',
  tool_use_id: 'toolu_plan',
  input: { plan: '...' },
  plan_content: '...'
}
```

收益：消费方可以明确知道这是计划审批，而不是普通工具日志。

### 4. `TodoWrite` → `todo_update`

改造前：`TodoWrite` 只是普通工具调用。

改造后额外输出：

```ts
{
  type: 'todo_update',
  tool_use_id: 'toolu_todo',
  input: { todos: [...] },
  todos: [...]
}
```

收益：桌面端可以展示任务列表，IM 端未来可以播报任务进度。

### 5. `web_search_tool_result` → `web_search_result`

改造前，WebSearch 结果主要藏在：

```ts
{
  type: 'tool_raw_result',
  tool_name: 'WebSearch',
  raw_content: {
    type: 'web_search',
    results: [...]
  }
}
```

改造后保留 raw result，同时额外输出一等事件：

```ts
{
  type: 'web_search_result',
  tool_use_id: 'toolu_web',
  results: [
    { title: '...', url: '...' }
  ]
}
```

收益：消费方无需解析 `tool_raw_result.raw_content`。

### 6. `tool_use_summary` → `tool_summary`

改造前，`tool_use_summary` 会合成为多条 `tool_result`，同一段 summary 可能重复挂到多个工具上。

改造后保留旧的 `tool_result`，同时额外输出：

```ts
{
  type: 'tool_summary',
  tool_use_ids: ['toolu_1', 'toolu_2'],
  summary: '读取了 2 个文件并完成分析'
}
```

收益：消费方可以把多个工具调用折叠成一条摘要。

### 7. 子 Agent 生命周期

改造前，子 Agent 主要通过 `agent_content` 间接表达过程。

改造后增加：

```ts
{
  type: 'agent_start',
  parent_tool_use_id: 'toolu_agent',
  description: '...',
  prompt: '...'
}
```

```ts
{
  type: 'agent_done',
  parent_tool_use_id: 'toolu_agent',
  status: 'success',
  input_tokens: 8000,
  output_tokens: 1200
}
```

```ts
{
  type: 'agent_error',
  parent_tool_use_id: 'toolu_agent',
  error: '...',
  category: 'token_limit'
}
```

收益：消费方可以明确维护子 Agent 的启动、完成和失败状态。

### 8. `system.init` → `session_init`

改造前只记录 `sdkSessionId`，不发事件。

改造后额外输出：

```ts
{
  type: 'session_init',
  session_id: 'sess_123',
  model: 'claude-sonnet-4'
}
```

收益：消费方可以知道 SDK 会话已初始化。

### 9. `system.api_retry` → `api_retry`

改造后输出：

```ts
{
  type: 'api_retry',
  attempt: 2,
  reason: 'Connection reset',
  next_retry_at: 1720000000
}
```

收益：前端或 IM 端未来可以显示“API 重试中”，避免用户误以为卡住。

### 10. `rate_limit_event` → `rate_limit`

改造后输出：

```ts
{
  type: 'rate_limit',
  rate_limit_type: 'requests',
  resets_at: '2026-07-13T10:30:00Z',
  is_using_overage: false,
  overage_status: 'disabled'
}
```

收益：消费方可以展示额度和限流状态。

### 11. 用户停止 → `cancelled`

改造前，用户停止可能被当作普通错误。

改造后，以下情况会被识别为用户停止：

```text
result.subtype === 'user_abort'
result.subtype === 'error_interrupted'
errors 中包含 interrupt / user abort
```

输出：

```ts
{
  type: 'cancelled',
  reason: 'user_abort'
}
```

并记录：

```ts
finishReason = 'user_abort'
```

收益：消费方可以区分“用户主动停止”和“系统失败”。

### 12. 错误分类

真实错误输出：

```ts
{
  type: 'error',
  error: 'context length exceeded',
  category: 'token_limit'
}
```

`onError` 签名扩展为：

```ts
onError?(error: string, category?: ErrorCategory)
```

收益：消费方可以根据 `category` 决定展示、重试或告警策略。

## 完整消息回复时序

下面是一轮完整回复的大致过程：

```text
用户请求
  ↓
route.ts / h5 route.ts / IM conversation
  ↓
engine.process(request, callbacks)
  ↓
ClaudeAdapter.createForgeQuery()
  ↓
for await SDKMessage
  ↓
MessageMapper.mapMessage(msg)
  │
  ├─ system.init
  │    → session_init
  │
  ├─ stream_event.thinking
  │    → thinking_start / thinking_delta
  │
  ├─ stream_event.text
  │    → text_delta
  │
  ├─ stream_event.tool start
  │    → tool_use_start
  │
  ├─ stream_event.tool stop
  │    → tool_use
  │    → question_request       // AskUserQuestion
  │    → plan_review_request    // ExitPlanMode
  │    → todo_update            // TodoWrite
  │    → agent_start            // Agent / Task
  │
  ├─ user.tool_result
  │    → tool_result
  │    → tool_raw_result
  │    → agent_content          // Agent 工具结果
  │    → web_search_result      // WebSearch
  │
  ├─ tool_use_summary
  │    → tool_result
  │    → tool_summary
  │
  ├─ tool_progress
  │    → tool_progress
  │
  ├─ sub-agent stream
  │    → agent_content
  │
  ├─ sub-agent result
  │    → agent_done / agent_error
  │
  ├─ system.api_retry
  │    → api_retry
  │
  ├─ rate_limit_event
  │    → rate_limit
  │
  ├─ result user_abort
  │    → cancelled
  │
  ├─ result error
  │    → error + category
  │
  └─ result success
       → 记录 finishReason / cost / duration / tokens
  ↓
dispatchSseEvent(event, callbacks)
  ↓
AgentCallbacks
  ├─ sse-adapter.ts
  │    → console.log('[semantic] ...')
  │    → emit SSE event
  │
  ├─ IM aggregator
  │    → 暂时不处理新增语义事件
  │
  └─ future consumers
       → 可按事件类型定制
  ↓
SDK 流结束
  ↓
adapter.onDone({
  text,
  blocks,
  inputTokens,
  outputTokens,
  sdkSessionId,
  subtype,
  finishReason,
  totalCostUsd,
  durationMs,
  numTurns,
})
```

## 示例：一次完整回复中的事件序列

假设用户请求：

```text
帮我分析项目，并修改 README，如果需要请先问我。
```

可能出现如下事件序列：

```text
session_init
thinking_start
thinking_delta
text_delta

tool_use_start          // Grep
tool_use                // Grep with full input
tool_result
tool_raw_result

tool_use_start          // TodoWrite
tool_use
todo_update

tool_use_start          // Agent
tool_use
agent_start
agent_content
agent_done

tool_use_start          // WebSearch
tool_use
tool_raw_result
web_search_result

tool_use_start          // AskUserQuestion
tool_use
question_request

api_retry               // 如果 API 重试
rate_limit              // 如果收到限流状态

result success
onDone({ finishReason: 'success', totalCostUsd, durationMs, numTurns })
```

如果用户中途停止：

```text
cancelled
onDone({ finishReason: 'user_abort' })
```

如果发生真实错误：

```text
error + category
onDone({ finishReason: 'error' })
```

## SSE 临时日志

当前只在 `src/lib/engine/consumers/sse-adapter.ts` 添加了临时日志。

可观察日志包括：

```text
[semantic] tool_use_start
[semantic] question_request
[semantic] plan_review_request
[semantic] todo_update
[semantic] session_init
[semantic] cancelled
[semantic] api_retry
[semantic] rate_limit
[semantic] web_search_result
[semantic] tool_summary
[semantic] agent_start
[semantic] agent_done
[semantic] agent_error
[semantic] error
```

这些日志用于方案 A 的手动验证。测试通过后可以删除日志，但保留事件透传。

## 各端后续定制方向

### 桌面端

桌面端可以基于 SSE 事件实现卡片化 UI：

```text
tool_use_start        → 工具占位卡
tool_use              → 填充工具参数
question_request      → 问题卡
plan_review_request   → 计划审批卡
todo_update           → Todo 面板
web_search_result     → 搜索结果卡
tool_summary          → 工具组摘要
agent_start           → Agent 树新增节点
agent_done            → Agent 节点完成
agent_error           → Agent 节点失败
rate_limit/api_retry  → 顶部状态提示
cancelled             → 停止状态
error + category      → 友好错误提示
```

### H5 端

H5 端复用 `createSseCallbacks`，因此自动能收到这些事件。后续可以做轻量展示：

```text
question_request      → 移动端底部问题面板
plan_review_request   → 移动端确认弹层
todo_update           → 简化进度条
web_search_result     → 折叠链接列表
api_retry/rate_limit  → toast / banner
```

### IM 端

IM 端目前未消费新增语义事件。未来可以在 `agent-callback-aggregator.ts` 中按需实现：

```text
question_request      → 发送需要用户回答的 IM 消息
plan_review_request   → 发送计划审批消息
todo_update           → 节流播报任务进度
web_search_result     → 发送参考来源摘要
tool_summary          → 发送工具执行摘要
agent_start/done      → 播报子任务状态
cancelled             → 用户主动停止时不报错
error + category      → 按错误类型给出提示
```

### 未来自动化消费方

未来如果有 webhook、workflow 或规则引擎，可以直接订阅语义事件：

```text
rate_limit            → 暂停任务
api_retry             → 标记任务重试中
error.category        → 决定是否自动重试
question_request      → 暂停等待用户输入
plan_review_request   → 进入审批流
agent_done            → 汇总子任务结果
```

## 验证方式

当前已运行：

```bash
pnpm typecheck
```

结果通过。

建议后续补充单元测试：

```text
message-mapper.test.ts
stream-dispatcher.test.ts
```

重点覆盖：

```text
tool_use_start
todo_update
question_request
plan_review_request
web_search_result
tool_summary
session_init
api_retry
rate_limit
cancelled
agent_start / agent_done / agent_error
error category
AgentResult.finishReason / cost / duration / turns
```

## 设计收益总结

本次改造最大的收益是：

```text
把“Claude SDK 原始消息理解能力”从消费层上移到 engine adapter 层。
```

改造前：

```text
桌面端判断 AskUserQuestion
H5 端判断 AskUserQuestion
IM 端判断 AskUserQuestion
未来自动化端再判断一次 AskUserQuestion
```

改造后：

```text
MessageMapper 判断一次 AskUserQuestion
所有消费方收到 question_request
```

因此后续新增 Claude 工具或消息类型时，优先在 mapper 中标准化为一等事件，然后各端只负责表现层定制。

最终目标是：

```text
Claude SDK 原始消息
  ↓
统一语义事件
  ↓
不同端各自表现
```

而不是：

```text
Claude SDK 原始消息
  ↓
不同端各自解析
  ↓
重复逻辑和不一致行为
```
