# 第三章：Agent 循环 -- 双层循环与工具执行

> 对应源文件：`packages/agent/src/agent-loop.ts`

## 核心循环结构

agent 循环有两层嵌套：

```
外层循环（follow-up loop）
│   当 agent 本来要停下时，检查有没有后续消息
│   有 → 把消息加入上下文，继续
│   没有 → 退出
│
└── 内层循环（tool-call loop）
    │   stream AI 响应 → 检查是否有工具调用
    │   有 → 执行工具 → 检查 steering 消息 → 继续
    │   没有 → 退出内层循环
    │
    └── 每一轮：
        1. 注入 pending 消息（steering）
        2. 调用 LLM 获取 assistant 响应
        3. 如果有 tool calls → 执行
        4. 检查 steering 消息队列
        5. 回到 1 或退出
```

### 用伪代码表达

```
function runLoop():
    pending = getSteeringMessages()  // 开始前检查一次

    while true:                       // 外层循环

        while hasMoreToolCalls or pending.length > 0:  // 内层循环

            // 注入 steering 消息
            if pending:
                加入上下文
                pending = []

            // 调用 LLM
            message = streamAssistantResponse()

            if message 出错或被取消:
                return  // 直接结束

            // 处理工具调用
            if message 包含 toolCalls:
                执行工具
                hasMoreToolCalls = !全部工具都说了 terminate

            // 检查新的 steering 消息
            pending = getSteeringMessages()

        // 内层循环结束了，检查 follow-up
        followUp = getFollowUpMessages()
        if followUp:
            pending = followUp
            continue    // 继续外层循环
        else:
            break       // 真正结束
```

---

## Steering 和 Follow-Up 消息

这两种消息机制是 pi 交互设计的关键。

### Steering（转向消息）

> 在 AI 正在工作时，用户输入的新指令

场景：AI 正在帮你写代码（内层循环还在转），你突然打了一句"等等，用 TypeScript 而不是 JavaScript"。

这条消息进入 steering 队列。每次内层循环完成一轮工具执行后，会检查队列。如果有消息，就注入到上下文中，AI 下一轮就能看到你的新指令。

```
时间线：
用户: "帮我写一个排序函数"
  AI: [思考...] → toolCall: write("sort.js", ...)
  AI: 执行 write 工具                 ← 同时用户打了 "用 TypeScript"
  AI: 检查 steering 队列 → 发现新消息！
  AI: 把 "用 TypeScript" 加入上下文
  AI: [思考...] "好的，我用 TypeScript 重写"
  AI: toolCall: write("sort.ts", ...)
```

### Follow-Up（后续消息）

> 在 AI 完全停下来后，等待处理的新请求

场景：AI 完成了当前任务（内层循环结束了），但用户在 AI 工作期间又输入了一个新问题。

这条消息进入 follow-up 队列。外层循环检查到有后续消息，把它加入上下文，重新启动内层循环。

```
时间线：
用户: "帮我写排序函数"
  AI: [写完了] stopReason: "stop"
  AI: 内层循环结束
  AI: 检查 follow-up 队列 → 发现 "再写一个搜索函数"
  AI: 把新消息加入上下文
  AI: 重新进入内层循环
  AI: [开始写搜索函数...]
```

### 区别总结

| 特性 | Steering | Follow-Up |
|------|----------|-----------|
| 什么时候注入 | 每次工具执行后 | 内层循环结束后 |
| 类比 | "中途插嘴" | "接着问" |
| 典型场景 | 修正 AI 正在做的事 | 在 AI 空闲时追加任务 |

---

## 消息转换流程

每次调用 LLM 前，消息列表会经过两步转换：

```
AgentMessage[]
    │
    ▼  transformContext()（可选）
AgentMessage[]     ← 仍然是 AgentMessage，但可能被裁剪了
    │
    ▼  convertToLlm()（必须）
Message[]          ← LLM 能理解的标准消息
    │
    ▼  构建 Context
Context { systemPrompt, messages, tools }
    │
    ▼  调用 streamSimple()
AssistantMessageEventStream
```

`transformContext` 的典型用途：

- **上下文窗口管理**：当消息太多时，删除最早的消息
- **注入外部上下文**：从数据库拉取相关信息注入对话
- **上下文压缩**：把旧消息压缩成摘要（compaction）

---

## 工具执行流程

当 AI 返回包含 tool calls 的消息时，每个工具调用经过以下生命周期：

```
1. 查找工具
   在 tools 列表中查找 toolCall.name 对应的 AgentTool
   找不到 → 返回错误 ToolResult

2. 参数预处理（prepareArguments）
   如果工具定义了 prepareArguments，先修正参数
   比如把 file_path 映射为 path

3. 参数验证（validateToolArguments）
   用 TypeBox schema 验证参数格式
   验证失败 → 返回错误 ToolResult

4. 执行前钩子（beforeToolCall）
   调用 config.beforeToolCall()
   返回 { block: true } → 阻止执行，返回错误 ToolResult

5. 执行工具（tool.execute()）
   实际运行工具逻辑
   抛异常 → 捕获，返回错误 ToolResult
   正常返回 → AgentToolResult

6. 执行后钩子（afterToolCall）
   调用 config.afterToolCall()
   可以修改 result 的 content、details、isError、terminate

7. 生成 ToolResultMessage
   包装为标准的 ToolResultMessage 加入上下文
```

### 并行执行 vs 串行执行

当 AI 一次返回多个工具调用时：

**串行模式** (`sequential`)：

```
toolCall_1: prepare → execute → finalize → emit
toolCall_2: prepare → execute → finalize → emit
toolCall_3: prepare → execute → finalize → emit
```

**并行模式** (`parallel`)，默认行为：

```
toolCall_1: prepare ──┐
toolCall_2: prepare ──┼── 所有 prepare 串行完成
toolCall_3: prepare ──┘
                      │
                      ▼
toolCall_1: execute ──┐
toolCall_2: execute ──┼── 允许的工具并发执行
toolCall_3: execute ──┘
                      │
                      ▼
所有 execute 完成后，按原始顺序 emit ToolResultMessage
```

注意：prepare 阶段始终是串行的（因为 `beforeToolCall` 可能需要用户确认）。只有 execute 阶段才并发。

如果某个工具自身声明了 `executionMode: "sequential"`，整个批次会退化为串行模式。

### 早停机制（terminate）

工具可以在返回结果时设置 `terminate: true`：

```typescript
execute: async () => ({
  content: [{ type: "text", text: "已退出" }],
  details: {},
  terminate: true,   // 建议停止循环
})
```

只有当**同一批次的所有工具都设了 terminate: true** 时，循环才会跳过下一次 LLM 调用直接结束。这确保了单个工具不会意外中止整个循环。

---

## streamAssistantResponse -- LLM 调用桥梁

这个函数是 agent 循环与 `ai` 包的连接点：

```
1. 如果配置了 transformContext → 先转换 AgentMessage[]
2. 调用 convertToLlm → 得到 Message[]
3. 构建 LLM Context（systemPrompt + messages + tools）
4. 如果配置了 getApiKey → 动态获取 API key
5. 调用 streamFn（默认是 streamSimple）
6. 消费事件流：
   - start → 把 partial AssistantMessage 加入上下文
   - text_delta/thinking_delta/toolcall_delta → 更新 partial，emit message_update
   - done/error → 用最终的 AssistantMessage 替换 partial
```

关键细节：**partial message 直接写入 context.messages**。这意味着在流式传输过程中，如果你去读 `context.messages`，最后一条就是当前正在生成的不完整消息。流结束时，它被替换为最终的完整消息。

---

## 小结

```
runLoop()
┌──────────────────────────────────────────────────┐
│  外层循环                                         │
│  ┌──────────────────────────────────────────────┐ │
│  │  内层循环                                     │ │
│  │  1. 注入 steering 消息                        │ │
│  │  2. transformContext → convertToLlm → LLM    │ │
│  │  3. 收到 AssistantMessage                    │ │
│  │  4. 如果有 toolCalls:                        │ │
│  │     prepare → validate → before → execute    │ │
│  │     → after → emit ToolResult               │ │
│  │  5. 检查 steering 队列                       │ │
│  └──────────────────────────────────────────────┘ │
│  内层循环结束 → 检查 follow-up 队列               │
│  有 → 继续外层循环                                │
│  没有 → 结束                                     │
└──────────────────────────────────────────────────┘
```

下一章我们看 `Agent` 类如何在这个底层循环上封装出好用的高层 API。
