salior/salior/hooks/registry.py

144 lines
4.6 KiB
Python

"""Hook event system — emit events, register handlers, dispatch callbacks."""
from __future__ import annotations
import asyncio
import fnmatch
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Awaitable, Callable
from salior.core.logging import setup_logging
log = setup_logging()
@dataclass
class HookEvent:
"""A hook event with metadata."""
name: str # e.g. "on_signal", "on_fill", "on_error"
data: dict # Event payload
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
source: str = "" # Which agent emitted this
HookHandler = Callable[[HookEvent], Awaitable[None]] | Callable[[HookEvent], None]
class HookRegistry:
"""Register and dispatch hook handlers."""
def __init__(self) -> None:
self._handlers: dict[str, list[HookHandler]] = {}
def on(self, event_name: str, handler: HookHandler) -> None:
"""Register a handler for an event."""
if event_name not in self._handlers:
self._handlers[event_name] = []
# Prevent duplicate registration
if handler not in self._handlers[event_name]:
self._handlers[event_name].append(handler)
log.debug("hook_registered", event=event_name, handler=handler.__name__)
def off(self, event_name: str, handler: HookHandler) -> None:
"""Unregister a handler."""
if event_name in self._handlers:
try:
self._handlers[event_name].remove(handler)
log.debug("hook_unregistered", event=event_name, handler=handler.__name__)
except ValueError:
pass
async def emit(self, event: HookEvent) -> None:
"""Fire all handlers for an event, async-safe."""
handlers = self._handlers.get(event.name, [])
if not handlers:
return
log.debug("hook_fired", event=event.name, count=len(handlers), data=event.data)
for handler in handlers:
try:
if asyncio.iscoroutinefunction(handler):
await handler(event)
else:
handler(event)
except Exception as e:
log.error("hook_handler_error", event=event.name, handler=handler.__name__, error=str(e))
def list(self) -> dict[str, int]:
"""List registered hooks with handler counts."""
return {name: len(handlers) for name, handlers in self._handlers.items()}
# Global registry — shared across all agents
global_hooks = HookRegistry()
# ─── Built-in hook events ─────────────────────────────────────────────────────
async def on_signal(coin: str, regime: str, conviction: float, reasoning: str) -> None:
"""Emit a signal event."""
await global_hooks.emit(HookEvent(
name="on_signal",
source="signal_agent",
data={"coin": coin, "regime": regime, "conviction": conviction, "reasoning": reasoning},
))
async def on_fill(
coin: str,
side: str,
size: float,
price: float,
exec_id: str,
mode: str,
) -> None:
"""Emit a fill event when an order fills."""
await global_hooks.emit(HookEvent(
name="on_fill",
source="exec_agent",
data={"coin": coin, "side": side, "size": size, "price": price, "exec_id": exec_id, "mode": mode},
))
async def on_execution(
coin: str,
side: str,
size: float,
price: float,
status: str,
error: str | None = None,
) -> None:
"""Emit an execution event (placed, filled, cancelled, failed)."""
await global_hooks.emit(HookEvent(
name="on_execution",
source="exec_agent",
data={"coin": coin, "side": side, "size": size, "price": price, "status": status, "error": error},
))
async def on_error(agent: str, error: str, details: dict | None = None) -> None:
"""Emit an error event."""
await global_hooks.emit(HookEvent(
name="on_error",
source=agent,
data={"agent": agent, "error": error, "details": details or {}},
))
async def on_risk_breach(reason: str, details: dict) -> None:
"""Emit a risk breach event."""
await global_hooks.emit(HookEvent(
name="on_risk_breach",
source="risk_agent",
data={"reason": reason, "details": details},
))
async def on_agent_health(agent: str, status: str, iteration: int) -> None:
"""Emit a health heartbeat."""
await global_hooks.emit(HookEvent(
name="on_agent_health",
source=agent,
data={"agent": agent, "status": status, "iteration": iteration},
))