编译器架构
agent-bundle 把一个项目编译为宿主原生的插件。本页描述的是编译器在今天的代码中的实际工作 方式:后续阶段读取的三层数据、生产与消费每一层的模块,以及这些阶段留下的产物清单。它是 #592 的文档切片:第 2 步的三层,以及第 3 步落地的 v2 权威清单(PR #604)。
除非明确写出包名,下文的路径都相对于 packages/agent-bundle/src/。类型名就是编译器导出的
那些,目前还不存在 ApplicationIR、ProjectionIR 或 ArtifactIR 类型。
本页描述的输出布局是复合插件根目录:产物输出处只有一个目录,由 targets 选择其中出现
哪些宿主投影
(#555 Wave 1,
PR #578)。不存在
artifact/<host>/ 分区,也不存在 plugin target。
三层
想了解这些箭头起点处的文件系统约定,请读项目结构;想了解 箭头终点处的字节,请读 Target 与产物。
应用层
发现在每次构建中运行一次,随后规范化冻结出一个与宿主无关的模型。今天这项工作由两个结构 分担;它们还不是同一个 IR。
发现产出什么
config/discover.ts 遍历项目并返回一个 DiscoveredProject:
routes/graph.ts 中的 compileRouteGraph 是可执行路由的编译器。它只 glob 下列根目录
(不含其他):
冻结后的结果是一个 CompiledRouteGraph(routes/types.ts):
servers[],CompiledServerSurface(generated/custom/command/remote/conflict),每个都持有 kind 为tool、resource、prompt或app的CompiledAgentRoute行。cli?,CompiledCliSurface(generated/conventional/conflict),带有CompiledCliCommand行,其 argv 语法来自字面量config.inputJsonSchema或 JSON 输入。events[],kind 为event-route的CompiledAgentRoute行。scripts[],kind 为script。providers[],CompiledProvider(不是路由 kind;它们包裹执行)。layouts?,CompiledLayout(root或server作用域)。事件路由与浏览器 App 永不被包裹。contracts?,路由本地的config.inputJsonSchema元数据,id 为contract:<module>#inputJsonSchema。digest,根据项目相对身份和声明元数据计算的 sha256。diagnostics,路由图错误(AB48xx与AB494x)。
每个 CompiledAgentRoute 携带 id、kind、source、约定式的 provenance、一份静态
提取的 config(或 emptyRouteConfig)、可选的有界 inputSchema、可选的 contract
(共享的 RouteContract id),以及,对 MCP 路由而言,serverId。事件路由还携带
event(来自 routes/public.ts 的 CanonicalAgentEvent)。
Skill、命令、规则、配置中声明的钩子、手写的 MCP 入口以及宿主原生的钩子文档不在这张
图里。它们留在与之并列的 DiscoveredProject / NormalizedPlugin 上。
规范化向前传递什么
config/normalize.ts 消费 DiscoveredProject 与已加载的配置,发出一个 NormalizedPlugin
(core/types.ts)。它就是之后每个规划器读取的对象。它并不保留
CompiledRouteGraph;PreparedProject(dev/project-service.ts)把 model 与
routeGraph 作为并列字段携带。插件对象持有:
- 身份(
metadata)与所选的targets(NormalizedTarget[],排序去重后的名称); skills、commands、rules、hooks、nativeHooks、mcpServers、mcpApps、scripts、assets、payloads;- 从路由图复制过来的
providers与layouts; runtime.node(生成式可执行文件的下限);- 可选的
state、notices、packageBuild、marketplace与宿主扩展。
配置声明的钩子与 src/events/** 路由都落在 NormalizedPlugin.hooks 中。事件路由分支会
设置 eventRoute 以及形如 hook:event-route:tool-before 的 id(规范事件名里的 /
换成 -);配置分支则不会。宿主原生的
钩子文档(Claude 的 hooks/hooks.json 源等)是第三个集合 nativeHooks。
config/validate.ts 针对同一个已发现的项目运行。它不会重新发现文件;它只判定发现阶段
已经找到的内容。
下一层接收什么
适配器与 build/compose.ts 接收完整的 NormalizedPlugin。它们不会再次调用
compileRouteGraph。共享的编译阶段(build/entries.ts、build/cli-bins.ts、
build/mcp-apps.ts)也读取这个模型:生成式 MCP 入口、路由式 CLI bin、脚本与 App 都从它
规划一次,然后归属到复合身份上。
投影层
投影是同一个 NormalizedPlugin 在某个宿主上的表示。配置键仍然是 targets。复合根目录
落地之后,这个键选择的是哪些投影会被规划进那唯一的输出目录,它并不命名产物、运行时、
分发形式或应用。
target 从哪里进入
agent-bundle.config.ts的targets,或 CLI 的--target。两者都省略时,adapters/registry.ts只接纳默认宿主portable。config/normalize.ts把所选名称存到NormalizedPlugin.targets上。build/compose.ts的planComposite调用sortedProjections(model.targets.map((t) => t.name)),并且只对这些名称运行registry.get(name).plan(model)。plugin不是 target。targets: ['plugin']与--target plugin都是AB4100。inspect --target是api.ts的inspect()上的显示过滤器:它只在打印的plans中保留该名称(未知名称是AB7004)。它不会改变selectedTargets或复合 身份。inspect --bundler仍然组合完整的所选集合。
adapters/composite-layout.ts 拥有选择相关的辅助函数:sortedProjections、
projectionIdentity(排序后的名称以 + 连接,例如 claude+codex)与 hookWrapperPath。
命令、规则、钩子或脚本上声明级的 targets 仍然表示“这个组件到达这些宿主”。当该集合
与所选集合有交集时,组件就会被包含。命令或规则若只限定在所选宿主的子集上,却位于另一个所选
宿主会按约定发现的目录中,则是 AB4105。Skill 总是发往每一个所选宿主
(skill.targets 就是所选集合);会改变字节的按宿主 Skill frontmatter 扩展是
AB4103。
谁来规划投影
每个内置适配器都实现 TargetAdapter.plan(model),并返回一个 TargetArtifactPlan
(adapters/types.ts):entries(write 或 copy)、可选的 hookEntries,以及
diagnostics。
这些能力 JSON 文件是注册表与生成的宿主参考页面所读取的宿主
证据:观察到的版本、清单路径、安装表面、路径 token、MCP 传输方式与组件支持。适配器据此
查询宿主能表达什么;compose.ts 在判定 AB4105 时查询 registry.hostsComponent 与
registry.artifactLayout,在对整个所选集合求通知投递声明的交集时查询
registry.noticeDelivery。共享的 MCP 入口与每个宿主的钩子包装器都由该交集接线,因此它们
声明的是同一组路由。
TargetRegistry(adapters/registry.ts)是注册适配器的唯一位置。createDefaultRegistry
先把 portable 注册为默认值,然后注册 codex、claude、cursor 与 amp。注册顺序不是
目标优先级:planComposite 在规划前先对所选集合排序(sortedProjections),复合身份就是
排序后的名称连接。不存在 plugin 适配器。
amp 是唯一一个插件形态为生成目录、而非一组声明式文档的内置宿主。它的规划在
.amp/plugins/{plugin}/ 下写出一个 index.js PluginAPI 工厂,由该 API 注册 Skill 与事件
回调;其 MCP 配置是 Skill 作用域的:一个规范服务器需要恰好一个 Amp Skill,而由编译器拥有的
本地服务器会被拒绝,因为固定的契约没有记录可用于启动它的插件根 token。逐表面的证据仍在生成的
宿主参考页面;面向作者的选择地图是
目标与能力地图。
compose 交给产物组装什么
composeProjections(build/compose.ts)合并所选的计划:
- 同一路径上字节相同的条目只保留一份。同一路径上字节不同则是
AB4103(投影按宿主名顺序 比较,路径按路径顺序比较)。 install/surface.ts的installSurfaceEntries作为一个合成的所有者被合并进来(任何 内置宿主都有INSTALL.md;选中cursor或portable时有install.mjs)。- 钩子条目是拼接的;它们不按路径合并。到达多个所选宿主的钩子会为每个宿主编译一个包装器
(
hooks/<name>.<host>.mjs);只到达单个宿主的钩子保留hooks/<name>.mjs(hookWrapperPath)。 - Codex 与 Cursor 的钩子/MCP 文档与各自的清单并列(
.codex-plugin/hooks.json、.cursor-plugin/mcp.json……)。当另一个所选宿主有到达约定hooks/hooks.json/.mcp.json/mcp.json路径的钩子或 MCP 服务器时,这些投影会发出一份空的屏蔽文档, 使目录发现无法加载该文件。
构建阶段拿到的 CompositePlan 携带 entries、hookEntries、selected、identity、
projections(每个所选宿主一份已规划的投影)、cliBin(当任一所选宿主接纳路由式 CLI
时为 true)以及 noticeDelivery。AB4106 更早在模型上判定,位于
config/validate.ts:把高级注册表上的适配器与任何其他 target 混在一起的选择不能共享
一个根目录。
编译后的 MCP 入口、脚本、路由式 CLI bin 与 MCP App 不会按宿主重新规划。
build/build.ts 以 target: composite.identity 把它们编译一次,并写到根目录。
产物层
产物组装消费 CompositePlan 与已编译的表面,并在 artifactRoot 写出一棵目录树(CLI 默认为
artifact/;output.distPath 或 --output 可以移动它)。
写入器
build/compile-stages.ts 安排工作顺序:先是 MCP App(MCP 入口会嵌入它们的 HTML),然后是
node 表面。
校验器
AB6005 读取每次宿主包、包构建 dist 与 MCP App 视图编译的证据。Rspack 保持的每个
ExternalModule 都必须是 Node 内建模块、pnpapi 或已输出的同级文件;其他 external 都会失败。
这就是 build/compiler.ts 与 build/external-policy.ts 强制的自包含规则;生成式插件编译器配置
同样实施该规则(build/rslib.ts 的 composeEntryLibConfig:autoExternal: false、
bundle: true、没有面向作者的 externals)。
AB7014 / AB7015 是 prepack 门禁:package.json 中的安装期依赖必须出现在打包后的
预构建 runtimeDependencies、打包后声明文件引用或消费者安装脚本中
(AB7014),并且消费者的 npm 必须能够抓取它(AB7015)。内联进 bundle 的包不算已使用;
编译证据会记录诊断中点名的 dist bundle。
复合根目录
以 targets: ['amp', 'claude', 'codex', 'cursor', 'portable'] 构建时,host-test 示例的根目录为:
Target 与产物是输出契约:哪个宿主读取哪份文档、
钩子包装器的命名规则,以及 AB4103 / AB4105 / AB4106。本页不重复那张表。
执行内核与 Agent Document
上面的编译器流水线止于磁盘上的文件。请求时是另一套独立的栈。已经存在的请求内核是
runAgentRequest(packages/rsc-runtime/src/agent-request.ts,以
@agent-bundle/runtime 导入)。生成式 MCP 路由、事件路由与渲染式 CLI 命令会调用它。
配置声明的钩子绕过它:生成的包装器解码宿主信封并直接调用作者的函数,没有
provider、布局或 Agent Document。原生钩子文档是适配器校验并合并的宿主 JSON;它们不是
编译出来的包装器,也不进入内核。
mcp-entry.ts 是 stdio 进程外壳。它不构造请求上下文。构造请求上下文的是
mcp-server-runtime.ts:身份、lineage、provider、runAgentRequest、布局(针对文档路由)
以及 MCP 投影。
Agent Document 是渲染式路由的规范响应。其类型位于
packages/rsc-runtime/src/agent-document.ts,以 @agent-bundle/runtime 导入:
version(当前为1)status:success|represented-error|failedroot:由Agent.*节点组成的树(result、text、markdown、context、json、progress、image、audio、resource、error)- 可选的
value(可 JSON 序列化的结构化结果)
MCP 的 tool 路由把这份文档降级为 CallToolResult 内容加 structuredContent。resource 与
prompt 路由则返回经校验的协议结果(来自 document.value)。CLI 的人类可读/JSON 模式
通过 CLI 输出契约渲染同一份文档。Workbench 通过
packages/workbench/src/runtime/agent-document-client.ts 检视它。原始的 CallToolResult
是互操作边界,而不是编写模型。
上下文值是 Observed<T>(@agent-bundle/runtime):state 为 available
(带有 source:native | receipt | derived)或 unavailable(带有
reason)。内核不会凭空发明宿主未提供的宿主或会话身份。
agent-bundle.manifest.json 的现状
写入器是 build/build.ts 的 manifestFor。它组装出一个 ArtifactManifest
(build/manifest.ts;TypeDoc:
ArtifactManifest),
来源是 manifestProjections、manifestBins / manifestHooks /
manifestMcpServers / manifestScripts、manifestDistribution,以及
build/manifest-routes.ts 的 artifactRoutesFor。writeManifest 再用
assembleArtifactManifest 将其序列化:规范的 stableJson 加一个末尾换行。
parseArtifactManifest 拒绝任何其他字节序列、重复键、未知键、缺失键、未排序
的数组,以及指向清单自身的文件条目。manifestVersion 为 5。公开读取方是
build/manifest-file.ts 的 readArtifactManifest:ok、missing(仅
ENOENT)或 invalid。
该 schema 在 TypeScript 中封闭(build/manifest.ts 中的
requireExactKeys)并且作为 JSON Schema 随包发布
(packages/agent-bundle/schemas/agent-bundle.manifest.schema.json),
由 build/manifest-schema.ts 经 api.ts / index.ts 导出为
artifactManifestSchema 与 validateArtifactManifestSchema。解析器仍是
更严格的权威:它还检查交叉引用、规范字节与排序。adapters/schemas/**
下的宿主 plugin.json / hooks.json / mcp.json schema 与
schemas/** 下的 Agent Skills frontmatter 是另一份契约;网站的
宿主参考 /
事件参考页面由
adapters/capabilities/*.json 与 docs/diagnostics.md 生成,而不是由
这份清单生成。
每个分节与字段见 产物清单。本页只保留大纲。
写入器发出的分节
文档分为两部分,各有自己的版本号。公开契约(manifestVersion: 6)
是消费者读取的部分。编译器记录(compiler,recordVersion: 1)是
这次运行的操作性账目,生产者、项目摘要、来源、适配器修订、固定的
schema、验证结果,它的变化不会推动 manifestVersion;消费者读取方
永远不需要它。
谁读取各个字段
生产环境的读取方调用 readArtifactManifest 或 parseArtifactManifest。
测试不在此列,已安装宿主的证明套件除外。
不存在 portable 的 install / doctor 宿主
(BundleIdentityHost 是 amp | claude | codex | cursor)。
doctor 覆盖 claude、codex 与 cursor;Amp 没有生命周期探测
(DoctorHost 将其排除)。
第 3 步落地了什么
#592 的第 3 步
让这个文件成为编译后的应用索引(manifestVersion 2)。原先差距列表中的
这些编号项现在都在清单里,只有 (5) 除外:
- 路由图。
routes记录服务器、工具、资源、提示、App、CLI 命令、 事件路由、provider、布局、契约与digest。 - MCP / CLI / App 清点。
executables.mcpServers[]记录每台服务器 (entry、apps[]);executables.bins[]记录路由式 CLI。 - 钩子语义。
executables.hooks[].kind为config或event-route。作者写进宿主钩子文档的原生命令不是行。agent-bundle.hooks.json已删除。 - Install / doctor 身份。
readBundleIdentity读取application以及builtInHost匹配该宿主的投影。 - 能力视图。 有意不记录。通知投递交集在构建时由
build/compose.ts的compositeNoticeDelivery推导;inspect的omittedFeatures仍来自模型。把该交集写进清单是后续的 #592 工作。 - 作为一等行的可执行文件。
executables.bins[]、hooks[]、mcpServers[]与scripts[]。 - 分发元数据。
distribution.channels以及可选的distribution.install。
字段契约见 产物清单。
代码今天在哪里分叉
#592 点名了本页不得掩盖的那些分裂。这里只列路径,不给修复。
发现不是一张图。 compileRouteGraph 覆盖可执行路由。Skill、命令、规则、配置钩子、
原生钩子与手写的 MCP 入口是 DiscoveredProject / NormalizedPlugin 上彼此平行的集合。
之后需要“每一个表面”的消费者仍要遍历多个字段。
五个规划器,一次合并。 planClaudeArtifacts、planCodexArtifacts、
planCursorArtifacts、portableAdapter.plan 与 ampAdapter.plan 各自遍历整个模型并输出
宿主文档。
compose.ts 合并的是字节;它并没有用单个 project(application, capabilities) 函数取代
这些规划器。
事件执行。 约定式 src/events/** 支持普通 .ts 处理器、渲染 .tsx 处理器和显式延迟加载的
.view.tsx 模块。宿主原生文档仍是适配器输入。Provider 通过请求上下文延迟解析。
MCP 与 CLI。 同级 <tool>.cli.ts 投影工具,不创建第二个操作。
flag 元数据来自字面量 config.inputJsonSchema;编译后的可执行文件加载运行时 schema。
检查阶段不解释 schema 导入或别名链。
浏览器桥接曾被分叉。 现在只有一个宿主侧中继,位于
web-host/browser/frame-relay.ts(McpAppFrameRelay),由 Workbench、serve-app、开发态
/web/<server>/<app> 与产物的 <plugin> web 页面共同挂载
(#564 已落地);App 使用
agent-bundle/app 的类型化客户端(#594 已落地)。
与清单并存的文件系统探测。 install、uninstall 与 doctor 通过
install/identity.ts 的 readBundleIdentity 读取身份(application、
projections[].builtInHost、documents.plugin、市场名称)。它们不会探测
<from>/<host> 或宿主的 plugin.json 来寻找根目录。Eval 仍对整张文件表按
每个 projections[].host 各哈希一遍(evalTargetDigests),而不是记录一份
按投影划分的文件视图。Workbench 检视器从 files[] 重建一棵树,按
projections[] 各标注一次,并从 executables 读取可执行文件,而不是从路径
前缀推导。
共享表面中残留的按 target 逻辑。 共享的 MCP / CLI / App 编译使用
composite.identity,但钩子包装器仍然固化一个 target。
dev/artifacts/artifact-executables.ts 把 executables.scripts[] 展开为
每个列出该脚本的宿主一行。artifact-inspection-service.ts 为每个
projections[] 宿主把同一棵树各标注一次。通知投递是所有所选宿主的交集
(compose.ts 的 compositeNoticeDelivery),应用到每一个共享的 MCP
入口上,并且不写进清单。
能力选择保持窄范围。 事件路由可以声明 requires: ['events.toolBefore.deny']
这样的能力行;编译器根据固定的能力表选择支持它的宿主。其他组件仍使用 targets;当事件路由的行为
确实依赖特定宿主时,targets 也仍是它的逃生口。
路线图
#592 是总纲。本页记录三层 (第 2 步)与 v2 清单(第 3 步)。它之下的实现切片各有自己的 issue:
第 3 步以版本 2 落地了权威的 agent-bundle.manifest.json;后续的可执行、分发与路由契约字段
把当前契约推进到 manifestVersion 6。
字段契约见 产物清单。#592
会一直保持打开,直到该 issue 上剩余的验收标准落地。
下一步
- 项目结构:发现所遍历的
src/根目录。 - 配置模型:这些根目录所填充的配置。
- MCP 服务器与 MCP App 与钩子,大多数 作者最先遇到的两个可执行表面。
- 产物清单:
agent-bundle.manifest.json的每一个分节与字段。 - Target 与产物:复合根目录。
- 交付捆绑包:根目录如何编译,以及其中装了什么。
- 产物校验:重新读取这份清单的那些检查。