Kilo v2 核心架构指南:插件化服务容器、Hook 契约与 Effect Schema 域模型设计
【免费下载链接】kilocodeKilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/ki/kilocode
Kilo 是一个开源的全栈智能体(agentic)工程平台,其代码库正处于 v2 大规模重构期。specs/v2/instructions.md 是本次 v2 移植工作的核心纲领:它定义了"把行为从巨型应用服务中剥离、下沉到插件"的总体方向,并给出了服务容器、插件 Hook、Schema 域模型、状态与事件、代码风格等一整套落地规范。阅读本文后,你将掌握 v2 服务的标准形态(以Catalog、AccountV2、AgentV2为样板)、Hook 的触发约定与适用边界、插件启动(boot)的组装方式,以及如何在packages/core中用 Effect Schema 建模领域、用 Immer 草稿暴露可变状态。
一、v2 的总体方向:瘦身核心,插件承载策略
v2 重构的第一条原则是把行为从大型应用服务中移出,下沉到插件中。核心服务应当变成"小而类型化的容器":自己持有状态、暴露简单操作、在合适的位置触发 Hook,把策略(policy)与集成(integration)相关的逻辑留给插件实现。文档给出的目标形态是:
packages/core只承载领域 Schema、类型化错误、状态容器、事件与插件 Hook 契约;- 插件负责实现 provider 特定、config 特定、auth 特定、模型发现(model-discovery)与生成(generation)等行为;
- 服务在设计中天然支持热重载(hot-reload):更新是细粒度的、可观测的,不需要拆除整个进程;
packages/opencode会逐渐变薄:UI、服务端路由、CLI、存储胶水与旧版兼容代码应调用核心服务,而不是自己持有领域逻辑。
换句话说,v2 的目标不是"在新包里复刻旧架构",而是"让服务更容易被替换、更容易被理解"。文档明确要求:不要整包搬运旧服务,应当先移植领域形态与容器 API,再把具体行为留在 Hook 之后交给插件实现。
二、Service Shape:以 Catalog、AccountV2、AgentV2 为样板的标准服务形态
文档要求 v2 核心服务遵循统一的"Service Shape"。以Catalog、AccountV2、AgentV2为样板,一个标准服务模块自上而下依次是:
- 模块顶部定义 Schema 与品牌化(branded)ID;
- 用
Schema.TaggedErrorClass定义预期失败的类型化错误; - 定义包含小操作集合的
Interface; - 暴露
Context.Service; - 用私有内存状态实现
layer; - 暴露带显式依赖的
defaultLayer; - 用
export * as Name from "./file"自导出。
在 packages/core/src/catalog.ts 中可以看到这一模板的完整落地:文件首行即export * as Catalog from "./catalog",随后定义了ProviderRecord、DefaultModel等类型,用Schema.Literals(["provider.use"])声明策略动作,并用export const Event = Catalog.Event复用领域事件。Service通过Context.Service<Service, Interface>()("@opencode/v2/Catalog")声明,接口里只有provider与model两组小操作(get、all、available、default、small),完全符合"dumb container"的定位。
AccountV2(packages/core/src/account.ts)则展示了 Schema 层的写法:ID、OrgID、AccessToken、RefreshToken等全部通过Schema.String.pipe(Schema.brand(...))得到品牌化类型;Info、Org、Login用Schema.Class建模;预期失败被建模为AccountRepoError、AccountServiceError、AccountTransportError三个TaggedErrorClass,并组合成AccountError联合类型。
2.1 容器 API 的动词约束
v2 容器 API 应当"哑":优先使用get、all、available、default、update、remove、activate这类小型领域动词,而不是透出内部数据结构。两条关键约定是:
update(id, draft => ...)用于注册与变更:调用方通过回调拿到可变草稿并就地修改,由容器负责持久化与归一化。Catalog的 draft 中provider.update、model.update都遵循这一形态,且会在回调之后调用normalizeApi做baseURL→api.url的字段归一化(见 packages/core/src/catalog.ts)。- 提交变更前触发 Hook,提交后发布事件:Hook 用于让插件有机会丰富(enrich)、取消(cancel)或校验(validate)变更;事件则用于其他服务或前端对已提交的领域事实做出反应。
2.2 策略的归属边界
文档给出一条重要边界:不要把应用策略直接写进核心服务,除非它是领域不变量(domain invariant)。例如:
- 解析模型端点的继承关系(endpoint inheritance)是
Catalog拥有的职责——这正是 packages/core/src/catalog.ts 中projectModel在做的事:模型未显式声明 API 时,从 provider 继承api与request(headers、body)配置,并保留模型自身的variant; - 而"决定注册哪些 provider"是插件拥有的职责——
packages/core/src/plugin/internal.ts的启动批次中逐个add(...)了ConfigReferencePlugin、AgentPlugin、CommandPlugin、ModelsDevPlugin、ConfigAgentPlugin、ConfigCommandPlugin、ConfigSkillPlugin、所有ProviderPlugins、ConfigExternalPlugin、ConfigProviderPlugin、VariantPlugin,这些注册决策都不属于任何单个核心服务的领域不变量。
三、Plugin Hooks:v2 的扩展边界
插件是 v2 的扩展边界。当某个逻辑应当由集成方而不是容器自身提供时,就应把 Hook 加入PluginV2.HookSpec。文档给出了完整的 Hook 约定:
- 输入不可变、输出可变:Hook 接收不可变输入,外加可变输出;
- 可变对象输出以 Immer 草稿(draft)形式暴露,插件在草稿上就地修改;
- 需要允许插件阻止变更时,包含
cancel: boolean; - Hook 必须顺序触发,保证排序确定性;
- Hook 命名面向领域,如
provider.update、model.update、account.activate、agent.generate; - Hook 的载荷要小且用核心 Schema 类型化。
3.1 应该使用 Hook 的场景
文档明确列出五类应该使用 Hook 的场景:
- 注册 provider 与模型(registering providers and models);
- 应用由 env/account/config 推导出的启用状态(enablement);
- 转换 SDK/provider 选项(transforming SDK/provider options);
- 实现生成类行为,如 agent generation;
- 在"选择属于策略而非状态"时决定默认值。
3.2 不应该使用 Hook 的场景
文档同时划出红线:不要用 Hook 作为传输层关注点(transport concerns)、UI 行为或兼容性垫片(compatibility shims)的垃圾场。也就是说,Hook 是领域扩展点,不是万能后门。
从源码看,Hook 的触发被设计成"可重放(replayable)的变换":State.transform接受一个对草稿的回调,该回调可以在 reload 时被重新执行(见 packages/core/src/state.ts 中对"replayable transform"的注释)。插件对草稿的修改因而具备幂等、可重放的性质,这正是细粒度重配置(granular reconfiguration)的基础。
四、Plugin Boot:组合优先的启动组装
内置核心插件由启动模块负责注册。文档指向的路径为packages/core/src/plugin/boot.ts;在本次检查的仓库结构中,该职责实际由 packages/core/src/plugin/internal.ts 承载(模块自导出为PluginInternal,其 layer 在State.batch中批量注册内置插件,并打上PluginInternal.bootspan)。
当一个新核心服务需要开放给插件使用时,文档给出四条步骤:
- 把服务加入 boot layer 的依赖类型(
Requirements,见 packages/core/src/plugin/internal.ts:目前包含AgentV2、Catalog、CommandV2、Config、EventV2、FileSystem、FSUtil、Global、HttpClient、Integration、Location、ModelsDev、Npm、Reference、SkillV2); - 在 layer 内 yield 出该服务;
- 在
add中把服务provideService给每个插件 effect(packages/core/src/plugin/internal.ts 中loaded.effect通过Effect.provideService注入了全部服务); - 仅当不会产生循环依赖时,才把该服务的 default layer 加入
PluginBoot.defaultLayer。
文档强调:boot 只做组合(composition),本身不应包含 provider、account、agent 或 model 的策略。
在运行时侧,packages/core/src/plugin.ts 的PluginV2.Service提供了add、remove、wait三个操作:add用KeyedMutex按插件 ID 加锁、在State.batch内关闭旧 Scope、fork 新 Scope 执行插件 effect,并发布Event.Added;它还内置了加载循环检测("Plugin load cycle detected")与失败记录,wait则允许调用方等待插件完成加载或拿到失败结果。这印证了文档所述"服务天然热重载、更新不需要拆除整个进程"的设计:插件生命周期完全由 Scope 管理,替换即关闭旧 Scope + 开启新 Scope。
五、边界(Boundaries):core 不反向依赖 opencode
v2 的模块边界是硬性的:
packages/core不得从packages/opencode导入。如果 core 需要某个类型或概念,应先在 core 中移动或重塑领域形态;- 避免整包搬运旧服务:先移植领域形态与容器 API,把具体行为留在 Hook 之后由插件实现;
- 移植一个 opencode 服务时的标准步骤是:识别它持有的状态 → 识别调用方真正需要的操作 → 识别哪些分支属于策略或集成行为 → 在
packages/core中建模状态与操作 → 为策略/集成分支添加 Hook → 在旧包代码保持可用、调用方逐步迁移期间继续工作。
也就是说,v2 允许新旧并行:在调用方完成增量迁移之前,旧包代码必须继续工作。
六、Schemas And Types:以 Effect Schema 作为公开契约
v2 把Effect Schema 当作公开契约,具体约定包括:
- 用品牌化 Schema(branded schemas)表达 ID;
- 用
Schema.Class或Schema.Struct建模领域数据; - 用
Schema.TaggedErrorClass建模预期错误; - 在合适处复用 core 已有辅助工具,如
DeepMutable、statics和整数 Schema。
文档还建议:优先把Info对象作为存储的领域记录;当 update API 需要在首次变更时创建记录时,为Info添加静态empty(...)构造器。这一点在源码中得到了严格执行:
Catalog的provider.update/model.update在记录不存在时分别调用ProviderV2.Info.empty(providerID)与ModelV2.Info.empty(providerID, modelID)惰性创建(见 packages/core/src/catalog.ts);AgentV2的update同样用Info.empty(id)兜底(见 packages/core/src/agent.ts)。
此外,Schema 要保持稳定和显式:不要拿 opencode 的 config 形态当 core 领域形态,除非该 config 形态本身就是领域模型。领域 ID 的 Schema 定义在共享的 schema 包中(如 packages/schema/src/agent.ts 的Schema.String.pipe(Schema.brand("AgentV2.ID"))),core 侧通过export const ID = Agent.ID复用。
七、State And Events:私有状态 + 已提交事件
状态管理遵循三条约定:
- 状态对服务 layer 私有:持久化或并发需求下使用不可变替换(immutable replacement)或 Effect refs;
- 只为已提交的领域变更发布事件,不为"尝试中的变更"发布事件:事件名描述领域事实,例如
catalog.model.updated; - v2 的目标是细粒度重配置:一次模型更新应让依赖方只对该模型更新做出反应,而不是触发全局重载。
实现层面,packages/core/src/state.ts 提供了State.create(初始状态 + 草稿工厂 + finalize 回调)、State.batch(将多个 transform 的 reload 批量合并执行,见 packages/core/src/state.ts)以及Transformable接口(transform+reload)。Catalog的finalize会在策略存在时对所有 provider 执行policy.evaluate("provider.use", ...),移除被拒绝的 provider,然后发布Event.Updated(见 packages/core/src/catalog.ts)——这正是"Hook 先于提交、事件后于提交"的实例:策略裁决发生在 finalize 阶段,事件只在最终提交后发出。
八、Style:核心代码风格清单
文档给出了可逐条对照的本地风格要求:
- 组合用
Effect.gen(function* () { ... }); - 公开服务方法用
Effect.fn("Domain.method"),便于可观测性打点(如CatalogV2.provider.get、Plugin.add、Plugin.load); - 小型内部变更辅助函数用
Effect.fnUntraced; - 类型化失败用
yield* new ErrorClass(...)抛出; - 除非辅助函数真的命名了一个概念,否则保持最少;
- 除非现有插件边界确实需要,否则禁止
any; - 没有具体持久化或外部消费需求时,不写兼容代码。
整体原则是"最小的正确移植(the smallest correct port)":目标是让服务更容易被替换、更容易被推理,而不是在新包里重建旧架构。
九、小结:从指令到代码的落地闭环
specs/v2/instructions.md虽然以"移植工作笔记"的形式存在,但它实际上定义了 Kilo v2 整个核心包的架构契约:
| 关注点 | 规范要求 | 仓库落地示例 |
|---|---|---|
| 服务形态 | Schema + 类型化错误 + Interface + Service + layer + 自导出 | catalog.ts、account.ts、agent.ts |
| 容器 API | 小领域动词、update(id, draft => ...)、先 Hook 后事件 | catalog.ts |
| 插件 Hook | 输入不可变、输出为 Immer 草稿、顺序触发、领域命名 | state.ts、plugin.ts |
| 插件启动 | 纯组合、服务注入、防循环 | plugin/internal.ts |
| 模块边界 | core 不反向导入 opencode、逐步增量迁移 | plugin/internal.ts 依赖清单 |
| Schema | Effect Schema 为公开契约、branded ID、Info.empty(...) | packages/schema/src/agent.ts、catalog.ts |
| 状态与事件 | 状态私有、事件只发已提交事实、细粒度重配置 | state.ts、catalog.ts |
| 代码风格 | Effect.gen/Effect.fn/TaggedErrorClass、禁any | 上述全部源码文件 |
对于希望参与 Kilo v2 开发的工程师,这份文档就是"如何为 core 新增一个服务、如何把一个旧 opencode 服务移植成 v2 形态"的操作手册:先照 Service Shape 搭出容器,把策略留在 Hook 之后,把启动交给 boot 层组合,剩下的交给 Effect Schema 与细粒度事件去保证类型安全与可观测性。你可以在 specs/v2/instructions.md 阅读原始指令全文,并对照上述源码文件逐条验证实现。
【免费下载链接】kilocodeKilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/ki/kilocode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考