KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
02 · 插件全景:54 类插件怎么拼出一个运行时 — keel 龙骨
packages/ 下有 54 个顶层分类、307 个二级包(去掉 test-support/ 的 7 个测试基建包正好 300)。vendor/ 下另有 9 个 vendored 的 Cordis 生态包。理解这个仓库的难点不在任何一个包多大,而在分类之间的分层关系:谁是能力接缝(seam)、谁是接缝的具体实现、谁是把它们拼起来的 bundle。
packages/ 下有 54 个顶层分类、307 个二级包(去掉 test-support/ 的 7 个测试基建包正好 300)。vendor/ 下另有 9 个 vendored 的 Cordis 生态包。理解这个仓库的难点不在任何一个包多大,而在分类之间的分层关系:谁是能力接缝(seam)、谁是接缝的具体实现、谁是把它们拼起来的 bundle。
这一章按「从底层能力到产品表面」的顺序过一遍全景,并说明 bundle/ 与 boot/ 的分工。
一、数量与分类
统计口径是 packages/<分类>/<包>/package.json 恰好两级,嵌套的 fixture 目录不计。
| 分类 | 包数 | 分类 | 包数 | 分类 | 包数 |
|---|---|---|---|---|---|
| client | 59 | session | 20 | experimental | 20 |
| util | 16 | subagent | 10 | shell | 10 |
| api | 9 | host | 9 | core | 8 |
| fs | 7 | llm | 7 | test-support | 7 |
| bundle | 6 | context | 6 | skill | 6 |
| web | 6 | boot | 5 | compaction | 5 |
| credentials | 5 | interaction | 5 | extensions | 4 |
| goal | 4 | sandbox | 4 | session-query | 4 |
| ssh | 4 | storage | 4 | typert | 4 |
| workflow | 4 | hooks | 3 | jobs | 3 |
| lsp | 3 | preset | 3 | sdk | 3 |
| spill | 3 | subprocess | 3 | terminal | 3 |
| attachment | 2 | deliverables | 2 | feedback | 2 |
| guard | 2 | mcp | 2 | ptc-runtime | 2 |
| webhook | 2 | acp | 1 | browser-use | 1 |
| computer-use | 1 | document | 1 | identity | 1 |
| plan | 1 | runtime-diagnostics | 1 | schedule | 1 |
| settings | 1 | todo | 1 | workspace | 1 |
experimental/ 下的 20 个包(agent-team、ptc-runtime-python、browser-use-*、computer-use-*、speech-to-text、webworker-* 等)是尚在实验阶段的独立产品线,默认不进任何 preset。
二、核心八包与模型层
core/ 只有 8 个包,但它们是运行时骨架:agent(Agent 接口、AgentRegistry、事件表)、agent-loop(默认驱动)、session(事件日志)、tools(工具注册表 + 调度器)、system-prompt(提示词装配)、scope(作用域)、agent-default-model、agent-tool-presentation。
llm/ 7 个包分成三个层次:
llm是契约层:registerAdapter、prepareCall、stream、Message/ContentBlock/TokenUsage类型,以及PreparedLlmCall。llm-deepseek与llm-pi-ai是两个 adapter 实现。前者硬编码单一 provider(index.ts: PROVIDER = 'deepseek-official'),后者构建在@earendil-works/pi-ai上,用一份 catalog 覆盖多家 provider。llm-retry、token-meter、deepseek-llm-api-extensions、plugin-package-inventory-deepseek是横向插件。
三、工具家族:能力接缝 + 具体实现
工具类插件几乎都遵循同一个二分法:tool-* 是对模型的接口,*-local / *-sandbox 是其背后的能力接缝实现。以 shell/(10 包)为例:
tool-bash / tool-pwsh / tool-bash-persistent / tool-pwsh-persistent ← 模型侧工具
bash-local / pwsh-local ← 直接在本机 spawn 的实现
bash-sandbox / pwsh-sandbox ← 经 ctx.sandbox.confine() 包装 argv 的实现
shell-env / shell ← 环境快照与上下文服务
同构的还有 fs/(tool-fs/tool-fs-search/tool-str-replace-editor + fs-local/fs-sandbox/fs-observation-policy/fs)、lsp/(tool-lsp + lsp/lsp-stdio)、jobs/(tool-jobs + jobs/jobs-local)、ssh/(ssh + fs-ssh/sandbox-ssh/subprocess-ssh)、spill/(spill + spill-local/spill-policy)。
其余工具按功能成组:web/(tool-web 统一入口,web-fetch-http 与 web-search-{deepseek,exa,perplexity} 是可换的检索实现)、interaction/(tool-ask-user、user-questions、user-approval、commands、permission-presets)、skill/(skill 契约 + skill-filesystem/skill-office/skill-badge 来源 + tool-skill)、workflow/(workflow + workflow-ptc + tool-workflow + tool-ralph)、goal/、guard/(repeat-tool-reminder、timeout-policy)、todo/、plan/plan-mode、deliverables/、document/office-to-pdf。
四、会话:20 个包的三层结构
session/ 是除 client/ 外最大的一类,可以拆成三层:
- 日志与派生:
core/session是本体;session/session-projection+session-projection-cache提供「把事件日志折叠成宿主状态」的可插拔投影;session-stats、session-turn-outline、session-title*(4 个)是具体投影。 - 格式与迁移:
session-format(编解码链契约)+session-format-catalog(历史版本目录)+session-format-v0-to-v1/v1-to-v2/v2-to-v3/v3-to-v4四段相邻迁移。 - 持久化与遥测:
session-persistence(契约)+session-persistence-jsonl(实现,.jsonl与.jsonl.zstd)、session-checkpoint-policy、session-telemetry/session-telemetry-otel、session-log-deepseek。
注意:本 commit 没有 session-persistence-sqlite。 SQLite 只出现在另外两处:storage/storage-sqlite(通用存储后端)与 session-query/session-query-sqlite(会话全文检索,web profile 里配 path: ':memory:'、openAt: never)。
五、沙箱、子进程与存储
sandbox/(4):sandbox(接缝契约)、sandbox-local(本机三后端)、sandbox-windows-acl(Windows restricted token)、sandbox-policy(会话级模式)。subprocess/(3):subprocess契约 +subprocess-local+win32-process。storage/(4):storage契约 +storage-domain/storage-json/storage-sqlite。subagent/(10):subagent本体 + 6 个 provider 实现(spawn-in-process/fork-in-process/claude-code/codex/acp/dsh-sdk)+in-process-driver+tool-subagent+tool-subagent-control。mcp/(2)、hooks/(3:hook-protocol/hooks-claude-code/hooks-codex)、acp/(1)、webhook/(2)、schedule/(1,cron)。
六、UI 六成在 client
client/ 59 个包是纯浏览器侧:ui-slots 与 ui-primitives 是底座,ui-chat/ui-trajectory/ui-tool/ui-approval/ui-session/ui-conversation 是会话视觉,ui-settings-*(9 个)是设置面板,ui-sidebar-*(6 个)是侧栏,其余是各类小部件与 modules/resources/store/connection/locale/hmr/file-upload 等基础设施。这些包不在 Node 进程里,通过 ctx.slots 注册到宿主提供的槽位。
七、bundle 与 boot 的分工
这是最容易混淆的一层。
bundle/(6 包)是「配置补丁集」,不是代码。base/web-app/headless/acp-app/sdk-app/sdk-minimal各带一个或几个cordis.patch.yml,内容是insert:的行列表。base的 README 明说「this package is not a library you import」。补丁按层叠加,同一id的行整行覆盖(不是合并),所以任何随模式变化的行都必须由各模式 bundle 完整重述。boot/(5 包)是「怎么把补丁变成活对象」:app-boot读cordis.yml、装 Loader 守卫、按DEFAULT_PROFILE_BUNDLES = ['@deepseek-ai/dsh-base']组出补丁序列,最后驱动@deepseek-ai/cordis-plugin-loader直到插件树稳定;cmdline、config-editor、hmr、plugin-manager是围绕它的运维能力。preset/(3 包)是 base 之下的第三层:agent-preset-registry管理「每个会话用哪套 preset」,agent-preset是声明config.plugins的插件形式,persona是最小的人格插件。web-app 里agent-preset-registry的配置是default: standard,四个 preset 声明作为独立 patch 文件列在presets/下。
八、vendor/:9 个 vendored 包
vendor/ 不是只有一个 cordis:cordis(内核)、cosmokit(工具函数与 DisposableList)、loader(配置树加载)、include(patch 应用)、group(cordis:group)、hmr、timer(cordis-plugin-timer)、logger-console、schemastery(z 校验)。这些以源码形式进仓库,而不是作为 npm 依赖,是为了让「上游行为」成为可 review 的代码。
九、能力接缝是这套分层的通用形状
把上面的分类抽象一层,会看到同一个形状反复出现:一个不实现的契约包 + 若干个可替换的实现包。
| 契约包 | 实现包 | 接缝方法 |
|---|---|---|
sandbox/sandbox |
sandbox-local、sandbox-windows-acl |
SandboxProvider.confine(argv, policy, signal) |
subprocess/subprocess |
subprocess-local、win32-process |
进程创建 |
storage/storage |
storage-json、storage-sqlite、storage-domain |
存储后端 |
subagent/subagent |
6 个 provider 包 | registerProvider(provider) |
skill/skill |
skill-filesystem、skill-office |
技能来源 |
session/session-persistence |
session-persistence-jsonl |
会话落盘 |
ptc-runtime/ptc-runtime |
ptc-runtime-node(+ experimental/ptc-runtime-python) |
PtcRuntime.run/resolve |
docs/capability-seams.md 把这套约定写成文档。它的实用价值是:换执行环境(本地 / 容器 / 远程 / 不同 OS)只需要替换接缝实现,工具定义、提示词、会话日志都不动。代价是每条接缝都要定义一套接口与错误语义,且「哪个实现被激活」由 profile 的补丁决定,读代码时看不见。
api/(9 包)是这条形状的宿主侧例外:它不是接缝实现,而是把运行时能力暴露成 HTTP/JSON 的控制器(session-controller、workspace-controller、terminal-controller、gateway 等),给桌面端与 Web 端用。
代码地图
| 机制 | 位置 | 要点 |
|---|---|---|
| 内核 | vendor/cordis/src/index.ts |
16 行,只做 re-export |
| 工具契约与调度 | packages/core/tools/src/index.ts |
1983 行;含 TOOL_RUNTIME_SCHEDULER 与 wireSchemas |
| 模型契约 | packages/llm/llm/src/index.ts |
registerAdapter / prepareCall / stream |
| DeepSeek adapter | packages/llm/llm-deepseek/src/index.ts |
PROVIDER = 'deepseek-official',注册单 provider |
| 通用 adapter | packages/llm/llm-pi-ai/src/provider.ts |
PROTOCOLS 只开放三种可手写路由 |
| 会话本体 | packages/core/session/src/index.ts |
1310 行;Session.append / deriveMessages |
| 格式迁移链 | packages/session/session-format/src/chain.ts |
defineSessionFormatMigration 强制相邻版本 |
| 沙箱契约 | packages/sandbox/sandbox/src/index.ts |
SandboxProvider.confine 抽象方法 |
| 子代理契约 | packages/subagent/subagent/src/index.ts |
registerProvider 注册 provider |
| 基础 bundle 补丁 | packages/bundle/base/cordis.patch.yml |
单个 insert: 行列表,后被按 id 覆盖 |
| Web bundle 补丁 | packages/bundle/web-app/cordis.patch.yml |
注册 agent-preset-registry(default: standard) |
| 启动装配 | packages/boot/app-boot/src/index.ts |
驱动 Loader,暴露 dshHomePath 给 !!js 表达式 |
| profile 默认 bundle | packages/boot/app-boot/src/profile.ts |
DEFAULT_PROFILE_BUNDLES = ['@deepseek-ai/dsh-base'] |
| preset 声明示例 | packages/bundle/web-app/presets/standard.patch.yml |
146 行,config.id: standard、order: 1 |
| 接缝文档 | docs/capability-seams.md |
记录「契约包 + 实现包」这套约定的清单 |
| PTC 运行时契约 | packages/ptc-runtime/ptc-runtime/src/types.ts |
PtcRuntime.resolve/run 与 PtcBindingFunction |
| 实验包策略 | scripts/experimental-package-policy.ts |
对 experimental/ 的独立校验规则 |
关键取舍
能力与其实现拆成两个包,代价是包数量膨胀到 300。tool-bash 与 bash-local 分开,意味着换沙箱实现不需要改工具定义,也让 bash-sandbox 与 bash-local 能共用一个 tool-bash。代价是「这个工具到底跑在哪」需要跨包追踪,且每个能力都要维护一套接缝接口。
bundle 用 YAML 补丁而不是 TypeScript 组合,代价是覆盖语义比合并更硬。
补丁是「整行替换 config」,所以 ptc preset 与 standard 逐行同构却必须复制整份 150 行文件。好处是 web 编辑器保存的改动可以按 id 精准回写,且补丁层数就是唯一的合并规则。
模型层把契约与实现分开且只给两个 adapter,代价是新增 provider 要走 catalog。llm 不知道任何 provider,llm-deepseek 只认一个,llm-pi-ai 靠上游 catalog 覆盖多家。新增一个非 catalog 的 provider,要么写新 adapter 包,要么走 llm-pi-ai 的三种手写协议之一(见第 06 章)。
experimental/ 的 20 个包不删,代价是仓库表面变大。scripts/experimental-package-policy.ts 给它们一套独立规则(例如不要求 README 完整、不进 preset)。保留而非放在分支里,是为了让实验代码持续通过类型检查与 lint。
自测题
packages/shell/下有 10 个包。请划分出「模型侧工具」「执行后端」「上下文服务」三类,并说明如果要把 bash 换成容器执行,需要新增哪些包、修改哪些包。session/的 20 个包里,哪些参与「把日志变成可读状态」,哪些参与「把旧格式变成新格式」?session-projection与session-format的职责边界在哪?bundle/base的 README 说「这个包不是你 import 的库」。那么packages/boot/app-boot读到的补丁最终以什么形式进入运行时?session-query-sqlite在 web profile 里被配成openAt: never。这与会话持久化完全无关吗?请解释它实际解决的是哪类需求。- 为什么
vendor/里的包要以源码入库,而不是写在package.json的dependencies里?这样做对pnpm-lock.yaml和升级策略分别意味着什么?