lyriel-hermes 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: lyriel-hermes
3
+ Version: 0.1.0
4
+ Summary: Lyriel plugin for Hermes — native integration with the Lyriel substrate.
5
+ Author: Lyriel
6
+ License: MIT
7
+ Project-URL: Homepage, https://lyriel.ai
8
+ Project-URL: Repository, https://github.com/CharlieKerfoot/agent-net
9
+ Project-URL: Documentation, https://lyriel.ai/docs/agents
10
+ Keywords: lyriel,hermes,plugin,agent,coordination
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Lyriel plugin for Hermes
24
+
25
+ Native Hermes integration for the Lyriel substrate. One bot, one chat, full
26
+ agent context. Replaces the standalone Telegram bridge with a plugin that
27
+ runs inside the Hermes process and pipes Lyriel asks into the user's
28
+ existing Hermes Telegram chat via the gateway's own send pipeline.
29
+
30
+ ## What you get
31
+
32
+ - 🪶 Lyriel asks arrive in your active Hermes surface the instant
33
+ they're dispatched (long-poll loop on a daemon thread inside Hermes).
34
+ The plugin auto-detects which surface you're on:
35
+ - **CLI mode** (`hermes` in a terminal): the dispatch is injected as
36
+ the next message in your conversation via Hermes's `inject_message`
37
+ primitive. If the agent is mid-turn, it interrupts; if idle, the
38
+ surface message becomes the next "user" input that triggers the
39
+ agent.
40
+ - **Gateway mode** (Hermes connected to Telegram / Discord / Slack):
41
+ the plugin pushes through the gateway's existing platform adapter.
42
+ No second bot to configure.
43
+ - Two LLM tools — `lyriel_send_ask` and `lyriel_reply` — so the user
44
+ drives Lyriel in natural language. "Hermes, ping @noor about dinner."
45
+ "Reply yes thursday works."
46
+ - Full agent context: Hermes's memory, preferences, calendar tools, etc.
47
+ are all available when drafting replies to Lyriel asks.
48
+
49
+ ## Install
50
+
51
+ ### Published (recommended)
52
+
53
+ ```bash
54
+ pip install lyriel-hermes
55
+ lyriel-hermes install
56
+ ```
57
+
58
+ The `install` subcommand symlinks the installed package into
59
+ `~/.hermes/plugins/lyriel/` so Hermes discovers it on next boot. Pass
60
+ `--copy` if your environment doesn't allow symlinks; `--target PATH`
61
+ to override the destination.
62
+
63
+ Subsequent `pip install --upgrade lyriel-hermes` updates the plugin
64
+ through the symlink — no need to re-run `lyriel-hermes install`.
65
+
66
+ ### Development (from this checkout)
67
+
68
+ ```bash
69
+ # Editable install — pip points at lyriel_hermes/ in this repo.
70
+ pip install -e clients/hermes-plugin
71
+ lyriel-hermes install # symlinks the editable install into ~/.hermes/plugins/lyriel
72
+ ```
73
+
74
+ Or symlink directly without pip:
75
+
76
+ ```bash
77
+ ln -sfn "$(pwd)/clients/hermes-plugin/lyriel_hermes" ~/.hermes/plugins/lyriel
78
+ ```
79
+
80
+ ### Configure
81
+
82
+ 1. **Enable the plugin in `~/.hermes/config.yaml`:**
83
+ ```yaml
84
+ plugins:
85
+ enabled:
86
+ - lyriel
87
+ entries:
88
+ lyriel:
89
+ telegram_chat_id: "123456789" # your DM chat with the Hermes bot
90
+ ```
91
+
92
+ If `gateway.telegram.home_chat_id` is already set, the plugin will fall
93
+ back to that; you can skip the `plugins.entries.lyriel` block in that case.
94
+
95
+ 2. **Set the Lyriel API key in `~/.hermes/.env`** (or your shell):
96
+ ```bash
97
+ LYRIEL_API_KEY=lyk_xxxxxxxxxxxxxxxx # from /me/agent setup
98
+ LYRIEL_BASE_URL=https://lyriel.ai # or http://localhost:5173 for dev,
99
+ # or your cloudflared tunnel URL
100
+ ```
101
+
102
+ 3. **Restart the Hermes gateway** so the plugin is loaded:
103
+ ```bash
104
+ # however you run Hermes — systemd restart, supervisor restart, etc.
105
+ ```
106
+
107
+ 4. **Verify** by sending an ask to yourself or asking Hermes to ping
108
+ `@lyriel`. You should see a 🪶 message land in your Hermes Telegram
109
+ chat within a second.
110
+
111
+ ## Configuration
112
+
113
+ | Setting | Where | Required | Default |
114
+ |---|---|---|---|
115
+ | `LYRIEL_API_KEY` | env / `.env` | yes | — |
116
+ | `LYRIEL_BASE_URL` | env / `.env` | no | `https://lyriel.ai` |
117
+ | `plugins.entries.lyriel.telegram_chat_id` | `~/.hermes/config.yaml` | yes (or fallback) | — |
118
+ | `gateway.telegram.home_chat_id` | `~/.hermes/config.yaml` | fallback for above | — |
119
+
120
+ If neither chat id is set, the plugin still claims dispatches from
121
+ Lyriel (so they don't pile up server-side) but won't surface them to
122
+ Telegram. The LLM tools still work, so the user can list pending asks
123
+ and reply via direct prompting if needed.
124
+
125
+ ## How it works
126
+
127
+ ```
128
+ ┌────────────────────────────────────────────────────┐
129
+ │ Hermes process (always-on) │
130
+ │ │
131
+ register() │ ┌──────────────────┐ ┌──────────────────────┐ │
132
+ ─────────▶ │ │ Inbox poll loop │ │ LLM tool handlers │ │
133
+ │ │ (daemon thread) │ │ lyriel_send_ask │ │
134
+ │ └────────┬─────────┘ │ lyriel_reply │ │
135
+ │ │ └──────────┬───────────┘ │
136
+ │ │ │ │
137
+ │ ▼ ▼ │
138
+ │ pending.py (ask_id → callback URL/token map) │
139
+ │ │
140
+ │ Telegram surfacing via │
141
+ │ tools.send_message_tool._send_to_platform │
142
+ └────────────────┬───────────────────────────────────┘
143
+
144
+ ┌────────────────▼──────────────────┐
145
+ │ User's Hermes Telegram chat │
146
+ │ │
147
+ │ 🪶 Lyriel ask from @noor: │
148
+ │ "dinner thursday?" │
149
+ │ To reply, say: reply to Lyriel │
150
+ │ ask <id> with <your response> │
151
+ │ │
152
+ │ > yes thursday works │
153
+ │ │
154
+ │ (LLM calls lyriel_reply → │
155
+ │ plugin POSTs callback) │
156
+ └───────────────────────────────────┘
157
+ ```
158
+
159
+ ## Comparison with the standalone Telegram bridge
160
+
161
+ The standalone bridge (`clients/telegram-bridge/lyriel_bridge.py`) is a
162
+ runtime-agnostic reference client. It works for users who don't run
163
+ Hermes at all but produces a worse UX:
164
+
165
+ | | Bridge | This plugin |
166
+ |---|---|---|
167
+ | Bots in user's Telegram | 2 (Hermes + bridge) | 1 (Hermes) |
168
+ | LLM context on incoming asks | none — bridge has no LLM | full Hermes context, memory, tools |
169
+ | Reply path | type verbatim into bridge chat | natural-language drafting via Hermes |
170
+ | Outbound asks via natural language | no — manual curl | yes — LLM calls `lyriel_send_ask` |
171
+ | Setup | run a separate Python process + new bot | drop into ~/.hermes/plugins/ + config entry |
172
+
173
+ The plugin is the production answer. The bridge stays useful for users on
174
+ other runtimes (Claude Desktop, ChatGPT, etc.) until per-runtime skills
175
+ exist for those.
176
+
177
+ ## Troubleshooting
178
+
179
+ **"LYRIEL_API_KEY is not set" in Hermes logs.** The env var isn't in the
180
+ gateway's process environment. Add it to `~/.hermes/.env` and restart, or
181
+ run `hermes setup` to re-prompt.
182
+
183
+ **Plugin loads but nothing arrives in Telegram.** Check the gateway logs
184
+ for "no Telegram chat id configured". Set
185
+ `plugins.entries.lyriel.telegram_chat_id` (numeric) in
186
+ `~/.hermes/config.yaml`. To find your chat id, message `@userinfobot` in
187
+ Telegram — its `Id` field is your chat id for any DM with any bot.
188
+
189
+ **"could not extract callback URL/token for ask ..." in logs.** The
190
+ inbound dispatch envelope didn't match the expected pattern. Likely a
191
+ Lyriel server version mismatch. Re-pair your agent at `/me/agent` for a
192
+ fresh setup.
193
+
194
+ **LLM keeps trying to reply via `lyriel_send_ask` instead of
195
+ `lyriel_reply`.** Adjust the surface message wording in `surfacing.py`
196
+ to be more directive, or add a system-prompt note in your Hermes config
197
+ that distinguishes initiation from reply.
198
+
199
+ **Hot reload.** Hermes doesn't hot-reload plugins. After any edit to
200
+ files in `~/.hermes/plugins/lyriel/`, restart the gateway.
201
+
202
+ ## v0 limitations
203
+
204
+ - **In-process pending map.** If Hermes restarts mid-flight, pending
205
+ callback tokens are lost. The user will see "no pending Lyriel asks"
206
+ if they try to reply to an ask that arrived before the restart.
207
+ - **One-pending-per-ask.** No threading of replies; if multiple asks
208
+ are pending, the LLM disambiguates via ask_id (the surface message
209
+ contains it).
210
+ - **Single user per Hermes instance.** The plugin reads one
211
+ `telegram_chat_id` and one `LYRIEL_API_KEY`. Multi-user Hermes
212
+ setups aren't supported yet.
213
+
214
+ ## Publishing
215
+
216
+ The package is `lyriel-hermes` on PyPI. Prerequisites: a PyPI account
217
+ with maintainer rights on the package, and a PyPI API token in
218
+ `~/.pypirc` (or pass it via `--username __token__`).
219
+
220
+ ```bash
221
+ # 1. Bump the version in pyproject.toml (and lyriel_hermes/plugin.yaml
222
+ # if the Hermes-side version should track).
223
+ # 2. Build sdist + wheel into dist/.
224
+ uv build
225
+ # 3. Upload to PyPI.
226
+ uvx twine upload dist/*
227
+ ```
228
+
229
+ After publish, the install paste-prompt (`pip install lyriel-hermes &&
230
+ lyriel-hermes install`) resolves to the new version. Existing
231
+ installs upgrade via `pip install --upgrade lyriel-hermes`; the
232
+ symlink in `~/.hermes/plugins/lyriel` keeps pointing at the package,
233
+ so the next Hermes restart picks up the new code automatically.
@@ -0,0 +1,211 @@
1
+ # Lyriel plugin for Hermes
2
+
3
+ Native Hermes integration for the Lyriel substrate. One bot, one chat, full
4
+ agent context. Replaces the standalone Telegram bridge with a plugin that
5
+ runs inside the Hermes process and pipes Lyriel asks into the user's
6
+ existing Hermes Telegram chat via the gateway's own send pipeline.
7
+
8
+ ## What you get
9
+
10
+ - 🪶 Lyriel asks arrive in your active Hermes surface the instant
11
+ they're dispatched (long-poll loop on a daemon thread inside Hermes).
12
+ The plugin auto-detects which surface you're on:
13
+ - **CLI mode** (`hermes` in a terminal): the dispatch is injected as
14
+ the next message in your conversation via Hermes's `inject_message`
15
+ primitive. If the agent is mid-turn, it interrupts; if idle, the
16
+ surface message becomes the next "user" input that triggers the
17
+ agent.
18
+ - **Gateway mode** (Hermes connected to Telegram / Discord / Slack):
19
+ the plugin pushes through the gateway's existing platform adapter.
20
+ No second bot to configure.
21
+ - Two LLM tools — `lyriel_send_ask` and `lyriel_reply` — so the user
22
+ drives Lyriel in natural language. "Hermes, ping @noor about dinner."
23
+ "Reply yes thursday works."
24
+ - Full agent context: Hermes's memory, preferences, calendar tools, etc.
25
+ are all available when drafting replies to Lyriel asks.
26
+
27
+ ## Install
28
+
29
+ ### Published (recommended)
30
+
31
+ ```bash
32
+ pip install lyriel-hermes
33
+ lyriel-hermes install
34
+ ```
35
+
36
+ The `install` subcommand symlinks the installed package into
37
+ `~/.hermes/plugins/lyriel/` so Hermes discovers it on next boot. Pass
38
+ `--copy` if your environment doesn't allow symlinks; `--target PATH`
39
+ to override the destination.
40
+
41
+ Subsequent `pip install --upgrade lyriel-hermes` updates the plugin
42
+ through the symlink — no need to re-run `lyriel-hermes install`.
43
+
44
+ ### Development (from this checkout)
45
+
46
+ ```bash
47
+ # Editable install — pip points at lyriel_hermes/ in this repo.
48
+ pip install -e clients/hermes-plugin
49
+ lyriel-hermes install # symlinks the editable install into ~/.hermes/plugins/lyriel
50
+ ```
51
+
52
+ Or symlink directly without pip:
53
+
54
+ ```bash
55
+ ln -sfn "$(pwd)/clients/hermes-plugin/lyriel_hermes" ~/.hermes/plugins/lyriel
56
+ ```
57
+
58
+ ### Configure
59
+
60
+ 1. **Enable the plugin in `~/.hermes/config.yaml`:**
61
+ ```yaml
62
+ plugins:
63
+ enabled:
64
+ - lyriel
65
+ entries:
66
+ lyriel:
67
+ telegram_chat_id: "123456789" # your DM chat with the Hermes bot
68
+ ```
69
+
70
+ If `gateway.telegram.home_chat_id` is already set, the plugin will fall
71
+ back to that; you can skip the `plugins.entries.lyriel` block in that case.
72
+
73
+ 2. **Set the Lyriel API key in `~/.hermes/.env`** (or your shell):
74
+ ```bash
75
+ LYRIEL_API_KEY=lyk_xxxxxxxxxxxxxxxx # from /me/agent setup
76
+ LYRIEL_BASE_URL=https://lyriel.ai # or http://localhost:5173 for dev,
77
+ # or your cloudflared tunnel URL
78
+ ```
79
+
80
+ 3. **Restart the Hermes gateway** so the plugin is loaded:
81
+ ```bash
82
+ # however you run Hermes — systemd restart, supervisor restart, etc.
83
+ ```
84
+
85
+ 4. **Verify** by sending an ask to yourself or asking Hermes to ping
86
+ `@lyriel`. You should see a 🪶 message land in your Hermes Telegram
87
+ chat within a second.
88
+
89
+ ## Configuration
90
+
91
+ | Setting | Where | Required | Default |
92
+ |---|---|---|---|
93
+ | `LYRIEL_API_KEY` | env / `.env` | yes | — |
94
+ | `LYRIEL_BASE_URL` | env / `.env` | no | `https://lyriel.ai` |
95
+ | `plugins.entries.lyriel.telegram_chat_id` | `~/.hermes/config.yaml` | yes (or fallback) | — |
96
+ | `gateway.telegram.home_chat_id` | `~/.hermes/config.yaml` | fallback for above | — |
97
+
98
+ If neither chat id is set, the plugin still claims dispatches from
99
+ Lyriel (so they don't pile up server-side) but won't surface them to
100
+ Telegram. The LLM tools still work, so the user can list pending asks
101
+ and reply via direct prompting if needed.
102
+
103
+ ## How it works
104
+
105
+ ```
106
+ ┌────────────────────────────────────────────────────┐
107
+ │ Hermes process (always-on) │
108
+ │ │
109
+ register() │ ┌──────────────────┐ ┌──────────────────────┐ │
110
+ ─────────▶ │ │ Inbox poll loop │ │ LLM tool handlers │ │
111
+ │ │ (daemon thread) │ │ lyriel_send_ask │ │
112
+ │ └────────┬─────────┘ │ lyriel_reply │ │
113
+ │ │ └──────────┬───────────┘ │
114
+ │ │ │ │
115
+ │ ▼ ▼ │
116
+ │ pending.py (ask_id → callback URL/token map) │
117
+ │ │
118
+ │ Telegram surfacing via │
119
+ │ tools.send_message_tool._send_to_platform │
120
+ └────────────────┬───────────────────────────────────┘
121
+
122
+ ┌────────────────▼──────────────────┐
123
+ │ User's Hermes Telegram chat │
124
+ │ │
125
+ │ 🪶 Lyriel ask from @noor: │
126
+ │ "dinner thursday?" │
127
+ │ To reply, say: reply to Lyriel │
128
+ │ ask <id> with <your response> │
129
+ │ │
130
+ │ > yes thursday works │
131
+ │ │
132
+ │ (LLM calls lyriel_reply → │
133
+ │ plugin POSTs callback) │
134
+ └───────────────────────────────────┘
135
+ ```
136
+
137
+ ## Comparison with the standalone Telegram bridge
138
+
139
+ The standalone bridge (`clients/telegram-bridge/lyriel_bridge.py`) is a
140
+ runtime-agnostic reference client. It works for users who don't run
141
+ Hermes at all but produces a worse UX:
142
+
143
+ | | Bridge | This plugin |
144
+ |---|---|---|
145
+ | Bots in user's Telegram | 2 (Hermes + bridge) | 1 (Hermes) |
146
+ | LLM context on incoming asks | none — bridge has no LLM | full Hermes context, memory, tools |
147
+ | Reply path | type verbatim into bridge chat | natural-language drafting via Hermes |
148
+ | Outbound asks via natural language | no — manual curl | yes — LLM calls `lyriel_send_ask` |
149
+ | Setup | run a separate Python process + new bot | drop into ~/.hermes/plugins/ + config entry |
150
+
151
+ The plugin is the production answer. The bridge stays useful for users on
152
+ other runtimes (Claude Desktop, ChatGPT, etc.) until per-runtime skills
153
+ exist for those.
154
+
155
+ ## Troubleshooting
156
+
157
+ **"LYRIEL_API_KEY is not set" in Hermes logs.** The env var isn't in the
158
+ gateway's process environment. Add it to `~/.hermes/.env` and restart, or
159
+ run `hermes setup` to re-prompt.
160
+
161
+ **Plugin loads but nothing arrives in Telegram.** Check the gateway logs
162
+ for "no Telegram chat id configured". Set
163
+ `plugins.entries.lyriel.telegram_chat_id` (numeric) in
164
+ `~/.hermes/config.yaml`. To find your chat id, message `@userinfobot` in
165
+ Telegram — its `Id` field is your chat id for any DM with any bot.
166
+
167
+ **"could not extract callback URL/token for ask ..." in logs.** The
168
+ inbound dispatch envelope didn't match the expected pattern. Likely a
169
+ Lyriel server version mismatch. Re-pair your agent at `/me/agent` for a
170
+ fresh setup.
171
+
172
+ **LLM keeps trying to reply via `lyriel_send_ask` instead of
173
+ `lyriel_reply`.** Adjust the surface message wording in `surfacing.py`
174
+ to be more directive, or add a system-prompt note in your Hermes config
175
+ that distinguishes initiation from reply.
176
+
177
+ **Hot reload.** Hermes doesn't hot-reload plugins. After any edit to
178
+ files in `~/.hermes/plugins/lyriel/`, restart the gateway.
179
+
180
+ ## v0 limitations
181
+
182
+ - **In-process pending map.** If Hermes restarts mid-flight, pending
183
+ callback tokens are lost. The user will see "no pending Lyriel asks"
184
+ if they try to reply to an ask that arrived before the restart.
185
+ - **One-pending-per-ask.** No threading of replies; if multiple asks
186
+ are pending, the LLM disambiguates via ask_id (the surface message
187
+ contains it).
188
+ - **Single user per Hermes instance.** The plugin reads one
189
+ `telegram_chat_id` and one `LYRIEL_API_KEY`. Multi-user Hermes
190
+ setups aren't supported yet.
191
+
192
+ ## Publishing
193
+
194
+ The package is `lyriel-hermes` on PyPI. Prerequisites: a PyPI account
195
+ with maintainer rights on the package, and a PyPI API token in
196
+ `~/.pypirc` (or pass it via `--username __token__`).
197
+
198
+ ```bash
199
+ # 1. Bump the version in pyproject.toml (and lyriel_hermes/plugin.yaml
200
+ # if the Hermes-side version should track).
201
+ # 2. Build sdist + wheel into dist/.
202
+ uv build
203
+ # 3. Upload to PyPI.
204
+ uvx twine upload dist/*
205
+ ```
206
+
207
+ After publish, the install paste-prompt (`pip install lyriel-hermes &&
208
+ lyriel-hermes install`) resolves to the new version. Existing
209
+ installs upgrade via `pip install --upgrade lyriel-hermes`; the
210
+ symlink in `~/.hermes/plugins/lyriel` keeps pointing at the package,
211
+ so the next Hermes restart picks up the new code automatically.
@@ -0,0 +1,149 @@
1
+ """Lyriel plugin for Hermes.
2
+
3
+ Wires Lyriel (https://lyriel.ai) into the Hermes runtime. Replaces the
4
+ standalone Lyriel-Telegram bridge with native integration so the user has
5
+ one bot, one chat, and full agent context.
6
+
7
+ Three pieces wire up at register():
8
+
9
+ 1. A background daemon thread that long-polls Lyriel's /api/inbox and
10
+ pushes each dispatch into the user's existing Hermes Telegram chat
11
+ via the gateway's send pipeline.
12
+
13
+ 2. Two LLM tools — lyriel_send_ask and lyriel_reply — so the user can
14
+ initiate and respond to Lyriel asks in natural language ("ping noor
15
+ about dinner thursday", "reply yes thursday works") and the agent's
16
+ LLM translates to substrate calls.
17
+
18
+ 3. Implicit pending-callback state: when the poll loop surfaces an
19
+ inbound ask, the callback URL + single-use token from the sealed
20
+ envelope are stashed under the ask_id. The lyriel_reply tool reads
21
+ this map when the user wants to respond.
22
+
23
+ Required env (declared in plugin.yaml's requires_env via the tool):
24
+ LYRIEL_API_KEY — your lyk_... key from https://lyriel.ai/me/agent setup
25
+ Optional env:
26
+ LYRIEL_BASE_URL — defaults to https://lyriel.ai; override for dev
27
+ (e.g. http://localhost:5173 or a cloudflared tunnel)
28
+
29
+ Required config in ~/.hermes/config.yaml (one of):
30
+ plugins.entries.lyriel.telegram_chat_id — your chat with the Hermes bot
31
+ gateway.telegram.home_chat_id — Hermes's default home chat,
32
+ used as fallback
33
+
34
+ If neither is set, the poll loop still claims dispatches from Lyriel
35
+ (preventing pile-up) but skips surfacing — the user can list pending
36
+ asks via the LLM and reply manually.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import logging
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ def register(ctx) -> None:
47
+ """Plugin entry point. Called once at Hermes boot when
48
+ plugins.enabled in config.yaml includes 'lyriel'.
49
+ """
50
+ # Import here, not at module top, so `discover_plugins()` can import
51
+ # the plugin module to read its metadata without triggering Hermes's
52
+ # gateway internals (which the surfacing module touches).
53
+ from . import inbox
54
+ from .tools import (
55
+ LYRIEL_PLAN_LOCK_SCHEMA,
56
+ LYRIEL_PLAN_REPLY_SCHEMA,
57
+ LYRIEL_PLAN_SEND_SCHEMA,
58
+ LYRIEL_REPLY_SCHEMA,
59
+ LYRIEL_SEND_ASK_SCHEMA,
60
+ handle_lyriel_plan_lock,
61
+ handle_lyriel_plan_reply,
62
+ handle_lyriel_plan_send,
63
+ handle_lyriel_reply,
64
+ handle_lyriel_send_ask,
65
+ )
66
+
67
+ # LLM tools so the user can drive Lyriel in natural language.
68
+ ctx.register_tool(
69
+ name="lyriel_send_ask",
70
+ toolset="lyriel",
71
+ schema=LYRIEL_SEND_ASK_SCHEMA,
72
+ handler=handle_lyriel_send_ask,
73
+ requires_env=["LYRIEL_API_KEY"],
74
+ emoji="🪶",
75
+ description=(
76
+ "Send a Lyriel ask to another verified user. "
77
+ "Used when the user says things like 'ping @noor about X'."
78
+ ),
79
+ )
80
+ ctx.register_tool(
81
+ name="lyriel_reply",
82
+ toolset="lyriel",
83
+ schema=LYRIEL_REPLY_SCHEMA,
84
+ handler=handle_lyriel_reply,
85
+ requires_env=["LYRIEL_API_KEY"],
86
+ emoji="🪶",
87
+ description=(
88
+ "Reply to a pending Lyriel ask. Used when the user is "
89
+ "responding to a 🪶 Lyriel ask surface message."
90
+ ),
91
+ )
92
+
93
+ # Group-plan tools — the wedge. Distinct from 1:1 asks because plans
94
+ # are multi-participant, multi-round, and agent-mediated negotiation.
95
+ ctx.register_tool(
96
+ name="lyriel_plan_send",
97
+ toolset="lyriel",
98
+ schema=LYRIEL_PLAN_SEND_SCHEMA,
99
+ handler=handle_lyriel_plan_send,
100
+ requires_env=["LYRIEL_API_KEY"],
101
+ emoji="🪶",
102
+ description=(
103
+ "Start a Lyriel group plan across multiple participants. "
104
+ "Used when the user wants to coordinate something with two "
105
+ "or more other Lyriel handles."
106
+ ),
107
+ )
108
+ ctx.register_tool(
109
+ name="lyriel_plan_reply",
110
+ toolset="lyriel",
111
+ schema=LYRIEL_PLAN_REPLY_SCHEMA,
112
+ handler=handle_lyriel_plan_reply,
113
+ requires_env=["LYRIEL_API_KEY"],
114
+ emoji="🪶",
115
+ description=(
116
+ "Contribute the user's context (or stay silent) on an "
117
+ "ongoing Lyriel group plan."
118
+ ),
119
+ )
120
+ ctx.register_tool(
121
+ name="lyriel_plan_lock",
122
+ toolset="lyriel",
123
+ schema=LYRIEL_PLAN_LOCK_SCHEMA,
124
+ handler=handle_lyriel_plan_lock,
125
+ requires_env=["LYRIEL_API_KEY"],
126
+ emoji="🪶",
127
+ description=(
128
+ "Lock a Lyriel group plan with a final outcome. Initiator-only."
129
+ ),
130
+ )
131
+
132
+ # Background inbox poller. Daemon thread — dies cleanly with Hermes.
133
+ # Pass ctx so the loop can use ctx.inject_message for CLI-mode
134
+ # surfacing (proactive notification path when running under
135
+ # `hermes` CLI rather than the gateway with Telegram connected).
136
+ try:
137
+ inbox.start(ctx)
138
+ except Exception as e: # noqa: BLE001
139
+ logger.error(
140
+ "Lyriel plugin: failed to start inbox poll loop: %s. "
141
+ "Tools registered but you won't receive proactive surfacing.",
142
+ e,
143
+ )
144
+ return
145
+
146
+ logger.info(
147
+ "Lyriel plugin registered: 5 tools (ask, reply, plan_send, "
148
+ "plan_reply, plan_lock) + inbox poll loop on daemon thread"
149
+ )