由浅入深看懂 Agent Runtime 的七天演进
这是我自己开发 Nosis 的历程记录。
项目地址:https://github.com/EvannZhongg/Nosis.git
素材来自项目自身的 Git 历史:从最初提交 e1d15a4(2026-09-09)到当前提交 8f7f019(2026-09-16),一共七天、131 个提交,其中 93 个触及了 agent_core 或 interfaces/bridge。
我一直想做一个属于自己的 Agent,但很长一段时间里都不知道从哪下手——对它的理解只停留在一些基本的调用运行逻辑上,至于工具、上下文、状态这些东西该怎么组织,完全没有概念。这次总算从头写了一遍,七天之后再回头看,几乎每个阶段的改动我都要重新想一遍,才说得清“当时为什么非改不可”。于是我把这段历程按时间顺序重抄了一遍——这篇笔记的叙述顺序就是我的理解顺序,由浅入深分三层:
- 先看懂形态:这个阶段的 Runtime 长什么样;
- 再看懂边界:哪些职责从谁手里交给了谁;
- 最后才看懂原因:为什么它非改不可。
具体到每一个阶段,我固定回答三个问题:
- 当时到底被什么问题卡住了?
- Runtime 的职责和边界发生了什么变化?
- 我的理解比上一阶段多了哪一层?
下面的记录,就是这三问一层层推进的过程。涉及的“Runtime”不只是 Agent.run(),还包括驱动一次执行所必需的上下文管理、Tool 系统、Session、持久化、授权、取消、子 Agent、MCP 和前端适配语义。
0. 先记住四句话
回头看,七天里最后沉淀出四条“架构不变量”。刚开工时我对它们完全无感,等把七个阶段抄完,才发现前面所有剧情都在往这四句话上收。一开始读不懂也没关系,可以先跳过,读完再回头对照:
- 执行语义只在 Agent Core 实现一次;
- Runtime 只在 Bridge 装配一次;
- 真实执行历史只写入 append-only Journal,其他表现形式都从中投影;
- Tool 是无状态能力,角色差异和环境依赖由 Runtime 在调用时绑定。
如果读完全文只记住一句话,我选这句:
Nosis 从一个只会一问一答的同步包装器,逐步演化成一个“记录事实、投影视图、装配能力、协调并发并接受实时控制”的持久化执行引擎。
1. 总览:七天,七种 Runtime 形态
| 阶段 | 时间 | Runtime 的主要形态 | 核心变化 |
|---|---|---|---|
| 0. 最小对话内核 | 09-09 | 单次 Provider 调用 | 建立 Agent + Session + Provider 最小闭环 |
| 1. Agent Loop 成形 | 09-09~09-10 | 模型与 Tool 循环执行 | Tool、Workspace、限制、事件、授权进入 Core |
| 2. 多前端统一 Runtime | 09-11 | Bridge 驱动的独立 Runtime 进程 | TUI/GUI 不再各自承担执行语义 |
| 3. 长上下文 Runtime | 09-12~09-13 | 可流式输出、可压缩上下文 | 从“超限失败”变为“归档后继续执行” |
| 4. 可装配的多 Agent Runtime | 09-13~09-14 | MCP、子 Agent、共享 Tool Catalog | 能力从对象内依赖改为 Runtime 装配和调用时注入 |
| 5. 持久化执行状态机 | 09-14~09-16 | append-only Journal + 多种投影 | 真实执行历史与 Provider 对话正式分离 |
| 6. 长生命周期交互 Runtime | 09-16 | 可恢复、可接管、可询问、可 steering | Runtime 不再等同于一次请求,而是 Session 级活动实体 |
七个阶段里,真正改变职责边界或状态模型的只有几次(我在下面把它们标成“转折点”),其余提交都是在补能力、补约束、补语义。我按这个节奏画了张时间线,方便自己回看:
flowchart LR
A["阶段0
最小对话内核
09-09"] --> B["阶段1
Agent Loop
09-09~10"]
B --> C["阶段2
Bridge 统一 Runtime
09-11"]
C --> D["阶段3
长上下文 + 流式
09-12~13"]
D --> E["阶段4
MCP / 子 Agent / Catalog
09-13~14"]
E --> F["阶段5
append-only Journal
09-14~16"]
F --> G["阶段6
长生命周期交互
09-16"]
下面逐个阶段抄笔记。
2. 阶段 0:最小对话内核
2.1 一次输入,一次模型调用
初始提交 e1d15a4 的调用链短得可爱:
1 | CLI |
此时 Agent 只有三个依赖:LLMProvider、Session 和 system prompt。run() 也只做四件事:追加用户消息、构造请求、调用 Provider、保存助手回复。它还不是循环——模型不能调 Tool,也没有中途事件、取消、授权和上下文压缩。
2.2 这一层读懂了什么
我一开始以为“最小版本”没什么可看的,回头看它其实已经埋好了四根桩:
- Provider 通过抽象接口隔离,Agent 不直接依赖某家 SDK;
- Session 是独立对象,而不是 CLI 里的一组临时变量;
- system prompt 从执行代码中分离(后来还被单独命名为 Soul);
- Agent Core 与入口代码分目录,为后续拆分留了空间。
紧接着的 42e9b60 加入时间戳、Session Record 和 token usage。它的意义不是“多显示了几个字段”,而是第一次把一次模型调用的执行记录当成可持久化数据,而不是一堆聊天文本。
2.3 卡住的地方
- Agent 只能回答,不能行动;
- Session 本质还是 Provider 历史的容器,没有表达真实执行状态;
- CLI 同时负责配置、运行和展示——一旦出现第二个前端,语义必然被复制。
这三条限制,直接引出了后面的三条主线:Tool Loop、结构化执行事件、Bridge。
3. 阶段 1:从单次补全到 Agent Loop
3.1 7f22d93:run() 变成循环
这是第一次根本性变化。模型响应开始带 tool_calls,Agent 必须反复执行:
1 | 构造请求 → 调用模型 |
这次提交加了统一的 Tool 抽象、Tool Definition、Tool Call、Tool Result 和第一个内置 Tool,Session 也必须保存 assistant tool-call message 与 tool result message,才能满足 Provider 的消息协议。
我的理解:从这一刻开始,Runtime 的核心不再是 complete(),而是显式的 Agent Loop。后面所有的上下文压缩、并发、取消、steering,都是在这个循环的安全边界上继续长出来的。
3.2 防止失控循环:终止条件第一次被显式化
Tool Loop 一出现,模型重复调用同一个工具的问题马上就来了:
a361c72增加可配置的 Tool Call 上限;2b1d008把判断收敛为“相同工具名 + 相同参数的连续重复”,避免把合理的多次调用误判成死循环。
这里的重点不是“限制调用总数”,而是识别没有产生新进展的调用模式。
3.3 Workspace 成为执行边界
eb7b43f 引入 Workspace 感知的入口,03812fc 加入读取、编辑、搜索和目录浏览工具。Workspace 从一个启动路径,变成了 Tool 能力的边界:
- system prompt 能描述当前工作区;
- 文件 Tool 通过统一路径解析限制访问范围;
- 后来的 shell、附件、Session 分组、子 Agent 都以 Workspace 为装配基础。
这一步把项目从“普通聊天 Agent”推向了“coding / working Agent”。
3.4 上下文、事件与授权
同一天还发生了三件后来影响很大的事:
7689f21首次加入上下文窗口验证。当时策略仍是“超过 hard limit 就失败”,但 Runtime 开始主动掌握上下文预算,而不是把错误完全交给 Provider。4444b41加入结构化AgentEvent。此前 CLI 只能在run()返回后展示结果,此后界面可以观察执行中的状态。29ebad8引入需要人工批准的 shell Tool,69bb062把“是否允许执行”和“如何执行命令”拆开:
1 | Shell Tool |
这是一个很早、但非常关键的职责分离:Bridge 后来才能负责交互式审批,而 Core 只依赖授权接口;MCP Tool 也复用了同一类政策层。
3.5 Tool 从散落实例走向配置化工厂
3bdfd5f 给文件读取加入范围和行号,6b34054 把内置 Tool 收口到统一工厂与配置开关。这个工厂后来被共享 Tool Catalog 取代,但它完成了必要的中间步骤。
阶段结束时,架构大致是:
1 | CLI |
问题也在这里埋下:CLI 已经承担了过多 Runtime 装配职责,而 GUI 马上就要成为第二个调用方。
4. 阶段 2:Bridge 成为唯一 Runtime 装配点
4.1 GUI 逼出了“双 Runtime”风险
09-11 先补齐产品化基础:
e873500:初始化用户配置,并按 Session 存储;d341306:加入最小 GUI、Session 列表和模型选择;8039345:大 Tool Result 不再全塞进上下文,而是保存为 Session Artifact;725fd3b、831319f:限制文件搜索、读取和 shell 输出规模。
GUI 一出现,一个架构问题就无法回避了:如果 TUI 和 GUI 都各自创建 Agent、读配置、执行 Tool、处理授权,项目就会长出两套 Runtime 语义。
4.2 abdfc6a:从 CLI 重构为 Bridge + TUI
这次提交删掉了原 agent_cli 的执行入口,建立独立进程 python -m interfaces.bridge,用 newline-delimited JSON 与前端通信:
1 | TUI ───────────────┐ |
Bridge 开始负责:读 Provider 与 Agent 配置、创建 Provider/Session/Tool/执行器、把 AgentEvent 翻译成协议消息、转发 Tool 审批和取消、控制 Runtime 进程生命周期。随后 bf907c6 让 GUI 也适配同一个 Bridge——“两个界面共用同一执行语义”从目标变成了代码结构上的事实。
由此形成了当前最重要的依赖方向:
1 | interfaces/* ──> agent_core |
Core 提供机制,Bridge 决定装配,前端只处理协议和呈现。后面的 MCP 生命周期、子 Agent 角色、权限 preset、Skills、Session 恢复,全都照着这个边界走。
4.3 这一层读懂了什么
我以前会把 Bridge 理解成“一层 API 转发”,这次才看明白它解决的是运行时拥有者问题:
- Provider 的选择属于装配,不属于 Agent Loop;
- Shell / MCP 的用户确认需要前端交互,但不能进入 Core;
- TUI 与 GUI 必须共享取消和 Session 语义;
- Runtime 需要独立于页面或终端组件存在。
所以 Bridge 是前端与 Core 之间的唯一执行通道,不是可绕开的辅助层。
5. 阶段 3:从上下文超限失败到可持续运行
5.1 第一个归档模型
73b01c5 把上下文策略从“超过窗口就报错”改成“把旧历史总结成 checkpoint 再继续运行”,同时新增 Consolidator prompt、归档摘要和 Session 持久化字段。
这一步把 Agent 从短对话程序变成了可以长期运行的 Runtime,但第一版归档和 Agent Loop 耦合太紧,于是后面跟着一串语义修正:
7606a39:向 UI 报告上下文压缩事件;7dee8ff:禁止归档仍在执行的 turn,避免丢失当前工具链;07feec6:压缩阈值和目标可配置;6324769:拒绝无法合理划分预算的过小上下文窗口;2cb3b3a:把历史时间戳合并成单个 timeline system message,减少对原消息的污染;f59b856:历史 Tool 链不携带时间戳和 reasoning,保持 Provider 格式稳定。
5.2 流式输出让 Runtime 变成持续事件源
563d111 把 reasoning delta 从 Provider 经 Agent 和 Bridge 流到两个 UI,ca6a8a5 统一了 Provider 回调契约。此前结构化事件主要描述离散状态,此后 Runtime 同时输出文本增量、推理增量和离散执行事件。
这就要求 UI 不能再把一次 turn 当作“一个最终响应”,而必须根据结构化事件逐步构建界面状态。
5.3 dfb610d:抽出 ContextManager
随着构造请求、计数、阈值判断、归档、历史格式化不断变多,Agent 已经同时承担执行循环和上下文策略。dfb610d 把这些职责抽到 ContextManager,一次性从 Agent 移走约两百行上下文逻辑:
Agent决定什么时候调用模型、什么时候执行 Tool、什么时候继续或结束;ContextManager决定发给模型什么、是否需要压缩、如何产生 checkpoint;Session保存对话与归档状态;- Provider 负责模型调用和 token 计数。
这是我这一阶段最想记住的一点:这是 Core 内部第一次形成清晰的“执行控制面”和“上下文数据面”分离,后面的 Journal 重构能落地,很大程度上就是因为先有了它。
6. 阶段 4:MCP、子 Agent 与可装配 Tool Runtime
6.1 MCP:远程 Tool 进入统一 Tool 系统
0063af8 加入 MCP Client:Server 配置、生命周期管理、动态 Tool 发现、命名空间和审批,全部进入既有 Tool 体系。远程工具以 mcp__server__tool 命名,Agent 不需要知道它来自本地类还是 MCP Server。
职责依旧按层划分:Core 提供 MCP 配置模型、客户端机制和 Tool 适配;Bridge 启停 Server、把远程 Tool 加入 Runtime、把审批转发给前端;前端只显示启动状态和审批请求。随后 651c648 把“单个 MCP Server 启动失败”从“终止整个 Bridge”改成可报告状态,563133c 把审批粒度从 Server 级细化到 Tool 级。
6.2 子 Agent 第一版:把委派做成 Tool
0563453 引入 subagent Tool:父 Agent 通过普通 Tool Call 委派任务,子 Agent 使用独立 Provider、Tool 配置和 Session,最后只把最终报告作为 Tool Result 返回。
这个设计我现在觉得很妙:子 Agent 没有进入主 Agent Loop 的特殊分支,而是复用了 Tool 协议,因此天然获得了 Tool Call/Result 的上下文回灌、授权和事件边界、并行 Tool Batch 的可能性、Artifact 与 Session 存储能力。
后面 e4226b2 把子 Agent Tool 收进 Registry 并给它们 Workspace,ae5cd63 把子 Session 重新嵌套回父 Session,6611261 允许子 Agent 继承触发它的用户图片。
6.3 b401880:共享 Tool Catalog + 配置化角色
子 Agent 第一版很快暴露出新的重复:不同 Agent 各自创建 Tool 实例、各自持有依赖,角色和 Tool 对象绑死。b401880 对 Tool Runtime 做了大规模重构:
1 | Runtime |
这次重构确立了当前 Tool 系统的三条原则:
- Tool 实例必须无调用状态,可以被多个 Agent 和并行线程共享;
- Workspace、Session、Executor、MCP、视觉 Provider、子 Agent Runtime 等依赖,通过
ToolExecutionContext在调用时传入; - 一个角色能看到哪些 Tool,由 Catalog 按名称选择形成
ToolSet,而不是重新实例化一套工具。
子 Agent 也从单一配置变成角色注册表,subagent(role, task) 成为唯一委派入口。
6.4 多模态同样遵循装配原则
32871d2:用户附件作为多模态内容进入 Session;bb7abe1:视觉模型可以通过read_image直接查看 Workspace 图片;5534775:文本模型在显式配置视觉 Provider 后可以使用analyze_image;e29aafb:图片 token 按计费模型上界估算,并在路径解析后去重;4c6b7f9:按文件 magic bytes 而不是声明类型识别附件。
最终并没有增加一个通用的“视觉模式”开关,而是根据 Runtime 已解析出的 Provider 能力决定 Tool 是否存在:缺少依赖时 Tool 不进入 ToolSet,而不是等模型调用后再失败。
6.5 并发 Tool 的四种顺序
803b5ea 允许并发 Tool 完成后立即向 UI 发出结果,但给 Provider 的 Tool Result 仍按模型调用顺序排列。后续 Journal 又保存了真实完成顺序,于是系统明确区分四种顺序:
| 顺序 | 含义 |
|---|---|
| Invocation order | 模型发出调用的顺序 |
| Completion order | 工具实际完成、UI 收到结果的顺序 |
| Journal order | 事实实际发生并落盘的顺序 |
| Provider projection order | 为满足 Provider 协议而重排后的顺序 |
从“并发实现细节”升级为“Runtime 明确定义的执行语义”,这是我在这一阶段印象最深的变化。
7. 阶段 5:从保存聊天记录到保存执行事实
7.1 Session 先成为 Workspace 级持久实体
cf447b0 把 Session 移入用户配置目录,并让媒体路径相对 Workspace 解析;492fd90 让每个 Session 与 Workspace 绑定;975b794 再按 Workspace 分组 Session,并保留被中断 turn 已经产生的内容。
此时 Session 已经不只是聊天历史:它决定 Artifact 路径、附件解析、子 Agent Transcript、恢复入口和并发隔离。
7.2 a31d0b7:执行结果随发生随保存
早期实现倾向于等一整个 turn 完成后才保存。a31d0b7 改成 Agent 每 settle 一个 item 就立即持久化,使断线、取消或进程异常时,已经发生的消息和 Tool Result 不会全部丢失。
但这仍然是在“保存消息”:Tool 是否已开始执行、是否完成、turn 为什么结束,这些事实还没有统一的数据模型。
7.3 b01e999:append-only Journal,最大的一次状态模型重构
b01e999 把 Session 重构为 append-only Runtime Journal。每个事件包含单调递增的 seq、唯一 event_id、turn_id,Tool 事件还带 tool_call_id。典型事实包括:
turn_started/turn_completed/turn_failed/turn_cancelled;message_appended;model_completed;tool_started/tool_completed/tool_failed/tool_cancelled;context_archived。
关键变化不是“文件格式从 JSON 变成 JSONL”,而是建立三个不同视图:
1 | append-only Journal |
Journal 保存“实际发生了什么”,Provider Context 只投影“下一次请求允许看到什么”。比如一个 Tool 已经 started 但进程退出、没有可靠结果:
- 真实历史中保留 started,并在恢复时标为 unknown;
- Runtime 不会自动重放,因为 Tool 可能已经产生副作用;
- Provider 投影会排除不完整的 Tool Batch,避免构造非法消息链。
这一下就把此前最难缠的一组问题解开了:崩溃恢复、取消、中断副作用、并发完成顺序、Provider 格式约束,不再互相冲突。
7.4 上下文压缩改用 Cursor 和 ContextUnit
Journal 重构之后,按“已归档 turn 数”记录压缩位置就不够了,因为一个 turn 内可能有多次模型调用、多个 Tool Batch 和 steering。于是陆续:
4506832:归档位置改为原始 item cursor,不再保留 archived turns 作为第二套索引;7672710:移除旧 provider-message wrapper,并要求明确 archive cursor;82262b5:以ContextUnit为不可拆分边界,并原样保留最近若干单元;50cf2fb:删除 Runtime 已不再使用的旧 Provider Message 投影。
当前 ContextUnit 的核心规则是:普通消息各自构成一个单元;带 Tool Calls 的 assistant message、该批全部 Tool Results 和尾随 Tool Media 共同构成一个单元。压缩游标不能落在 Tool Batch 中间,因此不会生成 Provider 无法接受的残缺工具链。
同一批提交还校正了预算语义:output_reserve_tokens 只为下一次输出预留输入空间;max_generation_tokens 是可选的生成策略,不再和上下文预算混为一谈;压缩到 trigger 前的边界,不再同时维护一个容易造成过度压缩的 target。
8. 阶段 6:Runtime 成为可接管的长生命周期实体
8.1 GUI Server 开始拥有 Runtime
7d3db87 改变了 GUI 的所有权模型:Bridge 进程由 GUI Server 按 Session 持有,浏览器页面只是 attach / detach。页面断开不再等价于 Runtime 结束,运行中的 turn 可以继续,重连后恢复事件和待审批状态。ced07a7 又明确同一 Session 的 live attachment 只由一个页面拥有,避免多个页面同时发控制消息。
Runtime 的生命周期,就此从“一个 WebSocket 连接”变成“一个活动 Session”。
8.2 Skills 采用渐进加载
b331969 增加 Skills,但没有把所有 SKILL.md 全文塞进 system prompt。Runtime 启动时只注册名称和描述,模型需要时调用 read_skill 分段读取完整指令和引用文件。
这延续了上下文管理一贯的方向:能力可以很多,但常驻上下文只保留“发现能力”所需的最小信息。
8.3 Agent 主动询问用户
9f3f91c 增加 ask_user Tool,由 Bridge 注入交互回调,且只提供给 main Agent:
1 | Agent Tool Call |
这里依然没有让 Core 依赖 UI:Core 只知道 Tool Context 里存在一个询问回调,问题怎么展示、怎么回答,全由 Bridge 协议处理。
8.4 运行中 steering 与安全点
1edc4f0 允许用户在 turn 运行期间追加指令。Bridge 的唯一输入 Reader 把 user_steer 路由到当前 TurnControl,Agent 只在安全点接收:
- 一次模型调用结束后;
- 完整 Tool Batch 已写回后;
- 准备返回最终答案前。
Steering 最终作为普通 user message 写入 Session,因此自动获得 Journal、token 统计、上下文压缩和恢复语义,不需要一套平行状态。“只在安全点应用”则避免了两件事:不会在 Provider 流中间修改请求,也不会把新消息插进 assistant tool_calls 与 Tool Results 之间。
8.5 取消与权限成为 Session 级控制
5b6b8e6 让停止发送 chunk 的 Provider stream 也能被取消,取消不再依赖 Provider 继续产出 chunk。2041f70 增加 Session 级权限 preset:
ask_for_approval:Shell 和配置要求确认的 MCP Tool 继续询问用户;full_access:跳过这层人工确认,但不改变 ToolSet,也不额外引入 Sandbox。
权限状态随后写入独立 Session metadata;60e6554 把 metadata 统一为 session.json;8f7f019 从 Session 所在目录反推 Workspace,删掉重复存储的信息。
项目后期有一个很一致的设计倾向:状态只保留一个真实来源,能从结构推导出来的字段就不再重复保存。
9. 当前架构:一次看懂的分层、装配与执行
9.1 分层和职责
1 | TUI (Ink + React) ─────────┐ |
| 层 | 当前职责 | 不应承担的职责 |
|---|---|---|
| TUI / GUI | 渲染事件、采集输入、展示授权与问题 | Agent Loop、Tool 执行、上下文压缩 |
| Bridge | 配置解析、Runtime 装配、进程与协议、MCP 生命周期、交互路由 | 模型决策、Tool Result 回灌、归档算法 |
| Agent Core | Agent Loop、上下文、Session、事件、Tool 抽象、子 Agent、Provider/MCP 机制 | UI、浏览器、终端和协议消息 |
9.2 Bridge 的装配顺序
当前 Bridge.start() 大致按以下顺序建立 Runtime:
1 | 读取配置与 .env |
“机制在 Core,组合在 Bridge”在这段流程里体现得最清楚:Core 提供所有构件,但只有 Bridge 知道配置文件、当前 Workspace、前端交互和进程生命周期。
9.3 一次 turn 的当前执行时序
1 | begin_turn,立即写 Journal |
异常、取消和进程中断都会映射为明确的 turn / tool 状态;已经发生的副作用,不会因为缺少 Tool Result 就被假定为“没有执行”。
9.4 核心对象关系
1 | Bridge |
最关键的所有权规则:
- Agent 只拥有一次对话循环所需的引用,不拥有共享 Tool 实例;
- ToolCatalog 持有无状态 Tool,ToolSet 只是按角色绑定上下文后的视图;
- Session 是运行事实的内存状态,JsonlSessionStore 是它的 append-only 持久化;
- ContextManager 只负责 Provider 上下文,不负责执行 Tool;
- TurnControl 只承载当前 turn 的取消与 steering;
- Bridge 拥有 Runtime 级资源,以及所有需要前端参与的交互。
10. 贯穿整个过程的五个设计变化
10.1 从“先抽象”转为“第二个消费者出现时抽象”
早期先用最小 Agent 和 CLI 验证闭环;Tool 多起来才加工厂;GUI 出现才建 Bridge;子 Agent 出现才重构共享 Catalog。大部分抽象都有明确的第二个消费者,而不是预先设计。 于是每个层次都对应真实的压力:
- Provider 抽象 → 模型实现可替换;
- Bridge → TUI 与 GUI;
- Tool Catalog → main Agent 与多个 subagent role;
- Journal projection → 执行历史与 Provider 对话的不同要求。
10.2 从“保存结果”转为“记录事实,再生成结果视图”
Session 的演进最能体现这一点:
1 | 消息列表 |
最终模型不是把持久化结构直接发出去,而是从真实执行历史中投影出合法请求。持久化因此不再被某家模型 API 的消息格式绑架。
10.3 从“Tool 持有能力”转为“Runtime 注入能力”
早期 Tool 可以在构造时拿到 Workspace 或 Executor;共享 Catalog 重构后,Tool 只描述自身能力,具体依赖由 ToolExecutionContext 提供。结果是:同一 Tool 实例可以服务多个 Agent;Tool 可用性由当前 Runtime 是否提供依赖决定;并行执行不共享调用状态。
10.4 从“前端生命周期”转为“Session 生命周期”
Bridge 独立进程、GUI Server 持有 Runtime、页面 attach/detach、append-only Journal 和恢复逻辑共同完成了这一变化。当前 Session 可以跨页面、跨连接,甚至跨异常退出保留真实进度。
10.5 不断删除过渡结构
演进中有多次主动删除:旧 Agent archive wrapper、create_tools alias、可配置 shell timeout、重复 Provider projection、兼容 fallback、Bridge 侧取消清理、重复 Workspace metadata。
项目并不是只在叠加能力,而是在新职责边界稳定后及时移除旧路径。 当前架构的清晰度,很大程度上来自这些删除。
11. 我抄下来的八条开发方法
- 先建立最小可运行闭环,用真实需求暴露边界,而不是预设完整框架;
- 每加入一种执行能力,都同时补齐终止条件、错误语义、持久化和 UI 事件;
- 第二个调用方出现时统一执行入口,避免复制 Runtime;
- 当一个对象同时承担“流程”和“策略”时再拆分,例如 Agent 与 ContextManager;
- 并发和恢复以“实际发生的事实”为准,外部协议格式通过 projection 适配;
- Runtime 依赖统一在装配层解析,通过显式 Context 注入;
- 新状态尽量复用现有语义,例如 steering 直接成为普通 user message;
- 新结构稳定后删除旧 wrapper、fallback 和重复数据源。
12. 附录:Runtime 相关提交索引(按时间)
标记为 里程碑 的提交改变了职责边界或状态模型;其余提交主要补齐能力、约束或语义。
2026-09-09:最小内核与 Tool Loop
e1d15a4Initial commit:建立 Agent、Provider、Session 和 CLI 的最小闭环。(里程碑)42e9b60Add timestamped session records and token usage:执行记录开始携带时间和 usage。95eb231Rename system prompt to Soul:将 Agent 身份提示独立命名。7f22d93Add tool calling support:单次补全变为可迭代 Tool Loop。(里程碑)
2026-09-10:Workspace、事件、安全和 Tool 装配
a361c72Add configurable tool call limit:加入 Tool 循环终止条件。2b1d008Refine repeated tool call detection:按连续相同调用检测无进展循环。eb7b43fAdd workspace-aware CLI entry point:Workspace 进入 Runtime 上下文。03812fcAdd workspace file tools:加入文件读取、编辑、搜索和目录浏览。7689f21Add context window validation:Runtime 主动验证上下文 hard limit。4444b41Add structured agent execution events:执行过程变为结构化事件流。(里程碑)29ebad8Add approved shell tool and grouped event output:Shell 引入人工审批。69bb062Separate shell policy from command execution:授权策略与命令执行解耦。(里程碑)3bdfd5fAdd ranged file reading with line numbers:文件读取获得有界范围语义。6b34054Add configurable built-in tool factory:Tool 创建从 CLI 收口到统一工厂。
2026-09-11:持久化产品化与 Bridge
e873500Add user configuration initialization and per-session storage:建立用户级配置和独立 Session 存储。d341306Add minimal assistant-ui GUI with sessions and model selection:第二前端出现,暴露统一 Runtime 的需求。8039345Store large tool results as session artifacts:大结果转为可分段读取的 Artifact。725fd3bBound file reading and search results:限制文件工具的上下文占用。831319fAdd configurable shell timeout and bounded command output:限制命令执行时间和输出。a4443a2Increase maximum shell timeout to 15 minutes:放宽长任务执行上限。abdfc6aRestructure interfaces: replace agent_cli with bridge + TUI:Bridge 成为唯一 Runtime 装配与协议入口。(里程碑)bf907c6Merge PR #1: adapt the GUI to the bridge architecture:GUI 接入同一 Bridge,完成双前端统一。
2026-09-12:长上下文与流式 Runtime
5035553Make shell execution work on Windows:补齐跨平台命令执行。526b11aRun shell commands in a POSIX shell on every platform:统一 Shell 语法和执行语义。24db70eAdd a web_search Tool backed by Exa:外部检索进入统一 Tool 系统。43ee630Rename the project to Nosis:统一项目身份和配置目录命名。73b01c5Archive context into a summary instead of failing at the window limit:上下文从超限失败改为 checkpoint 压缩。(里程碑)7606a39Report context compression to the UIs:压缩成为可观察 Runtime 事件。7dee8ffNever archive the turn that is still running:归档不破坏活动 Tool 链。07feec6Make context compression configurable:压缩策略进入 Agent 配置。6324769Reject a context window too small to divide:明确无可用预算时的启动失败。2cb3b3aMove message timestamps into a single timeline system message:历史时间信息与原消息内容分离。563d111Stream model reasoning deltas through the agent and interfaces:Runtime 输出连续 reasoning 事件。(里程碑)
2026-09-13:ContextManager、MCP、子 Agent 和多模态
0063af8Add MCP client support with namespaced tools and approvals:远程 Tool 进入统一 Runtime。(里程碑)651c648Report MCP server startup failures instead of aborting the bridge:MCP 局部失败可观察、不中止全部 Runtime。f59b856Keep historical tool chains without timestamps or reasoning:历史 Tool 链保持 Provider 合法格式。ca6a8a5Always pass the reasoning delta callback to providers:统一 Provider 流式回调契约。dfb610dExtract context building and archiving into ContextManager:上下文策略从 Agent Loop 中独立。(里程碑)2ace3d2Drop the leftover Agent archive wrapper:删除抽取后的过渡 wrapper。c6d9476Fail archiving when no turn is active:归档要求明确活动 turn。c5e66f6Show MCP startup progress as transient status instead of transcript:运行状态与对话历史分离。0563453Add a subagent tool with its own provider and tool config:委派能力作为 Tool 进入 Runtime。(里程碑)e4226b2Collect subagent tools through the registry and give them a workspace:子 Agent 获得注册表和 Workspace。32871d2Accept image attachments as multimodal message content:Session 和 Provider 请求支持多模态内容。cf447b0Move session storage to the user config directory and resolve media paths against the workspace:Session 与 Workspace 媒体路径形成稳定关系。ae5cd63Nest subagent transcripts under the parent session again:子 Agent 历史归属父 Session。644bd3cReject a subagent without either a sessions directory or a parent session:明确子 Session 存储不变量。fb389fcDrop the create_tools alias and correct a misleading test name:删除 Tool 工厂过渡别名。6611261Let subagents see the images of the turn that spawned them:子 Agent 继承触发 turn 的用户附件。7a2b2cdResolve provider names from the config load that already reads the file:消除 Provider 配置重复读取。492fd90Bind each session to its workspace and keep GUI sessions running side by side:Session 与 Workspace 绑定,并支持 GUI 并行 Session。
2026-09-14:共享能力、角色化和并发语义
975b794Group sessions by workspace and keep what an interrupted turn produced:Session 按 Workspace 分组,并保留中断进度。(里程碑)563133cDecide MCP approval per tool and infer the transport:MCP 审批细化到 Tool。b401880Share one Tool catalog across agents and turn sub-agents into configured roles:共享无状态 Tool Catalog,子 Agent 角色化。(里程碑)5534775Give each subagent role its own provider and derive analyze_image from capability:角色级 Provider 与能力派生 Tool。446b598Order enabled tools by the canonical list so the schema order is stable:Tool Schema 顺序稳定化。bb7abe1Let a vision model read a workspace image itself:视觉模型获得直接读图通道。e29aafbPrice an image at the highest billing model and dedup after resolution:图片 token 估算与路径去重语义稳定化。803b5eaEmit tool completions as they finish and commit them in call order:区分完成顺序和 Provider 顺序。(里程碑)08ac1aeShut a starting bridge and its MCP servers down without orphanage:Bridge 启动期取消不会遗留 MCP 进程。b325a88Keep large shell output on disk and clean up its spool:Shell 大输出以 spool + Artifact 管理。
2026-09-15:预算语义与输出边界
e9dd06bAdd write_file for replacing a whole file atomically:加入原子整文件写入能力。15d9527Accept streamed text blocks and let the GUI drop a session:Provider 流兼容文本块,Session 可删除。86f3d3dSplit the output reserve from the per-call generation limit:上下文预留与生成上限分离。(里程碑)6eab91fOnly cap generation when a policy asks for it:默认不主动限制 Provider 输出。ee76bd2Compress to a trigger, not a target size:简化压缩模型,删除 target 语义。9de8b73Tell the consolidator to shrink the checkpoint:防止 checkpoint 自身持续膨胀。4c6b7f9Accept an attachment by its bytes, not its declared type:媒体类型由真实字节决定。b914ab6Write down the layer contract the code already keeps:将 Core、Bridge、前端的既有边界正式写入项目约束。
2026-09-16:Journal、可恢复执行与实时交互
a31d0b7Store a turn’s items as the agent settles them:执行项随发生立即持久化。c1dfbd3Let the shell take a timeout longer than the default:单次调用可覆盖默认 timeout。119b3a7Drop the configurable shell timeout:删除 Agent 配置中的执行器细节。b621a55Take the shell’s timeout bounds from the executor:timeout 能力边界归 Command Executor 所有。b01e999Record execution as an append-only journal and project it for the provider:真实执行 Journal 与 Provider 投影分离。(核心里程碑)af50c76Give every turn a durable identity and drop the dead journal plumbing:turn 获得稳定身份,删除旧持久化路径。f343d57Drop the cancel cleanup the agent already performs:取消清理只保留 Core 实现。4506832Archive context by raw item cursor and drop archived turns:压缩位置改为 item cursor。7672710Drop the provider-message wrapper and require the archive cursor:删除旧消息 wrapper 和隐式归档位置。b331969Load skills progressively through a read_skill tool:Skills 采用发现信息常驻、完整内容按需读取。(里程碑)7d3db87Let the GUI server own runtimes so pages attach to a running turn:GUI Runtime 生命周期独立于页面连接。(里程碑)82262b5Keep a verbatim context-unit tail when compressing:压缩以完整 ContextUnit 为边界。(里程碑)50cf2fbDrop the provider-message projection the runtime no longer uses:删除已被 ContextUnit 投影取代的旧实现。21bcc37Drop the compatibility fallbacks the current pins rule out:删除依赖版本已不需要的兼容路径。ced07a7Let one page own a session’s live attachment:明确活动 Session 的页面控制权。9f3f91cLet the agent ask the user to pick an answer:Agent 可通过结构化 Tool 暂停并询问用户。(里程碑)1edc4f0Let the user steer a turn that is already running:运行中指令通过安全点进入同一 turn。(里程碑)5b6b8e6Cancel a provider stream that has stopped sending chunks:取消不再依赖 Provider 继续产出 chunk。2041f70Let a session choose between asking for approval and full access:权限成为 Session 级 Runtime 状态。60e6554Store a session’s metadata in a file named after it:Session metadata 收口到session.json。8f7f019Derive a session’s workspace from the directory that holds it:Workspace 从目录结构推导,删除重复状态。
13. 结语与我还留在笔记最后的问题
七天的演进最后汇合成几条线:Tool Loop 让 Agent 从回答器变成执行器;结构化事件和 Bridge 让多个前端共享同一 Runtime;ContextManager 让长对话和压缩成为独立机制;Tool Catalog、Context 注入、MCP 和角色化子 Agent 让能力可组合;append-only Journal 让执行事实、恢复和 Provider 格式彻底解耦;Runtime ownership、ask_user、steering 和权限 preset 让 Session 成为可持续、可接管的活动实体。
所以当前最值得保留的,不是某个具体类,而是开头那四条不变量:执行语义只在 Core 实现一次、Runtime 只在 Bridge 装配一次、真实历史只写 Journal 其余皆投影、Tool 无状态且依赖在调用时注入。它们解释了项目为什么能在快速加入 GUI、MCP、子 Agent、多模态、并发、恢复和实时交互之后,依赖方向依然干净。
回头看,这三层理解是逐层加上去的:最早只能复述“哪个阶段加了什么”,接着才看清每次重构把职责从谁手里交给了谁,最后才认出这些取舍都来自同一条主线——把执行语义收紧到一处,把容易变化的部分推到装配层和投影层。写到这里,开头那句“当时为什么非改不可”,才算勉强有了答案。
最后留几个我自己还没想透的问题,等下一轮再读代码:
- 并发 Tool 的四种顺序里,UI 展示用 completion order、Provider 用 projection order,那如果用户想“按事实顺序回看一次执行”,最自然的入口应该是 replayed Session 还是 Journal 原文?
ask_user和 steering 都让用户在 turn 中途介入,这两条通道未来会不会统一成一种“运行中交互”语义?- 当子 Agent 也能装配自己的子 Agent 时,Journal 的
turn_id嵌套和 ContextUnit 边界还能不能保持现在的简洁?
如果这篇笔记对同样在啃 Agent Runtime 的人有一点帮助,那它的目的就达到了。