记录系统设计中的关键决策、权衡和理由。
Skills 包含大量能力描述,如何提供给 Agent?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 预先注入到 system prompt | 实现简单 | 浪费 token,Agent 无法选择需要的 skill |
| 作为工具动态加载 | 按需加载,Agent 自主选择 | 需要实现 skill 工具 |
选择:作为工具动态加载
理由:
参考:
Skills 如何存储?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 文件系统(Markdown) | 易于编辑,支持版本控制,零依赖 | 搜索能力弱 |
| 数据库 | 搜索强大,支持元数据 | 编辑困难,需要额外服务 |
选择:文件系统(Markdown)
理由:
实现:
~/.reson/skills/ # 全局 skills
└── error-handling/SKILL.md
./project/.reson/skills/ # 项目级 skills
└── api-integration/SKILL.md
Experiences 如何存储?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 文件系统 | 简单,零依赖 | 搜索慢,不支持向量检索 |
| 数据库(PostgreSQL + pgvector) | 向量检索,统计分析,高性能 | 需要数据库服务 |
选择:数据库(PostgreSQL + pgvector)
理由:
实现:
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);
是否需要事件总线(EventBus)来通知任务状态变化?
选择:不需要事件系统
理由:
替代方案:
Trace 和 Step 如何存储?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 文件系统(JSON) | 简单,易于调试,可直接查看 | 搜索和分析能力弱 |
| 数据库 | 搜索强大,支持复杂查询 | 初期复杂,调试困难 |
选择:文件系统(JSON)用于 MVP,后期可选数据库
理由(MVP阶段):
后期迁移到数据库的时机:
实现接口保持一致:
class TraceStore(Protocol):
async def save(self, trace: Trace) -> None: ...
async def get(self, trace_id: str) -> Trace: ...
# ...
通过 Protocol 定义,可以无缝切换实现。
工具返回的数据可能很大(如 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
理由:
参考:Browser-Use 的 ActionResult.extracted_content 和 long_term_memory
某些工具只在特定网站可用(如 Google 搜索技巧),是否需要域名过滤?
选择:支持域名过滤(可选)
设计:
@tool(url_patterns=["*.google.com", "www.google.*"])
async def google_advanced_search(...):
"""仅在 Google 页面可用的工具"""
...
理由:
url_patterns=None,所有页面可用实现:
tools/url_matcher.py)registry.get_schemas_for_url())浏览器自动化需要输入密码、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
}
}
理由:
实现:
tools/sensitive.py)参考:Browser-Use 的 _replace_sensitive_data
是否需要记录工具调用统计(调用次数、成功率、执行时间)?
选择:内建统计支持
设计:
class ToolStats:
call_count: int
success_count: int
failure_count: int
total_duration: float
last_called: Optional[float]
理由:
用途:
LLM 生成的工具参数是否允许用户编辑?
选择:支持可选的参数编辑
设计:
@tool(editable_params=["query", "filters"])
async def advanced_search(
query: str,
filters: Optional[Dict] = None,
uid: str = ""
) -> ToolResult:
"""高级搜索(用户可编辑 query 和 filters)"""
...
理由:
editable_params=[])适用场景:
日期: 2026-02-04
自主长程 Agent(非交互式工具)如何有效管理 Context?
选择:基于 OpenCode 方案,增强计划管理和回溯能力
核心设计:
工具:
goal:线性计划管理(add, done, abandon, focus)explore:并行探索-合并(系统管理分支 msg list 和结果汇总)回溯机制:
详细设计:见 docs/context-management.md
日期: 2026-02-04(更新)
选择:独立的 Goal Tree + 线性 Message List
理由:
日期: 2026-02-04(更新)
选择:Goal 完成或放弃时生成 summary
goal(done="summary") - 正常完成goal(abandon="原因") - 放弃(包含失败原因,避免重蹈覆辙)日期: 2026-02-04(更新)
选择:基于 Goal 状态的增量压缩
日期: 2026-02-04
选择:explore 工具,基于 Sub-Trace 机制
设计:
background:LLM 概括的背景(可选,为空则继承全部历史)branches:具体探索方向列表(每个方向创建独立的 Sub-Trace)执行:
Step 工具等核心功能如何让 Agent 知道?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 写在 System Prompt | 始终可见 | 每次消耗 token,内容膨胀 |
| 作为普通 Skill | 按需加载 | 模型不知道存在就不会加载 |
| 分层:Core + 普通 | 核心功能始终可见,其他按需 | 需要区分两类 |
选择:Skill 分层
设计:
agent/skills/core.md,自动注入到 System Promptagent/skills/{name}/,通过 skill 工具加载到对话消息理由:
实现:
build_system_prompt() 时自动读取并拼接skill 工具时返回内容到对话消息日期: 2026-02-03
在 execution trace v2.0 开发中引入了 ErrorCode、StepError 和 feedback 机制,但代码审查发现这些功能完全未被使用。
选择:删除未使用的代码
理由:
影响:
日期: 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 - 完全未使用关联需求:
保留:
parent_id - 唯一的树结构关联字段删除:
tool_call_id - messages 中已包含,Step 不需要重复paired_action_id - 与 parent_id 冗余span_id - 分布式追踪功能,当前用不到日期: 2026-02-03
execution trace v2.0 引入了 Blob 存储系统用于处理大输出和图片,但实际分析后发现该系统过度设计且与 Agent 现有架构冗余。
Agent 的文件处理方式:
Blob 系统的问题:
删除内容:
agent/execution/blob_store.py 整个文件BlobStore 协议及所有实现extract_images_from_messages() 方法restore_images_in_messages() 方法store_large_output() 方法output_preview、blob_ref保留方案:
data 字段直接存储 messages(包含 base64)YAGNI 原则:
架构更简洁:
符合 Agent 场景:
数据完整性:
日期: 2026-02-08
原有架构存在以下问题:
execution/ 和 goal/ 目录职责边界模糊Trace.context 命名与 ToolContext 等概念混淆subagents/ 目录与 Agent 预设概念重复目录重组:
execution/ + goal/ → trace/(统一执行追踪和计划管理)subagents/ 目录,逻辑整合到 trace/task_tool.pycore/config.py,合并到 runner.py 和 presets.py命名调整:
Trace.context → Trace.meta(TraceMeta 数据类)统一 Agent 模型:
task 工具创建的子 Traceask_human 工具创建的阻塞式 TraceAgent 预设替代 Sub-Agent 配置:
core/presets.py 定义 Agent 类型(default, explore, analyst 等).agent/presets.json 可覆盖工具目录简化:
file/ 和 browser/ 做子目录分类builtin/goal 和 task 工具放 trace/(Agent 内部控制)统一工具,三种模式:
subagent 支持三种模式:explore(并行探索)、delegate(委托执行)、evaluate(结果评估)agent/tools/builtin/subagent.pyExplore 模式的并行执行:
asyncio.gather() 实现真并行read_file, grep_content, glob_files, goal)权限隔离:
subagent 外的所有工具subagent,防止无限递归Sub-Trace 信息存储:
meta.json 中events.jsonl 记录 sub_trace_started 和 sub_trace_completed 事件sub_trace_ids 字段记录关联关系asyncio.gather() 充分利用 I/O 等待时间,提升效率日期: 2026-02-10
Agent(含 sub-agent)有时不创建 goal 就直接执行工具调用,导致 message 的 goal_id 为 null。这造成:
_update_goal_stats 跳过 null goal_id)| 方案 | 优点 | 缺点 |
|---|---|---|
| 预创建 root goal | 保证非 null | 复杂任务多一层无意义嵌套,需要溶解逻辑 |
| 全面接受 null | 无改动 | 丢失统计、可视化、context 锚点 |
| 按需自动创建 | 仅在需要时兜底,不干扰正常规划 | 首轮含 goal() 调用时该轮消息仍为 null(可接受) |
选择:按需自动创建 + prompt 引导
触发条件(三个 AND):
goal_tree.goals 为空(尚无任何目标)tool_calls(正在执行操作)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
is_auto_root 标记、溶解逻辑或 display ID 特殊处理goal() 调用时该轮消息 goal_id 为 null,仅影响一轮,属于规划阶段的过渡消息日期: 2026-02-11
原 AgentRunner.run() 存在以下问题:
task 字符串同时充当 user message、GoalTree mission、trace.taskif trace_id: 分支耦合在一起subagent.py 越权管理 Trace 生命周期(创建 Trace、GoalTree 等本该由 Runner 处理的事务)选择:参数分层 + 统一 run(messages, config) 入口
参数分三层:
List[Dict] 任务消息,支持多模态三种模式通过 RunConfig 区分:
trace_id=Nonetrace_id=已有ID, insert_after=Nonetrace_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"] 列表。
实现:agent/core/runner.py, agent/trace/models.py, agent/tools/builtin/subagent.py
日期: 2026-02-11
模型需要知道当前任务中有哪些可以交互的实体(子 Agent、正在对接的人类),但不应该把所有持久联系人都注入到 context。
选择:按任务关系分类,活跃协作者随 GoalTree 注入
按"与当前任务的关系"(而非 human/agent)分两类:
feishu_get_contact_list)trace.context["collaborators"],周期性注入到 LLM 上下文各工具(subagent、feishu 等)负责维护 collaborators 列表,Runner 只负责读取和注入。
实现:agent/core/runner.py:AgentRunner._build_context_injection
日期: 2026-02-12
原 subagent 工具存在几个问题:
mode 参数区分三种行为(explore/delegate/evaluate),参数组合复杂,模型容易用错evaluation_input 里放结果,又要在 criteria 里放标准,信息分散Dict/List[Dict]/Any,无语义类型Literal/Union:无法为新工具签名生成正确的 JSON Schemasubagent → agent + 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) — 评估目标执行结果
criteria 参数messages 中内部统一为 _run_agents() 函数,single = len(tasks)==1 区分 delegate/explore 行为。
agent/trace/models.py)ChatMessage = Dict[str, Any] # 单条 OpenAI 格式消息
Messages = List[ChatMessage] # 消息列表
MessageContent = Union[str, List[Dict[str, str]]] # content 字段(文本或多模态)
放在 models.py 而非新文件——与存储层 Message dataclass 描述同一概念的不同层次。
Literal/Union_type_to_schema() 新增:
Literal["a", "b"] → {"type": "string", "enum": ["a", "b"]}Union[str, List[str]] → {"oneOf": [...]}Any → {}(无约束)Union[str, List[str]] 在 Schema 中生成 oneOf,LLM 能正确理解参数格式agent/trace/models.py — 类型别名agent/tools/schema.py — Literal/Union 支持agent/tools/builtin/subagent.py — agent + 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
日期: 2026-02-12
需要从前端控制 Agent 的创建、启动(含从任意位置重放)、插入用户消息、打断运行。原有 API 将 continue 和 rewind 拆分为两个独立端点,但它们本质上是同一操作(在某个位置运行),仅 insert_after 是否为 null 的区别。此外,缺少停止和反思机制。
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}/continue 和 POST /{id}/rewind 两个端点。
stop 端点 + Runner 取消机制POST /api/traces/{id}/stop
Runner 内部维护 _cancel_events: Dict[str, asyncio.Event],agent loop 在每次 LLM 调用前检查。stop() 方法设置事件,loop 退出,Trace 状态置为 stopped。
Trace.status 新增 "stopped" 值。
reflect 端点 — 追加反思 prompt 运行POST /api/traces/{id}/reflect
{
"focus": "optional, 反思重点"
}
在 trace 末尾追加一条内置反思 prompt 的 user message,以续跑方式运行 agent。Agent 回顾整个执行过程后生成经验总结,结果自动追加到 ./cache/experiences.md。
不单独调用 LLM、不解析结构化数据——反思就是一次普通的 agent 运行,只是 user message 是预置的反思 prompt。
经验存储从 MemoryStore(内存/数据库)简化为 ./cache/experiences.md 文件:
GET /api/experiences 直接读取文件内容返回控制类(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 → 读取经验文件内容
continue 和 rewind 合并后端点总数不增反减(3 → 3 控制 + 2 学习)run 就是"在某个位置运行",insert_after 自然区分续跑和回溯,与 RunConfig 设计一致sendMessage() 直接透传 branchPoint 作为 insert_after,无需判断调哪个 APIagent/trace/models.py — Trace.status 增加 "stopped"agent/core/runner.py — _cancel_events 字典,stop() 方法,agent loop 检查取消;experiences_path 参数,_load_experiences() 方法,新建时注入经验到 user messageagent/trace/run_api.py — 合并 continue/rewind 为 run,新增 stop/reflect 端点,GET /api/experiences 读取文件api_server.py — 注入 experiences_router实现:agent/trace/run_api.py, agent/core/runner.py, agent/trace/models.py
日期: 2026-02-13
原有的消息管理使用线性列表 + status=abandoned 标记,导致:
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]
agent/trace/models.py — Message 新增 parent_sequence,status/abandoned_at 保留但标记弃用agent/trace/store.py — 新增 get_main_path_messages(),Trace 追踪 head_sequenceagent/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
日期: 2026-02-13
Message Tree 解决了消息层面的分支问题,但 GoalTree 是独立的状态,不适合从消息树派生(压缩会使目标创建消息脱离主路径,但目标应该保留)。
选择:GoalTree 保持独立管理,rewind 时快照 + 重建
Rewind 流程:
events.jsonl(rewind 事件的 goal_tree_snapshot 字段)current_id,让 Agent 重新选择焦点快照用途:仅用于非运行态下查看历史版本,运行时和前端展示只使用当前干净的 goal.json。
Agent 自主废弃:Agent 调用 goal(abandon=...) 时,abandoned goals 正常保留在 GoalTree 中,前端逐一收到事件,可以展示废弃的分支。
用户 Rewind:不展示废弃的分支。GoalTree 被清理为只包含存活 goals,用户可通过"历史版本"页面查看快照。
agent/core/runner.py:_rewind() — 快照旧树到事件,重建干净树agent/trace/store.py — rewind 事件增加 goal_tree_snapshot实现:agent/core/runner.py
日期: 2026-02-13
长时间运行的 Agent 会累积大量 messages,超出 LLM 上下文窗口。需要在保留关键信息的前提下压缩历史。
选择:Level 1 确定性过滤 + Level 2 LLM 总结,压缩不修改存储
to_prompt() 支持两种模式:
include_summary=False(默认):精简视图,用于日常周期性注入include_summary=True:含所有 completed goals 的 summary,用于压缩时提供上下文压缩视图追加到第一条 user message 末尾(构建 llm_messages 时的内存操作,不修改存储)。
每轮 agent loop 构建 llm_messages 时:
大多数情况下 Level 1 足够。
触发条件:Level 1 之后 token 数仍超过阈值(默认 max_tokens × 0.8)。
做法:在当前消息列表末尾追加压缩 prompt → 主模型回复 → summary 作为新消息存入 messages/,其 parent_sequence 跳过被压缩的范围。
不使用 utility_llm,就用主模型。压缩和反思都是"在消息列表末尾追加 prompt,主模型回复"。
触发 Level 2 压缩之前,先在消息列表末尾追加反思 prompt → 主模型回复 → 结果追加到 ./cache/experiences.md。反思消息为侧枝(parent_sequence 分叉,不在主路径上)。
messages/ 始终保留原始消息每次压缩的 summary 消息通过 parent_sequence 跳过被压缩的范围。Rewind 时,如果 rewind 点在某次压缩之后,该压缩的 summary 仍在主路径上,压缩保持生效;如果 rewind 点在压缩之前,summary 脱离新主路径,原始消息自动恢复。无需特殊恢复逻辑。
agent/trace/goal_models.py — to_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 20 引入 parent_sequence 消息树后,控制 API(run_api.py)和查询 API(api.py)的接口语义需要同步更新。原有的 insert_after、include_abandoned 等概念不再匹配消息树模型。
insert_after → after_sequence(统一续跑/回溯/分支)问题:原 insert_after 创建了"续跑"和"回溯"的二分概念,但在消息树模型中,二者本质相同——都是指定新消息的 parent_sequence。
决策:TraceRunRequest.insert_after 重命名为 after_sequence。RunConfig.insert_after 同步重命名为 after_sequence。Runner 根据 after_sequence 与当前 head_sequence 的关系自动判断行为:
| 情况 | 判断条件 | 行为 |
|---|---|---|
| 续跑 | after_sequence 为 None 或 == head_sequence |
从末尾追加 |
| 回溯 | after_sequence 在当前主路径上且 < head_sequence |
截断后追加,GoalTree 快照+重建 |
| 分支切换 | after_sequence 不在当前主路径上 |
切换到该分支追加(预留,暂不实现) |
前端只需传 after_sequence(从哪条消息后面接着跑)和 messages(可为空),不需要理解内部模式。
_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 有消息。
问题:当前 reflect 以续跑方式调用 run_result(),会进入完整 agent loop(含工具调用、GoalTree 操作),可能产生副作用。且 head_sequence 恢复不在 try/finally 中,异常时会丢失。
决策:
RunConfig(max_iterations=1, tools=[]) 限制为单轮无工具 LLM 调用问题:GET /api/traces/{id}/messages 的 include_abandoned 参数基于旧的 abandoned 标记概念,不再适用于消息树。
决策:替换为两个参数:
mode: main_path(默认)| all — 返回当前主路径消息或全部消息head: 可选 sequence 值 — 指定从哪个 head 构建主路径(默认用 trace.head_sequence)mode=main_path 调用 get_main_path_messages();mode=all 调用 get_trace_messages()。
Rewind 事件 payload 中增加 head_sequence 字段,便于前端感知分支切换后的新 head 位置。
agent/trace/run_api.py — TraceRunRequest.after_sequence、reflect 隔离agent/core/runner.py — RunConfig.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