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 个包分成三个层次:

三、工具家族:能力接缝 + 具体实现

工具类插件几乎都遵循同一个二分法: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/ 外最大的一类,可以拆成三层:

  1. 日志与派生:core/session 是本体;session/session-projection + session-projection-cache 提供「把事件日志折叠成宿主状态」的可插拔投影;session-stats、session-turn-outline、session-title*(4 个)是具体投影。
  2. 格式与迁移:session-format(编解码链契约)+ session-format-catalog(历史版本目录)+ session-format-v0-to-v1 / v1-to-v2 / v2-to-v3 / v3-to-v4 四段相邻迁移。
  3. 持久化与遥测: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)。

五、沙箱、子进程与存储

六、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 的分工

这是最容易混淆的一层。

八、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。

自测题

  1. packages/shell/ 下有 10 个包。请划分出「模型侧工具」「执行后端」「上下文服务」三类,并说明如果要把 bash 换成容器执行,需要新增哪些包、修改哪些包。
  2. session/ 的 20 个包里,哪些参与「把日志变成可读状态」,哪些参与「把旧格式变成新格式」?session-projection 与 session-format 的职责边界在哪?
  3. bundle/base 的 README 说「这个包不是你 import 的库」。那么 packages/boot/app-boot 读到的补丁最终以什么形式进入运行时?
  4. session-query-sqlite 在 web profile 里被配成 openAt: never。这与会话持久化完全无关吗?请解释它实际解决的是哪类需求。
  5. 为什么 vendor/ 里的包要以源码入库,而不是写在 package.json 的 dependencies 里?这样做对 pnpm-lock.yaml 和升级策略分别意味着什么?

进入 keel 阅读