集成新的Beaver后端服务到应用实例中,替换原有的nanobot实现。 主要变更包括: - 在Dockerfile和环境配置中添加Beaver相关路径和配置变量 - 更新工作目录结构从.nanobot到.beaver - 实现Beaver引擎加载器,支持配置文件加载和工具组装 - 添加内置工具如ListDirectoryTool、ReadFileTool、SearchFilesTool - 更新消息处理流程,支持通道适配器和网关模式 - 重构技能系统,支持显式工具提示和嵌入式检索 - 改进错误处理和生命周期管理 此变更使应用实例能够使用统一的Beaver后端进行AI代理运行时管理。
42 KiB
Beaver Backend 施工指南
这份文档不是蓝图,也不是迁移映射,而是“真正开始施工时怎么下手”的执行指南。
目标是:按运行时主链路,一步一步把 backend-old 的能力迁进新的 beaver 后端,并且始终保证我们先打通主链,再扩外围。
1. 施工总原则
先把几条原则定死,否则很容易又回到旧项目那种“边写边散”的状态。
1.1 先打通主链,再补外围
不要一上来拆 Web,不要先做页面,不要先做渠道接入。
施工顺序必须是:
- 运行时主链
- memory / skills / tools
- delegation / team
- CLI / Web / channels / gateway
1.2 先做最小可运行链路
第一阶段目标不是“全部功能迁完”,而是先让新 beaver 后端具备一条最小闭环:
input -> session -> context -> provider -> tool loop -> save turn -> output
只要这条链没通,后面的多 agent、Web、MCP、cron 都不应该大规模开工。
1.3 一次只收口一层边界
每一阶段必须回答三个问题:
- 本阶段新增了什么文件
- 本阶段替换了旧项目哪几个函数
- 本阶段结束后,能跑通什么能力
1.4 统一以 beaver.engine.AgentLoop 为唯一运行内核实现
这里说的不是“系统里只会存在一个 loop 实例”,而是:
- 整个后端只维护一套
AgentLoop实现代码 - 可以同时存在多个 agent 运行实例
- 但这些实例都必须复用同一个
beaver.engine.AgentLoop - 它们的差异只能来自 profile、context、toolset、skills、permissions,而不是来自不同执行栈
后续所有 agent 角色都必须围绕这同一套内核装配:
- CLI agent instance
- delegated local agent instance
- team member agent instance
- future A2A local specialist instance
不允许再出现“CLI 一套 loop、delegation 一套 loop、team 一套 loop”的情况。
2. 从运行时视角看,系统到底怎么工作
我们先把最终运行时主链画出来。后续所有施工都围绕这条链拆。
2.1 目标主链
一次标准请求应该按下面顺序流动:
interfaces/*- CLI / Web / Gateway 收到输入
services/agent_service.py- 创建或复用
AgentLoop
- 创建或复用
engine/session/*- 恢复 session 与历史消息
memory/curated/*- 读取 frozen snapshot
skills/resolver/runtime.py- 决定本轮注入哪些 skills
engine/context/builder.py- 拼装 system prompt + messages
engine/providers/*- 调用模型
engine/loop.py- 解析模型输出、执行 tool、迭代下一轮
tools/*- 执行文件、shell、web、memory、session_search、spawn 等工具
engine/loop.py
- 汇总最终 assistant 输出
engine/session/*
- 写回本轮消息
engine/session/store.py+engine/session/search.py
- 记录 transcript,供
session_search、resume、history 使用
interfaces/*
- 把结果回传给用户
2.2 第一条必须先打通的链
真正施工时,不要直接从 message bus + async background task + WebSocket 开始。
第一条应该先打通的是:
process_direct(task) -> build context -> call provider -> execute tools -> save session -> return text
也就是说:
- 先做 direct run
- 再做 bus run
- 再做 Web / gateway
这样复杂度最低,也最容易验证。
3. 施工顺序总览
后续建议严格按下面顺序施工。
阶段 0:运行时前置件
目标:把主链运行所需的基础模型和公共组件先补齐。
先做这些文件:
beaver/foundation/config/schema.pybeaver/foundation/config/loader.pybeaver/foundation/config/paths.pybeaver/foundation/events/messages.pybeaver/foundation/events/message_bus.pybeaver/foundation/events/process.pybeaver/foundation/models/run_result.pybeaver/foundation/utils/helpers.pybeaver/foundation/utils/llm_audit.py
主要迁移来源:
backend-old/nanobot/config/schema.pybackend-old/nanobot/config/loader.pybackend-old/nanobot/config/paths.pybackend-old/nanobot/bus/events.pybackend-old/nanobot/bus/queue.pybackend-old/nanobot/agent/process_events.pybackend-old/nanobot/agent/run_result.pybackend-old/nanobot/utils/helpers.pybackend-old/nanobot/llm_audit.py
完成标准:
- 新后端已经有稳定的 config / bus / event / result 基础件
- 后续 engine、tools、services 不再依赖旧
nanobot.*
4. 第一施工阶段:先把单 agent 主链做出来
这是最关键的一阶段,也是整个项目的起点。
4.1 先做 session 层
先实现:
beaver/engine/session/models.pybeaver/engine/session/manager.pybeaver/engine/session/store.pybeaver/engine/session/search.py
直接参考旧文件:
backend-old/nanobot/session/manager.py
额外参考:
hermes-agent的hermes_state.pyOpenHarness的 harness 分层思路
这里的目标不是简单把旧 SessionManager 搬过来,而是把 session 拆成 4 层:
models.py- 放
SessionRecord、MessageRecord、SessionUsage - 只放数据结构,不放数据库逻辑
- 放
store.py- 放 SQLite 实现
- 负责
sessions/messages表、WAL、FTS5、写入与查询
search.py- 放
list_sessions_rich()、search_messages()、resolve_session_id()这类检索逻辑 - 明确这是 session 子系统的一部分,不再挂到 memory 下面
- 放
manager.py- 作为运行时门面
AgentLoop、services、interfaces只优先依赖它,而不是直接操作 SQLite
这四层的职责必须分开:
session保存完整会话过程与历史恢复memory只保存 durable factssession_search只是 session transcript 的检索能力skills保存稳定方法论
现有的:
beaver/memory/search/transcript_store.py
要明确视为临时过渡实现。它是为了先让 MCP session_search tool 可用而建的,不是最终归宿。
真正开工 session 层时,应把它的能力并回:
beaver/engine/session/store.pybeaver/engine/session/search.py
然后让:
beaver/tools/builtins/session_search.py
改为直接依赖 engine/session 的 store / manager,而不是继续单独维护一套“memory search store”。
优先迁移的类和函数:
- 旧
Session-> 拆成SessionRecord+ conversation replay 相关模型 - 旧
SessionManager-> 改成SessionManager门面层 get_or_create_loadsaveget_history
但新 session 层必须新增这些 Hermes 风格能力:
ensure_session(session_id, source, model, parent_session_id=None)append_message(session_id, role, content, tool_name=None, tool_calls=None, ...)get_messages_as_conversation(session_id)update_system_prompt(session_id, system_prompt)update_usage(session_id, input_tokens, output_tokens, ...)end_session(session_id, end_reason)reopen_session(session_id)get_session(session_id)list_sessions_rich(limit, include_children=False)search_messages(query, role_filter=None, limit=...)resolve_session_id(session_id_or_prefix)
session schema 也要从第一天就按 Hermes 思路建好,而不是后补:
sessionsidsourcemodelsystem_promptparent_session_idstarted_atended_atmessage_counttoken / cost / usage相关字段
messagessession_idrolecontenttool_nametool_callstimestamp
messages_fts- 用 FTS5 做全文搜索
这一步要支持 lineage:
- 正常 direct run 可以只有一个 root session
- 后续 compression / resume / delegation / team member session 都通过
parent_session_id挂到同一条会话链 session_search和 session browser 都要按 lineage 理解,而不是只看单个碎片 session
4.1.1 session 层第一批施工顺序
按下面顺序落文件,不要反过来:
models.pystore.pysearch.pymanager.py
原因:
- 先把数据结构和 SQLite 能力定住
- 再把搜索能力挂上
- 最后才让
manager做统一门面
4.1.2 session 层第一批必须先跑通的函数
第一批不要贪多,先跑通这 8 个:
ensure_sessionappend_messageget_sessionget_messages_as_conversationupdate_system_promptlist_sessions_richsearch_messagesclose
只要这 8 个通了,process_direct()、history replay、session_search 就都有基础了。
4.1.3 session 层与 runtime 的接点
后续 AgentLoop 必须按下面方式使用 session 层:
- 请求进入时:
ensure_session(...)
- context 组装完成后:
update_system_prompt(...)
- 读取历史消息时:
get_messages_as_conversation(...)
- 每次 user / assistant / tool 产出后:
append_message(...)
- 会话结束或取消时:
end_session(...)
4.1.4 第一批 session 层不要做的事
不要一上来就做这些:
- 复杂 session compaction
- team transcript 合并策略
- Web session API
- gateway 专属优化
- 跨数据库抽象层
先把单机 SQLite + WAL + FTS5 跑通即可。
4.1.5 session 层如何一步步做 code review
每次完成 session 子系统的一轮改动,不要直接看“能不能跑”,而是按下面顺序 review。
第一步:先看职责是否串层
按文件检查:
models.py- 只能有数据结构与序列化/反序列化辅助
- 不能出现 SQL
- 不能出现 runtime orchestration
store.py- 只能负责 SQLite schema、写入、读取、事务
- 不能写
session_search的上层业务流程
search.py- 只能负责 browse / FTS / resolve 逻辑
- 不能负责写入
manager.py- 只能做 facade
- 不要把复杂 SQL 又塞回 manager
只要出现“某层开始顺手做别层的事”,这轮 review 就先不过。
第二步:看 schema 是否支撑后续 runtime
逐项检查 sessions / messages 表字段是否够用:
sessions是否有:idsourcemodelsystem_promptparent_session_idstarted_atlast_activeended_atmessage_count
messages是否有:session_idrolecontenttool_nametool_callstimestamp
- 是否已经有 FTS5
- 是否已经有支持 lineage 的
parent_session_id
这一步的核心问题是:
“等后面接 AgentLoop、resume、delegation、session_search` 时,会不会缺字段?”
第三步:看写路径是否完整
按运行时顺序 review:
ensure_session()- 新 session 能不能被创建
append_message()- user / assistant / tool message 能不能都写入
- tool_calls 是否正确序列化
update_system_prompt()- assembled prompt snapshot 能不能落盘
update_usage()- usage 是增量还是绝对覆盖,语义是否清楚
end_session()/reopen_session()- 生命周期状态是否闭环
这里只问一件事:
“如果一轮对话完整跑完,session 数据是否真的闭环写全了?”
第四步:看读路径是否能支撑 prompt 组装
重点检查:
get_session()get_messages_as_conversation()get_history()
要问:
- provider replay 需要的字段有没有漏
- tool_calls 有没有被正确还原
- leading non-user trimming 是否合理
- 会不会把本地存储字段脏带进 prompt
第五步:看 search 是否真的能服务 session_search
重点检查:
list_sessions_rich()search_messages()resolve_session_id()
要问:
- FTS query sanitization 是否足够
- recent mode 返回的信息够不够 UI / tool 用
- prefix resolve 是否会误命中多个 session
- exclude_sources / role_filter 是否真的生效
第六步:看并发与持久化风险
这里不要只看“代码风格”,要看数据安全:
- SQLite 是否开启了 WAL
- 写事务是否明确
BEGIN IMMEDIATE是否正确check_same_thread=False后有没有线程锁保护- schema 初始化是否幂等
这一步问的是:
“两个入口同时写 session 时,会不会炸?”
第七步:看兼容路径是不是临时且可收敛
现在的:
beaver/memory/search/transcript_store.py
是兼容层。
review 时要明确:
- 它有没有新长逻辑
- 它是不是只是薄封装
- 后续是否可以安全删掉
兼容层一旦开始长业务逻辑,就说明架构又开始回退了。
第八步:最后才看命名、注释、可读性
这一步最后看:
- 命名是否统一用
session而不是混成memory/transcript/history - 中文注释是否解释了设计意图,而不是重复代码
- manager / store / search 的边界是否一眼能看懂
第九步:做最小行为验证
每轮 review 最后至少手跑这几条:
- 创建 session
- 写入 user message
- 写入 assistant message
- 更新 system prompt
- 读取 history
- FTS 搜索关键词
- recent mode 浏览 session
如果这 7 条没过,这轮 review 不算通过。
为什么先做它:
因为没有 session,就没有:
- 历史消息窗口
- transcript 持久化
- session_search 的真实后端
- resume / history / lineage
loop 无法闭环。
完成标准:
- 能创建 session
- 能读取历史
- 能写回消息
- 能记录 assembled system prompt snapshot
- 能用 FTS5 搜历史消息
session_search可以直接复用 session store
4.2 再做 provider 契约层
先实现:
beaver/engine/providers/base.pybeaver/engine/providers/registry.pybeaver/engine/providers/factory.pybeaver/engine/providers/runtime.py
然后再迁最常用 provider:
beaver/engine/providers/custom.pybeaver/engine/providers/codex.pybeaver/engine/providers/litellm.pybeaver/engine/providers/anthropic.py
对应旧来源:
backend-old/nanobot/providers/base.pybackend-old/nanobot/providers/registry.pybackend-old/nanobot/providers/openai_codex_provider.pybackend-old/nanobot/providers/litellm_provider.pybackend-old/nanobot/providers/custom_provider.py
额外参考:
Hermes-agent的 provider runtime resolutionHermes-agent的 auxiliary routingHermes-agent的 fallback model/providerOpenHarness的模块化边界
provider 层不要再做成“一个厂商一个世界”,而是要拆成 4 层:
base.py- 统一 provider 契约
- 统一
LLMResponse/ToolCallRequest
registry.py- 只放 provider 元数据与匹配规则
- 不放网络请求逻辑
runtime.py- 做 Hermes 风格 runtime resolution
- 决定最终
provider / model / api_base / api_mode / auth path
factory.py- 作为对 engine 暴露的唯一装配入口
- 统一产出
main / fallback / auxiliaryprovider 组合
4.2.1 provider 层的目标结构
最终 provider 子系统应该是:
beaver/engine/providers/base.pybeaver/engine/providers/registry.pybeaver/engine/providers/runtime.pybeaver/engine/providers/factory.pybeaver/engine/providers/chain.pybeaver/engine/providers/custom.pybeaver/engine/providers/codex.pybeaver/engine/providers/litellm.pybeaver/engine/providers/anthropic.py
4.2.2 provider 不按厂商数扩类,而按 API path 收敛
实现原则:
- 大部分 OpenAI-compatible / LiteLLM-compatible provider 走
litellm.py - Anthropic 走
anthropic.py的 native messages path - OpenAI Codex 走
codex.py的 Responses path - 自定义 OpenAI-compatible endpoint 走
custom.py - embedding runtime 作为独立配置线存在,不再默认继承主聊天 provider 的 provider 语义
- 当前 embedding 只支持 OpenAI-compatible
/v1/embeddings
也就是说:
- provider registry 可以很多
- 但真正的执行路径只有少数几条
4.2.3 第一批必须先支持的 provider
第一批先把这些 provider 的 runtime path 定住:
openaianthropicopenrouteropenai_codexcustomgithub_copilotdeepseekgemini
第二批再补这些旧后端已有 provider:
aihubmixsiliconflowvolcenginedashscopezhipumoonshotminimaxvllmgroq
4.2.4 provider 层必须照 Hermes 做的能力
这几个能力要从第一天就在设计里留位置:
api_modechat_completionsanthropic_messagescodex_responses
fallback_model- 主 provider/model 失败后切换备用
- 由
FallbackProviderChain统一执行 failover
auxiliary routing- 主对话与辅助任务可用不同 provider/model
- 由
resolve_auxiliary_runtime()做独立解析
OpenRouter provider routingsortonlyignoreorderrequire_parametersdata_collection
4.2.5 第一批 provider 施工顺序
按下面顺序落代码:
base.pyregistry.pyruntime.pyfactory.pychain.pycustom.pycodex.pylitellm.pyanthropic.py
原因:
- 不先定 runtime resolution,后面 provider 实现会继续散
- 不先定 registry,factory 就会出现 if/else 污染
chain先把 fallback 行为从AgentLoop里拿走custom和codex最容易先单独落地litellm收口大多数 provideranthropic作为 native path 独立出来
4.2.6 第一批 provider 层必须先跑通的函数
第一批先跑通这些:
find_by_namefind_by_modelfind_gatewayresolve_provider_runtimeresolve_fallback_runtimeresolve_auxiliary_runtimemake_provider_from_runtimemake_main_providermake_fallback_providermake_aux_providermake_provider_bundleFallbackProviderChain.chat()- 至少一个 provider 的
chat()
第一阶段不要求把所有 provider 全迁完,只要求先有一个能跑通主链的 provider。
建议先选:
OpenAICodexProvider- 或
CustomProvider
完成标准:
AgentLoop能拿到 provider- 能发出一次最小模型请求
- provider 解析不再散落在 CLI / Web / gateway
- registry / runtime / factory 三层边界清楚
4.3 再做 context builder
实现:
beaver/engine/context/builder.py
参考旧文件:
backend-old/nanobot/agent/context.py
优先实现的函数:
build_system_promptbuild_messagesadd_tool_resultadd_assistant_message
这一版必须改掉的地方:
- 不再直接读取 live memory
- 只注入 frozen snapshot
- 给 skills 预留注入点
- 给 current session / channel metadata 预留注入点
完成标准:
- 能从 session history + frozen memory + skills 拼出 prompt
- 输出结构稳定,后续便于测试
4.4 再做 tools 基础设施
实现:
beaver/tools/base.pybeaver/tools/registry/tool_registry.pybeaver/tools/assembler/task_assembler.py
参考旧文件:
backend-old/nanobot/agent/tools/base.pybackend-old/nanobot/agent/tools/registry.py
然后先迁最小工具集:
beaver/tools/builtins/filesystem.pybeaver/tools/builtins/shell.pybeaver/tools/builtins/web.pybeaver/tools/builtins/message.pybeaver/tools/builtins/memory.pybeaver/tools/builtins/session_search.py
第一阶段可以暂时不迁:
spawncronmcp wrapper
因为先要保证单 agent tool loop 可运行。
完成标准:
- registry 可以注册工具
- provider 返回 tool call 时可以找到并执行工具
- memory / session_search 已纳入统一工具集合
- 本地工具描述采用 MCP-style
name/description/inputSchema ToolAssembler能按 task description 用 embedding 召回本轮 top10 工具- activated skill frontmatter 里的
tools能参与本轮工具选择 - 只读 filesystem tools 已接入:
list_directoryread_filesearch_files
- filesystem tools 强制限制在
ToolContext.workspace - 相对路径逃逸、绝对路径逃逸、符号链接逃逸都会拒绝
- 二进制文件读取会拒绝,搜索会跳过二进制 / 大文件
当前工具选择规则已经定为:
- always tools 每轮默认可用
memorysession_searchskill_viewlist_directoryread_filesearch_files
- activated skill 可以显式声明工具:
---
tools:
- terminal
- read_file
---
ToolAssembler合并:- always tools
- skill hints
- task embedding top10 tools
- 第一版只信任 frontmatter / metadata 的显式
tools,不从正文里猜工具名 - 如果 skill 声明了尚未注册的工具,先忽略,不阻断 run
filesystem 这一版只做只读,不做写文件 / shell:
- 先让 agent 能看见 workspace 结构、读源码、搜文本
- 写文件和 shell 属于高风险工具,必须等 permission gates 明确后再接
- 当前 workspace 边界只保证路径隔离,不等价于完整权限系统
4.5 最后实现第一版 AgentLoop
这是第一施工阶段的收口点。
实现:
beaver/engine/loader.pybeaver/engine/loop.py
参考旧文件:
backend-old/nanobot/agent/loop.py
第一版必须先实现这些函数:
AgentLoop.__init__boot_set_tool_context_process_message_run_agent_loop_save_turnprocess_directrunstop
第一版暂时不要迁的逻辑:
_connect_mcpreload_mcp_servers- 复杂 background consolidation
- team delegation
因为现在 memory 体系已经不是旧的 consolidate_memory() 了,这些旧逻辑不能硬搬。
第一阶段验收方式:
- 用 CLI 或脚本创建一个 loop
- 调
process_direct("hello") - 能返回模型回复
- 工具调用能生效
- session 能写回
只要这一条通了,新的 beaver runtime 就算正式开工成功。
5. 第二施工阶段:把 memory / skills 接进主链
第一阶段打通的是“能跑”;第二阶段打通的是“跑得像 Beaver”。
5.1 memory 接入主链
当前已经有:
beaver/memory/curated/store.pybeaver/memory/curated/snapshot.pybeaver/memory/search/transcript_store.py(临时过渡实现)beaver/tools/builtins/memory.pybeaver/tools/builtins/session_search.py
现在要做的是把它们真正装进运行时:
需要改的地方:
beaver/engine/loader.py- 初始化
MemoryStore load_from_disk()- capture snapshot
- 初始化
beaver/engine/context/builder.py- 注入 frozen snapshot
beaver/engine/loop.py- 在
_save_turn后把消息写进 session store
- 在
beaver/tools/builtins/session_search.py- 改为依赖
beaver/engine/session/search.py
- 改为依赖
beaver/memory/search/transcript_store.py- 在 session 层稳定后删除或保留为兼容薄封装,不再作为主实现
完成标准:
memorytool 真正能写持久记忆- 新 session 能读到上次写入的 frozen snapshot
session_search能直接搜 session transcript
5.2 skills 接入主链
实现:
beaver/skills/catalog/loader.pybeaver/skills/catalog/utils.pybeaver/skills/resolver/runtime.py
参考旧文件:
backend-old/nanobot/agent/skills.py
先迁这些函数:
list_skillsget_skill_metadataload_skillload_skills_for_contextbuild_skills_summaryget_always_skills
接入点:
engine/loader.pyengine/context/builder.pyengine/loop.py
第二阶段要求做到:
- 主 agent 能按上下文注入 skills
- skills 不只是文档目录,而是运行时上下文的一部分
- 激活后的 skill 正文按 Hermes 风格走显式消息注入,而不是长期塞进 system prompt
- skill 的选择不再由 AgentLoop 内部硬编码完成,而是交给外置
SkillAssembler SkillAssembler采用最直接的 LLM 选择器:- 输入 task description
- 输入候选 skill 摘要
- 先用 embedding 做语义召回
- 输出应该激活的 skills
- embedding 配置通过 provider bundle 的独立
embedding runtime传入;若没有显式 embedding 配置,则只有主链本身是 OpenAI-compatible 时才允许继承api_base/api_key - skill frontmatter 可声明本 skill 推荐工具;这些 tool hints 会交给
ToolAssembler
当前和长期目标的关系:
- 已完成基础入口:
- curated memory CRUD
session_searchskill_viewSkillAssemblerToolAssembler
- 还没完成长期智能体治理:
- 智能体定期整理 / 提示记忆
- 复杂任务完成后自主创建技能
- 技能在使用过程中自我提升
- FTS5 + LLM 摘要的跨会话回忆增强
- Honcho 风格辩证用户建模
- agentskills.io 开放标准兼容
6. 第三施工阶段:把 direct run 扩成标准 runtime
当 direct run 已经稳定后,再把它扩成“完整的运行时内核”。
这一阶段的核心思想先明确为两句:
Session = Durable Memory + Event SourceHarness = Stateless Orchestrator
也就是说,后面的 Beaver 不应再把任务进度、运行中间态、恢复点藏在进程内对象里,而应尽量写回外部 Session。
6.0 这一阶段的目标架构
这一阶段要把当前的“最小单 agent 主链”推进成下面这种结构:
Session- 是外部持久化记忆
- 是唯一事实来源
- 本质上是 append-only event stream
Harness- 只负责编排
- 自身不持有不可恢复状态
- 崩溃后可由新实例读取 Session 接管
ContextBuilder- 不再直接依赖进程内状态
- 只从 Session / curated memory / skills 中提取当前需要的上下文
ToolAssembler- 不把所有工具无脑暴露给模型
- 按 task description / activated skill hints 选择本轮工具
- 输出 provider 可消费的 tool schema
这一步不是要一口气做完 fork / rewind / checkpoint 全套系统,而是先把“Session-first, Stateless Harness”这条主线立住。
6.1 第一步:先把 Session 升级为事件源模型
这是第六阶段真正的第一步,也是后续 runtime 生命周期的基础。
目标:
- 让
Session不只是聊天记录表,而是运行事件源 - 让
AgentLoop不再依赖进程内隐式状态来判断“任务做到哪了” - 让新的 Harness 实例理论上可以只靠 Session 恢复运行现场
第一步先做这些文件:
beaver/engine/session/models.pybeaver/engine/session/store.pybeaver/engine/session/manager.pybeaver/engine/loop.pybeaver/engine/context/builder.pybeaver/engine/loader.py
6.1.1 具体怎么做
先在 session 层引入“事件优先”的视角:
- 保留现有
sessions表- 它继续承担 projection / summary row 的角色
- 例如
last_active、message_count、preview、累计 usage
- 强化
messages表的事件语义- 它不只是聊天记录
- 而是当前阶段的主事件流
- 给事件加清晰类型边界
userassistanttool- 后续可扩
system_event/checkpoint_event/run_event
然后在 AgentLoop.process_direct() 中,明确把运行过程拆成“事件追加”:
session_started/ensuredrun_startedskill_activation_snapshottedtool_selection_snapshottedsystem_prompt_snapshotteduser_message_addedassistant_message_addedtool_result_recordedrun_failed或run_completed
并且每次 run 都要带独立 run_id,这样同一个 session 内的多次运行才能被切开。
注意:
第一步不一定要真的新建一张 session_events 表,先把现有 messages 作为主事件流用起来也可以。
关键不是表名,而是:
- 运行进度要能从外部事件重建
- 不依赖进程内变量才能知道“上一步发生了什么”
6.1.2 这一小步里具体要改哪些函数
优先改这些函数:
SessionStore.append_message()- 明确它承担事件追加语义
SessionManager.get_history()- 不只是“取最近聊天记录”
- 而是“从事件流里切一段 provider 需要的上下文”
SessionManager.get_run_event_records()- 能按
run_id读取某一次运行的事件片段
- 能按
SessionManager.list_run_ids()- 能发现当前 session 内有哪些 run
AgentLoop.process_direct()- 继续保留 direct run 入口
- 但内部按事件阶段组织代码
ContextBuilder.build_messages()- 明确消费的是“上游裁剪后的事件片段”
- 而不是默认依赖进程内连续状态
6.1.3 第一步完成后的结果
这一小步做完后,应达到:
- Session 已经是当前运行事实的主要来源
- AgentLoop 即使重建实例,也能读出:
- 当前 session 里有哪些 run
- 某一次 run 的起点和结束点
- 当前历史消息
- 上一次 system prompt snapshot
- 工具执行痕迹
- 失败点
- 后续继续做:
run()stop()close()/shutdown()- fork / rewind / checkpoint 才不会回到“全靠进程内状态续命”
6.2 第二步:把 runtime 生命周期协议补齐
当前已完成的最小骨架:
EngineLoader返回的EngineLoadResult已经具备 runtime 容器语义EngineLoadResult.close()已能统一关闭已登记的 closeablesAgentLoop.boot()/close()已建立成对协议AgentService.close()/shutdown()已可作为接口层统一释放入口AgentLoop.run()/stop()/submit_direct()已形成最小运行循环AgentService.start()/stop()/submit_direct()已可包装该运行循环
只有在 6.1 稳住后,才开始补统一生命周期:
- 扩
closeables / shutdown hooks - 明确 provider/client 等更多资源的释放协议
- 再补更复杂的 bus / worker / 调度语义
这一步的目标:
- 明确谁创建 runtime
- 明确谁拥有 runtime
- 明确谁负责释放 session/provider/client 等资源
这一阶段的 lifecycle 语义也已经定死:
start():让一个AgentLoop实例进入运行模式- 运行模式下:所有外部任务只能走
submit_direct() - 运行模式下:不允许外部再直接调用
process_direct() stop():instance-scoped,只停止当前这个AgentLoop实例stop()不是 session-scoped,也不是 platform-scopedstop()调用后拒绝新任务,已入队任务收尾退出stop()/shutdown()应支持 graceful timeout,必要时允许 force cancelclose():只有在实例已停止后才能释放 runtime 资源
这一阶段也明确了模型配置的归属:
- 大模型 provider / api_key / api_base 属于 backend runtime config
- Web / Gateway / Channel 不单独保存模型密钥
- 前端请求不传 API Key,只传
message/session_id/user_id等业务输入 - Docker 单实例部署时,每个用户 sandbox 读取自己的实例配置
- 默认使用新 Beaver 实例目录:
/root/.beaver/config.json/root/.beaver/workspace
- 新 Beaver 命名优先使用:
BEAVER_CONFIG_PATHBEAVER_HOME/config.json
- 兼容迁移期旧命名:
NANOBOT_CONFIG_PATHNANOBOT_HOME/config.json
app-instance 镜像也已经切到新 Beaver 后端:
- Dockerfile 只安装
backend/beaver - 不再复制旧
backend/nanobot、backend/bridge、vendoredswarms - entrypoint 通过
python -m uvicorn beaver.interfaces.web.app:create_app --factory启动 Web - 容器内默认配置与 workspace 使用:
/root/.beaver/config.json/root/.beaver/workspace
6.2.1 Web / Gateway 现在如何接这套 lifecycle
这一层现在已经开始落成真正的宿主层,而不是只停留在文档占位:
-
beaver/interfaces/web/app.py- FastAPI lifespan 启动时:
- 创建或接收
AgentService - 如果 Web 自己创建 service,则
await service.start()
- 创建或接收
- Web 层现在已经有最小正式 schema:
WebChatRequestWebChatResponseWebStatusResponse
- Web 请求处理时:
- 用结构化 schema 校验输入
- 只允许走
await service.submit_direct(...) - 将常见 runtime / config 错误收成明确的 HTTP 层错误
- 外部注入但尚未进入 running mode 的 service,返回
503
/api/ping- 返回
status/running/mode - 不会为了 health check 额外 boot runtime
- 返回
- app 关闭时:
- 如果 Web 自己创建 service,则
await service.shutdown(timeout_seconds=5.0, force=True)
- 如果 Web 自己创建 service,则
- 如果 Web 自己接管 lifecycle 且
start()失败:- 立即
close()做 startup cleanup
- 立即
- FastAPI lifespan 启动时:
-
beaver/interfaces/gateway/main.pyrun_gateway()启动时:- 如果 gateway 自己创建 service,则
await service.start()
- 如果 gateway 自己创建 service,则
- 持有最小
MessageBus - 可选接收
ChannelManager/ channel adapters ChannelManager和channels参数二选一:- 传
ChannelManager:外部提前配置好 channel - 传
channels:gateway 内部创建ChannelManager并注册这些 channel
- 传
- inbound 流向:
- channel adapter 发布
InboundMessage MessageBus.inbound- gateway bridge 常驻消费
- 调
await service.handle_inbound_message(...)
- channel adapter 发布
- outbound 流向:
AgentService内部完成InboundMessage -> OutboundMessage映射- gateway bridge 将结果写回
MessageBus.outbound - 如果启用了
ChannelManager,则分发给对应 channel adapter - 未启用
ChannelManager时,保留直接消费bus.outbound的最小测试能力
- 同时等待
stop_event - 停机时:
- 先尝试
await service.shutdown(timeout_seconds=5.0, force=True) - 再等待 bridge 协程收尾;必要时取消 bridge
- 再等待 outbound dispatch 协程收尾;必要时取消 dispatch
- 先尝试
- 如果 gateway 自己接管 lifecycle 且
start()失败:- 立即
close()做 startup cleanup
- 立即
- 未处理完的 inbound:
- 不再静默丢弃
- 会被冲刷成结构化 outbound error
-
beaver/foundation/events/message_bus.py- 已经补了最小:
MessageBusInboundMessageOutboundMessage
- 当前只做双队列桥接:
inboundoutbound
- 还没有 broker / topic routing / retry / persistence
- 已经补了最小:
-
beaver/interfaces/channels/*- 已经补了最小 channel adapter 层:
ChannelAdapterChannelManagerMemoryChannelAdapter
- 当前 channel 职责很窄:
- 把外部输入发布成
InboundMessage - 接收并投递
OutboundMessage
- 把外部输入发布成
MemoryChannelAdapter只用于本地测试和内嵌接入,不是正式消息 broker
- 已经补了最小 channel adapter 层:
所以现在已经明确:
- Web / Gateway 属于宿主层
- 它们不直接 new
AgentLoop或绕过运行模式 - 它们复用:
start()submit_direct()stop()shutdown()
- ownership 语义:
- 自己创建的
AgentService:自己负责 lifecycle - 外部注入的
AgentService:默认不自动 start/shutdown,除非显式要求接管
- 自己创建的
- gateway 已经从“只会常驻等待”推进到“最小消息桥接层”
- external inbound message
- channel adapter
MessageBus.inboundservice.handle_inbound_message(...)MessageBus.outbound- channel adapter outbound delivery
但这一阶段还没做:
- realtime streaming
- platform-level supervisor
- 更复杂的 bus 语义(retry / routing / persistence)
- 外部真实 channel adapter
6.3 第三步:回填 bus 模式
实现:
beaver/foundation/events/message_bus.pybeaver/engine/loop.py::run
参考旧逻辑:
backend-old/nanobot/agent/loop.py
需要补的函数:
- 从 inbound 读取消息
- 通过
AgentService.handle_inbound_message(...)映射到 runtime 调用 - 发布 outbound
当前这一步的最小收口方式已经确定为:
MessageBus只负责协议和队列gateway只负责宿主、常驻消费和 channel 分发InboundMessage -> AgentRunResult -> OutboundMessage的映射收口在AgentServiceAgentLoop继续只关心 agent 执行内核,不直接感知 bus 协议
注意:
只有在 process_direct() 稳定,并且 6.1 / 6.2 已经把 Session-first + lifecycle 骨架立住后,才做 run() 的长循环版本。
6.4 单 agent lifecycle 如何扩展到 team
这里也先把关系写清楚,避免后面 team 层重走弯路:
- team 不会共用一个
AgentLoop来跑所有成员 - 每个 team member 都应该是一个独立的
AgentService / AgentLoop实例 - 每个 member 自己有:
start()submit_direct()stop()close()
- team coordinator 在上层管理这些 member 实例,而不是绕开它们的 lifecycle
- 因此当前这套 lifecycle 首先是 member-level lifecycle,后面才能往 team-level / platform-level 扩
7. 第四施工阶段:加入 delegation 和单机 subagent
现在才开始动 multi-agent。
7.1 先做 registry
实现:
beaver/coordinator/registry/models.pybeaver/coordinator/registry/workspace_store.pybeaver/coordinator/registry/agent_registry.pybeaver/coordinator/registry/local_subagent_store.py
来源:
backend-old/nanobot/agent/agent_registry.pybackend-old/nanobot/agent/subagents.py
7.2 再做本地单机 delegation
实现:
beaver/engine/runtime/local_runner.pybeaver/coordinator/delegation/manager.pybeaver/coordinator/delegation/events.pybeaver/coordinator/delegation/announcement.py
来源:
backend-old/nanobot/agent/subagent.pybackend-old/nanobot/agent/delegation.py
这一阶段的范围:
- 先支持
spawn_subagent - 先支持 local delegation
- 暂不急着接 swarms team
完成标准:
- 主 agent 可以调用子 agent
- 子 agent 与主 agent 复用同一个
AgentLoop - 只是 profile / toolset / prompt context 不同
8. 第五施工阶段:接回群组讨论和流程化 team
这阶段才开始回收旧 agent_team 和 swarms bridge 的成果。
8.1 先做 team types / planner / policy
实现:
beaver/coordinator/team/types.pybeaver/coordinator/planner/swarms.pybeaver/coordinator/backends/swarms/policy.py
8.2 再做 bridge / adapter
实现:
beaver/coordinator/backends/swarms/bridge.pybeaver/coordinator/backends/swarms/adapter.pybeaver/coordinator/backends/swarms/runtime.py
注意:
- 不再引入
third_party/ - 不再允许旧式
sys.path注入 swarms必须作为 adapter/backend,而不是平台内部结构
8.3 最后做 orchestrator
实现:
beaver/coordinator/team/orchestrator.pybeaver/coordinator/team/target_resolver.pybeaver/coordinator/team/provisioning.py
这一阶段完成后,才算真正恢复:
- 群组讨论
- 流程化 team
- skills 约束下的 multi-agent 执行
9. 第六施工阶段:最后才拆入口层
这时候再拆 CLI / Web,成本最低,也最稳。
9.1 CLI
从:
backend-old/nanobot/cli/commands.py
拆到:
beaver/interfaces/cli/main.pybeaver/interfaces/cli/commands/agent.pybeaver/interfaces/cli/commands/web.pybeaver/interfaces/cli/commands/cron.pybeaver/interfaces/cli/commands/providers.pybeaver/interfaces/cli/tty.py
9.2 Web
从:
backend-old/nanobot/web/server.py
拆到:
beaver/interfaces/web/app.pybeaver/interfaces/web/deps.pybeaver/interfaces/web/realtime.pybeaver/interfaces/web/auth.pybeaver/interfaces/web/routes/*.pybeaver/interfaces/web/schemas/*.py
只有在 engine / services 已稳定后,Web 才值得拆。
10. 第一批真正建议开工的文件
如果现在立刻开始干,建议按下面顺序提交,不要跳。
提交 1:foundation 前置件
文件:
beaver/foundation/config/schema.pybeaver/foundation/config/loader.pybeaver/foundation/config/paths.pybeaver/foundation/events/messages.pybeaver/foundation/events/message_bus.pybeaver/foundation/events/process.pybeaver/foundation/models/run_result.pybeaver/foundation/utils/helpers.py
提交 2:session + provider 基础
文件:
beaver/engine/session/models.pybeaver/engine/session/manager.pybeaver/engine/session/store.pybeaver/engine/session/search.pybeaver/engine/providers/base.pybeaver/engine/providers/registry.pybeaver/engine/providers/factory.py- 至少一个真实 provider
提交 3:context + tool registry
文件:
beaver/engine/context/builder.pybeaver/tools/base.pybeaver/tools/registry/tool_registry.py- 最小 builtins
提交 4:第一版 AgentLoop
文件:
beaver/engine/loader.pybeaver/engine/loop.pybeaver/services/agent_service.py
目标:
- 跑通
process_direct
提交 5:memory / skills 正式接入
文件:
beaver/memory/*beaver/skills/catalog/*beaver/skills/resolver/runtime.pyengine接入改动
11. 第一阶段验收清单
在开始 Web / delegation 之前,必须满足以下条件:
beaver.interfaces.cli.main能启动一个最小 loopAgentLoop.process_direct()可用- session 历史能读写
- provider 能完成一次普通回复
- provider 能触发工具调用
memorytool 可写- 新 session 能读到 frozen snapshot
session_search能直接搜 session transcript- skills 能注入到 system prompt
如果这 9 条没过,不要进入下一阶段。
12. 施工时要避免的错误
12.1 不要先拆 Web
web/server.py 很大,但它不是第一施工点。
先拆它,只会让你在 engine 还没稳的时候同时维护两套未完成装配。
12.2 不要先做 team orchestration
multi-agent 很吸引人,但没有稳定的单 agent runtime,team 层只会把问题放大。
12.3 不要把旧 memory consolidation 直接搬过来
新 memory 基线已经确定是 Hermes 风格:
- CRUD memory tool
- frozen snapshot
- session_search
所以旧的 _consolidate_memory() 路径不能原样迁。
12.4 不要在新 backend 中继续扩散 nanobot 命名
允许在迁移说明文档里引用旧路径,但新代码文件、类名、导出都必须收敛为 beaver。
13. 一句话施工结论
从 engine/session -> providers -> context -> tools -> AgentLoop.process_direct() 这条最小运行时主链开始施工。
先把单 agent 运行内核打通,再把 memory / skills 接进去,再做 delegation / team,最后才拆 CLI / Web。