with one click
plugin-installer
// Install a user plugin from a URL into `{OPENAGENTD_CONFIG_DIR}/plugins/`. Use when the user provides a URL and asks to add / install a plugin.
// Install a user plugin from a URL into `{OPENAGENTD_CONFIG_DIR}/plugins/`. Use when the user provides a URL and asks to add / install a plugin.
OpenAgentd workflow for well-formatted, detailed conventional commits.
OpenAgentd workflow for investigating bugs, regressions, sessions, and runtime issues.
OpenAgentd workflow for syncing project documentation with recent code changes.
OpenAgentd workflow for version bumps, release PRs, GitHub releases, and release notes.
Install, update, remove, or restart Model Context Protocol (MCP) servers in `{OPENAGENTD_CONFIG_DIR}/mcp.json`. Use when the user asks to add / remove / list / restart an MCP, or to enable a server like filesystem, github, postgres, puppeteer, brave-search.
Update or upgrade the agent's own configuration on request — swap the model, tune thinking/temperature, add tools/skills, change the image-generation provider/model, or install a new skill. Use when the user says things like "upgrade yourself", "switch your model to X", "use Gemini for images", "add the plugin-installer skill to yourself", "make yourself faster/smarter".
| name | plugin-installer |
| description | Install a user plugin from a URL into `{OPENAGENTD_CONFIG_DIR}/plugins/`. Use when the user provides a URL and asks to add / install a plugin. |
A plugin is a single .py file in {OPENAGENTD_CONFIG_DIR}/plugins/ that
hooks into the agent loop. Two contracts are valid:
Functional — async def plugin() returning an event dict:
async def plugin():
async def before(input, output):
# input: {tool, session_id, run_id, agent_name, call_id}
# output: {args} ← mutate in place to rewrite tool args
# raise to abort: result becomes "Error: <message>"
...
async def after(input, output):
# input: {tool, session_id, run_id, agent_name, call_id, args}
# output: {output} ← mutate to rewrite the result the LLM sees
...
return {
"tool.before": before,
"tool.after": after,
"applies_to": lambda agent_name, role: True, # optional
}
Class-based — class Plugin(BaseAgentHook) for the full hook surface.
Override only the methods you need — all defaults are transparent no-ops or
pass-throughs.
Observe hooks (read/mutate state; no return value unless noted):
| Method | When called |
|---|---|
on_start() | Agent system starts up |
on_end() | Agent system shuts down |
before_agent(ctx, state) | Before the agent loop begins |
after_agent(ctx, state, response) | After the loop completes |
before_model(ctx, state, request) | Before each LLM call — return a modified ModelRequest or None |
on_model_delta(ctx, state, chunk) | Each streaming chunk from the LLM |
after_model(ctx, state, response) | After each full LLM response is assembled |
on_rate_limit(ctx, state, retry_after, attempt, max_attempts) | Provider returns 429 |
Intercept hooks (must call and return the handler result):
| Method | Wraps |
|---|---|
wrap_model_call(ctx, state, request, handler) | Each LLM call — await handler(request) |
wrap_tool_call(ctx, state, tool_call, handler) | Each tool execution — await handler(ctx, state, tool_call) |
Files prefixed with _ are skipped. Roles are lead (team orchestrator),
member (team worker), agent (direct callers).
web_fetch the URL as-is. If the response is HTML (GitHub blob
URL), ask for the raw URL and stop.async def plugin( or class Plugin(.
If not, refuse — it's not a plugin..py, no leading _.{OPENAGENTD_CONFIG_DIR}/plugins/<name>.py.(Agent, role) on first call.| User says | Do |
|---|---|
| "list plugins" | ls {OPENAGENTD_CONFIG_DIR}/plugins/ |
| "remove X" | Delete or rename …/plugins/X.py to _X.py |
| "write me a plugin" | Decline. LLM-authored in-process Python isn't worth it. |
blob URL; ask for raw.plugin_load_failed (the loader skips broken files defensively).