"""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}, ))