opencode/packages/plugin/src/README.md
Filip 8de0323975
feat(core): add /goal with a client-side done check (#59437)
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
2026-10-28 19:05:53 -05:00

6.2 KiB

OpenCode V2 Promise Plugin API

The Promise plugin API at @opencode/plugin is the async/await equivalent of @opencode/plugin/effect. It grants plugins the same two in-process capabilities:

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

The Promise API uses Promises instead of Effects for setup, runtime hook callbacks, hook registration, reload, and Registration.dispose. Transform editor callbacks remain synchronous.

Defining A Plugin

import { Plugin } from "@opencode/plugin"

export default Plugin.define({
  id: "example",
  setup: async (ctx) => {
    await ctx.provider.transform((editor) => {
      editor.update("example", (provider) => {
        provider.name = "Example"
      })
    })
  },
})

Plugin setup registers hooks imperatively through each domain's hook method. It may return a synchronous or asynchronous cleanup function. OpenCode awaits the cleanup when the plugin is unloaded or replaced:

setup: async (ctx) => {
  const timer = setInterval(refresh, 60_000)
  return () => clearInterval(timer)
}

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

A registration may be removed early through dispose:

const registration = await ctx.model.transform(applyModelPolicy)
await registration.dispose()

Transform Hooks

Transform hooks contribute to stateful domains. The editor is synchronous, so load asynchronous data before registering a transform or reloading its domain:

const description = await loadReviewerDescription()

await ctx.agent.transform((agent) => {
  agent.update("reviewer", (item) => {
    item.description = description
    item.mode = "subagent"
  })
})

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.

await 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:

await ctx.aisdk.hook("sdk", async (event) => {
  if (event.package !== "@ai-sdk/xai") return
  const mod = await import("@ai-sdk/xai")
  event.sdk = mod.createXai(event.options)
})

await ctx.aisdk.hook("language", (event) => {
  if (event.model.providerID !== "xai") return
  event.language = event.sdk.responses(event.model.modelID)
})

Session context is mutable immediately before provider dispatch:

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

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

When a Session finishes model work with no pending input, the experimental.idle hook runs just before it goes idle. It is experimental: its name and shape may still change. It does not run after interruption, failure, or control-only work such as a manual compaction. Input the hook admits runs in the same execution instead of the Session going idle, so a plugin that always admits input must bound its own loop.

event.background lists background shells and subagents still running for the Session or its descendants. When one of the Session's own shells or subagents finishes, its notice wakes the Session and the hook runs again, so a plugin that waits for that work can return. Work a descendant owns, such as a shell a subagent left running, wakes that descendant instead, so it is listed for information only. Notices admitted with resume: false, such as those recovered after a restart, wait for the next wake instead:

await ctx.session.hook("experimental.idle", async (event) => {
  if (event.background.some((work) => work.kind === "subagent" && work.sessionID === event.sessionID)) return
  await ctx.session.synthetic({ sessionID: event.sessionID, text: "Run the tests and fix any failures." })
})

A continuation runs inside the same execution, so a loop that is still going when the server restarts is resumed by restart recovery and the hook keeps running. Persist your own state if the loop should come back paused. A hook that fails, fails the execution like any other session hook, so handle errors from slow or remote work such as a judge model call.

Promise tools use complete executable tool values with async executors:

import { Schema } from "effect"

await ctx.tool.transform((tools) => {
  tools.add({
    name: "echo",
    options: { codemode: false },
    description: "Echo text",
    input: Schema.Struct({ text: Schema.String }),
    output: Schema.Struct({ text: Schema.String }),
    execute: async ({ text }) => ({ output: { text }, content: text }),
  })
})

Reloading A Domain

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

const source = { providers: await loadProviders() }

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

source.providers = await loadProviders()
await 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()