hirocli. It connects back to hirocli over a local WebSocket and speaks a JSON-RPC 2.0 protocol. The plugin is entirely responsible for translating between the unified Hiro League message format and whatever the third party requires.
Design goals
| Goal | How it is achieved |
|---|---|
| Dependency isolation | Each plugin is its own uv workspace member with its own virtualenv — no conflicts between e.g. python-telegram-bot and a WhatsApp SDK |
| Crash isolation | A plugin crash does not bring down hirocli or other plugins |
| Independent updates | Plugins are versioned and updated separately from the core server |
| Uniform interface | All plugins speak the same JSON-RPC 2.0 dialect over a local WebSocket |
| Developer ergonomics | hiro-channel-sdk provides the full contract; authoring a new plugin requires implementing 4 methods |
System overview

Components
hiro-channel-sdk
The shared library that defines the contract between hirocli and every plugin. It is a uv workspace member (hiroserver/hiro-channel-sdk/) and a declared dependency of every channel package.
| Module | Exports | Purpose |
|---|---|---|
models.py | UnifiedMessage, ChannelInfo, RpcRequest, RpcResponse | Pydantic data models |
base.py | ChannelPlugin | Abstract base class plugins implement |
rpc.py | build_*, parse_message | JSON-RPC 2.0 wire-format helpers |
transport.py | PluginTransport | WS client that connects to hirocli and dispatches calls |
ChannelManager
Lives inhirocli/channel_manager.py. On hirocli start it:
- Opens a WebSocket server on
ws://127.0.0.1:<plugin_port>(default18081). - Reads
~/.hirocli/channels/*.jsonto discover enabled channels. - Spawns one subprocess per enabled channel, appending
--hiro-ws <url>and--log-dir <path>. Subprocess stdout/stderr are discarded — each plugin writes its own rotating log file to the log directory. - Accepts incoming connections from plugin processes.
- Pushes the stored per-channel config (
configfield) to each plugin immediately after it registers. - Routes inbound messages from plugins to the configured
on_messagecallback. - On shutdown: sends
channel.stopto every plugin, waits 1 second, then terminates any remaining subprocesses.
CommunicationManager
The central message router betweenChannelManager and the application core — handles inbound/outbound queuing and permission checks.
See Communication Manager for full details.
AgentManager
The LLM worker that consumes text messages from the inbound queue, invokes a LangChain v1create_agent instance, and pushes replies to the outbound queue. Per-conversation memory is maintained using LangGraph’s InMemorySaver checkpointer, keyed by channel:sender_id.
See Agent Manager for full details.
Channel config files
Channel configuration is stored at~/.hirocli/channels/<name>.json and managed via hirocli channel setup|enable|disable|remove.
JSON-RPC 2.0 protocol
All messages are UTF-8 JSON over a single WebSocket connection per plugin. A notification has no"id" field (fire-and-forget). A request has an "id" and expects a response with the same "id".
Plugin → hirocli
| Method | Type | Params | When |
|---|---|---|---|
channel.register | notification | {name, version, description} | First frame after connect — mandatory |
channel.receive | notification | UnifiedMessage (dict) | Inbound message from third party |
channel.event | notification | {event: str, data: any} | Status changes, delivery receipts, errors |
hirocli → plugin
| Method | Type | Params | Purpose |
|---|---|---|---|
channel.send | notification | UnifiedMessage (dict) | Send a message out through this channel |
channel.configure | notification | {config: {…}} | Push credentials and settings |
channel.status | request | — | Health probe — expects {name, version, status} |
channel.stop | notification | — | Graceful shutdown signal |
Inbound message flow

Outbound message flow

UnifiedMessage format
All messages in the system useUnifiedMessage — the canonical cross-channel format defined in hiro-channel-sdk.
direction is always from hirocli’s perspective:
"inbound"— arriving from the third party (user sent something)"outbound"— to be sent to the third party
| Field | Type | Notes |
|---|---|---|
id | str | Auto-generated UUID hex |
channel | str | Plugin name, e.g. "telegram" |
direction | str | "inbound" or "outbound" |
sender_id | str | Channel-native user identifier |
recipient_id | str | None | Channel-native target identifier |
content_type | str | "text" | "image" | "audio" | "video" | "location" | "command" | "file" |
body | str | Text content or caption |
metadata | dict | Channel-specific extras — free-form |
timestamp | datetime | UTC, auto-generated |
Plugin lifecycle

hirocli stop: the stop event is set, channel.stop is sent to each plugin, hirocli waits 1 second, then terminates any remaining subprocesses.
uv workspace layout
hiro-channel-sdk as a workspace dependency, giving it an isolated virtualenv while sharing the workspace’s common resolved lockfile.
