Start typing to search the documentation.

Build navigation

RPC

Effect plugins can expose methods and events that return typed Effects and Streams.

Define

Use Rpc.define with Effect Schema to define the RPC.

src/rpc.ts
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 }),
    },
  },
})

Validation

Effect Schema validates values and infers their TypeScript types.

input: Schema.Struct({ query: Schema.String })

JSON Schema and Standard Schema are also supported.

Input and output

Use input for the method argument and output for its return value. Leave either one out when there is no value.

search: {
  input: Schema.Struct({ query: Schema.String }),
  output: Schema.Struct({ text: Schema.String }),
}

Errors

Add expected failures to errors. Each key becomes the error’s type.

errors: {
  not_found: Schema.Struct({ query: Schema.String }),
}

Events

Add events to the top-level events map. Event data must be an object.

events: {
  updated: {
    schema: Schema.Struct({ itemID: Schema.String, text: Schema.String }),
  },
}

Implement

Register the implementation from the plugin Effect:

src/index.ts
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),
})

The error map becomes the error type of each method Effect. Use context.error(...) to create a declared error.

Call

Once the RPC is registered, it can be called over HTTP or from another plugin.

HTTP

Create the Effect client, then create the RPC subclient:

import { OpenCode } from "@opencode/client/effect"
import { Effect } from "effect"
import { FetchHttpClient } from "effect/unstable/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)))

Plugin

Another Effect plugin can create a local subclient from its context:

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)

Subscribe

RPC events are Streams. Subscribe by event name and run the Stream in a scoped fiber:

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,
  )

Subscriptions are live only and close when Stream consumption stops.