对比 Cyber Agent(本项目)与 Claude Code v2.1.88(非官方重建,仅供研究)的架构设计异同,目标是找到可借鉴的设计思路,以及两者各自的取舍。
| 维度 | Cyber Agent | Claude Code |
|---|---|---|
| 语言/运行时 | Python / asyncio | TypeScript / Bun + Node.js |
| 定位 | 嵌入式 Agent 框架,供程序集成调用 | 面向开发者的交互式 CLI 工具 |
| LLM 接入 | 多 Provider(OpenRouter / Gemini / Anthropic 等) | 仅 Claude API |
| 持久化 | 文件系统(JSONL + JSON),消息树结构 | 内存中维护会话,JSONL 转录为副产品 |
| 多 Agent | 递归子 Trace,独立持久化,可跨设备 | Coordinator-Worker,内存中,不持久化 |
| 计划管理 | 原生 GoalTree(结构化、持久化、周期注入) | 无原生计划结构(TodoWriteTool 扁平备忘录) |
| 记忆层次 | 三层(Trace / Knowledge / Skill),Agent 主动写入 | 二层(CLAUDE.md / 历史压缩),人工维护 |
| 权限系统 | 预设白/黑名单(工具级) | 规则引擎(参数级 glob 匹配 + 交互审批) |
| 可观测性 | REST + WebSocket,结构化事件流 | 终端 UI + OpenTelemetry,无外部 API |
| UI 层 | 无终端 UI | 自研 Ink 终端框架(251KB) |
Cyber Agent Claude Code
─────────────────── ───────────────────────
Trace ↔ QueryEngine (执行容器)
AgentRunner.loop ↔ queryLoop (while true 循环)
FileSystemTraceStore ↔ recordTranscript (持久化)
GoalTree ↔ TodoWriteTool (计划管理,差距显著)
KnowHub + Reflect ↔ CLAUDE.md (跨任务记忆,差距显著)
两者都是"单次执行会话"的容器,跨 turn 维护消息历史和统计数据。
| 维度 | Trace | QueryEngine |
|---|---|---|
| 生命周期 | 持久化到文件系统 | 内存中,session 结束消失 |
| 消息结构 | 消息树(parent_sequence,支持 Rewind) |
线性数组 Message[] |
| 显式状态 | running / completed / failed / stopped |
无 |
| 计划结构 | GoalTree(持久化,周期注入) | 无 |
| 唯一标识 | trace_id(UUID,可跨设备引用) |
无专用 ID |
| 子执行单元 | 所有子 Agent 都是 Trace,结构统一 | AgentTool 各自独立,无统一模型 |
| 记忆去重 | head_sequence 管理主路径,消息树自动过滤 |
loadedNestedMemoryPaths 防止 CLAUDE.md 重复注入 |
loadedNestedMemoryPaths 和 head_sequence 解决了相似的问题(防止重复处理),但层次完全不同:前者是内容级去重,后者是结构级的消息主路径管理。
核心差异:Cyber Agent 把 Agent 执行视为持久化的、可被外部观测的任务;Claude Code 更偏向交互式会话,持久化是副产品而非一等公民。
两者都是 while true → 调 LLM → 执行工具 → 继续 的循环,但在五个维度上有实质差异。
queryLoop 用一个整体替换的 State 对象管理跨迭代状态,所有 continue 点都是 state = { ...newState }:
// queryLoop 的每个 continue 点——必须声明完整的下一轮状态
state = {
messages: [...messagesForQuery, ...assistantMessages, recoveryMessage],
maxOutputTokensRecoveryCount: maxOutputTokensRecoveryCount + 1,
hasAttemptedReactiveCompact, // 显式保留
maxOutputTokensOverride: undefined, // 显式重置
pendingToolUseSummary: undefined, // 显式重置
stopHookActive,
turnCount,
transition: { reason: 'max_output_tokens_recovery', attempt: 2 },
}
continue
这带来三个好处:
transition 字段:零成本可观测性:记录每次 continue 的原因(源码注释明说是专为测试设计的):transition: { reason: 'next_turn' }
transition: { reason: 'max_output_tokens_recovery', attempt: 2 }
transition: { reason: 'reactive_compact_retry' }
transition: { reason: 'stop_hook_blocking' }
transition: { reason: 'collapse_drain_retry' }
测试可以直接断言 state.transition.reason === 'reactive_compact_retry',而不必解析消息内容推断"压缩是否发生了"。这个字段也是天然的 debug 信息:任何时候 dump 出 state 就能知道循环目前处于哪个恢复阶段。
注意:state.messages 不是 API payload。实际发给 API 的是 prependUserContext(messagesForQuery, userContext),其中 messagesForQuery 是 state.messages 经过多层变换(compact 边界过滤 → 工具结果截断 → microcompact → autocompact)后的结果。State 本身不落盘,只有 queryLoop yield 出来的 Message 才被 QueryEngine 记录到 transcript。
AgentRunner 当前直接在迭代内修改变量,没有这个整体替换的抽象。
queryLoop 有 StreamingToolExecutor(feature gate),工具在模型还在流式输出期间就并发开始执行:
模型流式输出: [tool1 完整] ── [tool2 完整] ── [tool3 完整] ── 流结束
StreamingExecutor: └→ 立即开始执行 └→ 立即开始执行 └→ 立即开始执行
↓ 执行完立即 yield 结果,不等流结束
工具执行时间被完全"藏"在模型输出时间里。AgentRunner 等 LLM response 完整后才执行工具。
queryLoop 内置了多条恢复路径,全部通过 state = next; continue 在循环内动态触发:
prompt_too_long → 1. context_collapse drain(轻量,清空 staged collapses)
→ 2. reactive compact(重新压缩,LLM 调用)
→ 失败 → 暴露错误,return
max_output_tokens → 1. 升级到 64k(maxOutputTokensOverride,一次机会)
→ 2. 多轮文本恢复(最多 3 次,注入 meta 消息)
→ 3. 耗尽 → 暴露错误,return
模型降级(FallbackTriggeredError)→ 切换 fallbackModel,丢弃已有输出,重试
stop hook blocking → 注入 hook 错误消息,continue
AgentRunner 的恢复(_heal_orphaned_tool_calls)发生在进入循环之前(_build_history 阶段),不是循环内的动态恢复。
AgentRunner 主动、周期性注入 GoalTree + Collaborators:
if iteration % 10 == 0:
inject_context(goal_tree, collaborators)
queryLoop 的记忆是被动消费的:startRelevantMemoryPrefetch 在每轮开头触发异步预取,settled 且本轮尚未消费时才注入为 attachment。没有强制周期,也没有计划结构注入。
这是两个系统差距最大的维度之一。
Cyber Agent context_hooks:每 10 轮调用一次,返回 markdown 字符串注入为上下文。功能是只读注入,无法阻断或修改任何事物。
CC hooks:覆盖 25+ 事件,三种执行模式(shell / HTTP / TypeScript 回调),每个事件有专用的双向契约:
| 维度 | Cyber Agent context_hooks |
CC hooks |
|---|---|---|
| 事件数量 | 1(周期注入) | 25+(全生命周期) |
| 工具生命周期 | 无 | PreToolUse / PostToolUse / PostToolUseFailure / PermissionDenied |
| loop 控制 | 无 | Stop hook block → stopHookActive=true → 强制 continue |
| 修改输入/输出 | 无 | PreToolUse 改 toolInput;PostToolUse 改 MCP 输出 |
| 阻断执行 | 无 | PreToolUse deny;PostToolUse preventContinuation |
| 执行模式 | Python 函数 | shell / HTTP / TypeScript callback / postSampling |
| 会话级注册 | 随 AgentRunner 传入 | addFunctionHook(),Map-based,O(1),高并发友好 |
三个最重要的 hook 事件在 loop 上下文里:
allow/deny/ask + 修改 toolInput。允许外部系统做"无人值守的审批代理",比交互弹窗更适合自主运行场景。state.transition = 'stop_hook_blocking',loop 强制再 continue 一轮。这是"外部系统驱动 Agent 继续工作"的接入点。additionalContext 或改写消息本体。比 context_hooks 的定时注入更精确(每次用户输入时触发,而非每 10 轮)。queryLoop 在工具执行完毕后 fire-and-forget 一个 Haiku 摘要任务,不等它,Promise 存入 nextPendingToolUseSummary,下一轮迭代开头才 await:
// 工具执行完毕后立即触发(不阻塞当前轮)
nextPendingToolUseSummary = generateToolUseSummary({...})
// 下一轮迭代开头取结果——此时 Haiku ~1s 早已完成
const summary = await pendingToolUseSummary
Haiku 摘要的延迟被下一轮 5-30s 的 API 调用时间完全吸收。AgentRunner 没有对应机制。
每条 Message 有 sequence(全局递增)和 parent_sequence,构成树结构:
正常执行: 1 → 2 → 3 → 4 → 5
Rewind 到3: 3 → 6 → 7 (4,5 自动脱离主路径)
压缩 1-3: 8(summary,parent=None) → 6 → 7
侧分支: 5 → 6(reflection) → 7 (不在主路径)
5 → 8(summary, 主路径)
trace.head_sequence 始终指向主路径头,build_llm_messages 沿链回溯,侧分支自动被过滤。所有历史永久保留,Rewind 是非破坏性操作。
消息以数组形式存在内存中,转录文件是 append-only JSONL。没有树结构,压缩是破坏性的(原始消息不再恢复)。QueryEngine 维护完整的 mutableMessages,queryLoop 内的 state.messages 在压缩后被替换为压缩后版本。
关键差异:Cyber Agent 是非破坏性时间旅行;Claude Code 是有损折叠。代价是 Cyber Agent 存储开销更高,查询需要沿链回溯。
GoalTree 是一等公民,有独立数据模型(Goal)、专用工具(goal)、持久化(goal.json)和周期注入机制:
1. [in_progress] 分析代码架构
1.1. [completed] 读取项目结构
1.2. [in_progress] 分析核心模块
1.2.1. [pending] 分析 runner.py
1.2.2. [pending] 分析 tool_registry.py
msg.goal_id),压缩以 Goal 为粒度focus / done(附 summary)/ abandon(附原因)扁平的待办列表,markdown 文本,格式由模型自行决定。无层级结构,无与消息的关联,不跨 session 持久化,不参与压缩粒度决策。
结论:这是两个系统差距最大的维度之一。GoalTree 是架构级方案,TodoWriteTool 是临时补丁。
Layer 3: Skills(技能库)
Markdown 文件,领域知识和操作规范,启动时注入 system prompt
▲ 归纳
Layer 2: Knowledge(知识库)
数据库 + 向量索引(KnowHub),语义检索按需注入
▲ 提取(Reflect 机制)
Layer 1: Trace(任务状态)
当前任务的工作记忆:Messages + GoalTree
knowledge_save 结构化入库Layer 2: CLAUDE.md(持久指令)
项目/用户级 Markdown,自动发现(目录树遍历),注入 system prompt
Layer 1: Session 历史(工作记忆)
当前对话历史(内存),超长时自动压缩折叠
跨 session 保留的内容只有写入 CLAUDE.md 的部分,Agent 本身不会主动提炼知识。
| | Claude Code | Cyber Agent |
|--|-------------|-------------|
| 组织轴 | 人为定义的实体(用户、项目) | 执行本身涌现的语义主题 |
| 边界划定者 | 人(目录结构、项目边界) | Agent(type + tags + scopes) |
| 谁来写记忆 | 人(或由 Claude 辅助人写 CLAUDE.md) | Agent 自身(Reflect 触发后主动入库) |
Claude Code 的记忆是静态附加的上下文,边界由人预先划定。Cyber Agent 的记忆可以从执行本身涌现,围绕"自主执行的任务"或"跨任务的全局线索"(如某类 bug 的修复模式)自然积累。
Level 1(Goal 完成压缩,零 LLM 成本):以 Goal 为粒度,completed goal 的消息折叠为 long_term_memory 摘要,确定性,无额外 API 调用。
Level 2(LLM 侧分支压缩):POST /compact 触发,在 Trace 末尾创建 branch_type="compression" 侧分支,压缩结果作为 summary 消息插入主路径。原始消息保留在侧分支,可审计。
控制参数:RunConfig(goal_compression="none"|"on_complete"|"on_overflow")
每轮迭代前依次经过:HISTORY_SNIP → microcompact → CONTEXT_COLLAPSE → autocompact(token 接近阈值时触发)。触发后用摘要模型折叠历史,原始消息丢失。prompt_too_long 错误时进入 reactive compact(最后手段)。
| 方面 | Cyber Agent | Claude Code |
|---|---|---|
| 压缩粒度 | Goal 级(语义边界) | 全量历史折叠 |
| 压缩成本 | Level 1 零成本;Level 2 LLM 成本 | 始终有 LLM 成本 |
| 压缩可逆性 | ✅ 侧分支,原始消息保留 | ❌ 有损折叠,原始丢失 |
| 触发时机 | 主动(on_complete / on_overflow) | 被动(超限后)+ 手动 |
主 Agent(Trace A)
├── agent(task="研究X") → 子 Trace B(独立持久化,可单独续跑)
│ └── agent(task="分析子模块") → 子 Trace C(递归)
└── agent(agent_url="https://remote/", task="...")
→ 远程 Trace(agent://remote/xxx,跨设备)
task: List[str] 触发 asyncio.gather() 并行多 Agentagent(continue_from=sub_trace_id) 可续跑被中断的子 AgentCoordinator Agent
├── AgentTool(subagent_type="worker") → Worker 1(内存,非持久化)
├── AgentTool(subagent_type="worker") → Worker 2
└── SendMessageTool(to=agent_id) → 向活跃 Worker 发消息
Worker 以 <task-notification> XML 上报状态,通过 SendMessageTool 持续对话。
| 方面 | Cyber Agent | Claude Code |
|---|---|---|
| 子 Agent 持久化 | ✅ 独立 Trace | ❌ 内存中 |
| 续跑子 Agent | ✅ continue_from |
✅ SendMessageTool |
| 跨设备协作 | ✅ agent_url + 远程 Trace ID |
❌ 不支持 |
| 并行执行 | ✅ explore 模式 | ✅ 多 Worker 并行 |
| 评估反馈循环 | ✅ evaluate 工具 | ❌ 无原生评估工具 |
Cyber Agent(装饰器,轻量):
@tool(description="执行 shell 命令")
async def bash(command: str, ctx: ToolContext) -> ToolResult:
return ToolResult(
output=result.stdout,
long_term_memory=f"Ran: {command}", # 压缩后保留的摘要
)
Claude Code(接口,功能丰富): 每个工具实现约 15 个方法:call、description、checkPermissions、isReadOnly、isConcurrencySafe、prompt(注入 system prompt 的说明)、renderToolResultMessage(UI 渲染)等。接口更重,但权限检查、并发安全标记、进度报告、UI 渲染都内置其中。
ToolResult(
output="<10K tokens 完整内容>", # 第一次给 LLM 完整内容
long_term_memory="提取了 10000 字符", # 之后只看摘要
include_output_only_once=True
)
大输出只在首次注入完整内容,后续轮次自动替换为摘要,有效节约 context window。Claude Code 没有对应机制。
Cyber Agent 通过 AgentPreset.denied_tools 黑名单控制,粒度是整个工具。
Claude Code 有独立的 102KB 安全分析器(bashSecurity.ts):
"Bash(git *)" 自动允许所有 git 命令AgentPreset(
allowed_tools=["read", "glob", "grep"],
denied_tools=["write", "edit", "bash"],
)
工具级粒度,静态配置,无动态审批。适合离线/批量 Agent 场景。
"Bash(git *)" 支持 glob 模式匹配到参数级适合面向开发者的交互式场景。
/api/traces/{id}/watch:实时推送所有 Message 事件self_stats 和 cumulative_stats(token、cost、duration)关键差异:Cyber Agent 从架构上是服务,天然可集成监控;Claude Code 是工具,可观测性服务于操作者自己。
| 优势 | 价值 |
|---|---|
| 消息树 + Rewind | 非破坏性时间旅行,支持回溯,全历史可审计 |
| GoalTree | 结构化计划,与消息关联,驱动细粒度压缩 |
| Knowledge + Reflect | 跨任务知识积累,Agent 主动从经验中学习 |
| 记忆组织轴 | 围绕执行语义积累,不依赖人为划定边界 |
| evaluate 工具 | 内置评估-反馈循环,Agent 可自我校正 |
| ToolResult 双层记忆 | 精细的 context 节约机制 |
| 跨设备 Agent 协作 | 分布式 Agent 网络,协议级支持 |
| Context Injection Hooks | 灵活的外部事件注入(A2A IM、监控告警等) |
| 服务化 API | REST + WebSocket,天然可集成、可观测 |
| 设计 | 价值 |
|---|---|
| Loop State 整体替换 + transition 字段 | 消灭"隐式携带"bug;transition 记录每次 continue 的原因,可用于测试断言和 debug,扩展后也可通过 WebSocket 推出去实现循环级可观测性 |
| 流式工具并发执行 | 工具在模型流式输出期间就开始执行,延迟藏在模型输出时间里 |
| 循环内多层恢复路径 | prompt_too_long / max_tokens / 模型降级等错误在循环内动态恢复,无需重启 |
| 工具摘要异步隐藏 | Haiku 摘要在工具执行后 fire-and-forget,延迟藏在下一轮 API 调用时间里 |
| Bash 语义级安全分析 | 参数级权限控制,远比工具级黑名单精准,生产环境自主运行时重要 |
| ToolSearch 延迟加载 | 工具数量大时按需加载描述,节约 system prompt token |
| 权限规则自动学习 | "始终允许"自动写入配置,构建个性化规则库 |
| 工具生命周期 hooks(PreToolUse / PostToolUse) | 在工具执行前后插入外部逻辑:审批代理(PreToolUse allow/deny)、MCP 输出改写(PostToolUse updatedMCPToolOutput)、失败后通知(PostToolUseFailure)。Cyber Agent 目前对工具执行无介入点 |
| Stop hook(loop 控制) | 每轮结束时调用,可返回 block 强制 loop 再 continue 一轮(transition='stop_hook_blocking')。这是"外部事件驱动 Agent 继续"的最直接接入点 |
| UserPromptSubmit hook | 用户消息进队列时触发,可注入 additionalContext 或改写消息。比 context_hooks 定时注入更精确,也更适合做"收到外部信号时注入上下文"的触发器 |
| 工具结果自动存档(toolResultStorage) | 每个工具声明 maxResultSizeChars,超限时自动写入磁盘临时文件,API 收到 preview(前 2000 字节)+ 文件路径,模型可按需用 FileReadTool 读回完整内容。与 Cyber Agent 的 long_term_memory(手动摘要、原始内容丢弃)互补:这是零开发者工作量的兜底层,防止意外大输出撑爆 context,且原始内容可恢复 |
| 优先级 | 改进项 | 说明 |
|---|---|---|
| ★★★ | Loop State 整体替换 + transition 字段 | 将 AgentRunner loop 的跨迭代变量收拢为一个整体替换的 State 对象;每个 continue 点声明完整下一轮状态,消灭"忘记重置某字段"的隐患。新增 transition 字段记录每次 continue 的原因(tool_use / compressed / healed_orphan 等),可直接用于测试断言,也可通过 WebSocket 推出去作为循环级可观测事件 |
| ★★★ | 工具结果自动存档 | 每个工具声明 max_result_size(字符数),超限时自动写入临时文件,给模型 preview(前 N 字节)+ 文件路径,模型可用 read 工具读回完整内容。与现有 long_term_memory(开发者手写精炼摘要)互补:这是零开发者工作量的兜底层,防止 bash 大输出、webfetch 整页 HTML 等意外撑爆 context |
| ★★☆ | 工具并发执行 | 单次 LLM 响应包含多个 tool_use 时,用 asyncio.gather() 并行执行,而非顺序执行。对"同时读取多个文件"等场景延迟改善明显 |
| ★★☆ | 工具摘要异步隐藏 | 工具执行完毕后 fire-and-forget 启动摘要生成(用小模型),不阻塞当前轮,下一轮迭代开头取结果。摘要延迟藏在下一次 LLM 调用时间里,对用户零感知 |
| ★★☆ | 工具生命周期 hooks | 在 ToolRegistry / AgentRunner 层面引入 pre_tool_use / post_tool_use / post_tool_use_failure 三个事件。最高价值用例:① pre_tool_use 实现无人值守审批代理(尤其对 bash 危险命令);② post_tool_use_failure 触发外部通知或重试策略;③ post_tool_use 对特定 MCP 工具的输出做后处理 |
| ★★☆ | Stop hook + Loop 控制接入点 | 在每轮 loop 结束时(得到 stop_reason='end_turn')触发一个 hook,如果返回 block 则注入消息并 continue。这让外部系统(监控、测试框架)可以在运行时"插入"下一步指令,而无需重新发起整个对话 |
| ★☆☆ | 循环内多层恢复路径 | 目前 _heal_orphaned_tool_calls 在进入循环前执行。可考虑将更多恢复逻辑移入循环内(context 超限时自动触发压缩并 continue,而非报错退出) |
对比基于 Cyber Agent 文档(architecture.md)与 Claude Code v2.1.88 npm 包 source map 分析(非官方重建,仅供研究),生成日期:2026-04-02。