第 20 章 Agent 架构设计:完整 Harness · Agent架构实操二十 开始测验

Agent 架构设计:工具调用、权限、记忆、压缩与 MCP 集成

前19章依次加入各种能力。第20章不发明「终极框架」,也不复制第二套 Loop。只做一件事:把已实现的能力接到同一组合根,用跨功能场景证明边界同时成立。
⏱ 约 15 分钟 🏁 full_harness 🔗 边界一致 +full_harness

🎯 学习目标

学完能答
  • 完整 Agent 的核心是功能数量还是边界一致?模型负责什么,Harness 必须拥有什么?
  • P20 新增了业务模块吗?full_harness 是什么?
  • 组合根如何检查对象身份?共享关系错误会怎样?
  • 单一循环的真实执行顺序?
  • AgentRunner.close() 如何关闭资源?什么顺序?
  • 为什么 Prompt 提醒 ≠ 运行时硬边界?

🧠 核心概念

点击展开。
1 · 核心不是功能数量,而是边界一致

模型只负责提出下一步动作。Harness 必须拥有真正的控制权:

  • ToolRegistry 决定当前能看到和调用哪些工具;
  • PermissionPolicy 在 handler 前执行硬权限判断;
  • HookRegistry 提供生命周期扩展,但不能放宽系统拒绝;
  • MemorySession、CompactionManager、DynamicPromptProvider 控制上下文;
  • RecoveryManager 处理 429/529/输出截断/输入过长;
  • SQLite Task、Protocol、Mailbox、Worktree 保存可恢复状态;
  • JobSupervisor、CronRuntime、TeammateRuntime、McpRuntime 持有长期资源;
  • AgentRunner.close() 按逆序尝试关闭全部资源。
原则 Prompt 可提示模型「不要写出工作区」,但真正阻止越界写入的必须是运行时权限和路径解析。MCP server 可在 description 声称只读,但权限只能来自本地可信策略。
2 · P20 不新增业务模块,只接 full_harness

full_harness 是整合验收标记,不对应新业务模块。它表示 P1-P19 全部 Capability 已在同一组合根中连接并通过交叉验证。

四处连接:(1)profiles.ts 末尾追加 full_harness 增量;(2)prompting.ts 末尾增加可选 runtime_status 段落,Provider 用 statusProvider 每轮重读;(3)bootstrap.ts 汇总 MCP 连接与后台 pending work,只给 P20 安装状态 Provider;(4)固定入口 ch20.ts 仍只 runProfile(P20, ...)。runtime_status 固定在提示词最后一段并参与缓存键——只改 status、其余不变时 cacheHits 加一,之前的正文不变;它是状态同步不是授权来源:模型看到 mcp_connections 不等于有权调用那个工具,权限事实源始终是本地 policy。

3 · 组合根检查对象身份
资源必须共享的对象原因
CronJobSupervisor、EventInbox调度和后台完成事件进同一 typed inbox
TeammateSupervisor、Cron、Inbox、Mailbox队友生命周期/消息/事件由同一 owner 管
ProtocolTeammate、Mailbox请求状态与实际投递参与方必须一致
Work stealingSQLite Task store、Worktree claim service自动认领与手动认领用同一租约边界
WorktreeSQLite Task store、Git runnerTask/claim/binding 独立但引用同一 Task
MCPLead 的 live ToolRegistry动态工具只进 Lead,下次请求可见
启动阶段失败 buildAgent() 检查对象身份。依赖虽类型正确,只要共享关系错误,也在启动阶段明确失败,不带着错误接线跑起来。
4 · 单一循环的真实执行顺序

所有能力都接到同一个 AgentRunner,执行顺序固定:

memory turn lifecycle (beginTurn 选择记忆)
-> render dynamic prompt (identity/tools/workspace/skills/memory/runtime_status)
-> recovery manager complete (429/529/截断/输入过长恢复)
-> prepare tool calls (JSON解析+Zod校验)
-> PreToolUse Hook
-> PermissionPolicy (硬边界+规则+审批)
-> dispatch (同步或后台)
-> PostToolUse Hook
-> 配对回填 tool result
-> tool result processor (大结果落盘)
-> history processor (snip/micro/摘要)
-> Stop Hook (最多续跑一次)

没有任何能力绕过这条链路。Cron 事件、Teammate 消息、后台 job 完成都通过 EventInbox 进入 event-only turn,走同一执行路径。事件去重两层:#seenEventIds 防进程内重投,store 里的 ack 防跨重启重放——至少一次投递 + eventId 去重,对模型表现成恰好一次。恢复矩阵(429 按 Retry-After、529×3 切 fallback、length 抬预算、过长反应式压缩)全部作用在冻结的 requestMessages 副本上,恢复过程一条都不进 canonical history。

5 · close() 逆序关闭资源

AgentRunner.close() 按逆序尝试关闭全部资源:MCP connections → stdio 子进程 → Teammate workers → Cron scheduler → JobSupervisor 后台 jobs → 持久化 stores。每个资源的 close 都是幂等且 try/catch 包裹的,一个资源关闭失败不影响其他资源关闭。

这保证进程退出时不留下孤儿子进程、悬挂连接或未刷盘的状态。CLI 入口在 run() 结束后统一调用 close(),把错误码映射到进程退出码。1 个资源关闭失败原样抛,多个失败组成 AggregateError 一次报全;#closed 只在全部成功时置位——失败后可重试 close(),但不能再 run()。

🏗 完整 Harness 架构总览

19 章能力分层:控制层 / 上下文层 / 数据层 / 资源层,全部接到同一 AgentRunner。
控制层 (AgentRunner 唯一 Loop)
ToolRegistry · PermissionPolicy · HookRegistry · RecoveryManager
执行顺序:prepare→PreHook→权限→dispatch→PostHook→配对回填→结果处理→历史处理→Stop
─────────────────────────────────────
上下文层
MemorySession · CompactionManager · DynamicPromptProvider · TodoTracker
SkillRegistry(两级加载) · SubagentTool(历史隔离)
─────────────────────────────────────
数据层 (可恢复状态)
SQLite Task DAG · JsonProtocolStore · FileMailboxStore · Worktree bindings
MemoryStore(.memory/manifest) · Artifacts(transcript/tool-result)
─────────────────────────────────────
资源层 (长期持有,close() 逆序回收)
JobSupervisor · CronRuntime · TeammateRuntime · McpRuntime
EventInbox(typed FIFO,统一事件入口)
─────────────────────────────────────
组合根 buildAgent(P20)
检查对象身份 · 共享关系错误启动阶段失败 · 不接收调用方拼好的宽松策略

P1-P20 能力累进

🔑 一句话总结

完整 Agent = 模型决策 + Harness 控制权 + 边界一致;Prompt 是软提醒,运行时才是硬边界

19 章能力接到同一组合根,用跨功能场景证明边界同时成立。模型只提动作,Harness 拥有真正的控制权:工具/权限/Hook/上下文/恢复/状态/资源,close() 逆序回收。

QA 测验