Dim 源码分析与 Agent 运行时设计
Dim 把 Agent 的运行组织成有顺序的事件,再由各个能力组件维护自己的状态和在途工作。 这种设计让工具调用、取消、重载和恢复都有明确的归属,值得借鉴的是这些边界怎样配合。
Dim 是一个开源编码 Agent,本文以 2026-11-05 本地仓库对应的公开源码快照 6407067为依据,项目上游为 arcships/dim。该快照的 Rust workspace 版本是 0.1.0-alpha.18,分析范围是已提交的 Rust 核心、App Server、CLI/Web 链路与插件机制。本地尚未跟踪的 macOS 桌面封装不作为上游已发布能力。
1 先把客户端和运行引擎分开
使用者在终端输入请求,实际管理会话、调用模型和执行工具的是后面的服务与运行时。CLI 是客户端,切换到 Web 界面也不需要重新实现一套 Agent 循环。
几个主要模块的职责如下:
| 模块 | 负责什么 |
|---|---|
dim-agent-types | 可序列化的事件、消息、调用结果和身份结构 |
dim-agent-runtime | Agent、Session、分支、Timeline、分发与关闭协调 |
dim-agent-capabilities | 模型步骤、工具执行、权限、提示内容等具体能力 |
dim-app-server | 持久化、会话托管、本地 RPC、连接与产品配置 |
dim-client 与 dim-cli | 客户端通信和终端交互 |
dim-agent-aimux | 模型协议适配,将不同提供方接入统一的宿主接口 |
SQLite 是 App Server 的一种持久化实现,运行时通过存储接口工作。单独嵌入核心库,并不要求同时带上终端界面、Web、SQLite 和账户登录组件。模块边界
这里还要区分 ACP 和 MCP:ACP 用来对接 Agent 客户端,MCP 用来接入工具服务。它们连接的是不同方向,不能把“支持 MCP 工具”理解为“已经提供完整的客户端协议”。
这种分层对产品有直接价值:终端与 Web 可以围绕同一份会话事实显示状态,协议适配也不必侵入模型循环。
2 Timeline 记录决定后续行为的事实
每条分支拥有自己的 Timeline。事件带序号,状态机根据序号判断先后,不依赖网络返回速度或墙上时钟。
一次工具调用可能经历请求、开始、完成和结果等记录。它们让其他组件知道已经发生什么,也为恢复提供依据。普通调试日志可以帮助定位问题,但不能替代这类决定后续行为的状态记录。
分配序号和持久化发布有区别
沿着 Timeline::append 往下读,可以看到几个阶段:
- 在写入锁内处理事件草稿、分配连续序号。
- 生产方登记需要关联的状态,事件进入待发布队列。
- 存储层接受批次,完成保存后调用发布回调。
- Timeline 推进已发布位置,Dispatcher 才能向订阅者交付这些事件。
因此,append 返回事件对象,不等于所有订阅者已经处理完,也不能仅凭它就推断持久化回调已完成。Timeline 实现
源码中的命名尤其值得留意:committed() 返回最后分配的序号,包含尚未发布的事件;sequence() 返回持久化后已发布的位置;Dispatcher 还维护已经分发到哪里。阅读此类系统时,要跟踪具体契约,不能仅凭 committed 这个名字判断完成程度。
批次也有自己的边界。相关事件可以作为同一批次登记,读取和分支切点需要遵守合法的批次结束位置,避免继承一半业务状态。
3 顺序分发不代表等待每项外部工作完成
Dispatcher 负责按顺序交付事实。模型请求和工具执行可能耗时很长,如果订阅处理器一直等它们完成,后面的取消事件也会被堵住。
Dim 的能力组件在收到事件后登记自己的异步任务,随后返回;任务完成后,再把结果写回 Timeline。顺序由事实记录维护,外部 I/O 的生命周期由发起它的组件维护。
例如工具执行器消费中断时会记录中断状态,并取消正在执行的工作。若某个旧请求在中断后才到达,它还会检查这个请求所属的工作是否已被中断,给出未执行的结果,避免仅靠生产方的早期检查漏掉迟到请求。工具执行器
另一处关键实现是 Dispatcher::consume:重建状态期间先暂存新到的事实;重建返回已经覆盖的序号后,再交付这个位置之后的事件。这样恢复和实时消费之间才有明确的交接点。分发与重建
对自己的项目,可以先问一个具体问题:某个耗时请求尚未返回时,取消信号能否被处理,以及谁负责等待它真正结束。
4 模型提出工具调用之后还要经过哪些步骤
模型返回工具调用,只是产生了执行意图。它与业务动作之间还有几层程序契约。
Loop 收集本次模型输出中的工具请求,在模型输出正常结束后形成工具调用批次;ToolExecutor 再找到当前请求绑定的工具处理器,经执行准入后调用它。每个结果关联请求和调用标识,随后驱动下一步模型处理。循环状态机
工具绑定也有实际意义:工具可能在模型生成期间被替换或重载,不能简单地“按现在的同名工具执行过去的请求”。执行器会按请求标识查找绑定,找不到则返回明确错误。
这里至少有三种需要区分的状态:
- 模型已经提出调用,但还没有执行。
- 宿主已经接受执行,但工作仍在进行。
- 工具已经返回结果,相关事实还需要发布和被后续组件消费。
前端看到“调用开始”不能提前显示业务成功。类似地,工具失败也要成为可关联的结果,不能只在后台打印一行日志,让模型继续假设成功。
5 Timeline 分支让同一会话沿多条线工作
Dim 的 branch 是同一个 Session 内的一条行为线。子分支从父分支的某个切点继承历史,切点之后分别积累事件、组装上下文和进行压缩。
假设主线调查一个故障,可以在同一段已有事实基础上开一条线阅读测试,主线继续追实现。子线需要了解父线的新进展时,通过显式读取或消息获取,不是自动共享一份持续变化的提示词。
它和另外两种能力的区别是:
| 能力 | 身份与历史关系 |
|---|---|
| Branch | 同一 Session 的另一条线,继承父线截至切点的历史 |
| Collaboration member | 具有独立配置与任务的另一个 Agent,运行在自己的 Session |
| Fork | 从主线的合法历史位置复制出独立 Session,之后分别继续 |
架构支持多层分支,但模型使用的 branch 工具限制更窄:从 main 创建工具分支,工具创建的子线不继续开孙线。不能因为底层树结构支持嵌套,就推断模型工具拥有同样的操作范围。分支设计
会话分支并不是文件隔离
各条线共享 Session 的工作区。分出一条思考线,不会自动得到独立的 Git worktree。
文件工具为各条线保留读取绑定,编辑时核对文件是否在读取后发生变化。出现 file_modified_since_read 应重新读取并协调,不应直接强行覆盖。进程内的写协调也不能被理解为能锁住所有外部程序。文件编辑检查、写入边界
因此,拆分并行任务时仍要明确负责的文件和区域。增加分支可能提高并行度,也会增加模型调用和协调成本。
6 历史事实与模型上下文分别管理
Timeline 中有大量调度、生命周期和执行事实,模型不需要看到全部内部事件。上下文准备组件负责把相关历史组织成模型消息。
对插件新增的外部内容,项目要求通过已采纳输入或工具结果进入模型上下文。插件记录一条内部事件,不会因此自动获得向提示词注入文字的能力。模型本身的对话输出、系统提示和工具声明另有明确的组织路径。
上下文压缩也不等于删除原始 Timeline。完成一次压缩时,会记录它覆盖的历史位置、摘要和需要保留的边界;之后恢复上下文可以使用已有压缩事实,不必重新生成同一次摘要。
恢复快照是加速信息
为了避免每次都从头解释历史,运行时可以使用恢复快照。它需要匹配解释器、历史锚点和分支祖先范围;不兼容或损坏的候选快照可以跳过,继续寻找可用位置或从历史重建。
压缩事实和恢复快照承担不同职责。前者已经改变了模型历史的解释方式,后者是读取加速;不能为了清理缓存而随意删除前者。上下文解释与快照
这个区分适用于长任务产品:先确定哪些记录是业务依据,再决定哪些缓存可以丢弃和重新计算。
7 恢复状态时不自动重做外部动作
重新读到一个旧工具调用,并不意味着应该再次执行它。写文件、启动进程或调用外部服务可能已经发生,重复执行会产生新的副作用。
工具执行器重建状态时,会找出没有对应结果的历史调用批次,并补上重载处理的终态,而不是直接再次调用处理器。正常重载也会取消并等待旧一代持有的任务结束,再完成交接。工具重建路径
这并不构成外部系统的“恰好执行一次”保证。进程可能在外部动作成功、结果尚未持久化时崩溃;本地缺少结果记录,不能证明外部从未发生变化。恢复后应按工具的业务性质查询或核对结果,写操作仍需要幂等和冲突处理。
关闭过程也有独立所有者。运行时的 CloseOnce 只启动一次清理任务,调用方放弃等待并不会取消清理本身。这避免了“窗口关掉了,后台资源却无人负责”的情况。关闭任务所有权
客户端断开、结束一次 turn、关闭分支和关闭整个 Session 也要分别处理。断开终端连接不应被误解为删除持久会话,结束一条分支也不应顺带取消其他分支的全部工作。
8 插件按状态归属放进不同作用域
Dim 使用 rutis 管理插件、依赖和服务。组件放在哪一层,取决于它拥有的状态:
| 作用域 | 适合拥有的内容 |
|---|---|
| Process | 进程级服务、模型宿主、配置或公共资源 |
| Session | 会话身份、会话配置和这一会话的服务 |
| Branch | 本条线的 Timeline、上下文、模型步骤和工具执行状态 |
同一个服务名称会在所属作用域中解析。例如两个分支都能依赖 timeline,拿到的应是各自分支的实例,而不是一个全局可变对象。
跨语言调用沿用这套命名语义,参数和结果需要能够序列化。借用引用、内部句柄、锁内同步回调等接口不能直接变成跨进程调用;这些服务或方法会保留为 Rust-only。生成的服务目录明确区分了哪些接口可以跨语言。rutis 使用约定、实际组装生成的服务目录
插件生命周期也不只是调用一个初始化函数。组装时注册资源与清理,Session 或分支发布就绪后才开始相关后台工作;关闭时先结束自己发起的工作,再移除服务。可重建的业务状态和一代运行中持有的 I/O 资源,需要分别管理。
9 工具授权与插件信任不能混为一谈
模型决定调用工具时,需要经过 Permission、Approval 等执行准入。配置启用的插件、Hook 或 MCP 服务,则可能在模型提出工具调用之前就运行代码。
以 MCP 为例,项目条目的启用状态由用户本地配置控制,项目文件中的启用字段不能自行授予该权限。启用之后,还要理解该配置及其引用代码的信任范围。MCP 配置与启用实现
依赖声明描述组件需要哪些服务,不是操作系统沙箱。使用 Rust、把插件放到另一种语言运行时、或者开启模型工具的只读权限,都不能自动证明整个宿主没有写文件或联网能力。进程隔离和资源访问限制必须在执行边界上核对。
文章中的架构分析也不能替代实际安全验收。特别是可信本机插件,它与受到严格限制的外部调用方有不同的信任前提。
10 从这些设计中提取可复用做法
可以先把自己的 Agent 流程拆成四类问题,再决定需要引入多少机制:
- 事实:哪些结果会影响下一步决定,是否有持久记录和稳定的关联标识。
- 完成程度:接口返回表示已接受、已保存、已发布,还是业务动作已经结束。
- 所有权:模型请求、工具进程和取消任务由谁创建,谁负责等它们结束。
- 恢复:哪些状态可以重建,哪些外部结果必须查询,哪些动作绝不能盲目重试。
源码中的批次、生命周期和上下文测试提供了相应的检查入口。例如登记失败不能发布半个事件批次,关闭子分支必须等待其工作结束,恢复与实时事件交接不能漏掉或重复消费事实。这些是应验证的行为,不能用“项目能编译”代替。Timeline 批次用例、Session 生命周期用例
如果任务始终只有固定输入、一次生成和人工确认,可以先沿用小型工作流。需要同时支持多客户端、并行行为线、插件热重载和长任务恢复时,再逐步引入事件序号、作用域服务与生命周期管理,保持每个机制都有明确解决的问题。
延伸阅读
- Agent 架构与工具调用:理解最基本的模型与工具循环。
- ZCode 与 DeepSeek Harness:对比工作台与运行时的职责。
- 本地知识 Agent 的受限检索与来源核对:较小应用如何固定资料范围。
- AI 草稿保存的幂等与版本校验:外部写入怎样处理重复请求和冲突。
- 周报 Agent 的规则降级与草稿交付:固定工作流的另一种实现选择。