What the bridge relies on in OpenCode v2, verified against OpenCode 2.0.27 with
live probes and recorded turns. Official docs: https://opencode.ai/v2/docs/api.
tests/opencode-v2-contract.test.ts pins the request and event shapes below.
Talking to a live service: opencode2 service status prints the URL, basic
auth is opencode:<password> from ~/.config/opencode/service.json, and every
location-scoped route needs ?location[directory]=<cwd>.
POST /api/sessiontakes{id?, parentID?, title?, agent?, model?, location?, metadata?, permissions?}.PATCH /api/session/:idtakes{title?, metadata?, permissions?}.permissionsreplaces the session ruleset.DELETE /api/session/:id.GET /api/session/:idreturnsSession.Info, includingoutcome(succeeded/failed/interrupted),time.idle,tokens,cost, andmodel.GET /api/session/activereturns{data: {<sessionID>: ...}}. Use it for reconciliation.POST /api/session/:id/prompttakes{id?, text, files?[{uri,name?}], skills?[{id, mention}], metadata?, delivery?: "steer"|..., resume?}. Unknown keys are stripped, and a valid prompt starts a model call.POST /api/session/:id/forktakes{before?: messageID}.POST /api/session/:id/compacttakes{id?, delivery?}.POST /api/session/:id/interrupt(queryresume?) returns{interrupted}.POST /api/session/:id/revert/stagetakes{messageID, files?}.POST .../revert/commit.DELETE .../revertclears it.POST /api/session/:id/agenttakes{agent}.POST /api/session/:id/modeltakes{model:{providerID,id,variant?}}.PUT /api/session/:id/environmenttakes{variables: Record<string,string>}.PUT|DELETE /api/experimental/session/:id/instructions/entries/:key, with{value}for PUT.GET /api/session/:id/message?limit&order=asc|cursor=returns{data, cursor:{previous,next}}.nextis always present, so stop on an empty page. Messagetypeisuser,assistant,idle(withoutcome),compaction, and others.GET /api/session/:id/form.POST .../form/:formId/replytakes{answer: {<fieldKey>: string|number|boolean|string[]}}.DELETE .../form/:formId.POST /api/session/:id/permission/:requestID/replytakes{decision, message?}.GET /api/session/:id/diff?from=<userMsgId>&to=&context=returns per-file{file, patch, additions, deletions, status}. It returned[]in tests, so verify when it populates.PUT|DELETE /api/experimental/mcp/:server?location[directory]=, with{config}for PUT.GET /api/model?location[directory]=returns{providerID, id, name, variants[{id}], limit:{context,output}, capabilities:{input[]}, enabled, cost[], status}.GET /api/model/defaultreturns the same shape. Variant ids seen: none, minimal, low, medium, high, xhigh, max, thinking.GET /api/agent?location[directory]=lists agents: build (default), plan, general, explore, sophos, plus hidden compaction/title/summary agents.GET /api/skill?location[directory]=returns[{id, name, description, ...}].GET /api/integrationreturnsconnections[], where a status can beneeds_auth.GET /api/shell/:id/output?location[directory]=&cursor=<byte>&limit=returns{output, cursor, size, truncated}.GET /api/eventis SSE (data: {type, data}). It is volatile and does not replay.GET /api/experimental/session/:id/loghas sequence IDs and may allow replay.
Rules are [{action, resource, effect: allow|deny|ask}]. The agent ruleset
is evaluated first and then the session ruleset. The last match wins
(verified: a session rule of *→ask made shell ask under build).
Known actions: *, shell, read, edit (covers write/patch), grep, glob,
webfetch, websearch, question, subagent, external_directory, browser. Bare
reject behavior was verified: OpenCode then aborts the step and interrupts
execution with reason shutdown. Reject with message is untested; it should
feed back to the model.
permission.asked data looks like:
{id:"per_…", sessionID, action:"shell", resources:["echo hello"], save:["echo *"], source:{type:"tool", messageID, id:<toolCallId>}}.
session.inbox.enqueued/session.inbox.delivered {inboxID}. The inboxID equals the prompt message ID. Use it to confirm steers.session.execution.started|succeeded|failed{error}|interrupted{reason: user|shutdown|superseded|inactivity}.session.step.started {agent, model, assistantMessageID}.session.step.ended {finish, cost, tokens:{input,output,reasoning,cache:{read,write}}}.session.step.failed {error, tokens?}.session.usage.updated {cost, tokens}carries cumulative session totals. Use it forusage.total.session.retry.scheduled {assistantMessageID, attempt, at, error}.session.text|reasoning.started|delta|ended {assistantMessageID, ordinal, delta|text}.session.tool.input.started {id, name},.input.ended {text},session.tool.called {id, input},session.tool.progress {id, metadata},session.tool.success {id, content[], metadata}, andsession.tool.failed {id, error:{type,message}}.- Tool names and inputs in v2:
shell {command, timeout?}read {path}edit {path, oldString, newString}write {path, content}question {questions[{question, header, options[{label, description}]}]}subagent {agent, description, prompt}- There is no todo tool in v2.
- Shell flow:
session.tool.progress {metadata:{shellID}}, thenshell.created {info:{id, command, cwd, file}}, thenshell.exited {id, exit}. Success metadata is{status, exit}, and the content has the output plus"Exited with code N". - Edit success metadata looks like:
{files:[{file, patch:<unified diff>, status:"modified"|"added"|"deleted", additions, deletions}]}. Write metadata has no diff. - Subagent flow:
session.tool.progress {metadata:{sessionID:<child>, status:"running"}}. The result content looks like<subagent sessionID=… state="completed">…</subagent>. Child-session events carry the child'ssessionID. form.created {form:{id, sessionID, title, metadata:{kind:"question", tool:{messageID,id}}, fields:[{key:"q0", title, description, type:"string", options[{value,label,description}], custom:true}]}}.session.compaction.started{reason:auto|manual},.delta, and.ended(handled already).