agent24 架构基线

阅读快照:阅读快照(main @ bc923205f4a773836d0f872ced63c98f0cb653be,2026-11-12)。下文路径相对 agent24 仓库根。链接固定到该 SHA。

这份基线回答一个问题:当前架构能不能朝某个方向演进。判断时先看「扩展点」,再看「它明确还没接上的缝」。库里已有的类型,不等于产品路径已经调用它。

1. 目的与产品定位

上游 README 把 Agent24 写成 shell-agnostic 框架:Rust 内核加 daemon 提供个人 AI 助手的承载,外壳只经 HTTP/WS 连接。自带 Electron 参考壳,也允许 Tauri 等其他壳挂上。应用方从本框架 fork,再装场景模块。见 README.md 第 1-17 行。

根 package.json 的描述是另一句产品话:「24/7 personal & community agent platform (Electron shell + daemon + protocol)」,版本 0.6.0。见 package.json 第 2-5 行。

researcher 的 T004 记录作者口述:Agent24 是基础办公套装,Sin90 / Cos72 是可加载 OS。那是 2026-10 的产品说明,源码依据更早。本基线不改那份说明。代码侧能对上的是:桌面壳、Open Design sidecar、Documents 进程外模块、Knowledge 宿主接口、微信入站桥,都已经长在同一个 daemon 上。评估「要不要借鉴某项能力」时,要写明假设的是框架内核,还是这套办公壳的用户体验。两者的改动落点不同。

远程消费仍是目标态。daemon 绑定 127.0.0.1,没有公开 bind。见 serve_steps.rs 第 965-985 行。README 自己也写了跨设备要自备隧道和认证。

2. 技术栈

层 现状 证据
内核语言 Rust,workspace edition 2024,Apache-2.0 rust/Cargo.toml 第 78-81 行
安全约束 unsafe_code = forbid;非测试代码 unwrap_used / expect_used = deny rust/Cargo.toml 第 88-97 行
daemon agent24d 0.6.0,axum 路由,tokio 运行时,reqwest 调模型 agent24d/Cargo.toml 与路由表
持久化 sqlx + SQLite,WAL,外键,busy_timeout 5 秒 agent24-store/src/lib.rs 第 274-280、356-363 行
桌面 Electron + React 18 + Vite,Node >=22,pnpm 9.12 根 package.json 与 apps/desktop/package.json
协议 手写 protocol/openapi.yaml(本次约 8166 行)+ Rust 导出的 WS 事件 schema,生成 packages/api-client 的 TS 类型 README 架构图;CI 的 pnpm gen:api 约定见 AGENTS.md
Python ML worker 只有 Rust 侧契约。agent24-worker 不在 default-members,普通 cargo build 会跳过 rust/Cargo.toml 第 39-42 行

packages/node-daemon 仍是 v1 协议的 mock / 参考实现,承载 TS CapabilityModule。它不是桌面默认后端。桌面默认拉起 Rust daemon。见 README 第 45-55 行,以及下文第 9 节的拉起方式。

3. 模块与包布局

Rust workspace 成员见 rust/Cargo.toml 第 1-37 行。按职责分成四块:

仓库自带分层图把依赖方向定成 L0 契约、L1 纯领域、L2 存储、L3 能力服务、L4 agent loop、L5 agent24d、L6 进程外模块、L7 外壳。该图生成于 f818b0bf,见 docs/ARCHITECTURE-LAYERS.md 第 17-72 行。本次没有重跑 cargo metadata。新增 crate 时以那份脚本和当前 Cargo.toml 为准,不要只改叙述。

TypeScript 侧:apps/desktop,以及 packages/ 下的 api-client、node-daemon、wechat-bridge、data-dir、contract-tests、documents-accept。pnpm workspace 只包含 apps/* 和 packages/*。

vendor/ 有两个 submodule(xiaoheishu、gemma-chrome-translate)。CI 明确 submodules: false。见 .github/workflows/ci.yml 第 33 行。

4. 核心 agent loop 与编排

产品路径上的一次运行由 RunManager 持有。它拼在一起的是 store、ModelRouter、ToolRegistry、可选的 WorkspaceService 与 SessionMemory、事件 sink,以及一棵取消令牌树。见 agent24-agent/src/lib.rs 第 580-609 行。

start_run 的语义是先落库再后台执行:

  1. 生成 run_<ulid>,状态 Queued。取消令牌先于行可见写入内存表,避免客户端列出 run 时还没有令牌。见第 1545-1571 行。
  2. 带 workspace_id 的 run 走 workspace admission,和租约一起写入。见第 706-725 行。
  3. 入站 run 把 session、run、inbound_runs 放进同一事务,重复事件不会再开一条。见 start_run_inbound 第 1497-1509 行。
  4. execute 放在独立 task 里。panic 会把 run 打成失败并清掉令牌,避免永远停在非终态。见第 1612-1638 行。

execute(第 2295 行起)把 run 转成 Running,发出 RunStarted,再做断言召回。召回失败只记警告,不阻断这次模型调用。随后进入 run_loop。

run_loop(第 2923-3159 行)是共享的 completion 与工具回合,上限 MAX_ITERATIONS = 10(第 62 行):

调度器是另一条进入 loop 的路。agent24-scheduler 用 cron / every / at 触发 run。due 的 next_run_at 在触发前就推进并落库,宕机期间错过的 tick 只补一次,不回放。见 agent24-scheduler/src/lib.rs 第 1-20 行。agent 还可以用 SelfWakeTool 给自己建一次性 schedule。见 serve_steps.rs 第 596-600 行。

探索子代理是结构上的只读:它拿到的 registry 只有 fs_read,没有写、shell、网络,也没有它自己。见 agent24-tools/src/lib.rs 第 1859-1879 行 与注册处第 585-594 行。

agent24-decide 没有接到这条 loop。agent24d 的依赖注释写明:目前只提供 GET /api/v1/decide/profile,决策级联仍是未接线的 D1。见 agent24d/Cargo.toml 第 28-29 行。

5. 工具与插件

内核工具是 Tool trait:JSON Schema 参数、风险等级、超时、出口授权、审批绑定。默认超时 30 秒。见 agent24-tools/src/lib.rs 第 626-687 行。

默认内置集只有四个:http_fetch、fs_read、fs_write、shell_exec,工作区目录作读写白名单和 shell cwd。见第 1850-1857 行。用户可以把第三方工具的风险调松,但不能把 builtin 或 module 工具调松。见 effective_risk 第 1887-1918 行。

启动时还会挂上:已安装领域 OS 声明的模块工具、Knowledge 工具(仅当 run 带了 binding)、~/.agent24/mcp.json 里的 MCP 工具、explorer、self-wake。见 serve_steps.rs 第 503-619 行。

MCP 工具适配成同一个 Tool。名字带服务器前缀。风险类固定 External,调用前走 authorize_egress_if_restricted。服务器自己不能把风险降下来。见 agent24-mcp/src/lib.rs 第 253-343 行。daemon 同时暴露一个 MCP server,把 run 能力向外提供。这是「内核当 MCP 客户端」和「内核当 MCP 服务端」两条不同的边。

审批在 ApprovalBroker。默认没有 Guardian,gated 调用等人决定。Guardian 只自动放行它判成低风险的调用,模型不可用或高风险仍落到人。会话级 grant 有上限 1024,溢出时清空,下次再问。见 agent24-policy/src/lib.rs 第 242-257 行。

TS CapabilityModule 仍只活在 node-daemon 内部,没有对外 SDK。和 Rust Tool 不是同一条加载链。README 第 148-176 行写了这个边界。新能力若要进桌面默认路径,应走 Rust Tool、模块工具声明,或进程外领域 OS,而不是再写一个只在 mock daemon 里可见的 TS 模块。

技能契约是另一件事。ADR-040 已接受为设计约束,并写明仍是零代码:没有技能加载器、版本表、评测跑器。见 docs/adr/ADR-040-skill-contract-v1.md 第 4-14 行。仓里的 skills/knowledge-entry/ 是一份技能内容,不是运行时。

6. 记忆与状态

有三层,不要混成「已经有一套分层记忆产品」:

  1. 权威业务状态在 agent24-store 的 SQLite(agent24.db):session、run、消息、审批、schedule、审计。Store 自己写明它只校验和记账,不建立调用方授权。见 lib.rs 第 221-238 行。审计日志带 prev_hash / hash 链。
  2. 会话工作记忆是 canonical session。verbatim 超过 40 条就折叠成滚动摘要,保留最近 20 条,摘要硬顶 4000 字符。见 agent24-memory/src/session.rs 第 1-48 行。RunManager 上的 memory 可以是 None,那时 run 只从当前 prompt 开始。加载失败时,普通错误退化为空上下文;导入不完整则失败,避免假装这段历史不存在。见 lib.rs 第 904-939 行。
  3. 跨会话个人记忆是断言账本。产品路径的 recall 走 KvStore::retriever(),而这个方法返回的是 FtsRetriever。见 session_memory.rs 第 153-172 行 与 memory/src/lib.rs 第 576-578 行。暂停开关关掉后,召回直接返回空。REST 在 /api/v1/memory/assertions 与 /api/v1/memory/settings,owner 不是请求参数。

向量检索是库原语。VectorRetriever 注释写明真正的 OmlxEmbedder 还在等 ML worker,这一片交付的是 trait、存储、余弦和 FTS 回退,用的是测试 embedder。见 vector.rs 第 1-25 行。本次在 agent24d 产品路径上没有搜到 VectorRetriever 的调用。

事件日志、巩固器、condenser、artifact 都在 agent24-memory 里导出。它们是可调用的库,不是「loop 每轮都会跑」的证明。README 写明 L3 ATIF 轨迹、SkillBank、自进化框架没有对应符号。本次在 agent24-agent 里搜索 SkillBank / atif 也没有命中。

领域 OS 的记忆隔离靠 ScopedMemory 句柄和 (org, space) 键前缀。README 同时写明:进程外模块与内核同 UID、无沙箱,句柄保证挡不住直接打开 sqlite 文件。这是当前安全模型的一部分,不是文档笔误。

7. 模型与 provider 抽象

最小契约是 ModelProvider:name、outbound_destination、complete、models。没有路由,也没有通用重试。见 agent24-models/src/lib.rs 第 225-246 行。

实现体是 OpenAiCompatProvider,用来接 oMLX、Ollama、LM Studio 和远程 OpenAI 兼容端点。标成 Local 的实例必须 loopback_only():不读代理环境变量,不跟随重定向。见第 332-354 行。思考开关按实例打开,不按 tier 推断。

ModelRouter 在 trait 之上做档位选择。TaskProfile 只有 Privacy 和 Complexity。LocalOnly 永不产生 Remote。未知 tier 字符串解析成 Remote。见 router.rs 第 30-98 行。Unavailable 进入指数冷却,上限默认 60 秒,成功则清零。

daemon 启动调用的是 ModelRouter::from_env()。见 serve_steps.rs 第 322 行。from_env 的实际分支:

README 仍写「生产启动只构造 oMLX 与 Ollama,iDoris 在 P4 门后」。代码已经有 IDORIS_URL 这条旁路,以及 /api/v1/idoris/* 的安装、连接、启停路由。见 server.rs 第 966-988 行。评估模型相关方案时以 from_env 为准,并注明 README 这句话落后于这次提交。

agent loop 默认把 TaskProfile::default() 交给路由,再用 run 的来源策略把它收成 LocalOnly。见 lib.rs 第 691-704 行与 2966-2968 行。没有独立的「按任务自动生成复杂度」步骤。Lora tier 在枚举里存在,from_env 没有注册 LoRA provider。

8. 接口与 API

外壳只应走 v1 REST + WebSocket。路由表在 build_router_with_modules。领域 OS 路由先折进 kernel,再套 auth layer。注释写明顺序不能反,否则模块路由会变成无认证空洞。见 server.rs 第 947-954 行。

这次读到的主要面:

见 server.rs 第 959-1198 行。

认证有两种。桌面在非 Windows 上拉起 Rust daemon 时用 capability 模式:serve --port 0 --auth-mode capabilities --host-bootstrap-stdio。Windows 和 node mock 仍是 legacy 单 token。见 backend-manager.ts 第 141-145 行与第 420 行。分层文档说路由级资源授权还没做。本次没有逐条路由复核授权中间件,所以这里只记录桌面启动参数,不把「每个路由都已按资源授权」写成事实。

微信桥是开发用传输。它把完成的用户文本 POST 到 /api/v1/inbound/events,回复固定「已记录」。准入、run、审批留在 daemon 和桌面。见 packages/wechat-bridge/package.json 第 5 行 与 bridge.ts 第 1-4 行。这次 main 的最新合并就是这条桥(PR #1819)。

packages/api-client 只有生成的类型,不是一个带重试的 HTTP SDK。外壳自己发请求。

9. 扩展点

后续评估「架构是否撑得住」时,优先对到下面这些缝。每一条都写了它今天能做什么,以及它还不能代替什么。

扩展点 今天能做的 还不能代替的 证据
ModelProvider + ModelRouter 加一个 OpenAI 兼容端点,并放进 Local / Remote / Lora 运行时设置页、按任务自动画像、独立 Claude 协议、LoRA 训练 from_env 与 TaskProfile
Tool + ToolRegistry 内置工具、MCP、模块工具、只读子代理 不受审批的任意插件,或在 node-daemon 里写完就出现在桌面 serve_steps.rs 注册顺序
进程外领域 OS agent24 os install,独立进程,自己的库,/api/v1/<name>/* 进程内 DomainModule 的生产实现。README 写明生产级 in-process 实现已经没有,Sin90 已迁出 README 第 117-119 行
附着模块 用户自己启动的进程,经 agent24 os attach 注册,反向命令走同一条连接 附着模块自己的入站 REST 面 server.rs 第 1125-1170 行
sidecar 桌面 SidecarManager 托管 Open Design(TS,POSIX) Rust agent24-sidecar-host。分层文档写它未接入 Desktop,本次未看到 desktop 调用它 docs/ARCHITECTURE-LAYERS.md 第 374 行,需随代码再核
调度器 cron 触发 run,模块回调投递,self-wake 把任意外部事件都变成受监督的后台作业 scheduler 文件头
入站事件 allowlist、policy、微信桥写入 /inbound/events,再 start_run_inbound 统一的多入口准入。分层文档把 Nostr、语音、手机遥控列成未完成的 Q12 微信桥与分层文档第 390 行
记忆库 FTS 召回、摘要压缩、断言账本、向量原语 产品路径上的语义召回、技能库、自进化 retriever() 与 vector.rs
组件锁 iDoris / Hyphae 的组件安装与状态 技能加载器。ADR-040 仍是零代码 ADR-040
协议 手写 OpenAPI + 事件 schema 生成 TS 由 Rust 类型生成 OpenAPI。分层文档列为计划项 B4 分层文档第 151 行

两条扩展不要并成一条。领域 OS 决定「这台 agent 是什么产品」,能力模块 / 工具决定「多会一件事」。README 第 79-110 行把权限词表仍是两套这件事记成未还的债。

10. 与性能相关的设计

这些是代码里已经写成约束的点,不是压测结果。本次没有跑基准。

11. 测试与部署

CI(.github/workflows/ci.yml)包括:Hyphae lock 校验、pnpm typecheck / lint / test、Open Design 打包脚本测试、Rust fmt + clippy + test(超时 30 分钟)。另有桌面 release、Windows check、sidecar Windows workflow。提交约定在 AGENTS.md:Rust 门禁是 cargo fmt --all --check、cargo clippy --workspace --all-targets -- -D warnings、cargo test --workspace。改了 apps/ 或 packages/ 才跑 pnpm。改了 protocol/ 要 lint OpenAPI 并确认生成物无 diff。

发布流程把集成分支 ab/<方向> 上的 task PR 和进入 main 的 release PR 分开。release 要求按用户路径做 CLI 用例。纯界面除外。Open Design 下载、presence、Comm 私钥、Knowledge 凭据只在桌面验收。见 AGENTS.md 第 12-27 行。

桌面打包脚本会编 agent24d 与 agent24-cli。macOS 额外编 documents 模块。见 apps/desktop/package.json 的 build:mac / build:win / build:linux。版本号 0.6.0。

本次没有执行这些门禁,也不把 CI 绿写成这次阅读的结论。

12. 已知限制与技术债

下面每条都在本次源码或该 SHA 的文档里有直接句子。未复核的分层文档条目标了「文档声称」。

  1. daemon 只绑 127.0.0.1。多端远程消费需要外加隧道和认证。
  2. ModelRouter::from_env 的默认槽是 oMLX 与 Ollama。IDORIS_URL 会换成另一条网关路径。没有通用的远程 provider 注册表,也没有 LoRA 槽的生产注册。
  3. 主 loop 的 TaskProfile 默认简单档,只会被来源策略收紧为 LocalOnly。复杂度不会自动升到 Complex。
  4. 产品召回是 FTS。向量检索的真实 embedder 未接上,daemon 也未调用 VectorRetriever。
  5. 技能运行时不存在。ADR-040 自己写「零代码」。
  6. migration_coordinator::ENABLED 为 false。main.rs 第 115 行用编译期断言锁住,生产启动不调用 run。见 migration_coordinator.rs 第 19-20 行。
  7. knowledge-store feature 默认关闭。注释写 knowledge.db 没有生产调用方。见 agent24d/Cargo.toml 第 14-16 行。Knowledge 工具只出现在带 binding 的 run 上。
  8. 进程外模块无沙箱,和内核同 UID。记忆句柄不是安全边界。
  9. 领域 OS 与能力模块的权限词表仍是两套。
  10. Windows 桌面仍走 legacy 单 token。workspace-bound run 的完整路径文档标为 Unix。本次只复核了 selectedAuthMode。
  11. agent24-worker 无产品调用方。Python ML 服务未实现。语义召回、转写、LoRA 都卡在这条边上。
  12. loop 与 HTTP 路由集中在两个超大文件。协议文件也超过 8000 行。小改动的冲突和审查成本高。
  13. README 的「今天生产只走 oMLX/Ollama、iDoris 在 P4」与 from_env 的 IDORIS_URL 分支不一致。引用 README 能力表之前先对代码。
  14. 文档声称、本次未逐行复核:capability 的路由级资源授权、OpenAPI 改由 Rust 生成、领域 OS 包签名与文件哈希、sidecar-host 未接入桌面、Web3 身份未接线。来源是 ARCHITECTURE-LAYERS.md 第 366-392 行,对应更早的 f818b0bf。要用这些条做结论时,先在当前 SHA 上重读对应文件。

13. 评估新方向时怎么用

收到一条外部材料时,用这份基线做三步,详细分析写进该条目的 analysis.md,结论写进 recommendation.md。

  1. 它落在哪条扩展点上。对得上表中某一行,就引用那一行的文件。对不上,写明要新开的缝,不要把它说成「插件一下就行」。
  2. 产品路径有没有调用。crate 里有类型,只说明库原语在。要看到 agent24d 启动或 run_loop 调用,才能说当前架构已经支持。
  3. 它会不会削弱已经守住的约束:LocalOnly 不外泄、工具审批、来源标签、loopback 绑定、取消在非终态仍然有效、WAL 与审计链。削弱任何一条都要在建议里写成风险,而不是实现细节。

基线过期的信号:main 的 SHA 与 阅读快照 不同,或本文件引用的函数在新提交里改了签名。那时先更新快照和受影响的小节,再评估新条目。不要用 2026-10 的 54cec44 结论覆盖这次基线,也不要让这份基线回写 T004 至 T008。