The plugin is named 'iris' (IrisAdapter, IRIS_HOME_CHANNEL, label Iris), but several docs still referred to it as the android platform/plugin and to the product as 'the Android app'. Rename name-mentions to iris/IRIS and product-mentions to 'Iris app'; keep legitimate OS references (androidApp, Android SDK, Android 10, androidx, test_android.py, ...). Also includes pi-lens markdown-lint autofixes (table spacing, trailing newlines) in the touched files.
8.9 KiB
01 — Architecture
System diagram
┌────────────────────────────── User's machine (home server / PC) ─────────────────────────────┐
│ │
│ hermes gateway (ONE process) │
│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Agent core (run_agent.py) ── sessions (SQLite + FTS5) ── cron scheduler │ │
│ │ │ │ │
│ │ ▼ legacy stream callbacks (delta / tool-progress / commentary) │ │
│ │ ┌──────────────────────────────┐ ┌────────────────────────────────┐ │ │
│ │ │ IRIS PLATFORM PLUGIN │◄───────►│ WebSocket SERVER (websockets) │ │ │
│ │ │ IrisAdapter │ JSON │ ws://host:8790/ws │ │ │
│ │ │ (BasePlatformAdapter) │ frames │ + media + FCM-token + pairing │ │ │
│ │ │ • send / edit / stream │ └───────────────┬────────────────┘ │ │
│ │ │ • media cache │ │ WSS │ │
│ │ │ • outbox (SQLite) │ │ │ │
│ │ │ • push (FCM / ntfy) │─────────────────────────┼──────────┐ │ │
│ │ └──────────────────────────────┘ │ │ │ │
│ └───────────────────────────────────────────────────────────┼──────────┼───────────┘ │
└───────────────────────────────────────────────────────────────┼──────────┼───────────────────────┘
│ │ push (FCM HTTP v1 / ntfy)
┌──────────────────┼──────────▼─────────┐
│ │ Google FCM cloud │
▼ │ │ │
┌────────────────────────┐ │ ▼ │
│ IRIS APP (ANDROID) │◄──┴── (wake) ┌──────────┐
│ (Kotlin / Compose) │ WSS │ PHONE │
│ • WS client (OkHttp) │◄───────────►│ MIX 2S │
│ • ExoPlayer │ │ (API 29) │
│ • FCM service │ └──────────┘
└────────────────────────┘
DESKTOP APP (Compose Multiplatform)
• same shared code, WSS to same server
• tray + OS notifications (no FCM), big-screen two-pane
Process model
- One
hermes gatewayprocess hosts the agent core, the session store, the cron scheduler, and ouririsplatform plugin. The plugin's WebSocket server runs on the gateway's asyncio loop (started inIrisAdapter.connect()). - The app is a client. It initiates the WS connection to the gateway
(outbound), so no inbound port is needed on the phone. For LAN/remote access
the user points the app at the gateway's LAN IP / Tailscale name / a WSS
tunnel (see
09-pairing-security.md). - Push is the only inbound path to a sleeping phone, and it goes through a cloud relay (FCM or ntfy), not a direct connection.
Why the messaging gateway is the connection point (not tui_gateway)
hermes has two "gateways": the messaging gateway (hermes gateway, which
serves Telegram/Discord/… and cron) and the tui_gateway JSON-RPC backend
(used by the TUI and the existing Electron desktop app). We deliberately use the
messaging gateway because:
- Cron delivery is native. Cron jobs resolve
deliver=iris:<chat>[:<thread>]through the platform registry and call our adapter'ssend(). No bridging. send_messagetool routing works out of the box (pluginparse_target_ref_fn).- Slash commands are dispatched by the gateway's command pipeline — the app
just sends
/cmd argsas a message. - Coexistence. The same agent is reachable via Telegram and the app at once; sessions/channels are shared.
The tui_gateway WS protocol is not reused; we define a clean, purpose-built
protocol (04-wire-protocol.md) that borrows familiar names (message.*,
tool.*, reasoning) but is owned by our plugin.
Key architectural decisions + rationale
| Decision | Rationale |
|---|---|
Community-style platform plugin (register(ctx) → ctx.register_platform) |
Zero hermes-core changes; survives hermes updates; follows ADDING_A_PLATFORM.md "Plugin Path". |
| Single WS transport for chat, streaming, tools, media, pairing, FCM-token | One connection, one auth, one dependency (websockets, already core). Media as chunked binary frames avoids adding an HTTP server. |
websockets + httpx only |
Both are hermes core deps → the plugin adds zero new Python dependencies (respects hermes supply-chain pinning policy). |
| Structured tool events, app-side verbosity | Per the requirement: the gateway sends full tool data; the app decides everything/truncated/nothing. |
| Reasoning split in the adapter | The gateway prepends reasoning to the final text when show_reasoning is on; the adapter splits the stable prefix into a reasoning field so the app renders a clean collapsible block. |
Channels/threads = chat_id/thread_id |
The gateway's SessionSource already models this; cron delivery already targets platform:chat_id:thread_id. We map app concepts onto existing primitives. |
| SQLite outbox + sync cursor | Offline delivery + reconnect catch-up without re-reading full history. |
| Compose Multiplatform | Desktop is "the Iris app, tweaked" → share protocol/state/UI; only platform services + layout differ. |
Data flow (one turn)
- App sends
message.send {text}(or/cmd). - Plugin builds a
MessageEvent(+media_urlsif attachments) →IrisAdapter.handle_message(event). - Gateway resolves the session (
chat_id/thread_id), runs the agent. - Agent streams:
stream_delta_callback→GatewayStreamConsumer→adapter.send()(first) /adapter.edit_message()(updates) →message.start/message.updateframes. - Tool calls:
tool_progress_callback→ progress queue →adapter.send()→tool.*frames (structured). - Intermediate beats:
interim_assistant_callback→ consumer →commentaryframes. - Final answer: consumer finalizes →
adapter.send()→messageframe (reasoning split into its own field). - If the app is disconnected at any point: frame is dropped to the outbox
and a push is fired; on reconnect the app
syncs the delta.
Implementation note: the main gateway uses the legacy callback path (not the ACP-only event-native
render_message_eventpath). We therefore map the legacysend/edit_message/progress calls to our frames. The exact tool-progress vs commentary classification is verified empirically in M2 by running the real gateway with a test WS client (see13-testing.md).