Add a new hooks system allowing users to run custom code at key lifecycle points in the agent's operation. This includes support for events such as `gateway:startup`, `session:start`, `agent:step`, and more. Documentation for creating hooks and available events has been added to `README.md` and a new `hooks.md` file. Additionally, integrate step callbacks in the agent to facilitate hook execution during tool-calling iterations.
175 lines
5.5 KiB
Markdown
175 lines
5.5 KiB
Markdown
# Event Hooks
|
|
|
|
The hooks system lets you run custom code at key points in the agent lifecycle — session creation, slash commands, each tool-calling step, and more. Hooks are discovered automatically from `~/.hermes/hooks/` and fire without blocking the main agent pipeline.
|
|
|
|
## Creating a Hook
|
|
|
|
Each hook is a directory under `~/.hermes/hooks/` containing two files:
|
|
|
|
```
|
|
~/.hermes/hooks/
|
|
└── my-hook/
|
|
├── HOOK.yaml # Declares which events to listen for
|
|
└── handler.py # Python handler function
|
|
```
|
|
|
|
### HOOK.yaml
|
|
|
|
```yaml
|
|
name: my-hook
|
|
description: Log all agent activity to a file
|
|
events:
|
|
- agent:start
|
|
- agent:end
|
|
- agent:step
|
|
```
|
|
|
|
The `events` list determines which events trigger your handler. You can subscribe to any combination of events, including wildcards like `command:*`.
|
|
|
|
### handler.py
|
|
|
|
```python
|
|
import json
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
LOG_FILE = Path.home() / ".hermes" / "hooks" / "my-hook" / "activity.log"
|
|
|
|
async def handle(event_type: str, context: dict):
|
|
"""Called for each subscribed event. Must be named 'handle'."""
|
|
entry = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"event": event_type,
|
|
**context,
|
|
}
|
|
with open(LOG_FILE, "a") as f:
|
|
f.write(json.dumps(entry) + "\n")
|
|
```
|
|
|
|
The handler function:
|
|
- Must be named `handle`
|
|
- Receives `event_type` (string) and `context` (dict)
|
|
- Can be `async def` or regular `def` — both work
|
|
- Errors are caught and logged, never crashing the agent
|
|
|
|
## Available Events
|
|
|
|
| Event | When it fires | Context keys |
|
|
|-------|---------------|--------------|
|
|
| `gateway:startup` | Gateway process starts | `platforms` (list of active platform names) |
|
|
| `session:start` | New messaging session created | `platform`, `user_id`, `session_id`, `session_key` |
|
|
| `session:reset` | User ran `/new` or `/reset` | `platform`, `user_id`, `session_key` |
|
|
| `agent:start` | Agent begins processing a message | `platform`, `user_id`, `session_id`, `message` |
|
|
| `agent:step` | Each iteration of the tool-calling loop | `platform`, `user_id`, `session_id`, `iteration`, `tool_names` |
|
|
| `agent:end` | Agent finishes processing | `platform`, `user_id`, `session_id`, `message`, `response` |
|
|
| `command:*` | Any slash command executed | `platform`, `user_id`, `command`, `args` |
|
|
|
|
### Wildcard Matching
|
|
|
|
Handlers registered for `command:*` fire for any `command:` event (`command:model`, `command:reset`, etc.). This lets you monitor all slash commands with a single subscription.
|
|
|
|
## Examples
|
|
|
|
### Telegram Notification on Long Tasks
|
|
|
|
Send yourself a Telegram message when the agent takes more than 10 tool-calling steps:
|
|
|
|
```yaml
|
|
# ~/.hermes/hooks/long-task-alert/HOOK.yaml
|
|
name: long-task-alert
|
|
description: Alert when agent is taking many steps
|
|
events:
|
|
- agent:step
|
|
```
|
|
|
|
```python
|
|
# ~/.hermes/hooks/long-task-alert/handler.py
|
|
import os
|
|
import httpx
|
|
|
|
THRESHOLD = 10
|
|
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
|
CHAT_ID = os.getenv("TELEGRAM_HOME_CHANNEL")
|
|
|
|
async def handle(event_type: str, context: dict):
|
|
iteration = context.get("iteration", 0)
|
|
if iteration == THRESHOLD and BOT_TOKEN and CHAT_ID:
|
|
tools = ", ".join(context.get("tool_names", []))
|
|
text = f"⚠️ Agent has been running for {iteration} steps. Last tools: {tools}"
|
|
async with httpx.AsyncClient() as client:
|
|
await client.post(
|
|
f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
|
|
json={"chat_id": CHAT_ID, "text": text},
|
|
)
|
|
```
|
|
|
|
### Command Usage Logger
|
|
|
|
Track which slash commands are used and how often:
|
|
|
|
```yaml
|
|
# ~/.hermes/hooks/command-logger/HOOK.yaml
|
|
name: command-logger
|
|
description: Log slash command usage
|
|
events:
|
|
- command:*
|
|
```
|
|
|
|
```python
|
|
# ~/.hermes/hooks/command-logger/handler.py
|
|
import json
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
LOG = Path.home() / ".hermes" / "logs" / "command_usage.jsonl"
|
|
|
|
def handle(event_type: str, context: dict):
|
|
LOG.parent.mkdir(parents=True, exist_ok=True)
|
|
entry = {
|
|
"ts": datetime.now().isoformat(),
|
|
"command": context.get("command"),
|
|
"args": context.get("args"),
|
|
"platform": context.get("platform"),
|
|
"user": context.get("user_id"),
|
|
}
|
|
with open(LOG, "a") as f:
|
|
f.write(json.dumps(entry) + "\n")
|
|
```
|
|
|
|
### Session Start Webhook
|
|
|
|
POST to an external service whenever a new session starts:
|
|
|
|
```yaml
|
|
# ~/.hermes/hooks/session-webhook/HOOK.yaml
|
|
name: session-webhook
|
|
description: Notify external service on new sessions
|
|
events:
|
|
- session:start
|
|
- session:reset
|
|
```
|
|
|
|
```python
|
|
# ~/.hermes/hooks/session-webhook/handler.py
|
|
import httpx
|
|
|
|
WEBHOOK_URL = "https://your-service.example.com/hermes-events"
|
|
|
|
async def handle(event_type: str, context: dict):
|
|
async with httpx.AsyncClient() as client:
|
|
await client.post(WEBHOOK_URL, json={
|
|
"event": event_type,
|
|
**context,
|
|
}, timeout=5)
|
|
```
|
|
|
|
## How It Works
|
|
|
|
1. On gateway startup, `HookRegistry.discover_and_load()` scans `~/.hermes/hooks/`
|
|
2. Each subdirectory with `HOOK.yaml` + `handler.py` is loaded dynamically
|
|
3. Handlers are registered for their declared events
|
|
4. At each lifecycle point, `hooks.emit()` fires all matching handlers
|
|
5. Errors in any handler are caught and logged — a broken hook never crashes the agent
|
|
|
|
Hooks only fire in the **gateway** (Telegram, Discord, Slack, WhatsApp). The CLI does not currently load hooks. The `agent:step` event bridges from the sync agent thread to the async hook system via `asyncio.run_coroutine_threadsafe`.
|