Official
opencode-v2
OpenCode 2 agent provider: runs opencode serve (2.x) and talks to its /api over HTTP and server-sent events.
The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/opencode-v2@0.1.0
Permissions in 0.1.0
Files
NOTES.md10.1 KB
# OpenCode 2 provider plugin — decisions
The reference for every shape here is the OpenCode source at the tag of the
installed version (github.com/anomalyco/opencode, `v2.0.16`):
| What | Where in the source |
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| HTTP routes, query and body shapes | `packages/protocol/src/groups/*.ts`, `packages/protocol/openapi.json` (served at `GET /openapi.json`; `GET /doc` is the web app) |
| Session events | `packages/schema/src/session-event.ts` |
| Permission, form and catalog events | `packages/schema/src/{permission,form,provider,model,agent,command,skill}.ts`, `event-manifest.ts` |
| Session semantics (inbox, steer and queue, execution, retries, instructions) | `specs/v2/session.md` |
| The event stream | `specs/v2/event-stream-architecture.md` |
| Catalog lifecycle | `specs/v2/catalog-config-plugin-lifecycle.md` |
| The subagent tool | `packages/core/src/tool/plugin/subagent.ts`, `session/subagent-completion.ts` |
| MCP tools and code mode | `packages/core/src/tool/mcp.ts`, `mcp/client.ts` |
The 1.x provider (`plugins/opencode`) stays in the repository, off by
default (`disabledByDefault`). The two serve different agent ids (`opencode`,
`opencode-v2`), so a chat started on one never resumes on the other: 2.x
sessions live in a different store.
| File | What it holds |
| ------------------------------------------ | ----------------------------------------------------------------------------------- |
| `main.ts` | registration and the settings that pick the binary or an external server |
| `server.ts` | the `opencode serve` child, credentials, requests |
| `api.ts` | Zod schemas of the API shapes and events the plugin reads |
| `agent.ts` | the agent: event stream, runs, subagents, approvals, forms, every `agent/*` handler |
| `map.ts` | tool rows, question cards, usage and history (pure) |
| `options.ts`, `prompt.ts`, `permission.ts` | options from the live catalog, prompt bodies, the permission ruleset |
## Transport
`opencode serve --hostname 127.0.0.1 --port 0` with a password made up for
the plugin's lifetime (2.x refuses anonymous requests). `--port 0` works in
2.x: the startup line `server listening on http://127.0.0.1:<port>` names
the port, and `GET /api/info` confirms the server. The binary must report a
2.x version; a 1.x binary is refused with a message naming the setting to
change.
Everything lives under `/api`. Catalog routes take
`location[directory]=<folder>`; the session list takes `directory`.
## One event stream
`GET /api/event` is one stream for every location (each frame carries its
`location`). It is volatile by contract: a slow reader is cut off and
events during a disconnect are lost. The plugin opens it before the first
prompt and waits for the first frame (`server.connected`). A dropped
connection reconnects with backoff; after it, runs whose session is no
longer active (`GET /api/session/active`) end with the outcome
`GET /api/session/{id}` reports, and pending permissions and forms are read
again (`GET /api/permission/request`, `GET /api/form`) so no card waits for an
answer that cannot come. Only a local server that exited, or an external one
unreachable for a minute, fails the runs.
## The catalog is built after the start
A server builds its providers, models and agents in the background after it
starts (catalog transforms run as plugins activate), so the first reads can
answer empty lists. A catalog with no usable model is read again for up to
20 s, at once when a catalog event (`provider.updated`, `model.updated`,
`agent.updated`, ...) arrives. Those events also drop the cached catalog and
send every chat of that folder its options again (`config_options`).
## Sessions, model and mode
- Create: `POST /api/session` with `location`, `agent`, `model` and the
permission ruleset.
- Model, variant and mode are session state in 2.x, not prompt fields:
`POST /api/session/{id}/model` `{model:{providerID,id,variant?}}` and
`POST /api/session/{id}/agent` `{agent}`. The plugin sends each one before a
prompt only when it changed since it last told the session.
- The model option value is `<providerID>/<id>`, where `id` is the catalog
entry's own id (two entries can share one provider model, for example a
"fast" entry).
- Prompt: `POST /api/session/{id}/prompt` `{id, text, files?, skills?}`. The
message id is `msg_<host item id>`. A prompt during a run is sent with
`delivery: "steer"` and returns the active run id (`steer: true`).
- Mentions stay in the text; each file or skill attachment names its span
(`mention: {start, end, text}`). Images and audio go as `data:` URIs.
- A prompt whose text starts with a command the server lists goes to
`POST /api/session/{id}/command` `{name, text}`.
- Cancel: `POST /api/session/{id}/interrupt` on the chat and its subagents,
after pending permissions are rejected and forms dismissed.
- Plugin instructions: the instruction entry `convergence`
(`PUT /api/experimental/session/{id}/instructions/entries/convergence`).
- Not offered: `rollback` (Divergence owns file restores; 2.x has no way to
forget messages without its own revert of files), usage limits, sign-in.
## Runs
`session.execution.started` / `succeeded` / `failed` / `interrupted` bound a
run. A run the plugin did not start (a queued prompt, a background subagent
result) is announced with `run_started`. Exactly one `run_finished` per run:
the run is removed before it is reported.
## Event mapping
- Text and reasoning: `session.{text,reasoning}.delta` append to the item
`<messageID>/<kind>/<ordinal>`; `.ended` replaces it with the whole block,
so a lost delta does not stay lost.
- Tools: a call id is unique only within a step, so the item id is
`<assistantMessageID>/<callID>`. `session.tool.input.started` starts the
row, `session.tool.called` gives its input and title, `.success` and
`.failed` end it. Edits carry the patches in `metadata.files`; commands
show their output as a terminal with `metadata.exit`. Plugin tools reach
OpenCode as `convergence_<tool>`; the row title drops the prefix.
- Usage: `session.usage.updated` (cumulative cost) with the tokens of the
last `session.step.ended`, and the context window of the model the step
ran on.
- `session.retry.scheduled` is a warning; `session.step.failed` an error.
## Subagents
The `subagent` tool creates a child session (`session.created` with
`parentID`, titled with the call's `description`), reports it in
`session.tool.progress` metadata (`{sessionID, status}`), and ends with
`<subagent sessionID=".." state="completed">TEXT</subagent>`. A call that
continues an earlier child (`sessionID` input) creates no session: the
progress event names it, and the plugin looks it up. A background call
answers at once (`status: "running"`); its result arrives later as a
`session.synthetic` message in the parent with `metadata.source:
"subagent"`. Every event of a child session goes to the chat with
`taskId` = the child; `cancel_task` interrupts the child and its own
children.
## Approvals and forms
`permission.asked` → an approval with OpenCode's replies (`once`,
`always` when the request names what it saves, `reject`), answered with
`POST /api/session/{id}/permission/{requestID}/reply {decision}`. Full access
answers `once` itself. The question tool asks through a form:
`form.created` → a question card; answered with
`POST .../form/{id}/reply {answer}`, dismissed with `DELETE .../form/{id}`.
## Permissions
The shared modes become a session ruleset (`PATCH /api/session/{id}
{permissions}`; the session's rules follow the agent's and the last match
wins). Supervised asks for everything but reads, searches, the question
tool and starting subagents; Auto-accept edits allows `edit`; Full access
allows everything. Auto is not offered: OpenCode has no approval reviewer.
## Plugin tools
The host serves the session's tools (`host/mcp.serve`); the plugin adds the
address for the folder with `PUT /api/experimental/mcp/convergence
{config:{type:"remote", url, codemode:false}}` — a runtime-only server, not
written to the user's config. `codemode: false` makes them direct tools
instead of entries behind OpenCode's code-mode `execute` tool. The status is
checked with `GET /api/mcp`; a failure is a warning in the chat.
## Tests
`pnpm -C plugins test` runs the mapping and agent tests against a scripted
server (`testing.test.ts`). The live test is
`CONVERGENCE_E2E_OPENCODE_MODEL=opencode/space-bunny-free cargo test -p
convergence-host --test e2e opencode_v2_calls_a_plugin_tool -- --ignored`:
one turn that calls a plugin tool, then the chat imported again from
OpenCode's history.Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.1.0latest | Oct 5, 2026 | >=2 <3 | 45.1 KB | 4 permissions | Listed |
No comments yet.