doc.haus Atlas Legal-document workflows on OpenCode, bound to their code
219 documents
packages/plugin/src/effect/README.md

Effect plugin API reference: Plugin.define with Effect setup, hook and reload capabilities, imperative per-domain registration owned by the plugin scope with early dispose, synchronous transform editors, runtime hooks, and model/provider examples. Read when writing Effect-style OpenCode plugins.

OpenCode V2 Effect Plugin API

The Effect plugin API grants plugins two in-process capabilities:

  • hook installs behavior at an OpenCode extension point.
  • reload reruns every transform hook for a stateful domain.

Defining A Plugin

import { Plugin } from "@opencode/plugin/effect"
import { Effect } from "effect"

export default Plugin.define({
  id: "example",
  effect: Effect.fn(function* (ctx) {
    yield* ctx.provider.transform((editor) => {
      editor.update("example", (provider) => {
        provider.name = "Example"
      })
    })
  }),
})

Plugin setup registers hooks imperatively through each domain's hook method.

Configuration supplied for the plugin is available as ctx.options.

Registrations are owned by the plugin scope. Closing the scope removes them automatically; a registration may also be removed early through dispose.

Transform Hooks

Transform hooks contribute to stateful domains. Their editor callbacks are synchronous, so load effectful data before registering a transform or reloading its domain:

yield *
  ctx.agent.transform((agent) => {
    agent.update("reviewer", (item) => {
      item.description = "Reviews code for regressions"
      item.mode = "subagent"
    })
  })

Registry reads rebuild synchronously when registrations changed, applying every transform in registration order to a fresh value; unchanged registries return the previous value. Values read earlier are never mutated. Notifications and resource reconciliation run separately from that materialization.

Available transform hooks are namespaced by domain:

ctx.agent.transform
ctx.command.transform
ctx.integration.transform
ctx.mcp.transform
ctx.model.transform
ctx.provider.transform
ctx.reference.transform
ctx.skill.transform
ctx.tool.transform
ctx.vcs.transform
ctx.websearch.transform

Provider transforms contribute provider settings and immutable model definitions. After provider availability is resolved, model transforms edit the complete active-provider candidate collection in order. Use ctx.model.transform for runtime model restrictions; editor.provider.get() reads source templates even when their provider is inactive.

Effect.gen(function* () {
  yield* ctx.model.transform((editor) => {
    editor
      .list()
      .filter((model) => model.cost.some((tier) => tier.output > 20))
      .forEach((model) => {
        editor.remove(model.providerID, model.id)
      })
  })
})

Runtime Hooks

Runtime hooks intercept live operations rather than rebuilding domain state:

yield *
  ctx.aisdk.hook(
    "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.hook("language", (event) =>
    Effect.sync(() => {
      if (event.model.providerID !== "xai") return
      event.language = event.sdk.responses(event.model.modelID)
    }),
  )

Hooks run sequentially in registration order. Later hooks observe mutations made by earlier hooks.

Session context is mutable immediately before provider dispatch:

yield *
  ctx.session.hook("context", (event) =>
    Effect.sync(() => {
      event.tools.read.description = "Read a file using narrow line ranges."
      delete event.tools.write
    }),
  )

yield *
  ctx.session.hook("retry", (event) =>
    Effect.sync(() => {
      if (event.attempt >= 3) event.decision = { retry: false }
    }),
  )

Reloading A Domain

When data captured by a transform changes, reload the affected domain:

Effect.gen(function* () {
  const source = { providers: yield* loadProviders() }

  yield* ctx.provider.transform((editor) => {
    source.providers.forEach((provider) => editor.add(provider))
  })

  source.providers = yield* loadProviders()
  yield* ctx.provider.reload()
})

loadProviders() returns entries shaped as { info: Provider.Info, models: readonly Model.Info[] }. Provider reloads also invalidate the active model result, so every model transform runs again with the refreshed definitions. Model callbacks edit raw overrides; provider defaults are merged once when the result is committed.

Available reload operations are:

ctx.agent.reload()
ctx.command.reload()
ctx.integration.reload()
ctx.mcp.reload()
ctx.model.reload()
ctx.provider.reload()
ctx.reference.reload()
ctx.skill.reload()
ctx.tool.reload()
ctx.vcs.reload()
ctx.websearch.reload()