How to Develop Agent Extensions¶
Agent extensions are Python projects that run as local processes under the Zelos Agent. Use one when you need to:
- talk to devices or buses
- stream telemetry into Zelos
- expose runtime actions
- package protocol or hardware integrations for reuse
For the concepts shared with app extensions, start at Develop Extensions.
Create a Project¶
The name must be kebab-case: lowercase letters, digits, and hyphens. The command creates a my-agent-extension/ directory and fails if it already exists.
Options:
zelos extensions create my-agent-extension \
--author "Jane Doe" \
--email "[email protected]" \
--github "janedoe" \
--description "Decode and stream device telemetry" \
--python-version 3.12 \
--output ~/src
| Option | Effect |
|---|---|
--author |
Author name in extension.toml, pyproject.toml, and LICENSE. Default: your OS user name. |
--email |
Author email in pyproject.toml. Default: <github>@users.noreply.github.com. |
--github |
GitHub owner. The manifest repository becomes https://github.com/<github>/<name>. Default: a handle derived from the author name. |
--description |
Project description. Default: empty. |
--python-version |
Python version in major.minor form. Default 3.11. |
-o, --output |
Parent directory for the project. Default: the current directory. |
--template-ref |
Template tag or branch. Default: the latest stable tag of zelos-extension-templates. |
--no-setup |
Only render the files. Skip the steps below. |
After it renders the template, the command:
- Runs
uv syncto install dependencies. - Runs
git initand makes an initial commit. - Installs the project into the local agent with
install-local. It skips this step when--hostpoints at another machine.
A failed step prints a warning and leaves the project in place.
The template ref comes from the GitHub API. If you hit its rate limit, set GITHUB_TOKEN or pass --template-ref.
In the Zelos App, Create Extension in the Extensions view does the same. Pick Agent Extension.
Project Layout¶
my-agent-extension/
├── extension.toml # Manifest
├── pyproject.toml # Python package and dependencies
├── main.py # Entry point
├── my_agent_extension/ # Your code: the name with hyphens as underscores
│ ├── __init__.py
│ └── extension.py # Example SensorMonitor class
├── config.schema.json # Configuration form schema
├── assets/icon.svg # Marketplace icon
├── tests/test_extension.py # Example tests
├── Justfile # install, dev, check, test, package, release
├── README.md
├── CONTRIBUTING.md
├── LICENSE
├── .github/workflows/ # CI.yml and release.yml
├── .github/dependabot.yml
├── .pre-commit-config.yaml
└── .vscode/ # Editor settings and recommended extensions
The generated extension.toml:
name = "My Agent Extension"
version = "0.1.0"
author = "Jane Doe"
repository = "https://github.com/janedoe/my-agent-extension"
icon = "assets/icon.svg"
readme = "README.md"
[config]
schema = "config.schema.json"
[host]
type = "agent"
[host.agent]
entry = "main.py"
python_version = "3.11"
[package]
paths = ["main.py", "my_agent_extension"]
The manifest name is the project name in title case, so the local ID is local.my-agent-extension. For every manifest field, see the Manifest Reference.
Write the Entry Point¶
The template's main.py shows the pattern to keep:
#!/usr/bin/env python3
import logging
import signal
from types import FrameType
import zelos_sdk
from zelos_sdk.extensions import load_config
from zelos_sdk.hooks.logging import TraceLoggingHandler
from my_agent_extension.extension import SensorMonitor
logger = logging.getLogger(__name__)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
# Connects to the agent in ZELOS_AGENT_URL (default http://localhost:2300)
zelos_sdk.init(name="my_agent_extension", actions=True)
# Send Python log records into Zelos as well
logging.getLogger().addHandler(TraceLoggingHandler("my_agent_extension_logger"))
config = load_config()
monitor = SensorMonitor(config)
zelos_sdk.actions_registry.register(monitor)
def shutdown_handler(signum: int, frame: FrameType | None) -> None:
logger.info("Shutting down...")
monitor.stop() # ends the run() loop
signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)
monitor.start()
monitor.run()
Keep these properties when you change it:
- Start under
if __name__ == "__main__":. Packaging and standalone action runs import the module to find actions. Code at import scope would start your extension during those imports. See Standalone Action Runs. - Handle
SIGTERM. The agent sends it to stop the extension, then force-kills aftergrace_seconds. Make the main loop exit and clean up. See Stop and Shutdown. - Read configuration with
load_config(). It applies schema defaults and validates. See Loading Configuration. - Write files to
ZELOS_DATA_DIR. The code directory is read-only after a marketplace install.
Async Extensions¶
The synchronous pattern fits most extensions. When you use async libraries, such as an async HTTP or websocket client, run an asyncio loop and stop it from the signal handler:
#!/usr/bin/env python3
import asyncio
import signal
import zelos_sdk
class AsyncExtension:
def __init__(self) -> None:
self.source = zelos_sdk.TraceSource("data")
self.source.add_event(
"event",
[zelos_sdk.TraceEventFieldMetadata("value", zelos_sdk.DataType.Float64)],
)
self._shutdown = asyncio.Event()
def stop(self) -> None:
self._shutdown.set()
async def run(self) -> None:
while not self._shutdown.is_set():
self.source.event.log(value=42.0)
try:
await asyncio.wait_for(self._shutdown.wait(), timeout=1.0)
except asyncio.TimeoutError:
pass
async def main() -> None:
extension = AsyncExtension()
loop = asyncio.get_running_loop()
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, extension.stop)
await extension.run()
if __name__ == "__main__":
zelos_sdk.init(name="async_extension")
asyncio.run(main())
loop.add_signal_handler is not available on Windows. There, use signal.signal and call loop.call_soon_threadsafe(extension.stop) from the handler.
Manage Dependencies¶
Declare dependencies in pyproject.toml, then lock them:
uv add aiohttp # adds the dependency and updates uv.lock
uv sync # installs into the project's .venv
Commit both pyproject.toml and uv.lock. Packaging includes both, and the agent installs the exact locked versions with uv sync --frozen. See Python Environment.
Develop and Test Locally¶
The template's just recipes cover the local loop:
just install # uv sync --extra dev, plus pre-commit hooks
just dev # uv run python main.py
just check # ruff lint
just format # ruff format and autofix
just test # pytest
just dev runs the extension against the agent at ZELOS_AGENT_URL, or localhost:2300. With no ZELOS_CONFIG_PATH, load_config() reads config.json in the project directory, so you can test specific values:
The template's .gitignore already excludes config.json.
The example tests in tests/test_extension.py use the check fixture from the Zelos pytest plugin. See Testing.
Run Inside Zelos¶
Install the project into the local agent. The agent reads your code from the project directory, so you do not reinstall after code edits:
zelos extensions install-local .
zelos extensions start local.my-agent-extension --config '{"sensor_name": "bench-01"}'
zelos extensions logs local.my-agent-extension
zelos live signals
zelos actions list
zelos extensions stop local.my-agent-extension
- Pass configuration as
--config '<json>'or--config-file config.json. The agent validates it againstconfig.schema.json. - After a code change, stop and start the extension, or use Save & Restart in the app's Configure dialog.
-
After a dependency change, rebuild the environment. Stop the extension first, because the agent refuses to reinstall a running extension:
Reinstall in the app's extension details does the same. Reinstall keeps the configuration and
data/. -
After you add or change a
standaloneaction, runzelos actions dumpto refreshactions.json, then restart the agent or reinstall the extension. Seeactions.json.
Package and Release¶
just package # zelos extensions package .
just release 0.2.0 # bump, format, lock, check, test, commit, tag
git push --follow-tags # the release workflow builds and publishes the archive
The generated extension.toml has a [package] section, which packaging requires. Preview the archive with zelos extensions package --list .. Packaging also writes actions.json into the project; commit it or add it to .gitignore.
For archive rules, the release workflow, and marketplace submission, see Package and Publish Extensions.