一段从 AFFiNE、Cordis、Effect 一路谈到 General Magic 的发言,真正想说什么?
原话最重要的意思只有一句:
普通插件是在主程序允许的位置增加功能;DeepSeek Harness(下称 DSH)则尽量把主程序本身也拆成插件。
这不是说 DSH 可以随意替换进程里的任何代码,而是说它把模型适配器、工具系统、Session、Prompt 和 Agent Loop 等产品级组件,都放进了同一套可组合、可卸载的运行时。
作者以前在 AFFiNE 尝试过相近方向,所以看到 DSH 时产生了强烈共鸣。后面提到 Effect、General Magic 和 PocketJS,是在说明这种设计为什么吸引他,以及他如何理解自己的职业经历。
整段发言可以分成三层:
| 层次 | 内容 | 可信度 |
|---|---|---|
| 代码事实 | DSH 的产品组件是插件;注册有生命周期;AFFiNE 有 DI、scope、override、dispose;Effect 有 Tag、Layer、TestClock | 可以核对 |
| 技术类比 | AFFiNE 与 DSH 在追求相似的可替换性;Cordis 与 Effect 有共同思想 | 有启发,但不能画等号 |
| 个人叙事 | AFFiNE 像 General Magic;PocketJS 可能成为新 Flash | 感受和展望,不是技术结论 |
一个能工作的 Agent,至少需要四样东西:
用户任务
↓
Agent Loop:决定下一步做什么
├── 调用模型
├── 调用工具
└── 把过程写入 Session
例如主程序允许插件监听:
模型调用前 → [插件可以做点什么]
模型调用后 → [插件可以做点什么]
工具执行前 → [插件可以做点什么]
这种架构很实用。Webpack 插件接入编译器预先定义的 hooks;Pi 扩展可以注册工具、命令、事件和 UI;VS Code 扩展通过 Contribution Points 与 Extension API 增加能力。
共同点是:主程序先决定哪些地方可以扩展,插件再从这些入口进入。
DSH 官方架构文档明确列出:模型适配器、工具注册表、Session 日志和 Agent Loop 本身都是插件,可以从配置中替换。
因此它允许这样的组合:
同一个 Agent 接口
├── Agent Loop A / Agent Loop B
├── DeepSeek / 其他模型适配器
├── 本地文件系统 / 远程沙箱
├── 本地子进程 / 远程执行环境
└── Web UI / Headless Runner
消费者依赖的是某项能力,而不是某个写死的实现。替换一个 provider,所有通过该能力工作的组件可以一起改变。
这就是原话中「任意逻辑默认能被第三方扩展」背后的真实含义:产品结构默认采用可替换组件,而不是只有少数外围功能允许安装插件。
但“任意”不能按字面理解。一个没有注册成服务、事件或可撤销 effect 的私有函数,仍然不能被第三方凭空替换。Cordis 运行时、JavaScript 进程和操作系统也依然构成底层基础。
安装插件很容易。真正困难的是:插件离开时,系统还能不能恢复干净。
假设一个插件做了四件事:
① 注册一个工具
② 监听一个事件
③ 提供一个模型服务
④ 启动一个定时任务
删除插件文件,并不会自动撤销这四件事。监听器可能继续存在,定时任务可能继续运行,其他组件可能还握着旧服务。
Cordis 因而同时管理两个方向:
例如:
Agent Loop
├── 需要 LLM 服务
├── 需要 Tools 服务
└── 需要 Session 服务
插件声明依赖。所需服务存在时,组件才能工作;provider 出现、消失或被替换时,运行时重新处理相关组件。
插件注册工具、服务或监听器时,同时登记撤销方法。插件卸载后,运行时执行相应清理。
所以 DSH 文档中的一句话非常关键:registrations are effects that unwind when their plugin unloads。这里的 effect 不是“视觉特效”,而是插件对运行时产生的一项可追踪改变。
不过 Cordis 只能撤销纳入管理的改变。插件已经发出的邮件、已经调用成功的支付接口、直接写入的外部数据库,不会因为插件卸载自动倒退。
这句话有事实基础,但容易被读成营销口号。
Cordis 团队公开了一篇 88 页的论文草稿《A Programming Paradigm for Spatiotemporal Composability》。论文建立了形式模型,讨论可逆 effects、动态依赖、组件组合与收敛性质。
它证明的是:
在论文定义的操作规则与假设成立时,组件的安装、依赖解析和撤销可以满足相应的数学性质。
它没有证明:
- 任意 JavaScript 副作用都可逆;
- 任意两个插件永远不会互相干扰;
- DSH 的实际实现不可能出现生命周期 bug;
- 只要用了 Cordis,所有代码自然就能热替换。
数学证明约束的是一个精确定义的模型。现实代码必须遵循这个模型,证明才有意义。
AFFiNE 表面上是一个类似 Notion 与 Miro 的工作空间,但团队曾希望它不只是一款应用,还能成为构建其他应用的底座。其编辑引擎 BlockSuite 也强调组件可以被 AFFiNE 和第三方共同复用、扩展。
这类团队经常遇到一个矛盾:
今天:为了产品交付,直接修改底层引擎
明天:再为一个产品需求加入特殊逻辑
后天:底层引擎充满产品特例
结果:所谓“通用底座”只能服务原来的产品
一种解决思路是把“需要什么能力”和“使用哪个实现”分开。
业务代码不直接写:
const storage = new AffineCloudStorage()而是表达:
我需要 Storage 服务。
运行时再决定提供哪个实现:
Storage
├── AFFiNE 云端存储
├── 本地存储
├── 第三方存储
└── 测试用假存储
这就是依赖注入(DI)最朴素的意义。
公开提交历史可以证实,EYHN 在 2024 年向 AFFiNE 提交了新的 infra framework;其中确实有 identifier、provider、override、scope、缓存与 dispose 等机制。BlockSuite 也拥有相近的容器与扩展设计。
RxJS 在这里负责另一件事:把不断变化的数据表示成可以订阅的流。例如当前文档、用户状态或同步状态发生变化时,订阅者收到更新。
所以原话中“DI 和 RxJS 结合管理业务对象生命周期”,可以理解为:
- DI 负责对象由谁创建、依赖什么、何时释放;
- RxJS/LiveData 负责状态如何变化、谁订阅变化;
- 两者共同降低业务模块之间的直接耦合。
但这不等于 AFFiNE 已经实现了 Cordis 式的完整动态组合。AFFiNE 的公开代码能证明它有替换和生命周期基础设施,不能证明所有业务逻辑都能在运行中任意装卸。
原作者真正的情绪是:
我们当年想把“应用”做成“平台”,但没有完全实现;DSH 从第一天就把这个目标放在架构中心。
Effect 是 TypeScript 的一套程序运行与资源管理体系。入门只需要认识三个概念:
| 概念 | 最简单的理解 |
|---|---|
Tag |
一项服务的类型化名字,例如 Database、Clock |
Layer |
如何创建这项服务、它还依赖什么、退出时怎样清理 |
Effect<A, E, R> |
程序成功返回 A,可能以 E 失败,运行需要 R |
因此,“把 Tag 看成 service key,把 Layer 看成 provider”是一个不错的入门类比。
两者的共同点是:
- 依赖明确;
- 实现可以替换;
- 资源有生命周期;
- 测试可以提供假的依赖。
但 Cordis 和 Effect 不是同一个东西:
| Cordis / DSH | Effect |
|---|---|
| 主要关心插件怎样安装、依赖、重组和卸载 | 主要关心一段程序怎样执行、失败、并发和释放资源 |
| 可以在普通 TypeScript 代码外管理注册 | 计算通常要表达为 Effect |
| 强调运行中的 provider 出现与消失 | 强调解释执行时提供所需服务 |
Cordis 论文也把 Effect-TS 列为相关但不同的工作。因此,“沿着 DSH 的思路会想到 Effect”成立;“把 DSH 做到底就等于 Effect”不成立。
假设程序规定:失败后十分钟重试。
如果代码直接调用真实定时器,测试可能需要真的等待,或者到处 mock setTimeout。
Effect 可以让程序依赖一个 Clock 服务:
生产环境 → 真实时钟
测试环境 → TestClock
测试时把虚拟时间直接推进十分钟,等待中的 Effect 就会继续执行。于是时间相关测试可以快速、确定地完成。
但“充分 Effect 化的代码里没有 setTimeout”是一条编程主张,不是语言事实;“把 E2E 墙钟时间压缩到单测时间”也有前提:所有等待必须经过可替换时钟。真实浏览器、网络、数据库和第三方库中的原生定时器不会自动加速。
到这里,原话已经从技术分析转入个人回忆。
General Magic 是一家商业上没有成功、但对后来产业影响很大的公司。其成员后来参与或创建了 Android、iPod、eBay 等重要项目。
作者把 AFFiNE 比作 General Magic,是在说:
项目没有完整实现最初的宏大目标,但在那里工作过的人,可能把形成的能力和理想带到其他地方继续发展。
这是一种组织类比,无法像代码一样证明。
“AFFiNE 的核心成员去了 Manus、VoidZero”没有给出姓名和对应履历,公开材料不足以核实这个笼统表述。把它当作作者的内部观察更合适。
PocketJS 则是一个真实的新项目:它用很小的 Rust 核心,让 Solid、Vue Vapor 等现代组件运行在 PSP、浏览器 WASM 与无头测试环境中。
作者问“PocketJS 能不能成为新一代 Flash”,是在期待它成长为跨设备内容运行时。现阶段它还没有 Flash 当年的分发规模、创作者生态和平台地位,因此这只是展望。
| 原话中的主张 | 结论 | 应当怎样理解 |
|---|---|---|
| DSH 不是普通 Hook 式插件系统 | 成立 | 产品级组件本身也进入插件树 |
| Agent Loop 也可以是插件 | 成立 | DSH 官方架构明确列出 |
| 插件卸载会撤销注册 | 成立但有限 | 只覆盖通过 Cordis 管理的 effects |
| 任意逻辑都能动态替换 | 过度表述 | 必须先被做成服务、事件或插件组件 |
| AFFiNE 做过 DI 与生命周期框架 | 成立 | 提交历史和公开代码可以验证 |
| AFFiNE 已完整实现 DSH 式动态组合 | 证据不足 | 公开实现显示的是部分共同基础 |
| DeepSeek 为此给出了数学证明 | 有依据,但范围有限 | 证明的是形式模型中的性质,不是任意现实代码 |
| DSH 做到极致就是 Effect | 类比,不是事实 | 二者有共同关注点,但运行模型不同 |
| TestClock 能让时间测试立即完成 | 有条件成立 | 时间操作必须通过 Effect Clock |
| AFFiNE 像 General Magic | 个人判断 | 是组织命运的类比 |
| PocketJS 可能成为新 Flash | 未来展望 | 当前无法验证 |
抛开“第一个”“任意”“做到极致”等夸张表达,它提出了一个值得认真对待的问题:
插件系统究竟只是让用户增加功能,还是允许产品的基本构成被重新组合?
前一种思路追求稳定:核心团队定义边界,第三方在边界外扩展。
后一种思路追求开放:模型、工具、存储、执行环境乃至 Agent Loop 都尽量成为可以替换的运行时组件。
DSH 明显选择了后者。这会带来更高的抽象成本、更复杂的代码和更难理解的生命周期;换来的则是组合不同 Agent、替换能力 provider,以及让 Agent 检查和改变自身运行环境的可能性。
事实上,DSH 已经提供自指 Cordis 工具:Agent 可以检查当前运行时、定义动态插件、运行和停止它们。但这些动态插件目前只存在于当前进程内,重启后消失,也不会自动晋升为正式插件。
因此,最准确的总结不是:
DSH 已经实现了任意逻辑的自我进化。
而是:
DSH 把 Agent 的主要行为结构放进了一个可检查、可组合、可撤销的插件运行时,为进一步的自修改实验提供了基础。
这已经很特别,但它仍然是一套正在开发中的工程系统,不是魔法。
- DeepSeek Harness:Architecture
- DSH 自指 Cordis 工具及其持久化限制
- Cordis 论文:A Programming Paradigm for Spatiotemporal Composability
- Effect:Managing Services
- Effect:Layer 源码与说明
- Effect:TestClock
- Wired:General Magic 的后续影响
- Introducing PocketJS
核对日期:2026-08-15。本文区分公开可验证事实、技术类比与第一人称经历;无法独立核实的私人会议、融资叙事和人员流向未作为事实采用。