# 设计决策

> 记录系统设计中的关键决策、权衡和理由。

---

## 1. Skills 通过工具加载 vs 预先注入

### 问题
Skills 包含大量能力描述，如何提供给 Agent？

### 方案对比

| 方案 | 优点 | 缺点 |
|------|------|------|
| **预先注入到 system prompt** | 实现简单 | 浪费 token，Agent 无法选择需要的 skill |
| **作为工具动态加载** | 按需加载，Agent 自主选择 | 需要实现 skill 工具 |

### 决策
**选择：作为工具动态加载**

**理由**：
1. **Token 效率**：只加载需要的 skill，避免浪费 context
2. **Agent 自主性**：LLM 根据任务决定需要哪些 skill
3. **可扩展性**：可以有数百个 skills，不影响单次调用的 token 消耗
4. **业界参考**：OpenCode 和 Claude API 文档都采用此方式

**参考**：
- OpenCode 的 skill 系统
- Claude API 文档中的工具使用模式

---

## 2. Skills 用文件系统 vs 数据库

### 问题
Skills 如何存储？

### 方案对比

| 方案 | 优点 | 缺点 |
|------|------|------|
| **文件系统（Markdown）** | 易于编辑，支持版本控制，零依赖 | 搜索能力弱 |
| **数据库** | 搜索强大，支持元数据 | 编辑困难，需要额外服务 |

### 决策
**选择：文件系统（Markdown）**

**理由**：
1. **易于编辑**：直接用文本编辑器或 IDE 编辑
2. **版本控制**：通过 Git 管理 skill 的历史变更
3. **零依赖**：不需要数据库服务
4. **人类可读**：Markdown 格式，便于人工审查和修改
5. **搜索需求低**：Skill 数量有限（几十到几百个），文件扫描足够快

**实现**：
```
~/.reson/skills/           # 全局 skills
└── error-handling/SKILL.md

./project/.reson/skills/   # 项目级 skills
└── api-integration/SKILL.md
```

---

## 3. Experiences 用数据库 vs 文件

### 问题
Experiences 如何存储？

### 方案对比

| 方案 | 优点 | 缺点 |
|------|------|------|
| **文件系统** | 简单，零依赖 | 搜索慢，不支持向量检索 |
| **数据库（PostgreSQL + pgvector）** | 向量检索，统计分析，高性能 | 需要数据库服务 |

### 决策
**选择：数据库（PostgreSQL + pgvector）**

**理由**：
1. **向量检索必需**：Experiences 需要根据任务语义匹配，文件系统无法支持
2. **统计分析**：需要追踪 success_rate, usage_count 等指标
3. **数量大**：Experiences 会随着使用不断增长（数千到数万条）
4. **动态更新**：每次执行后可能更新统计信息，数据库更适合

**实现**：
```sql
CREATE TABLE experiences (
    exp_id TEXT PRIMARY KEY,
    scope TEXT,
    condition TEXT,
    rule TEXT,
    evidence JSONB,

    confidence FLOAT,
    usage_count INT,
    success_rate FLOAT,

    embedding vector(1536),  -- 向量检索

    created_at TIMESTAMP,
    updated_at TIMESTAMP
);

CREATE INDEX ON experiences USING ivfflat (embedding vector_cosine_ops);
```

---

## 4. 不需要事件系统

### 问题
是否需要事件总线（EventBus）来通知任务状态变化？

### 决策
**选择：不需要事件系统**

**理由**：
1. **后台场景**：Agent 主要在后台运行，不需要实时通知
2. **已有追踪**：Trace/Step 已完整记录所有信息
3. **按需查询**：需要监控时，查询 Trace 即可
4. **简化架构**：避免引入额外的复杂性

**替代方案**：
- 需要告警时，直接在 AgentRunner 中调用通知函数
- 需要实时监控时，轮询 TraceStore

---

## 5. Trace/Step 用文件系统 vs 数据库

### 问题
Trace 和 Step 如何存储？

### 方案对比

| 方案 | 优点 | 缺点 |
|------|------|------|
| **文件系统（JSON）** | 简单，易于调试，可直接查看 | 搜索和分析能力弱 |
| **数据库** | 搜索强大，支持复杂查询 | 初期复杂，调试困难 |

### 决策
**选择：文件系统（JSON）用于 MVP，后期可选数据库**

**理由（MVP阶段）**：
1. **快速迭代**：JSON 文件易于查看和调试
2. **零依赖**：不需要数据库服务
3. **数据量小**：单个项目的 traces 数量有限

**后期迁移到数据库的时机**：
- Traces 数量超过 1 万条
- 需要复杂的查询和分析（如"查找所有失败的 traces"）
- 需要聚合统计（如"Agent 的平均成功率"）

**实现接口保持一致**：
```python
class TraceStore(Protocol):
    async def save(self, trace: Trace) -> None: ...
    async def get(self, trace_id: str) -> Trace: ...
    # ...
```

通过 Protocol 定义，可以无缝切换实现。

---

## 6. 工具系统的双层记忆管理

### 问题
工具返回的数据可能很大（如 Browser-Use 的 extract 返回 10K tokens），如何避免占用过多 context？

### 方案对比

| 方案 | 优点 | 缺点 |
|------|------|------|
| **单一输出** | 简单 | 大数据会持续占用 context |
| **双层记忆**（output + long_term_memory） | 节省 context，避免重复传输 | 稍微复杂 |

### 决策
**选择：双层记忆管理**

**设计**：
```python
@dataclass
class ToolResult:
    title: str
    output: str                           # 临时内容（可能很长）
    long_term_memory: Optional[str]       # 永久记忆（简短摘要）
    include_output_only_once: bool        # output 是否只给 LLM 看一次
```

**效果**：
```
[User] 提取 amazon.com 的商品价格
[Assistant] 调用 extract_page_data(url="amazon.com")
[Tool]
# Extracted page data

<完整的 10K tokens 数据...>

Summary: Extracted 10000 chars from amazon.com

[User] 现在保存到文件
[Assistant] 调用 write_file(content="...")
[Tool] (此时不再包含 10K tokens，只有摘要)
Summary: Extracted 10000 chars from amazon.com
```

**理由**：
1. **Context 效率**：大量数据只传输一次
2. **保留关键信息**：摘要永久保留在对话历史中
3. **Browser-Use 兼容**：直接映射到 Browser-Use 的 ActionResult 设计

**参考**：Browser-Use 的 ActionResult.extracted_content 和 long_term_memory

---

## 7. 工具的域名过滤

### 问题
某些工具只在特定网站可用（如 Google 搜索技巧），是否需要域名过滤？

### 决策
**选择：支持域名过滤（可选）**

**设计**：
```python
@tool(url_patterns=["*.google.com", "www.google.*"])
async def google_advanced_search(...):
    """仅在 Google 页面可用的工具"""
    ...
```

**理由**：
1. **减少 context**：在 Google 页面，35 工具 → 20 工具（节省 40%）
2. **减少 LLM 困惑**：工具数量少了，LLM 更容易选择正确工具
3. **灵活性**：默认 `url_patterns=None`，所有页面可用

**实现**：
- URL 模式匹配引擎（`tools/url_matcher.py`）
- 动态工具过滤（`registry.get_schemas_for_url()`）

---

## 8. 敏感数据处理

### 问题
浏览器自动化需要输入密码、Token，但不想在对话历史中显示明文，如何处理？

### 决策
**选择：占位符替换机制**

**设计**：
```python
# LLM 输出占位符
arguments = {
    "password": "<secret>github_password</secret>",
    "totp": "<secret>github_2fa_bu_2fa_code</secret>"
}

# 执行前自动替换
sensitive_data = {
    "*.github.com": {
        "github_password": "secret123",
        "github_2fa_bu_2fa_code": "JBSWY3DPEHPK3PXP"  # TOTP secret
    }
}
```

**理由**：
1. **保护隐私**：对话历史中只有占位符，不泄露实际密码
2. **域名匹配**：不同网站使用不同密钥，防止密钥泄露
3. **TOTP 支持**：自动生成 2FA 验证码，无需手动输入
4. **Browser-Use 兼容**：直接映射到 Browser-Use 的敏感数据处理

**实现**：
- 递归替换（`tools/sensitive.py`）
- 支持嵌套结构（dict, list, str）
- 自动 TOTP 生成（pyotp）

**参考**：Browser-Use 的 _replace_sensitive_data

---

## 9. 工具使用统计

### 问题
是否需要记录工具调用统计（调用次数、成功率、执行时间）？

### 决策
**选择：内建统计支持**

**设计**：
```python
class ToolStats:
    call_count: int
    success_count: int
    failure_count: int
    total_duration: float
    last_called: Optional[float]
```

**理由**：
1. **监控健康**：识别失败率高的工具
2. **性能优化**：识别执行慢的工具
3. **优化排序**：高频工具排前面，减少 LLM 选择时间
4. **零成本**：自动记录，性能影响 <0.01ms

**用途**：
- 监控工具健康状况（失败率、延迟）
- 优化工具顺序（高频工具排前面）
- 识别问题工具（低成功率、高延迟）

---

## 10. 工具参数的可编辑性

### 问题
LLM 生成的工具参数是否允许用户编辑？

### 决策
**选择：支持可选的参数编辑**

**设计**：
```python
@tool(editable_params=["query", "filters"])
async def advanced_search(
    query: str,
    filters: Optional[Dict] = None,
    uid: str = ""
) -> ToolResult:
    """高级搜索（用户可编辑 query 和 filters）"""
    ...
```

**理由**：
1. **人类监督**：Agent 生成的参数可能不准确，允许人工微调
2. **灵活性**：大多数工具不需要编辑（默认 `editable_params=[]`）
3. **UI 集成**：前端可以展示可编辑的参数供用户修改

**适用场景**：
- 搜索查询
- 内容创建
- 需要人工微调的参数

---

## 11. Context 管理方案选择

**日期**: 2026-02-04

### 问题
自主长程 Agent（非交互式工具）如何有效管理 Context？

### 决策
**选择：基于 OpenCode 方案，增强计划管理和回溯能力**

**核心设计**：
- 简单的工具接口（goal, explore）
- 复杂逻辑由系统处理（分支管理、context 压缩）

**工具**：
- `goal`：线性计划管理（add, done, abandon, focus）
- `explore`：并行探索-合并（系统管理分支 msg list 和结果汇总）

**回溯机制**：
- 未执行的步骤：直接修改 plan
- 已执行的步骤：移除原始信息，替换为简短 Summary

**详细设计**：见 [`docs/context-management.md`](./context-management.md)

---

## 12. 计划管理：独立 Goal Tree vs 统一到 Step

**日期**: 2026-02-04（更新）

### 决策
**选择：独立的 Goal Tree + 线性 Message List**

- **Goal Tree**：结构化的目标/计划（goal.json）
- **Message List**：线性的执行记录
- **关联**：每条 message 标记 goal_id

**理由**：
- 概念清晰：Plan 是"要做什么"，Message 是"怎么做的"
- 压缩精确：基于 goal 完成状态压缩对应的 messages

---

## 13. Summary 生成策略

**日期**: 2026-02-04（更新）

### 决策
**选择：Goal 完成或放弃时生成 summary**

- `goal(done="summary")` - 正常完成
- `goal(abandon="原因")` - 放弃（包含失败原因，避免重蹈覆辙）

---

## 14. Context 压缩策略

**日期**: 2026-02-04（更新）

### 决策
**选择：基于 Goal 状态的增量压缩**

- Message 关联 goal_id
- Goal 完成/放弃时，将详细 messages 替换为 summary message

---

## 15. 并行探索机制

**日期**: 2026-02-04

### 决策
**选择：explore 工具，基于 Sub-Trace 机制**

**设计**：
- `background`：LLM 概括的背景（可选，为空则继承全部历史）
- `branches`：具体探索方向列表（每个方向创建独立的 Sub-Trace）

**执行**：
- 为每个探索方向创建独立的 Sub-Trace（完整的 Trace 结构）
- 并行执行所有 Sub-Traces（使用 asyncio.gather）
- 汇总所有 Sub-Trace 的结果返回

---

## 16. Skill 分层：Core Skill vs 普通 Skill

### 问题
Step 工具等核心功能如何让 Agent 知道？

### 方案对比

| 方案 | 优点 | 缺点 |
|------|------|------|
| **写在 System Prompt** | 始终可见 | 每次消耗 token，内容膨胀 |
| **作为普通 Skill** | 按需加载 | 模型不知道存在就不会加载 |
| **分层：Core + 普通** | 核心功能始终可见，其他按需 | 需要区分两类 |

### 决策
**选择：Skill 分层**

**设计**：
- **Core Skill**：`agent/skills/core.md`，自动注入到 System Prompt
- **普通 Skill**：`agent/skills/{name}/`，通过 `skill` 工具加载到对话消息

**理由**：
1. **核心功能必须可见**：Step 管理等功能，模型需要始终知道
2. **避免 System Prompt 膨胀**：只有核心内容在 System Prompt
3. **普通 Skill 按需加载**：领域知识在需要时才加载，节省 token

**实现**：
- Core Skill：框架在 `build_system_prompt()` 时自动读取并拼接
- 普通 Skill：模型调用 `skill` 工具时返回内容到对话消息

---

## 总结

这些设计决策的核心原则：

1. **灵活性优先**：大多数特性都是可选的，保持系统简洁
2. **Token 效率**：通过动态加载、双层记忆等机制优化 context 使用
3. **可扩展性**：通过 Protocol 定义接口，便于后期切换实现
4. **安全性**：敏感数据占位符、域名匹配等机制保护隐私
5. **可观测性**：内建统计、完整追踪，便于监控和调试

---

## 10. 删除未使用的结构化错误功能

**日期**: 2026-02-03

### 问题
在 execution trace v2.0 开发中引入了 `ErrorCode`、`StepError` 和 feedback 机制，但代码审查发现这些功能完全未被使用。

### 决策
**选择：删除未使用的代码**

**理由**：
1. **YAGNI 原则**：不应该维护未使用的功能（You Aren't Gonna Need It）
2. **减少复杂度**：
   - ErrorCode 枚举难以穷举，且 Python 无法强制约束
   - StepError 与 ToolResult.error 存在设计重复
   - feedback 机制缺少使用场景和配套接口
3. **可恢复性**：需要时可以从 git 历史恢复
4. **向后兼容**：这些功能从未被使用，删除不影响现有代码

**影响**：
- ✅ 代码更简洁
- ✅ 减少维护负担
- ✅ 不影响现有功能
- ⚠️ 未来需要结构化错误时需要重新设计

---

---

## 11. Step 关联统一使用 parent_id

**日期**: 2026-02-03

### 问题
execution trace v2.0 设计中引入了多个跨节点关联字段（`tool_call_id`、`paired_action_id`、`span_id`），但实际分析后发现这些字段存在冗余。

### 现状分析

**字段使用情况**：
- `tool_call_id` - Step 对象中未使用，只在 messages 中使用
- `paired_action_id` - 完全未使用
- `span_id` - 完全未使用

**关联需求**：
1. **Action/Result 配对** - 通过 parent_id 已经建立（result.parent_id = action.step_id）
2. **与 messages 对应** - messages 中包含完整对话历史（含 tool_call_id）
3. **重试追踪** - 同一个 action 下的多个 result，通过 parent_id 即可

### 设计决策

**保留**：
- ✅ `parent_id` - 唯一的树结构关联字段

**删除**：
- ❌ `tool_call_id` - messages 中已包含，Step 不需要重复
- ❌ `paired_action_id` - 与 parent_id 冗余
- ❌ `span_id` - 分布式追踪功能，当前用不到

---

## 12. 删除 Blob 存储系统

**日期**: 2026-02-03

### 问题
execution trace v2.0 引入了 Blob 存储系统用于处理大输出和图片，但实际分析后发现该系统过度设计且与 Agent 现有架构冗余。

### 架构分析

**Agent 的文件处理方式**：
1. **用户输入**：提供文件路径（不是 base64）
2. **工具处理**：内置工具直接读写文件系统
3. **LLM 调用**：Runner 在调用 LLM 时才将文件路径转换为 base64
4. **Trace 存储**：Step 中存储的是 messages（已包含 base64）

**Blob 系统的问题**：
1. **冗余提取**：从 messages 中提取 base64 再存储，而 messages 已经在 Step.data 中
2. **功能重叠**：Agent 内置工具已经提供文件读写能力
3. **过度设计**：引入 content-addressed storage、deduplication 等复杂功能，但 Agent 场景不需要
4. **未被使用**：output_preview/blob_ref 字段从未在实际代码中使用

### 设计决策

**删除内容**：
- ❌ `agent/execution/blob_store.py` 整个文件
- ❌ `BlobStore` 协议及所有实现
- ❌ `extract_images_from_messages()` 方法
- ❌ `restore_images_in_messages()` 方法
- ❌ `store_large_output()` 方法
- ❌ Step 字段：`output_preview`、`blob_ref`

**保留方案**：
- ✅ Step 中的 `data` 字段直接存储 messages（包含 base64）
- ✅ Agent 内置工具处理文件操作
- ✅ 用户通过文件路径引用文件（不是 base64）

### 理由

1. **YAGNI 原则**：
   - 功能从未被使用
   - 未来需要时可以重新设计

2. **架构更简洁**：
   - 不需要额外的 blob 存储层
   - 文件处理统一走工具系统

3. **符合 Agent 场景**：
   - Agent 运行在本地，直接访问文件系统
   - 不需要像云服务那样做 blob 存储和 deduplication

4. **数据完整性**：
   - messages 中的 base64 已经足够
   - 不需要拆分成 preview + ref

---

## 13. 架构重组：统一 Trace 模型

**日期**: 2026-02-08

### 问题

原有架构存在以下问题：
1. `execution/` 和 `goal/` 目录职责边界模糊
2. `Trace.context` 命名与 `ToolContext` 等概念混淆
3. `subagents/` 目录与 Agent 预设概念重复
4. 文档散乱，多个重构相关文档过时

### 决策

**目录重组**：
- `execution/` + `goal/` → `trace/`（统一执行追踪和计划管理）
- 删除 `subagents/` 目录，逻辑整合到 `trace/task_tool.py`
- 删除 `core/config.py`，合并到 `runner.py` 和 `presets.py`

**命名调整**：
- `Trace.context` → `Trace.meta`（TraceMeta 数据类）

**统一 Agent 模型**：
- 所有 Agent（主、子、人类协助）都是 Trace
- 子 Agent = 通过 `task` 工具创建的子 Trace
- 人类协助 = 通过 `ask_human` 工具创建的阻塞式 Trace

**Agent 预设替代 Sub-Agent 配置**：
- `core/presets.py` 定义 Agent 类型（default, explore, analyst 等）
- 项目级 `.agent/presets.json` 可覆盖

**工具目录简化**：
- 仅对 `file/` 和 `browser/` 做子目录分类
- 其他工具直接放 `builtin/`
- `goal` 和 `task` 工具放 `trace/`（Agent 内部控制）

### 理由

1. **概念更清晰**：trace/ 统一管理"执行状态"，tools/ 专注"外部交互"
2. **减少冗余**：删除 subagents/ 目录，用统一的 Trace 机制
3. **命名准确**：meta 比 context 更准确表达"静态元信息"

---

## Subagent 工具设计

### 决策

**统一工具，三种模式**：
- 单一工具 `subagent` 支持三种模式：`explore`（并行探索）、`delegate`（委托执行）、`evaluate`（结果评估）
- 实现位置：`agent/tools/builtin/subagent.py`

**Explore 模式的并行执行**：
- 使用 `asyncio.gather()` 实现真并行
- 每个 branch 创建独立的 Sub-Trace
- 仅允许只读工具（`read_file`, `grep_content`, `glob_files`, `goal`）

**权限隔离**：
- Explore 模式：文件系统只读权限 + goal 工具，防止副作用
- Delegate/Evaluate 模式：除 `subagent` 外的所有工具
- 子 Agent 不能调用 `subagent`，防止无限递归

**Sub-Trace 信息存储**：
- Sub-Trace 的元信息存储在自己的 `meta.json` 中
- 父 Trace 的 `events.jsonl` 记录 `sub_trace_started` 和 `sub_trace_completed` 事件
- Goal 的 `sub_trace_ids` 字段记录关联关系

### 理由

1. **统一接口**：三种模式共享相同的 Sub-Trace 创建和管理逻辑，减少代码重复
2. **真并行**：Explore 模式使用 `asyncio.gather()` 充分利用 I/O 等待时间，提升效率
3. **安全性**：只读权限确保探索不会修改系统状态；禁止递归调用防止资源耗尽
4. **可追溯**：事件记录和 Goal 关联确保完整的执行历史，支持可视化和调试

---

