| name | cao-plugin |
|---|---|
| description | Create a new CAO (CLI Agent Orchestrator) plugin. Use this skill whenever the user wants to add a plugin that reacts to CAO lifecycle or messaging events, scaffold a plugin package, understand plugin requirements, or integrate an external system (Discord, Slack, dashboards, logging, metrics) with CAO. Also use when the user asks what plugin events are available, how plugin discovery works, or how to install a plugin into a CAO environment. |
Guide for creating a new CAO plugin. A "plugin" is a Python package installed alongside CAO that subscribes to CAO lifecycle and messaging events via typed async hooks.
A CAO plugin is a standalone Python package that:
- Subclasses
CaoPluginfromcli_agent_orchestrator.plugins - Registers async hook methods with
@hook("<event_type>")decorators - Is discovered via the
cao.pluginsPython entry-point group atcao-serverstartup - Runs fire-and-forget — plugin exceptions are caught and logged as warnings, never propagated back into CAO
Typical uses: forwarding inter-agent messages to chat apps, logging/observability, external dashboards, metrics export, alerting on session or terminal lifecycle.
Gather this information:
- Which events do you need? See
references/hook-events.mdfor the full catalog. - Does the plugin need persistent state across events? (HTTP client, DB connection, buffer) — if so, use
setup()/teardown(). - How is it configured? v1 has no injected config API — read env vars in
setup(), optionally viapython-dotenv. - What are the failure semantics of your integration? Remember CAO swallows hook exceptions — you must decide whether to log, retry, or drop on your own.
These are the non-negotiable contracts a plugin must satisfy to be loaded and dispatched to. Verify each one before calling your plugin complete.
Minimum viable layout:
my-cao-plugin/
├── pyproject.toml # Build config + entry-point declaration
├── my_cao_plugin/
│ ├── __init__.py # Can be empty
│ └── plugin.py # Contains the CaoPlugin subclass
├── tests/ # Optional but strongly recommended
│ └── test_plugin.py
├── env.template # Optional; only if the plugin reads env vars
└── README.md # Optional; install + config instructions for users
See examples/plugins/cao-discord/ in this repo for a complete reference implementation.
- Must subclass
CaoPluginfromcli_agent_orchestrator.plugins. - Must be zero-arg constructible — the registry instantiates plugins with
cls(). Do NOT define__init__with required parameters. - May override
async def setup(self) -> None— called once atcao-serverstartup after instantiation. - May override
async def teardown(self) -> None— called once atcao-servershutdown. - No other methods are required. Hooks are opt-in via the
@hookdecorator.
A raising setup() disables that plugin for the lifetime of the server process (warning logged, other plugins continue to load). A raising teardown() is logged and does not stop other plugins from tearing down.
A hook method must:
- Be
async def— sync hooks are not supported in v1. - Be decorated with
@hook("<event_type>")using the exact event-type string fromreferences/hook-events.md. - Accept exactly one positional argument: the typed event dataclass matching that event type.
- Return
None. - Be a regular method on the plugin class (the registry discovers hooks via
inspect.getmemberson the instance).
Multiple hook methods on the same plugin may subscribe to the same event type — each is dispatched independently. Execution order across hooks is not guaranteed.
Exceptions raised inside a hook are caught by the registry and logged as warnings. They do not affect CAO's primary operation and they do not stop other hooks for the same event from running.
Declare the plugin class under the cao.plugins entry-point group in pyproject.toml:
[project.entry-points."cao.plugins"]
my_plugin = "my_cao_plugin.plugin:MyPlugin"- The key (
my_plugin) is the plugin name used in CAO's startup log (Loaded CAO plugin: my_plugin). It has no other runtime effect. - The value must resolve to a class that is a subclass of
CaoPlugin. Entry points whose target is not aCaoPluginsubclass are skipped with a warning. - A single package may declare multiple entry points under
cao.pluginsif it ships multiple plugin classes.
No CAO-side configuration is required to enable a plugin — installation plus entry-point declaration is sufficient.
- Use
hatchlingas the build backend to match CAO's toolchain. - Target Python
>=3.10. - Declare
cli-agent-orchestratoras a runtime dependency soCaoPlugin,hook, and the event dataclasses are importable. - Declare any external libraries (e.g.
httpx,aiohttp,python-dotenv) your plugin uses.
Minimal pyproject.toml:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-cao-plugin"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"cli-agent-orchestrator",
# ... your plugin's deps
]
[project.entry-points."cao.plugins"]
my_plugin = "my_cao_plugin.plugin:MyPlugin"CAO does not inject configuration into plugins in v1. Options:
- Environment variables — read inside
setup()withos.environ.get(...). RaiseRuntimeErrorwith a clear message if a required var is missing so the startup log points the user at the misconfiguration. .envfiles — usepython-dotenv(load_dotenv(find_dotenv(usecwd=True))) insidesetup(). Process-level env vars override.envvalues, which is the expected precedence.- Config files — load any path you like inside
setup(); you own the format.
Ship an env.template alongside the plugin if it reads env vars, documenting every variable, whether it's required, and its default.
setup()is awaited exactly once, after the plugin class is instantiated at server startup.teardown()is awaited exactly once at server shutdown, only for plugins whosesetup()succeeded.- There is no hot reload — changes to an installed plugin require restarting
cao-server. - Event dispatch only happens after the underlying CAO operation succeeds (e.g.
post_create_terminalfires after the terminal is persisted, not before). - There are no
pre_*hooks today — you cannot veto or mutate an operation from a plugin.
- All dispatch is async — hooks are awaited sequentially per event but no ordering is guaranteed across plugins or within a plugin.
- No filtering API — every hook registered for an event type receives every event of that type; filter inside the handler if you need to.
- No delivery guarantees beyond "invoked once per successful dispatch" — plugins are responsible for their own retries, buffering, and error handling.
Create the layout from §1. Populate pyproject.toml per §5. references/plugin-template.md has a copy-paste skeleton.
In my_cao_plugin/plugin.py:
from cli_agent_orchestrator.plugins import CaoPlugin, PostSendMessageEvent, hook
class MyPlugin(CaoPlugin):
async def setup(self) -> None:
# Read config, open clients. Raise on misconfiguration.
...
async def teardown(self) -> None:
# Close clients, flush buffers. Safe to call after failed setup.
...
@hook("post_send_message")
async def on_message(self, event: PostSendMessageEvent) -> None:
...Keep teardown() robust to partial setup() failures — guard any resource access with hasattr or an initialized flag. See examples/plugins/cao-discord/cao_discord/plugin.py for the pattern.
Consult references/hook-events.md for the current event catalog, each event type's string, the matching dataclass, and available fields. Import event dataclasses from cli_agent_orchestrator.plugins.
Add the [project.entry-points."cao.plugins"] section to pyproject.toml per §4.
The plugin must be importable by the same Python environment that runs cao-server.
# Editable install into the CAO dev virtual environment
uv pip install -e ./my-cao-plugin
# Or, if CAO was installed as a tool:
uv tool install --reinstall cli-agent-orchestrator \
--with-editable ./my-cao-pluginRestart cao-server and check the startup log for one of:
Loaded CAO plugin: my_plugin— success.Failed to load plugin 'my_plugin'—setup()raised; check the traceback logged alongside.Plugin entry point 'my_plugin' is not a CaoPlugin subclass, skipping— the entry-point target is wrong.No CAO plugins registered (cao.plugins entry point group is empty)— the entry point is not declared, or the package was not installed into the same environment as CAO.
Unit tests for a plugin are straightforward because event dataclasses are zero-arg constructible:
import pytest
from cli_agent_orchestrator.plugins import PostSendMessageEvent
from my_cao_plugin.plugin import MyPlugin
@pytest.mark.asyncio
async def test_on_message_dispatches(monkeypatch):
plugin = MyPlugin()
await plugin.setup()
await plugin.on_message(
PostSendMessageEvent(
sender="a",
receiver="b",
message="hi",
orchestration_type="send_message",
)
)
await plugin.teardown()For plugins that make HTTP calls, use httpx.MockTransport (see examples/plugins/cao-discord/tests/test_plugin.py) rather than real network calls. Assert side-effects (requests sent, logs emitted) — do not assert CAO-side dispatch wiring, which is covered by CAO's own registry tests.
Reload the plugin after code changes:
# Stop cao-server, then reinstall if dependencies changed:
uv pip install -e ./my-cao-plugin --force-reinstall --no-deps
# Then restart cao-server.Troubleshooting checklist if Loaded CAO plugin: does not appear:
- Is the package installed in the same venv that runs
cao-server? (uv pip list | grep my-cao-plugin) - Does
pyproject.tomlcontain[project.entry-points."cao.plugins"]? - Does the entry-point value point to an actual
CaoPluginsubclass? (python -c "from my_cao_plugin.plugin import MyPlugin; print(MyPlugin.__mro__)") - Did
setup()raise? Checkcao-serverlogs for aFailed to load pluginwarning. - If using
.env, is it in the directory wherecao-serverwas launched (or a parent)?
When your plugin is complete, verify:
-
pyproject.toml—hatchlingbuild backend,cli-agent-orchestratordependency,cao.pluginsentry point -
my_cao_plugin/__init__.py— present (can be empty) -
my_cao_plugin/plugin.py—CaoPluginsubclass with zero-arg construction and at least one@hook-decorated async method -
env.template— if any env vars are read -
tests/test_plugin.py— setup/teardown happy and failure paths; at least one test per hook method -
README.md— install commands, config vars, troubleshooting - Plugin installs and shows
Loaded CAO plugin:incao-serverstartup logs
references/hook-events.md— Full catalog of supported event types, their string identifiers, and event dataclass fields.references/plugin-template.md— Annotated minimal plugin skeleton.examples/plugins/cao-discord/— Complete reference plugin (webhook forwarder with.envconfig, HTTP client lifecycle, unit tests).docs/feat-plugin-hooks-design.md— Full design document for the plugin system.