decisions.md 40 KB

设计决策

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


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. 动态更新:每次执行后可能更新统计信息,数据库更适合

实现

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 的平均成功率")

实现接口保持一致

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,避免重复传输 稍微复杂

决策

选择:双层记忆管理

设计

@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 搜索技巧),是否需要域名过滤?

决策

选择:支持域名过滤(可选)

设计

@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,但不想在对话历史中显示明文,如何处理?

决策

选择:占位符替换机制

设计

# 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. 工具使用统计

问题

是否需要记录工具调用统计(调用次数、成功率、执行时间)?

决策

选择:内建统计支持

设计

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 生成的工具参数是否允许用户编辑?

决策

选择:支持可选的参数编辑

设计

@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


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 Skillagent/skills/core.md,自动注入到 System Prompt
  • 普通 Skillagent/skills/{name}/,通过 skill 工具加载到对话消息

理由

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

实现

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

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

日期: 2026-02-03

问题

在 execution trace v2.0 开发中引入了 ErrorCodeStepError 和 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_idpaired_action_idspan_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_previewblob_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.pypresets.py

命名调整

  • Trace.contextTrace.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/
  • goaltask 工具放 trace/(Agent 内部控制)

理由

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

14. 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_startedsub_trace_completed 事件
  • Goal 的 sub_trace_ids 字段记录关联关系

理由

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

15. Goal 按需自动创建

日期: 2026-02-10

问题

Agent(含 sub-agent)有时不创建 goal 就直接执行工具调用,导致 message 的 goal_id 为 null。这造成:

  1. 统计信息丢失(_update_goal_stats 跳过 null goal_id)
  2. 可视化缺失结构(前端降级为合成 "START" 节点)
  3. LLM 缺少 context 锚点(goals 为空时不注入计划)

方案对比

方案 优点 缺点
预创建 root goal 保证非 null 复杂任务多一层无意义嵌套,需要溶解逻辑
全面接受 null 无改动 丢失统计、可视化、context 锚点
按需自动创建 仅在需要时兜底,不干扰正常规划 首轮含 goal() 调用时该轮消息仍为 null(可接受)

决策

选择:按需自动创建 + prompt 引导

触发条件(三个 AND):

  1. goal_tree.goals 为空(尚无任何目标)
  2. LLM 返回了 tool_calls(正在执行操作)
  3. tool_calls 中不包含 goal() 调用(LLM 未自行创建目标)

触发时机:LLM 返回后、记录消息前(runner.py agent loop 中)

行为:从 goal_tree.mission 截取前 200 字符作为 root goal description,创建并 focus。

Prompt 配合core.md 引导 LLM "先明确目标再行动",但不强制。

实现agent/core/runner.py:AgentRunner.run

理由

  1. 不干扰 LLM 自主规划:LLM 创建目标时,树结构完全由 LLM 控制,无多余嵌套
  2. 兜底覆盖遗漏:LLM 跳过目标直接行动时,系统自动补位
  3. 实现简单:无需 is_auto_root 标记、溶解逻辑或 display ID 特殊处理
  4. 可接受的 gap:首轮含 goal() 调用时该轮消息 goal_id 为 null,仅影响一轮,属于规划阶段的过渡消息

16. Runner 重新设计:参数分层与统一执行入口

日期: 2026-02-11

问题

AgentRunner.run() 存在以下问题:

  1. 签名臃肿(13 个参数),运行参数与任务内容混在一起
  2. task 字符串同时充当 user message、GoalTree mission、trace.task
  3. 新建/续跑逻辑通过 if trace_id: 分支耦合在一起
  4. subagent.py 越权管理 Trace 生命周期(创建 Trace、GoalTree 等本该由 Runner 处理的事务)
  5. 不支持回溯重跑(rewind)
  6. Agent 间通信的消息格式不统一

决策

选择:参数分层 + 统一 run(messages, config) 入口

参数分三层

  • Infrastructure(AgentRunner 构造时):trace_store, llm_call 等基础设施
  • RunConfig(每次 run 时):model, temperature, trace_id, insert_after 等运行参数
  • Messages(OpenAI SDK 格式):List[Dict] 任务消息,支持多模态

三种模式通过 RunConfig 区分

  • 新建:trace_id=None
  • 续跑:trace_id=已有ID, insert_after=None
  • 回溯:trace_id=已有ID, insert_after=N

回溯机制:Message 新增 status 字段(active/abandoned),插入点之后的消息标记为 abandoned,goals 按规则 abandon/保留。

任务命名:RunConfig.name 可选指定,未指定时由 utility_llm(小模型)自动生成标题。

活跃协作者注入:GoalTree 和 collaborators 信息每 10 轮注入一次(非每轮),减少 context 开销。

Subagent 简化:subagent 工具仍负责创建 Sub-Trace 和 GoalTree(需要自定义元数据和命名规则),然后将 trace_id 传给 RunConfig,由 Runner 接管执行。工具同时维护 trace.context["collaborators"] 列表。

理由

  1. OpenAI 格式统一:Agent 间传递消息用标准格式,兼容各种 LLM API
  2. 职责清晰:Runner 管 Trace 生命周期,工具只管业务逻辑
  3. 可组合:新建/续跑/回溯共享同一个执行流水线,差异仅在 Phase 1
  4. 回溯能力:支持从任意断点插入消息重新运行,原始数据保留(标记而非删除)

实现agent/core/runner.py, agent/trace/models.py, agent/tools/builtin/subagent.py


17. Active Collaborators:活跃协作者机制

日期: 2026-02-11

问题

模型需要知道当前任务中有哪些可以交互的实体(子 Agent、正在对接的人类),但不应该把所有持久联系人都注入到 context。

决策

选择:按任务关系分类,活跃协作者随 GoalTree 注入

按"与当前任务的关系"(而非 human/agent)分两类:

  • 持久存在:通过工具按需查询(如 feishu_get_contact_list
  • 任务内活跃:存 trace.context["collaborators"],周期性注入到 LLM 上下文

各工具(subagent、feishu 等)负责维护 collaborators 列表,Runner 只负责读取和注入。

理由

  1. 维度正确:人和 Agent 都可能是持久或任务内活跃的,不应按类型一刀切
  2. 开销可控:只注入活跃协作者(通常 2-5 个),不浪费 context
  3. 可扩展:未来新增通信渠道只需在对应工具中更新 collaborators 即可

实现agent/core/runner.py:AgentRunner._build_context_injection


18. 统一 Message 类型 + 重构 Agent/Evaluate 工具

日期: 2026-02-12

问题

subagent 工具存在几个问题:

  1. 概念冗余:单一工具通过 mode 参数区分三种行为(explore/delegate/evaluate),参数组合复杂,模型容易用错
  2. evaluate 的 criteria 参数多余:模型既要在 evaluation_input 里放结果,又要在 criteria 里放标准,信息分散
  3. 缺少消息线格式类型:工具参数和 runner 接口使用裸 Dict/List[Dict]/Any,无语义类型
  4. SchemaGenerator 不支持 Literal/Union:无法为新工具签名生成正确的 JSON Schema

决策

18a. 拆分 subagentagent + evaluate 两个独立工具

  • agent(task, messages, continue_from) — 创建 Agent 执行任务
    • task: str → 单任务(delegate),全量工具(排除 agent/evaluate)
    • task: List[str] → 多任务并行(explore),只读工具
    • 通过 isinstance(task, str) 判断,无需 mode 参数
  • evaluate(messages, target_goal_id, continue_from) — 评估目标执行结果
    • 代码自动从 GoalTree 注入目标描述,无 criteria 参数
    • 模型把所有上下文放在 messages

内部统一为 _run_agents() 函数,single = len(tasks)==1 区分 delegate/explore 行为。

18b. 增加消息线格式类型别名(agent/trace/models.py

ChatMessage = Dict[str, Any]                          # 单条 OpenAI 格式消息
Messages = List[ChatMessage]                          # 消息列表
MessageContent = Union[str, List[Dict[str, str]]]     # content 字段(文本或多模态)

放在 models.py 而非新文件——与存储层 Message dataclass 描述同一概念的不同层次。

18c. SchemaGenerator 支持 Literal/Union

_type_to_schema() 新增:

  • Literal["a", "b"]{"type": "string", "enum": ["a", "b"]}
  • Union[str, List[str]]{"oneOf": [...]}
  • Any{}(无约束)

理由

  1. 最少概念:两个单职责工具比一个多 mode 工具更易理解和使用
  2. 最少参数:evaluate 无需 criteria(GoalTree 已有目标描述),agent 的 messages 支持 1D/2D 避免额外参数
  3. 模型/代码职责分离:模型只管给 messages,代码自动注入 goal 上下文
  4. 类型安全Union[str, List[str]] 在 Schema 中生成 oneOf,LLM 能正确理解参数格式

变更范围

  • agent/trace/models.py — 类型别名
  • agent/tools/schema.pyLiteral/Union 支持
  • agent/tools/builtin/subagent.pyagent + evaluate 工具,_run_agents() 统一函数
  • agent/tools/builtin/__init__.py, agent/core/runner.py — 注册表更新
  • agent/tools/builtin/feishu/chat.py, agent/tools/builtin/browser/baseClass.py — 类型注解修正
  • agent/__init__.py — 导出新类型

实现agent/tools/builtin/subagent.py, agent/trace/models.py, agent/tools/schema.py


19. 前端控制 API:统一 run + stop + reflect

日期: 2026-02-12

问题

需要从前端控制 Agent 的创建、启动(含从任意位置重放)、插入用户消息、打断运行。原有 API 将 continuerewind 拆分为两个独立端点,但它们本质上是同一操作(在某个位置运行),仅 insert_after 是否为 null 的区别。此外,缺少停止和反思机制。

决策

19a. 合并 continue + rewind → 统一 run 端点

POST /api/traces/{id}/run
{
  "messages": [...],
  "insert_after": null | int
}
  • insert_after: null → 从末尾续跑(原 continue)
  • insert_after: N → 回溯到 sequence N 后运行(原 rewind)
  • messages: [] + insert_after: N → 重新生成(从 N 处重跑,不插入新消息)

删除 POST /{id}/continuePOST /{id}/rewind 两个端点。

19b. 新增 stop 端点 + Runner 取消机制

POST /api/traces/{id}/stop

Runner 内部维护 _cancel_events: Dict[str, asyncio.Event],agent loop 在每次 LLM 调用前检查。stop() 方法设置事件,loop 退出,Trace 状态置为 stopped

Trace.status 新增 "stopped" 值。

19c. 新增 reflect 端点 — 追加反思 prompt 运行

POST /api/traces/{id}/reflect
{
  "focus": "optional, 反思重点"
}

在 trace 末尾追加一条内置反思 prompt 的 user message,以续跑方式运行 agent。Agent 回顾整个执行过程后生成经验总结,结果自动追加到 ./.cache/experiences.md

不单独调用 LLM、不解析结构化数据——反思就是一次普通的 agent 运行,只是 user message 是预置的反思 prompt。

19d. 经验存储简化为文件

经验存储从 MemoryStore(内存/数据库)简化为 ./.cache/experiences.md 文件:

  • 人类可读可编辑(Markdown)
  • 可版本控制(git)
  • 新建 Trace 时由 Runner 读取并注入到第一条 user message 末尾
  • GET /api/experiences 直接读取文件内容返回

最终 API 设计

控制类(3 个端点,替代原来的 3 个):
  POST /api/traces              → 创建并运行(不变)
  POST /api/traces/{id}/run     → 运行(合并 continue + rewind)
  POST /api/traces/{id}/stop    → 停止(新增)

学习类(2 个端点,全新):
  POST /api/traces/{id}/reflect → 追加反思 prompt 运行,结果追加到 experiences 文件
  GET  /api/experiences         → 读取经验文件内容

理由

  1. API 更少continuerewind 合并后端点总数不增反减(3 → 3 控制 + 2 学习)
  2. 概念统一run 就是"在某个位置运行",insert_after 自然区分续跑和回溯,与 RunConfig 设计一致
  3. 前端简化sendMessage() 直接透传 branchPoint 作为 insert_after,无需判断调哪个 API
  4. 停止机制:asyncio.Event 轻量可靠,每次 LLM 调用前检查,不会在工具执行中途被打断
  5. 反思闭环:Run → Observe → Intervene → Reflect → Run,形成完整的学习循环
  6. 经验存储极简:一个 Markdown 文件,不需要数据库,人类可读可编辑可版本控制

变更范围

  • agent/trace/models.py — Trace.status 增加 "stopped"
  • agent/core/runner.py_cancel_events 字典,stop() 方法,agent loop 检查取消;experiences_path 参数,_load_experiences() 方法,新建时注入经验到 user message
  • agent/trace/run_api.py — 合并 continue/rewindrun,新增 stop/reflect 端点,GET /api/experiences 读取文件
  • api_server.py — 注入 experiences_router

实现agent/trace/run_api.py, agent/core/runner.py, agent/trace/models.py


20. Message Tree:用 parent_sequence 构建消息树

日期: 2026-02-13

问题

原有的消息管理使用线性列表 + status=abandoned 标记,导致:

  1. 压缩需要独立的 compression events + skip list 来标记跳过哪些消息
  2. 反思消息掺入主对话列表,需要额外过滤
  3. Rewind 需要标记 abandoned + 维护 GoalTree 快照
  4. build_llm_messages 逻辑复杂(过滤 abandoned + 应用 skip + 排除反思)

决策

选择:Message 新增 parent_sequence 字段,消息形成树结构

核心规则:build_llm_messages = 从 head 沿 parent_sequence 链回溯到 root

压缩:summary 的 parent_sequence 指向压缩范围起点的前一条消息,旧消息自然脱离主路径。

压缩前主路径:1 → 2 → 3 → ... → 41 → 42 → ...
压缩后:
  1 → 2 → 3 → ... → 41 (旧路径,脱离主路径)
       ↓
  2 → 45(summary, parent=2) → 46 → ...  (新主路径)

反思:反思消息从当前消息分出侧枝,不汇入主路径,天然隔离。

Rewind:新消息的 parent_sequence 指向 rewind 点,旧路径自动变成死胡同。

Rewind 到 seq 20:
  主路径原本:1 → ... → 20 → 21 → ... → 50
  Rewind 后:20 → 51(新, parent=20) → 52 → ...
  新主路径:1 → ... → 20 → 51 → 52 → ...
  旧消息 21-50 脱离主路径,无需标记 abandoned

build_llm_messages

def build_llm_messages(head_sequence, messages_by_seq):
    path = []
    seq = head_sequence
    while seq is not None:
        msg = messages_by_seq[seq]
        path.append(msg)
        seq = msg.parent_sequence
    path.reverse()
    return [m.to_llm_dict() for m in path]

不再需要的机制

  • Message.status (abandoned) → 树结构替代
  • Message.abandoned_at → 树结构替代
  • compression events in events.jsonl → summary.parent_sequence 替代
  • abandon_messages_after() → 新消息设 parent_sequence 即可
  • skip list / 过滤逻辑 → parent chain 遍历替代

变更范围

  • agent/trace/models.py — Message 新增 parent_sequencestatus/abandoned_at 保留但标记弃用
  • agent/trace/store.py — 新增 get_main_path_messages(),Trace 追踪 head_sequence
  • agent/trace/protocols.py — 新增 get_main_path_messages() 接口
  • agent/core/runner.py — agent loop 中设置 parent_sequence,rewind 使用新模型

实现agent/trace/models.py, agent/trace/store.py, agent/core/runner.py


21. GoalTree Rewind:快照 + 重建

日期: 2026-02-13

问题

Message Tree 解决了消息层面的分支问题,但 GoalTree 是独立的状态,不适合从消息树派生(压缩会使目标创建消息脱离主路径,但目标应该保留)。

决策

选择:GoalTree 保持独立管理,rewind 时快照 + 重建

Rewind 流程

  1. 把当前完整 GoalTree 快照存入 events.jsonl(rewind 事件的 goal_tree_snapshot 字段)
  2. 重建干净的 GoalTree:保留 rewind 点之前已 completed 的 goals,丢弃其余
  3. 清空 current_id,让 Agent 重新选择焦点

快照用途:仅用于非运行态下查看历史版本,运行时和前端展示只使用当前干净的 goal.json。

Agent 自主废弃:Agent 调用 goal(abandon=...) 时,abandoned goals 正常保留在 GoalTree 中,前端逐一收到事件,可以展示废弃的分支。

用户 Rewind:不展示废弃的分支。GoalTree 被清理为只包含存活 goals,用户可通过"历史版本"页面查看快照。

理由

  1. GoalTree 和 Messages 的生命周期不同——压缩可以移除消息但不能移除目标
  2. 快照 + 重建逻辑简单可靠,不需要 event sourcing
  3. 干净的 goal.json 让运行时和前端展示始终一致

变更范围

  • agent/core/runner.py:_rewind() — 快照旧树到事件,重建干净树
  • agent/trace/store.py — rewind 事件增加 goal_tree_snapshot

实现agent/core/runner.py


22. Context 压缩:GoalTree 双视图 + 两级压缩

日期: 2026-02-13

问题

长时间运行的 Agent 会累积大量 messages,超出 LLM 上下文窗口。需要在保留关键信息的前提下压缩历史。

决策

选择:Level 1 确定性过滤 + Level 2 LLM 总结,压缩不修改存储

22a. GoalTree 双视图

to_prompt() 支持两种模式:

  • include_summary=False(默认):精简视图,用于日常周期性注入
  • include_summary=True:含所有 completed goals 的 summary,用于压缩时提供上下文

压缩视图追加到第一条 user message 末尾(构建 llm_messages 时的内存操作,不修改存储)。

22b. Level 1:GoalTree 过滤(确定性,零成本)

每轮 agent loop 构建 llm_messages 时:

  • 始终保留:system prompt、第一条 user message、focus goal 的消息
  • 跳过 completed/abandoned goals 的消息(信息已在 GoalTree summary 中)
  • 通过 Message Tree 的 parent_sequence 实现(压缩 summary 的 parent 跳过被压缩的消息)

大多数情况下 Level 1 足够。

22c. Level 2:LLM 总结(仅在 Level 1 后仍超限时触发)

触发条件:Level 1 之后 token 数仍超过阈值(默认 max_tokens × 0.8)。

做法:在当前消息列表末尾追加压缩 prompt → 主模型回复 → summary 作为新消息存入 messages/,其 parent_sequence 跳过被压缩的范围。

不使用 utility_llm,就用主模型。压缩和反思都是"在消息列表末尾追加 prompt,主模型回复"。

22d. 压缩前经验提取

触发 Level 2 压缩之前,先在消息列表末尾追加反思 prompt → 主模型回复 → 结果追加到 ./.cache/experiences.md。反思消息为侧枝(parent_sequence 分叉,不在主路径上)。

22e. 压缩不修改存储

  • messages/ 始终保留原始消息
  • 压缩结果(summary)作为新消息存入 messages/
  • 通过 parent_sequence 树结构实现"跳过",不需要 compression events 或 skip list
  • Rewind 到压缩区域内时,原始消息自动恢复到主路径(summary 脱离新主路径)

22f. 多次压缩的恢复

每次压缩的 summary 消息通过 parent_sequence 跳过被压缩的范围。Rewind 时,如果 rewind 点在某次压缩之后,该压缩的 summary 仍在主路径上,压缩保持生效;如果 rewind 点在压缩之前,summary 脱离新主路径,原始消息自动恢复。无需特殊恢复逻辑。

变更范围

  • agent/trace/goal_models.pyto_prompt(include_summary) 双视图
  • agent/trace/compaction.py — 压缩触发逻辑、Level 1/Level 2 实现
  • agent/core/runner.py — agent loop 中集成压缩

实现agent/trace/compaction.py, agent/trace/goal_models.py, agent/core/runner.py


Decision 23: 控制 API 适配消息树

背景:Decision 20 引入 parent_sequence 消息树后,控制 API(run_api.py)和查询 API(api.py)的接口语义需要同步更新。原有的 insert_afterinclude_abandoned 等概念不再匹配消息树模型。

23a. insert_afterafter_sequence(统一续跑/回溯/分支)

问题:原 insert_after 创建了"续跑"和"回溯"的二分概念,但在消息树模型中,二者本质相同——都是指定新消息的 parent_sequence

决策TraceRunRequest.insert_after 重命名为 after_sequenceRunConfig.insert_after 同步重命名为 after_sequence。Runner 根据 after_sequence 与当前 head_sequence 的关系自动判断行为:

情况 判断条件 行为
续跑 after_sequence 为 None 或 == head_sequence 从末尾追加
回溯 after_sequence 在当前主路径上且 < head_sequence 截断后追加,GoalTree 快照+重建
分支切换 after_sequence 不在当前主路径上 切换到该分支追加(预留,暂不实现)

前端只需传 after_sequence(从哪条消息后面接着跑)和 messages(可为空),不需要理解内部模式。

23b. _rewind 完成目标检测修正

问题_rewind() 中使用 [m for m in all_messages if m.sequence <= cutoff] 过滤消息来检测 completed goals,但 all_messages 包含所有分支的消息,可能把其他分支的消息误判为回溯点之前的消息。

决策:改用 get_main_path_messages(trace_id, cutoff_sequence) 从 cutoff 沿 parent_sequence 链回溯,只获取实际主路径上的消息来判断哪些 goals 有消息。

23c. Reflect 隔离

问题:当前 reflect 以续跑方式调用 run_result(),会进入完整 agent loop(含工具调用、GoalTree 操作),可能产生副作用。且 head_sequence 恢复不在 try/finally 中,异常时会丢失。

决策

  • reflect 使用 RunConfig(max_iterations=1, tools=[]) 限制为单轮无工具 LLM 调用
  • head_sequence 恢复放入 try/finally
  • reflect 消息仍为侧枝(parent_sequence 分叉,不在主路径上)

23d. Messages 查询 API 适配消息树

问题GET /api/traces/{id}/messagesinclude_abandoned 参数基于旧的 abandoned 标记概念,不再适用于消息树。

决策:替换为两个参数:

  • mode: main_path(默认)| all — 返回当前主路径消息或全部消息
  • head: 可选 sequence 值 — 指定从哪个 head 构建主路径(默认用 trace.head_sequence)

mode=main_path 调用 get_main_path_messages()mode=all 调用 get_trace_messages()

23e. Rewind 事件增加 head_sequence

Rewind 事件 payload 中增加 head_sequence 字段,便于前端感知分支切换后的新 head 位置。

变更范围

  • agent/trace/run_api.pyTraceRunRequest.after_sequence、reflect 隔离
  • agent/core/runner.pyRunConfig.after_sequence_prepare_existing_trace_rewind 修正
  • agent/trace/api.py — messages 查询参数 mode/head

实现agent/trace/run_api.py, agent/core/runner.py, agent/trace/api.py