diff --git a/docs/zh/rate-limiting.md b/docs/zh/rate-limiting.md new file mode 100644 index 000000000..aa9037d40 --- /dev/null +++ b/docs/zh/rate-limiting.md @@ -0,0 +1,95 @@ +# 动态速率限制 + +PicoClaw 通过在每次发送请求之前对每个模型实施可配置的请求速率限制来防止 LLM 提供商 API 的 429 错误。与被动式冷却/回退系统(在收到 429 *之后*才激活)不同,速率限制是**主动式的**:它将出口 QPS 保持在提供商的免费层或计划限制内。 + +## 工作原理 + +### 令牌桶算法 + +每个限速模型都有一个令牌桶: + +- **容量** = `rpm`(突发大小等于每分钟限制) +- **补充速率** = `rpm / 60` 每秒令牌数 +- 每次 LLM 调用消耗一个令牌;如果桶为空,则调用阻塞直到令牌补充或请求上下文被取消 + +### 调用链集成 + +``` +AgentLoop.callLLM() + └─ FallbackChain.Execute() ← 遍历候选者 + ├─ CooldownTracker.IsAvailable() ← 如果处于 429 后冷却期则跳过 + ├─ RateLimiterRegistry.Wait() ← 新增:阻塞直到令牌可用 + └─ provider.Chat() ← 实际的 LLM HTTP 调用 +``` + +速率限制器在冷却检查**之后**和提供商调用**之前**运行,因此: +- 已处于冷却期的候选者被完全跳过(不消耗令牌) +- 可用的候选者被限制到配置的 RPM + +相同的检查也适用于 `ExecuteImage`。 + +### 线程安全 + +`RateLimiterRegistry` 是并发安全的。每个限速器的令牌桶使用细粒度互斥锁,因此并发 goroutine 各自独立获取令牌。 + +## 配置 + +在 `model_list` 中的任何模型上设置 `rpm`: + +```yaml +model_list: + - model_name: gpt-4o-free + model: openai/gpt-4o + api_base: https://api.openai.com/v1 + rpm: 3 # 每分钟最多 3 个请求 + api_keys: + - sk-... + + - model_name: claude-haiku + model: anthropic/claude-haiku-4-5 + rpm: 60 # 60 rpm(Anthropic 免费层) + api_keys: + - sk-ant-... + + - model_name: local-llm + model: openai/llama3 + api_base: http://localhost:11434/v1 + # 不设置 rpm → 无限制 +``` + +| 字段 | 类型 | 默认值 | 描述 | +|---|---|---|---| +| `rpm` | `int` | `0` | 每分钟请求数。`0` 表示无限制。 | + +### 与回退的交互 + +当模型配置了回退时,每个候选者独立限速: + +```yaml +model_list: + - model_name: gpt4-with-fallback + model: openai/gpt-4o + rpm: 5 + fallbacks: + - gpt-4o-mini # 必须也在 model_list 中;适用其自己的 rpm +``` + +如果当前候选者的桶为空且有更多可用候选者,PicoClaw 立即跳过本地饱和的候选者并尝试下一个回退。只有最后一个候选者等待令牌补充。如果在等待最后一个候选者时达到上下文截止时间,则传播等待错误。 + +对于解析为相同底层提供商/模型的 `model_list` 别名,速率限制按稳定的配置标识(例如 `model_name`)进行键控,而不是按解析后的运行时模型字符串。这为多密钥和基于别名的配置保留不同的 RPM 设置。 + +### 突发行为 + +桶开始时是**满的**(突发 = RPM)。对于 `rpm: 3`,前 3 个请求立即发出;后续请求间隔约 20 秒。 + +要为严格的 API 减少突发性,请设置较低的 `rpm` 并依赖稳态补充。 + +## 变更的文件 + +| 文件 | 变更内容 | +|---|---| +| `pkg/providers/ratelimiter.go` | `RateLimiter`(令牌桶)+ `RateLimiterRegistry` | +| `pkg/providers/ratelimiter_test.go` | 限速器和注册表的单元测试 | +| `pkg/providers/fallback.go` | `FallbackCandidate.RPM` 字段;`FallbackChain.rl`;`Execute`/`ExecuteImage` 中的 `Wait()` 调用 | +| `pkg/agent/model_resolution.go` | 从 `model_list` 解析候选者,保留稳定的配置标识并将 `RPM` 传播到 `FallbackCandidate` | +| `pkg/agent/loop.go` | 构建 `RateLimiterRegistry`,注册所有代理的候选者,传递给 `NewFallbackChain` | diff --git a/docs/zh/steering.md b/docs/zh/steering.md new file mode 100644 index 000000000..285ae0ef6 --- /dev/null +++ b/docs/zh/steering.md @@ -0,0 +1,193 @@ +# 转向(Steering) + +转向允许将消息注入到已运行的代理循环中,在工具调用之间进行中断,而无需等待整个周期完成。 + +## 工作原理 + +当代理正在执行一系列工具调用时(例如模型在单个回合中请求了 3 个工具),转向在每个工具**完成之后**检查队列。如果发现排队的消息: + +1. 剩余工具被**跳过**,并收到 `"Skipped due to queued user message."` 作为其结果 +2. 转向消息被**注入到对话上下文中** +3. 使用更新后的上下文(包括用户的转向消息)再次调用模型 + +``` +用户 ──► Steer("改变方法") + │ +代理循环 ▼ + ├─ tool[0] ✔ (已执行) + ├─ [轮询] → 发现转向! + ├─ tool[1] ✘ (已跳过) + ├─ tool[2] ✘ (已跳过) + └─ 新的 LLM 回合(包含转向消息) +``` + +## 作用域队列 + +转向现在按解析后的会话作用域隔离存储,而不是存储在单个全局队列中。 + +- 当前回合从其自己的作用域键写入和读取(通常是路由的会话键,如 `agent::...`) +- `Steer()` 在活动回合之外仍可通过传统回退队列工作 +- `Continue()` 首先为请求的会话作用域出队消息,然后回退到传统队列以保持向后兼容 + +这防止来自另一个聊天、DM 对等方或路由代理会话的消息被注入到错误的对话中。 + +## 配置 + +在 `config.json` 中的 `agents.defaults` 下: + +```json +{ + "agents": { + "defaults": { + "steering_mode": "one-at-a-time" + } + } +} +``` + +### 模式 + +| 值 | 行为 | +|-------|----------| +| `"one-at-a-time"` | **(默认)** 每轮询周期只出一个消息。如果队列中有 3 条消息,它们将在 3 个连续迭代中逐一处理。 | +| `"all"` | 在单次轮询中清空整个队列。所有待处理消息一起注入上下文中。 | + +环境变量 `PICOCLAW_AGENTS_DEFAULTS_STEERING_MODE` 可作为替代方案使用。 + +## Go API + +### Steer — 发送转向消息 + +```go +err := agentLoop.Steer(providers.Message{ + Role: "user", + Content: "改变方向,专注于 X", +}) +if err != nil { + // 队列已满(MaxQueueSize=10)或未初始化 +} +``` + +消息以线程安全的方式入队。如果队列已满或未初始化则返回错误。消息将在下一个轮询点被取出(当前工具完成后)。 + +### SteeringMode / SetSteeringMode + +```go +// 读取当前模式 +mode := agentLoop.SteeringMode() // SteeringOneAtATime | SteeringAll + +// 在运行时更改 +agentLoop.SetSteeringMode(agent.SteeringAll) +``` + +### Continue — 恢复空闲的代理 + +当代理空闲时(已处理完毕且最后一条消息来自助手),`Continue` 检查队列中是否有转向消息,并使用它们开始新的周期: + +```go +response, err := agentLoop.Continue(ctx, sessionKey, channel, chatID) +if err != nil { + // 错误(例如 "无可用的默认代理") +} +if response == "" { + // 队列中没有转向消息,代理保持空闲 +} +``` + +`Continue` 内部使用 `SkipInitialSteeringPoll: true` 以避免重复出队相同的消息(因为它已经提取了它们并直接作为输入传递)。 + +`Continue` 还会从提供的会话键解析目标代理,因此代理作用域的会话在正确的代理上继续,而不是总是使用默认代理。 + +## 循环中的轮询点 + +转向在代理周期中的以下点进行检查: + +1. **循环开始时** — 在第一次 LLM 调用之前,捕获在设置期间入队的消息 +2. **每个工具完成后** — 包括第一个和最后一个。如果发现转向且有剩余工具,它们会立即全部被跳过 +3. **直接 LLM 响应之后** — 如果在模型生成非工具响应时有新的转向消息到达,循环继续而不是返回过时的答案 +4. **回合完成之前** — 如果转向在回合结束时到达,代理立即开始续回合,而不是将消息留在队列中 + +## 为什么剩余工具会被跳过 + +当检测到转向消息时,批次中的所有剩余工具都会被跳过而不是执行。另一种方案——让所有工具完成然后再注入转向消息——已被考虑并拒绝。原因如下。 + +### 防止不必要副作用 + +工具可能产生**不可逆的副作用**。如果用户在代理执行批次中途中说"不,等等",执行剩余工具意味着那些副作用仍然会发生: + +| 工具批次 | 转向消息 | 使用跳过 | 不用跳过 | +|---|---|---|---| +| `[web_search, send_email]` | "不要发送" | 邮件**未**发送 | 邮件已发送,造成损失 | +| `[query_db, write_file, spawn_agent]` | "使用另一个数据库" | 只有查询运行 | 文件已写入 + 子代理已生成,全部浪费 | +| `[search₁, search₂, search₃, write_file]` | 用户完全改变话题 | 1 次搜索 | 3 次搜索 + 文件写入,全部无关 | + +### 避免浪费时间 + +需要数秒的工具(网页获取、API 调用、数据库查询)都会运行完成后代理才能看到用户的更正。在 3 个工具各需 3-4 秒的批次中,那是 10+ 秒的工作将被丢弃。 + +使用跳过,代理在当前工具完成后立即做出反应——通常在几秒内而不是等待整个批次。 + +### LLM 获得完整上下文 + +跳过的工具会收到带有明确错误结果的消息 (`"Skipped due to queued user message."`),因此模型知道哪些操作未执行。它可以决定是否在新上下文中重新执行它们,或完全采取不同的路径。 + +### 权衡:顺序执行 + +跳过要求工具**顺序**运行(之前的实现是并行运行)。当 LLM 在单个回合中请求多个独立工具时,这会引入延迟。实际上,大多数批次包含 1-2 个工具,因此与能够停止不需要的操作的收益相比影响很小。 + +## 跳过工具的结果格式 + +当转向中断批次时,未执行的每个工具都会收到带有以下内容的 `tool` 结果: + +``` +Content: "Skipped due to queued user message." +``` + +这通过 `AddFullMessage` 保存到会话并发送到模型,因此模型知道某些请求的操作未执行。 + +## 完整流程示例 + +``` +1. 用户: "搜索 X 的信息,写入文件,并发送消息给我" + +2. LLM 响应 3 个工具调用: [web_search, write_file, message] + +3. web_search 被执行 → 结果保存 + +4. [轮询] → 用户调用 Steer("不,搜索 Y") + +5. write_file 被跳过 → "Skipped due to queued user message." + message 被跳过 → "Skipped due to queued user message." + +6. 消息 "搜索 Y" 被注入到上下文 + +7. LLM 收到完整的更新上下文并做出相应响应 +``` + +## 自动总线耗尽 + +当代理循环 (`Run()`) 开始处理消息时,它生成一个后台 goroutine,持续从总线消费新的入站消息。这些消息通过 `Steer()` 自动重定向到转向队列。这意味着: + +- 任何渠道(Telegram、Discord 等)上的用户无需做任何特殊操作——当代理忙碌时,他们的消息会被自动捕获为转向 +- 音频消息在转向之前会被转录,因此代理接收的是文本。如果转录失败,原始(非转录)消息将按原样被转向 +- 只有解析为与活动回合**相同转向作用域**的消息才会被重定向。其他聊天/会话的消息会被重新排队到入站总线上,以便正常处理 +- `system` 入站消息不被视为转向输入 +- 当 `processMessage` 完成后,耗尽 goroutine 被取消,正常消息消费恢复 + +## 带媒体的转向 + +转向消息可以包含 `Media` 引用,就像正常的入站用户消息一样。 + +- 原始 `media://` 引用通过 `AddFullMessage` 保存在会话历史中 +- 在下一次提供商调用之前,转向消息会经过正常的媒体解析管道 +- 图像引用会转换为多模式提供商的数据 URL;非图像引用以与标准入站媒体相同的方式解析 + +这适用于回合内转向和通过 `Continue()` 进行的空闲会话继续。 + +## 注意事项 + +- 转向**不会中断**正在执行的工具。它等待当前工具完成,然后检查队列。 +- 在 `one-at-a-time` 模式下,如果多条消息快速入队,它们将每轮迭代处理一条。这让模型有机会单独对每条消息做出反应。 +- 在 `all` 模式下,所有待处理消息合并为一次注入。当您希望代理一次接收所有上下文时很有用。 +- 转向队列最大容量为 10 条消息 (`MaxQueueSize`)。当队列已满时 `Steer()` 返回错误。在总线耗尽路径中,错误被记录为警告,消息实际上被丢弃。 +- 在活动回合之外进行的手动 `Steer()` 调用仍然进入传统回退队列,因此较旧的集成继续工作。 diff --git a/docs/zh/subturn.md b/docs/zh/subturn.md new file mode 100644 index 000000000..19243afdc --- /dev/null +++ b/docs/zh/subturn.md @@ -0,0 +1,279 @@ +# 🔄 子回合机制 + +> 返回 [README](../README.md) + +## 概述 + +`SubTurn` 机制是 PicoClaw 的一项核心功能,允许工具生成孤立的嵌套代理循环来处理复杂的子任务。 + +通过使用 SubTurn,代理可以将问题分解并在独立的临时会话中运行单独的 LLM 调用。这确保中间推理、后台任务或子代理输出不会污染主对话历史。 + +## 核心能力 + +- **上下文隔离**:每个 SubTurn 使用 `ephemeralSessionStore`。其消息历史不会泄露到父任务中,并在完成时被销毁。临时会话最多保存 **50 条消息**;达到此限制时,旧消息会自动截断。 +- **深度和并发限制**:防止无限循环和资源耗尽。 + - **最大深度**:最多 3 层嵌套。 + - **最大并发**:每个父回合最多 5 个并发子回合(通过超时为 30 秒的信号量管理)。 +- **上下文保护**:支持软上下文限制 (`MaxContextRunes`)。它在达到提供商的硬上下文窗口限制之前主动截断旧消息(同时保留系统提示和最近上下文)。 +- **错误恢复**:通过压缩历史和重试自动检测并从提供商上下文长度超出错误和截断错误中恢复。 + +## 配置 (`SubTurnConfig`) + +生成 SubTurn 时,必须提供 `SubTurnConfig`: + +| 字段 | 类型 | 描述 | +| :--- | :--- | :--- | +| `Model` | `string` | 子回合使用的 LLM 模型(例如 `gpt-4o-mini`)。**必填。** | +| `Tools` | `[]tools.Tool` | 授予子回合的工具。如果为空,则继承父工具。 | +| `SystemPrompt` | `string` | 子回合的任务描述。作为第一条用户消息发送给 LLM(而不是作为系统提示覆盖)。 | +| `ActualSystemPrompt` | `string` | 可选的显式系统提示,用于替换代理的默认设置。留空以继承父代理的系统提示。 | +| `MaxTokens` | `int` | 生成响应的最大令牌数。 | +| `Async` | `bool` | 控制结果传递模式(同步 vs 异步)。 | +| `Critical` | `bool` | 如果为 `true`,即使父任务正常完成,子回合也会继续运行。 | +| `Timeout` | `time.Duration` | 最大执行时间(默认:5 分钟)。 | +| `MaxContextRunes`| `int` | 软上下文限制。`0` = 自动计算(模型上下文窗口的 75%,推荐),`-1` = 无限制(禁用软截断,仅依赖硬上下文错误恢复),`>0` = 使用指定的字符限制。 | + +> **注意:** `Async` 标志**不会**使调用非阻塞。它仅控制结果是否也传递到父级的 `pendingResults` 通道。两种模式都阻塞调用者直到子回合完成。若要实现真正的非阻塞执行,调用者必须在单独的 goroutine 中生成子回合。 + +## 执行模式 + +### 同步模式 (`Async: false`) + +这是标准模式,调用者需要立即获得结果才能继续。 + +- 调用者阻塞直到子回合完成。 +- 结果**仅**通过函数返回值直接返回。 +- 它**不会**传递到父级的待处理结果通道。 + +**示例:** +```go +cfg := agent.SubTurnConfig{ + Model: "gpt-4o-mini", + SystemPrompt: "Analyze the provided codebase...", + Async: false, +} +result, err := agent.SpawnSubTurn(ctx, cfg) +// 立即处理结果 +``` + +### 异步模式 (`Async: true`) + +用于"发射后不管"操作或并行处理,父回合稍后收集结果。 + +- 结果传递到父回合的 `pendingResults` 通道。 +- 结果也通过函数返回值返回(为保持一致性)。 +- 父级的代理循环会在后续迭代中轮询此通道,并将结果自动注入到进行中的对话上下文中作为 `[SubTurn Result]`。 + +**示例:** +```go +cfg := agent.SubTurnConfig{ + Model: "gpt-4o-mini", + SystemPrompt: "Run a background security scan...", + Async: true, +} +result, err := agent.SpawnSubTurn(ctx, cfg) +// 结果稍后也会通过通道注入到父循环中 +``` + +## 错误恢复和重试 + +SubTurn 为临时错误实现了自动重试机制: + +| 错误类型 | 最大重试次数 | 恢复操作 | +|:-----------|:------------|:----------------| +| 上下文长度超出 | 2 | 强制压缩历史并重试 | +| 响应截断 (`finish_reason="truncated"`) | 2 | 注入恢复提示并重试 | + +### 截断恢复 +当 LLM 响应被截断 (`finish_reason="truncated"`) 时,SubTurn 自动: +1. 从 `turnState.lastFinishReason` 检测截断 +2. 注入恢复提示:"您的上一个响应因长度被截断。请提供更短的完整响应..." +3. 最多重试 2 次 + +### 上下文错误恢复 +当提供商返回上下文长度错误(例如 `context_length_exceeded`)时: +1. 强制压缩消息历史(删除最旧的 50% 对话) +2. 使用压缩后的上下文重试 +3. 失败前最多重试 2 次 + +## 生命周期和取消 + +SubTurn 在独立上下文中操作,但保持与其父 `turnState` 的结构链接。 + +### 父任务正常完成 +当父任务正常完成时 (`Finish(false)`): +- **非关键**子回合收到信号,正常退出而不抛出错误。 +- **关键** (`Critical: true`) 子回合在后台继续运行。完成后,其结果作为**孤立结果**发出,以免丢失数据。 + +### 硬中止 +当父任务被强制中止时(例如用户使用 `/stop` 中断): +- 触发级联取消,立即终止所有子回合和孙回合。 +- 根回合的会话历史回滚到回合开始时拍摄的快照 (`initialHistoryLength`),防止脏上下文。SubTurn 不受此回滚影响,因为它们使用无论如何都会被丢弃的临时会话。 + +## 代理循环集成 + +### 处理期间的公交耗尽 + +当消息进入 `Run()` 循环时,代理在调用 `processMessage` 之前启动一个 `drainBusToSteering` goroutine。此 goroutine 与整个处理生命周期并发运行,持续消费总线上任何新的入站消息,将它们重定向到**转向队列**而不是丢弃它们。 + +这确保如果用户在代理处理(包括 SubTurn 执行期间)发送后续消息,消息不会丢失——它将通过 `dequeueSteeringMessages` 在工具调用迭代之间被取出。 + +当 `processMessage` 返回时,耗尽 goroutine 自动停止(通过可取消的上下文)。 + +### 待处理结果轮询 + +代理循环在每次迭代的两个点轮询异步 SubTurn 结果: +1. **LLM 调用之前**:将任何到达的结果作为 `[SubTurn Result]` 消息注入对话上下文。 +2. **所有工具执行之后**:在工具循环期间再次轮询,以捕获工具执行期间到达的结果。 +3. **最后一次迭代之后**:回合结束前最后一次轮询,以避免丢失延迟到达的结果。 + +### 回合状态跟踪 + +所有活动根回合都注册在 `AgentLoop.activeTurnStates` (`sync.Map`,按键为会话键) 中。这允许 `HardAbort` 和 `/subagents` 可观测性命令找到并操作活动回合。 + +## 事件总线集成 + +SubTurn 向 PicoClaw `EventBus` 发出特定事件以进行可观测性和调试: + +| 事件类型 | 发出时机 | 负载 | +|:------|:-------------|:--------| +| `subturn_spawn` | 子回合成功初始化 | `SubTurnSpawnPayload{AgentID, Label, ParentTurnID}` | +| `subturn_end` | 子回合完成(成功或错误) | `SubTurnEndPayload{AgentID, Status}` | +| `subturn_result_delivered` | 异步结果成功传递到父级 | `SubTurnResultDeliveredPayload{TargetChannel, TargetChatID, ContentLen}` | +| `subturn_orphan` | 结果无法传递(父级完成或通道已满) | `SubTurnOrphanPayload{ParentTurnID, ChildTurnID, Reason}` | + +## API 参考 + +### SpawnSubTurn(公共入口点) + +```go +func SpawnSubTurn(ctx context.Context, cfg SubTurnConfig) (*tools.ToolResult, error) +``` + +这是代理内部代码(例如测试、直接调用)的导出包级入口点。它从上下文获取 `AgentLoop` 和 `turnState`,并委托给内部的 `spawnSubTurn`。 + +**要求:** +- `AgentLoop` 必须通过 `WithAgentLoop()` 注入到上下文中 +- 父 `turnState` 必须存在于上下文中(从工具调用时自动设置) + +**返回:** +- `*tools.ToolResult`:包含带有子回合输出的 `ForLLM` 字段 +- `error`:定义错误类型之一或上下文错误 + +### AgentLoopSpawner(接口实现) + +```go +type AgentLoopSpawner struct { al *AgentLoop } + +func (s *AgentLoopSpawner) SpawnSubTurn(ctx context.Context, cfg tools.SubTurnConfig) (*tools.ToolResult, error) +``` + +这实现了 `tools.SubTurnSpawner` 接口,供需要生成子回合而无需直接导入 `agent` 包的工具使用(避免循环依赖)。它在委托给内部 `spawnSubTurn` 之前将 `tools.SubTurnConfig` → `agent.SubTurnConfig`。 + +### NewSubTurnSpawner + +```go +func NewSubTurnSpawner(al *AgentLoop) *AgentLoopSpawner +``` + +为给定的 AgentLoop 创建一个新的生成器实例。在工具注册期间将返回值传递给 `SpawnTool.SetSpawner()` 或 `SubagentTool.SetSpawner()`。 + +### Continue + +```go +func (al *AgentLoop) Continue(ctx context.Context, sessionKey string) error +``` + +通过将任何排队的转向消息作为新的 LLM 迭代注入来恢复空闲的代理回合。当代理正在等待且需要处理延迟的转向消息而没有新的入站消息到达时使用。 + +## 上下文传播 + +SubTurn 依赖上下文值进行正确操作: + +| 上下文键 | 用途 | +|:------------|:---| +| `agentLoopKey` | 存储 `*AgentLoop` 以供工具访问和 SubTurn 生成 | +| `turnStateKey` | 存储 `*turnState` 用于层次跟踪和结果传递 | + +### 注入依赖 + +```go +// 在调用可能生成 SubTurn 的工具之前 +ctx = WithAgentLoop(ctx, agentLoop) +ctx = withTurnState(ctx, turnState) +``` + +### 独立的子上下文 + +**重要**:子 SubTurn 使用从 `context.Background()` 派生的**独立上下文**,而不是父上下文。这一设计选择: + +- 允许关键 SubTurn 在父取消后继续 +- 防止父超时影响子执行 +- 子有自己的超时保护(`Timeout` 配置或默认 5 分钟) + +## 错误类型 + +| 错误 | 条件 | +|:------|:----------| +| `ErrDepthLimitExceeded` | SubTurn 深度超过 3 层 | +| `ErrInvalidSubTurnConfig` | 必填字段 `Model` 为空 | +| `ErrConcurrencyTimeout` | 所有 5 个并发槽位被占用 30 秒以上 | +| 上下文错误 | 信号量获取期间父上下文被取消 | + +## 线程安全 + +SubTurn 设计用于并发执行: + +- **父子关系**:在互斥锁下管理 (`parentTS.mu.Lock()`) +- **活动回合跟踪**:使用 `sync.Map` 进行并发访问 `activeTurnStates` +- **ID 生成**:使用 `atomic.Int64` 生成唯一 SubTurn ID(格式:`subturn-N`,每个 `AgentLoop` 实例全局单调) +- **结果传递**:在锁定下读取父状态,在通道发送前释放(可接受的小竞争窗口) + +## 孤立结果 + +当发生以下情况时结果变为孤立: +1. 父回合在 SubTurn 完成之前完成 +2. `pendingResults` 通道已满(缓冲区大小:16) + +当结果变为孤立时: +- 向 EventBus 发出 `SubTurnOrphanResultEvent` +- 结果**不会**传递到 LLM 上下文 +- 外部系统可以监听此事件以进行自定义处理 + +### 防止孤立结果 +- 对必须完成的重要 SubTurn 使用 `Critical: true` +- 监听 `SubTurnOrphanResultEvent` 以进行可观测性 +- 生成许多异步 SubTurn 时考虑 16 缓冲区限制 + +## 工具继承 + +### 当 `cfg.Tools` 为空时: +- SubTurn 继承父代理的**所有**工具 +- 工具注册在新的 `ToolRegistry` 实例中 +- 工具 TTL 与父级独立管理 + +### 当指定了 `cfg.Tools` 时: +- 只有指定的工具对 SubTurn 可用 +- 父工具**不会**合并 +- 使用此选项可限制 SubTurn 能力以提高安全性或专注度 + +**受限 SubTurn 示例:** +```go +cfg := agent.SubTurnConfig{ + Model: "gpt-4o-mini", + Tools: []tools.Tool{readOnlyTool}, // 仅只读访问 + SystemPrompt: "Analyze the file structure...", +} +``` + +## 参考 + +| 常量 | 值 | +|:---------|:------| +| `maxSubTurnDepth` | 3 | +| `maxConcurrentSubTurns` | 5 | +| `concurrencyTimeout` | 30s | +| `defaultSubTurnTimeout` | 5m | +| `maxEphemeralHistorySize` | 50 条消息 | +| `pendingResults` 缓冲区 | 16 | +| `MaxContextRunes` 默认值 | 模型上下文窗口的 75% |