Effect 插件可以暴露返回带类型 Effect 与 Stream 的方法和事件。

定义

使用 Rpc.define 配合 Effect Schema 来定义 RPC。
import { Rpc } from "@opencode/plugin/rpc"
import { Schema } from "effect"

export const Acme = Rpc.define({
  id: "acme",
  methods: {
    search: {
      input: Schema.Struct({ query: Schema.String }),
      output: Schema.Struct({ text: Schema.String }),
      errors: {
        not_found: Schema.Struct({ query: Schema.String }),
      },
    },
  },
  events: {
    updated: {
      schema: Schema.Struct({ itemID: Schema.String, text: Schema.String }),
    },
  },
})

校验

Effect Schema 会对值进行校验,并推断出它们的 TypeScript 类型。
input: Schema.Struct({ query: Schema.String })
同时支持 JSON Schema 和 Standard Schema。

输入与输出

用 input 表示方法参数,用 output 表示其返回值。若没有值,可省略其中之一。
search: {
  input: Schema.Struct({ query: Schema.String }),
  output: Schema.Struct({ text: Schema.String }),
}

错误

将预期的失败情况添加到 errors 中。每个键会成为错误的 type。
errors: {
  not_found: Schema.Struct({ query: Schema.String }),
}

事件

将事件添加到顶层的 events 映射中。事件数据必须是对象。
events: {
  updated: {
    schema: Schema.Struct({ itemID: Schema.String, text: Schema.String }),
  },
}

实现

在插件的 Effect 中注册实现:
import { Plugin } from "@opencode/plugin/effect"
import { Effect } from "effect"
import { Acme } from "./rpc.js"

export default Plugin.define({
  id: "acme-effect-plugin",
  effect: (ctx) =>
    Effect.gen(function* () {
      const registration = yield* ctx.rpc.register(Acme, {
        search: ({ query }, context) =>
          findText(query).pipe(
            Effect.flatMap((text) =>
              text
                ? Effect.succeed({ text })
                : Effect.fail(context.error("not_found", "Result not found", { query })),
            ),
          ),
      })

      const acme = ctx.rpc(Acme)
      const result = yield* acme.search({ query: "hello" })

      yield* registration.events.emit("updated", { itemID: "item-1", text: result.text })
    }).pipe(Effect.orDie),
})
错误映射会成为各个方法 Effect 的错误类型。使用 context.error(...) 创建已声明的错误。

调用

RPC 注册后,即可通过 HTTP 或从其他插件调用。

HTTP

创建 Effect 客户端,然后创建 RPC 子客户端:
import { OpenCode } from "@opencode/client/effect"
import { Effect } from "effect"
import { FetchHttpClient } from "effect/http"
import { Acme } from "opencode-acme-plugin/rpc"

const program = Effect.gen(function* () {
  const client = yield* OpenCode.make({ baseUrl: "http://localhost:4096" })
  const acme = client.rpc(Acme)
  return yield* acme.search({ query: "hello" })
})

const result = await Effect.runPromise(program.pipe(Effect.provide(FetchHttpClient.layer)))

插件

另一个 Effect 插件可以从其上下文创建本地子客户端:
import { Effect } from "effect"

effect: (ctx) =>
  Effect.gen(function* () {
    const acme = ctx.rpc(Acme)
    const result = yield* acme.search({ query: "hello" })
    yield* Effect.logInfo(result.text)
  }).pipe(Effect.orDie)

订阅

RPC 事件是 Stream。按事件名称订阅,并在有作用域的 fiber 中运行该 Stream:
import { Effect, Stream } from "effect"

const acme = ctx.rpc(Acme)

yield* acme.events.subscribe("updated").pipe(
  Stream.runForEach((event) => Effect.logInfo(event.data.text)),
  Effect.forkScoped,
)
订阅仅在实时状态下有效,Stream 消费停止时会关闭。