KiloCode Plugin V2 Effect API:用 Effect 为 Agent 领域注册 Transform 与 Runtime 钩子
【免费下载链接】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
KiloCode 的@kilocode/plugin包提供了一套基于 Effect 体系的 V2 插件 API,插件通过PluginContext在 OpenCode 的扩展点上安装行为:Transform 钩子参与有状态领域(agent、catalog、command、integration、reference、skill)的重建,Runtime 钩子拦截实时操作(如 AI SDK 的 sdk/language 创建)。读完本文,你可以掌握插件定义与生命周期管理、Transform/Runtime 钩子的注册语义与重建顺序,以及领域重载(reload)的触发方式,并理解其背后的类型契约(PluginContext、Hooks、Registration)。
定位与边界:两个进程内能力
V2 Effect 插件 API 明确为插件提供两个进程内(in-process)能力(见 README):
hook:在 OpenCode 的扩展点上安装行为;reload:针对某个有状态领域重新运行全部 transform 钩子。
同时,文档明确了一个边界:公开的 server 客户端(public server client)会单独暴露,目前刻意不纳入PluginContext。也就是说,当前 Effect API 面向的是宿主进程内部的领域构建与运行时拦截,而不是跨进程的远程调用。
插件包的版本与入口可以从 package.json 确认:包名@kilocode/plugin(当前版本 7.6.0),Effect API 通过子路径导出:
"exports": { ".": "./src/index.ts", "./v2/effect": "./src/v2/effect/index.ts", "./v2/effect/integration": "./src/v2/effect/integration.ts", "./v2/effect/plugin": "./src/v2/effect/plugin.ts", "./v2/promise": "./src/v2/promise/index.ts" }其中./v2/effect子路径即文档中import { define } from "@kilocode/plugin/v2/effect"的落点,其导出内容(index.ts)只有三项:
export type { PluginContext } from "./context.js" export { define } from "./plugin.js" export type { Plugin } from "./plugin.js"定义插件:define与 Effect 安装函数
插件由define构造,包含id与effect两个字段:
import { define } from "@kilocode/plugin/v2/effect" import { Effect } from "effect" export const Plugin = define({ id: "example", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((catalog) => { catalog.provider.update("example", (provider) => { provider.name = "Example" }) }) }), })三个关键语义(均来自 README):
- 命令式注册:插件的
effect在运行中“命令式地”安装钩子,它不返回任何钩子对象——副作用本身就是交付物; - 配置入口:为该插件提供的配置以
ctx.options的形式可用(PluginOptions,即Record<string, unknown>,定义于 options.ts); - 作用域所有权:所有注册项归插件作用域(scope)所有。作用域关闭时注册项被自动移除;也可以通过注册项返回的
dispose提前移除。
从源码结构看,plugin.ts 给出了精确的类型契约:
export interface Plugin<R = Scope.Scope> { readonly id: string readonly effect: (context: PluginContext) => Effect.Effect<void, never, R> } export function define<R = Scope.Scope>(plugin: Plugin<R>) { return plugin }即effect必须是一个以PluginContext为参数、返回Effect<void, never, R>的函数——无失败通道(never),资源需求默认是Scope.Scope。define本身是恒等函数,价值在于提供类型收窄与自文档化。同一个文件还定义了宿主侧的PluginDomain接口:
export interface PluginDomain { readonly add: (plugin: Plugin) => Effect.Effect<void> readonly remove: (id: string) => Effect.Effect<void> }这与PluginContext中的plugin字段对应,说明宿主可以通过ctx.plugin.add(plugin)/ctx.plugin.remove(id)在运行时动态装卸插件。
PluginContext的完整字段(context.ts)为:
export interface PluginContext { readonly options: PluginOptions readonly agent: AgentHooks & Reload readonly aisdk: AISDKHooks readonly catalog: CatalogHooks & Reload readonly command: CommandHooks & Reload readonly integration: IntegrationHooks & Reload readonly plugin: PluginDomain readonly reference: ReferenceHooks & Reload readonly skill: SkillHooks & Reload }可以看到:六个有状态领域(agent/catalog/command/integration/reference/skill)都附带Reload能力,而aisdk是纯 runtime 钩子集合,不携带重载语义。
Transform 钩子:参与有状态领域的重建
Transform 钩子作用于“有状态领域”。注册方式是向对应领域的命名空间传入一个改造函数,例如更新 agent 列表中的某一项:
yield * ctx.agent.transform((agent) => { agent.update("reviewer", (item) => { item.description = "Reviews code for regressions" item.mode = "subagent" }) })重建(rebuild)语义是这里的核心:OpenCode 在任何一个 transform 被注册或被释放(disposed)时都会重建该领域;重建从全新的领域状态出发,按注册顺序依次运行所有当前活跃的 transform。这保证了最终状态是所有活跃 transform 叠加后的结果,而不存在增量合并的歧义。
可用领域与命名空间(与 README 一致):
ctx.agent.transform ctx.catalog.transform ctx.command.transform ctx.integration.transform ctx.reference.transform ctx.skill.transform各领域的“草稿”(Draft)对象提供了 list/get/update/remove 级别的读写面。以 catalog.ts 为例:
export interface CatalogDraft { readonly provider: { list(): readonly CatalogProviderRecord[] get(providerID: string): CatalogProviderRecord | undefined update(providerID: string, update: (provider: ProviderV2Info) => void): void remove(providerID: string): void } readonly model: { get(providerID: string, modelID: string): ModelV2Info | undefined update(providerID: string, modelID: string, update: (model: ModelV2Info) => void): void remove(providerID: string, modelID: string): void readonly default: { get(): { providerID: string; modelID: string } | undefined set(providerID: string, modelID: string): void } } }即 catalog 的 transform 不仅可以按 providerID 修改/删除 provider 记录(CatalogProviderRecord携带provider信息与models映射),还可以修改具体模型、设置默认模型(model.default.set)。agent.ts 的AgentDraft额外提供default(id?: string): void,用于指定默认 agent;command.ts 的CommandDraft则以name为键,提供与 agent 一致的 list/get/update/remove 面。
Runtime 钩子:拦截实时操作而非重建状态
Runtime 钩子不改动领域状态,而是拦截“正在发生”的操作。aisdk命名空间下有两个钩子(事件结构见 aisdk.ts):
yield * ctx.aisdk.sdk( Effect.fn(function* (event) { if (event.package !== "@ai-sdk/xai") return const mod = yield* Effect.promise(() => import("@ai-sdk/xai")) event.sdk = mod.createXai(event.options) }), ) yield * ctx.aisdk.language((event) => { if (event.model.providerID !== "xai") return event.language = event.sdk.responses(event.model.api.id) })两个事件契约:
sdk事件:{ model, package, options, sdk? }——回调可替换event.sdk,例如按包名动态加载@ai-sdk/xai并创建对应 SDK 实例。由于加载是异步的,回调本身可以是 Effect(示例中用Effect.promise包装动态import);language事件:{ model, sdk, options, language? }——在 sdk 就绪后构造LanguageModelV3实例(类型来自@ai-sdk/provider),回调为同步函数。
执行顺序上,README 明确规定:钩子按注册顺序顺序执行,后注册的钩子可以观察到前一个钩子所做的修改(例如language钩子能读到sdk钩子写入的event.sdk)。
重载领域:数据变化后的显式刷新
当 transform 捕获的外部数据发生变化时,插件应主动重载受影响的领域:
let data = yield* loadCatalog() yield* ctx.catalog.transform((catalog) => { applyCatalog(data, catalog) }) data = yield* loadCatalog() yield* ctx.catalog.reload()要点:reload 归属于领域,而不是某个单独的注册项。ctx.catalog.reload()会重跑所有活跃的 catalog transform,并发布重建后的 catalog。可用的重载操作与 transform 领域一一对应:
ctx.agent.reload() ctx.catalog.reload() ctx.command.reload() ctx.integration.reload() ctx.reference.reload() ctx.skill.reload()从类型层面看,这一能力由 registration.ts 中的Reload接口承载(reload: () => Effect.Effect<void>),并由context.ts将其交叉进每个领域钩子类型。
类型契约速览
理解这套 API 只需掌握 registration.ts 中的三个基础类型:
export interface Registration { readonly dispose: Effect.Effect<void> } export interface Reload { readonly reload: () => Effect.Effect<void> } export type Hooks<Spec> = { readonly [Name in keyof Spec]: ( callback: (input: Spec[Name]) => Effect.Effect<void> | void, ) => Effect.Effect<Registration, never, Scope.Scope> }- 每个钩子的注册函数(如
ctx.agent.transform)接收一个回调,返回Effect<Registration, never, Scope.Scope>:即注册动作是随作用域生效的 Effect; Registration.dispose兑现了 README 中“注册项可提前移除”的承诺,且与 scope 自动清理互补;- 回调既可以是 Effect(支持异步副作用,如
aisdk.sdk中动态 import),也可以是普通同步函数(如aisdk.language),类型上统一为Effect<void> | void。
小结
KiloCode@kilocode/plugin的 V2 Effect API 以PluginContext为唯一入口,把插件行为划分为两类:面向有状态领域的Transform 钩子(注册/释放即触发“从全新状态按序重放”的重建,可用ctx.<domain>.reload()显式刷新)与面向实时操作的Runtime 钩子(aisdk.sdk/aisdk.language,按注册顺序串联、可相互观察修改)。注册的生命周期完全由 Effect 的 Scope 机制托管——scope 关闭自动清理,dispose支持提前移除。若你更习惯非 Effect 风格,同一包还导出了@kilocode/plugin/v2/promise子路径的 Promise 版 API(见 package.json 的 exports),而 Effect 版类型定义可直接在 packages/plugin/src/v2/effect/ 下逐文件阅读。
【免费下载链接】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),仅供参考