一个 Agent 是怎样被创建出来的?
从创建 Session 到 Agent Ready,中间到底发生了什么?
“Host 已启动”和“Agent 已经可以接任务”之间,还有一段重要过程。Web 页面创建 Session 时,系统需要确定 Preset、准备作用域、挂载能力,再把 Agent 发布到活跃注册表。
新建与恢复,起点不同,后半程相同
用户点击“新建会话”时,系统要创建一条空 Session;用户重新打开旧会话时,系统要从持久化存储加载原来的事件日志。两条路线的起点不同,但都会进入同一段安全装配:准备 Agent、运行 Preset setup,最后一次性发布。
| 场景 | Session 从哪里来 | Preset 从哪里来 | 后续共同动作 |
|---|---|---|---|
| 新建 | 创建新的 Session id 与空事件日志 | 用户选择,或使用当前默认值 | 准备作用域、挂载 Preset、发布 Agent |
| 恢复 | 从持久化 Provider 读取已有日志 | 从 Session 已记录的元数据解析 | 准备新作用域、重新连接原组合、发布 Agent |
注意,“恢复”不是把上次内存里的 Agent 对象找回来。那个对象已经随进程退出而消失了。系统恢复的是 Session 的事实历史,再围绕这份历史创建一个新的运行中 Agent。
第一站:先决定 Session 属于哪个 Preset
用户创建会话时,可以明确选择 Preset,也可以不填,让系统采用默认 Preset。
Web API 内部的 composeAgent() 会先解析这个选择。为什么要先解析?因为 Session 的头部元数据需要记录它采用的 Preset,不能等 Session 已经写入后再临时发现。
如果部署根本没有安装 Preset 管理器,composeAgent() 也可以返回一个只使用 Host 公共组合的 setup。这保留了简单部署的可能。
如果存在 Preset 管理器,它会确认请求的 id 有效,并返回两样东西:
- 要写进 Session 的 Preset id;
- 创建 Agent 时执行的 setup 回调。
第二站:先创建受控的 Agent 作用域
Agent Loop 的 createAgent() 不会直接把一个半成品 Agent 暴露出去。它先创建属于该 Agent 的作用域和 Session,再运行 setup。
这个顺序很重要。Preset 需要拿到 Agent 作用域,才能把它连接到正确的 Preset 作用域。
可以把 Agent 作用域想成新员工的身份卡。工具注册和事件监听会根据这张身份卡判断:这个 Agent 能看到什么,哪些策略应该覆盖它。
第三站:确保 Preset 已经常驻挂载
setup 调用 AgentPresets.mount() 后,Preset 管理器会进入 ensureStanding()。
如果这个 Preset 已经挂载,并且 agent.cordis.yml 没有变化,就直接复用现有常驻挂载。
如果尚未挂载,它会:
- 创建一个 Preset 作用域;
- 读取 agent.cordis.yml;
- 让 Cordis Loader 挂载其中的插件;
- 等待组装成功;
- 保存这次挂载,供后续 Agent 复用。
如果配置文件损坏或依赖不完整,这一步会失败,Agent 创建也会整体回滚,不会留下一个工具只装了一半的 Session。
第四站:把 Agent 作用域接到 Preset 上
Preset 准备好以后,bindScopeParent() 会建立作用域父子关系:
Agent → 选中的 Preset → Host
从此,这个 Agent 查询 ctx.tools 或 ctx.systemPrompt 的注册时,就能看到 Preset 和 Host 两层贡献。

图 8-1:相同 Preset 只需准备一份常驻组装,多个 Agent 通过各自作用域加入。
图里最值得注意的是 Host 位于最下方。Preset 不是另起炉灶,它站在第一阶段已经准备好的公共底座上。
第五站:setupAndPublish 让 Agent 正式可见
所有 setup 都成功以后,Agent Loop 才会把 Agent 发布到 ctx.agents 的活跃注册表。
此时 UI/API 才能稳定找到它、向它发送消息、订阅状态。这样可以保证外部世界看见的 Agent 都已经具备完整能力。
发布本身也不是简单地向一个 Map 写入对象。系统会先让 Session 和 Agent 进入各自注册表,发出创建事件,再通知 session-start,最后启动具体驱动。对外部插件来说,agent/created 是一个可靠信号:它收到的不是半成品,而是已经完成作用域 setup 的 Agent。
这是一种很常见但容易忽略的设计:先在内部完成装配,再一次性发布成品。
如果反过来先发布,再慢慢加载 Preset,就可能出现 UI 已经提交任务,但工具还未注册完成的竞态。
创建过程为什么要做成一笔“事务”
假设 Preset 中一共有十个插件,前九个已经注册了工具,第十个因为包不存在而加载失败。系统不能把前九项留在 Context 中,再返回一个“勉强能用”的 Agent,否则用户看到的能力会取决于失败发生在哪一步。
因此创建过程会保留一整套回滚路径:
- Session 先处于准备状态,不立刻进入公开存储;
- Agent 与它自己的作用域也先放在私有区域;
- setup 尝试挂载 Preset,并等待相关插件可用;
- 任一步抛错,就停止驱动、卸载已注册的 effect,并释放准备中的 Session;
- 只有全部成功,Session 与 Agent 才一起进入公开注册表。
这和办理新员工入职很像:账号、门禁、电脑和部门权限要么全部办完再通知到岗,要么撤销已办步骤,不能让一个只有半张门卡的人出现在正式排班里。
并发创建同一个 Session id 时也采用类似原则。多个请求可以同时准备,但最终进入注册表时只能有一个成功;失败方会清理自己的私有资源,不会覆盖已经发布的 Agent。
用时序图看,这一阶段有一个很明确的先后关系:先解析 Preset,再在内部完成挂载和作用域连接,最后才把 Agent 发布到 ctx.agents。虚线返回表示上一项工作已经完成。
恢复旧 Session 为什么还要重新装配
进程重启后,内存里的 Agent 消失了,但 SessionEvent 和 Preset id 还在持久化存储中。
用户重新打开旧会话时,系统会读取日志里记录的 Preset,再走一次 Agent 创建与作用域连接。这样恢复出来的 Agent 才能继续使用产生原历史的那套工具和提示词。
Session 保存“发生过什么”,Preset 保存“这类 Agent 怎样组成”。两者配合,才能既恢复记忆,又恢复工作能力。
恢复时还有一个容易忽略的原则:应优先采用 Session 当初记录的 Preset,而不是当前界面上的最新默认 Preset。默认值可能已经从 standard 改成 minimal,但旧 Session 的历史里也许使用过 standard 才有的工具。如果恢复时擅自换组合,历史与当前工具目录就对不上了。
可以把恢复过程分成两条线:
事实线:持久化存储 → SessionEvent → 重建对话与工具历史
能力线:Preset id → 常驻组合 → 恢复工具、提示词与策略
两条线在新创建的运行中 Agent 里重新汇合。前者回答“过去发生了什么”,后者回答“现在应该用什么能力继续”。
读源码时,先抓住四个责任人
源码里的函数很多,第一次阅读不必逐行追。先认清四个责任人即可:
| 责任人 | 主要工作 |
|---|---|
| API 的 composeAgent() | 解析 Preset,并生成创建阶段要执行的 setup |
| ctx.agents.create() / resume() | 给上层提供稳定的创建与恢复入口 |
| Agent Loop 的 setupAndPublish() | 准备私有对象、执行 setup、成功后发布,失败则回滚 |
| AgentPresets.mount() | 确保 Preset 常驻组合可用,并连接 Agent 的父作用域 |
把这四个点连起来,就能读懂主线。其余代码大多是在保护取消、并发、持久化和资源释放等边缘情况。
把出生流程压缩成六步
新建或恢复 Session → 解析 Preset → createAgent() → ensureStanding() → bindScopeParent() → setupAndPublish()
下一课先不急着让 Agent 开工。我们来具体看看标准、PTC、极简和创造四种内置 Preset,分别会组装出怎样的 Agent,又适合处理哪些任务。
四种工作模式:同一套 Harness,四种 Agent
标准、PTC、极简和创造四种 Preset,分别适合什么任务,又改变了 Agent 的哪些能力?