aiohttp
免费Use when building Python async HTTP services or clients with aiohttp - web server routing, middleware, WebSocket, SSE, streaming, client sessions, pytest-aiohttp testing, or troubleshooting SSL and timeout issues
中文简介正在准备中。当前显示原文说明。
Use when developing plugins, tools, and extensions for OpenCode v1 - plugin contract, crash-prevention rules, ctx/event APIs, subagent lifecycle (abort races, compaction, orphan detection), MCP integration, testing patterns, or publishing to npm
中文简介正在准备中。当前显示原文说明。
查看安装方法安装前可以查看交给 AI 代理的指令内容。
Complete, field-tested guide for developing plugins for OpenCode (v1.18+) AI coding agent.
Every rule, signature, and pattern below was verified against a production plugin that went through 5 versions and 3 distinct crash classes. The "Hard-won rules" sections are non-negotiable — violating them silently breaks the host.
| Extension Type | Best For | Example |
|---|---|---|
| Plugin (Hooks) | Event-driven automation, custom tools, recovery logic | Auto-resume stalled sessions, enforce agent selection |
| Tools | AI-triggered actions inside a plugin | task_complete, commit, custom search |
| MCP Servers | External service integrations | Databases, GitHub, filesystem, remote APIs |
| Skills | Knowledge/prompt templates (this file is one) | Framework patterns, project conventions |
| Commands | Interactive slash shortcuts | /hello, /deploy |
| Providers | Custom LLM backends | Alternative API gateways, self-hosted models |
A plugin is an async function (input, Options) => Promise<Hooks>. It is NOT an object with name/version/tools fields — that shape is outdated and the host ignores it.
@opencode-ai/plugin/dist/index.d.ts)export type Plugin = (input: PluginInput, options?: PluginOptions) => Promise<Hooks>
export interface Hooks {
dispose?: () => Promise<void>
event?: (input: { event: Event }) => Promise<void>
config?: (input: Config) => Promise<void>
tool?: { [key: string]: ToolDefinition }
auth?: AuthHook
provider?: ProviderHook
"chat.message"?: (input: ChatMessageInput) => Promise<void>
"tool.execute.before"?: (input: ToolExecInput) => Promise<void>
"tool.execute.after"?: (input: ToolExecInput) => Promise<void>
"command.execute.before"?: (input: CommandExecInput) => Promise<void>
"command.execute.after"?: (input: CommandExecInput) => Promise<void>
}
import type { Plugin } from "@opencode-ai/plugin"
import { tool } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async (ctx, options) => {
return {
event: async ({ event }) => {
// handle events
},
config: async () => {
// one-time init
},
tool: {
my_tool: tool({
description: "Does something useful",
args: {},
execute: async (_args, ctx) => "ok",
}),
},
}
}
export default MyPlugin
⚠️
confighook signature:(input: Config) => Promise<void>. It receives a Config object — you may ignore it but the parameter exists.
These rules were discovered through real production crashes. Each one has a regression test in the reference plugin.
OpenCode's plugin loader iterates every module export via Object.values(module) and treats each as a Plugin entrypoint:
// Inside opencode's plugin loader (deobfuscated):
for (let N of Object.values(pluginModule)) {
if (typeof N !== "function") throw TypeError("Plugin export is not a function")
const hooks = await N(ctx, options) // called as a Plugin!
pluginArray.push(hooks)
}
// later:
for (let N of pluginArray) {
await N.config?.(config) // crashes if N is null
}
If any export returns null → the host crashes with null is not an object (evaluating 'N.config'), surfaced in the TUI as Unexpected server error. Check server logs for details.
// src/index.ts — ONLY Plugin exports
export const MyPlugin: Plugin = async (ctx, options) => { ... }
export default MyPlugin
// src/index.ts
export function getLastAssistantError(messages) { ... return null } // called as Plugin, returns null, host crashes
export function backoffMs(attempt) { return 42 } // called as Plugin, returns number, host crashes
export const MyPlugin: Plugin = async (ctx, options) => { ... }
export default MyPlugin
// src/index.ts — bundle entry, only Plugin exports
export const MyPlugin: Plugin = async (ctx, options) => { ... }
export default MyPlugin
// src/test-utils.ts — NOT the bundle entry; tests import from here
export function getLastAssistantError(messages) { ... }
export function backoffMs(attempt, base, max) { ... }
Tests import utilities from ./test-utils; the bundled dist/index.js only exposes the Plugin. Verify with:
bun -e 'const m = await import("./dist/index.js"); console.log(Object.keys(m))'
# MUST print only: [ "MyPlugin", "default" ]
The event hook is called fire-and-forget by the host. If handleEvent() rejects, it becomes an unhandled promise rejection → bun process exits → OpenCode disappears from the TUI.
return {
event: async ({ event }) => {
handleEvent(event).catch((e) => {
console.error("[my-plugin] handleEvent error:", e)
})
},
}
Same rule applies to setInterval(async () => { ... }) bodies — wrap in a safe() boundary:
async function safe<T>(fn: () => Promise<T>, label: string): Promise<T | undefined> {
try { return await fn() }
catch (e) {
const msg = e instanceof Error ? e.message : String(e)
console.error(`[my-plugin] ${label}: ${msg}`)
return undefined
}
}
setInterval(() => {
safe(doPeriodicWork, "periodic timer").catch(() => {})
}, 5000)
OpenCode event payloads are not guaranteed to match the documented shape. Real-world crashes found:
todo.updated events arrive with properties.todos as {} (object) or undefined, not an arraysession.status properties.status is always { type: "idle" | "busy" | "retry" } — never a bare stringsessionID) may be missing on some eventsArray.isArray before .map()/.filter()case "todo.updated": {
const rawTodos = (event.properties as any)?.todos
const todos: Array<Record<string, unknown>> = Array.isArray(rawTodos) ? rawTodos : []
w.todos = todos.map((t) => ({ ... })) // safe
break
}
log() must never rethrowIf the OpenCode log API itself throws (network/server error), your log() helper's catch block must not propagate. Otherwise an error inside an error handler escapes:
async function log(level: "info" | "warn" | "error", msg: string) {
try {
await ctx.client.app.log({ body: { service: "my-plugin", level, message: msg } })
} catch (e) {
console.error("[my-plugin] log() failed:", e instanceof Error ? e.message : e)
// do NOT rethrow
}
}
sid before every SDK callSession IDs from events may be empty, undefined, or malformed. The SDK throws Expected 'id' to be a string if you pass garbage:
if (typeof sid !== "string" || !sid.startsWith("ses_")) return
await ctx.client.session.prompt({ path: { id: sid }, body: { ... } })
pluginAbortInFlight before calling session.abort()The session.status event with "interrupted" can arrive BEFORE session.error with MessageAbortedError. If you abort during this window, check the flag before treating it as user-cancel:
// In ensureWatch
w.pluginAbortInFlight = false
w.pluginAbortAt = 0
// Before aborting
w.pluginAbortInFlight = true
w.pluginAbortAt = Date.now()
try {
await ctx.client.session.abort({ path: { id: sid } })
} finally {
w.pluginAbortInFlight = false
w.pluginAbortAt = 0
}
// In session.error handler
case "session.error": {
if (props?.error?.name === "MessageAbortedError") {
if (w.pluginAbortInFlight && Date.now() - w.pluginAbortAt < 1000) {
// Plugin-initiated abort, not user ESC
} else {
w.userCancelled = true
}
}
break
}
See references/subagents-tools.md for the full pattern.
ctx)The ctx parameter is undocumented but stable across v1.18.x:
// ctx.client — API calls
await ctx.client.app.log({
body: { service: "my-plugin", level: "info", message: "..." }
})
const { data: sessions } = await ctx.client.session.list()
const { data: statusMap } = await ctx.client.session.status() // Record<sid, {type: "busy"|"idle"|"retry"}>
const messages = await ctx.client.session.messages({ path: { id: sid } })
await ctx.client.session.abort({ path: { id: sid } })
await ctx.client.session.prompt({
path: { id: sid },
body: {
parts: [{ type: "text", text: "continue" }],
agent, // optional: preserve selected agent
model, // optional: { providerID, modelID }
},
})
// ctx.ui — toast notifications (TUI)
await ctx.ui.toast({ title: "Done", message: "...", variant: "success" })
session.status() return shape// status() returns { data: Record<sid, status> } where status is:
type SessionStatus = { type: "idle" | "busy" | "retry" }
// NEVER a bare string — always access via .type
session.list() does NOT include statusThe Session type from session.list() has no status field. To check if a session is busy, call session.status() separately and build a Record<sid, string> map.
Plugins receive Server-Sent Events via the event hook. Real event types (v1.18+):
return {
event: async ({ event }) => {
const type = event.type as string
const sid = event.sessionID as string | undefined
const props = event.properties as Record<string, unknown> | undefined
switch (type) {
case "session.created":
case "session.updated":
case "session.idle": // legacy alias of session.status=idle
case "session.interrupted": // user pressed ESC
break
case "session.status": {
const status = props?.status as { type: string } | undefined
// status.type in "idle" | "busy" | "retry" | "interrupted"
break
}
case "session.error": {
const error = props?.error as { name: string; data?: { message: string } } | undefined
// error.name === "MessageAbortedError" → user pressed ESC
break
}
case "message.updated":
case "message.part.updated":
// props?.delta — streaming text delta
break
case "todo.updated": {
// ⚠️ props?.todos may be {} or undefined — validate with Array.isArray
break
}
case "tool.call":
case "tool.result":
case "command.executed":
break
}
},
}
The following reference files contain detailed documentation on specific topics. They are loaded on demand from ../SKILL.md:
references/subagents-tools.mdreferences/mcp-sdk-api.mdreferences/testing-patterns.mdreferences/config-publishing.mdThis is the generic TUI mask. The real error is in the log:
grep "level=ERROR" ~/.local/share/opencode/log/opencode.log | tail -20
Common causes (in order of likelihood):
null is not an object (evaluating 'N.config') — see Rule 1event hook or setInterval — see Rule 2todos.filter is not a function — see Rule 3Expected 'id' to be a string — see Rule 5grep "failed to load plugin" ~/.local/share/opencode/log/opencode.log
Check:
dist/index.js exists and has "main": "dist/index.js" in package.jsonls ~/.cache/opencode/packages/<name>@<ver>/node_modules/<name>/dist/index.jsbun.lock in ~/.config/opencode/ resolves to the version you expectgrep "my-plugin" ~/.local/share/opencode/log/opencode.log | tail -5
# Look for: path=my-plugin@X.Y.Z
Before publishing a plugin:
dist/index.js exports ONLY default (and optionally a named alias of the same function)event hook wraps async work in .catch()setInterval async bodies wrapped in try/catch or safe()log() helper never rethrowsArray.isArray, typeof, ?.)sid firstsession.status() accessed via .type, never compared as bare stringbun test passes with 0 failuresbun build produces dist/index.js with only Plugin exports还没有评价。欢迎分享你的使用感受。
无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。
还没有评论。来写第一条吧。
Use when building Python async HTTP services or clients with aiohttp - web server routing, middleware, WebSocket, SSE, streaming, client sessions, pytest-aiohttp testing, or troubleshooting SSL and timeout issues
中文简介正在准备中。当前显示原文说明。
Use when doing structural code search and rewriting - ast-grep linting, refactoring, multi-language patterns
中文简介正在准备中。当前显示原文说明。
Use when building GBA games with the BPCore Lua engine - entity, sprite and tilemap functions, SRAM save and load, link cable multiplayer protocol, camera and scrolling, or optimization patterns
中文简介正在准备中。当前显示原文说明。
Use when running background tasks with Celery - worker and broker configuration (Redis, RabbitMQ), task routing by name vs queue, chains/groups/chords, retry patterns (autoretry_for, retry_backoff), acks_late semantics, failure detection, and monitoring with Flower
中文简介正在准备中。当前显示原文说明。
Use when building Django applications - security hardening, authentication and permissions, ORM optimization, PostgreSQL features, Django 6.0, migrations, testing, and ecosystem libraries
中文简介正在准备中。当前显示原文说明。
Use when customizing Django Admin - save_formset, get_search_results, formsets, queryset optimization, db_index, custom URLs
中文简介正在准备中。当前显示原文说明。