tiphys.hooks
Tiphys hooks system.
Provides lifecycle events and hook registration for extensibility.
1""" 2Tiphys hooks system. 3 4Provides lifecycle events and hook registration for extensibility. 5""" 6 7from tiphys.hooks.base import BaseHook, FunctionHook, Hook, hook 8from tiphys.hooks.events import ( 9 EVENT_TYPES, 10 AgentRespondEvent, 11 AgentThinkEvent, 12 ErrorEvent, 13 GatewayBootEvent, 14 GatewayShutdownEvent, 15 HookEvent, 16 HookEventType, 17 MessageReceivedEvent, 18 MessageSentEvent, 19 PluginLoadEvent, 20 PluginUnloadEvent, 21 SessionEndEvent, 22 SessionStartEvent, 23 ToolCompleteEvent, 24 ToolExecuteEvent, 25) 26from tiphys.hooks.manager import HookManager, get_hook_manager 27 28__all__ = [ 29 # Base 30 "Hook", 31 "BaseHook", 32 "FunctionHook", 33 "hook", 34 # Events 35 "HookEvent", 36 "HookEventType", 37 "EVENT_TYPES", 38 "GatewayBootEvent", 39 "GatewayShutdownEvent", 40 "SessionStartEvent", 41 "SessionEndEvent", 42 "MessageReceivedEvent", 43 "MessageSentEvent", 44 "ToolExecuteEvent", 45 "ToolCompleteEvent", 46 "AgentThinkEvent", 47 "AgentRespondEvent", 48 "PluginLoadEvent", 49 "PluginUnloadEvent", 50 "ErrorEvent", 51 # Manager 52 "HookManager", 53 "get_hook_manager", 54]
14@runtime_checkable 15class Hook(Protocol): 16 """Protocol for hook implementations.""" 17 18 @property 19 def name(self) -> str: 20 """Unique name for this hook.""" 21 ... 22 23 @property 24 def event_types(self) -> list[str]: 25 """List of event types this hook handles.""" 26 ... 27 28 @property 29 def priority(self) -> int: 30 """Priority for execution order (lower = earlier).""" 31 ... 32 33 async def execute(self, event: HookEvent) -> None: 34 """Execute the hook with the given event.""" 35 ...
Protocol for hook implementations.
1771def _no_init_or_replace_init(self, *args, **kwargs): 1772 cls = type(self) 1773 1774 if cls._is_protocol: 1775 raise TypeError('Protocols cannot be instantiated') 1776 1777 # Already using a custom `__init__`. No need to calculate correct 1778 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1779 if cls.__init__ is not _no_init_or_replace_init: 1780 return 1781 1782 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1783 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1784 # searches for a proper new `__init__` in the MRO. The new `__init__` 1785 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1786 # instantiation of the protocol subclass will thus use the new 1787 # `__init__` and no longer call `_no_init_or_replace_init`. 1788 for base in cls.__mro__: 1789 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1790 if init is not _no_init_or_replace_init: 1791 cls.__init__ = init 1792 break 1793 else: 1794 # should not happen 1795 cls.__init__ = object.__init__ 1796 1797 cls.__init__(self, *args, **kwargs)
23 @property 24 def event_types(self) -> list[str]: 25 """List of event types this hook handles.""" 26 ...
List of event types this hook handles.
38class BaseHook: 39 """ 40 Base implementation for hooks. 41 42 Subclass this to create custom hooks with common functionality. 43 """ 44 45 def __init__( 46 self, 47 name: str, 48 event_types: list[str], 49 priority: int = 100, 50 ) -> None: 51 """ 52 Initialize the hook. 53 54 Args: 55 name: Unique name for this hook. 56 event_types: List of event types to handle. 57 priority: Execution priority (lower = earlier). 58 """ 59 self._name = name 60 self._event_types = event_types 61 self._priority = priority 62 63 @property 64 def name(self) -> str: 65 """Unique name for this hook.""" 66 return self._name 67 68 @property 69 def event_types(self) -> list[str]: 70 """List of event types this hook handles.""" 71 return self._event_types 72 73 @property 74 def priority(self) -> int: 75 """Priority for execution order.""" 76 return self._priority 77 78 async def execute(self, event: HookEvent) -> None: 79 """Execute the hook. Override in subclasses.""" 80 pass
Base implementation for hooks.
Subclass this to create custom hooks with common functionality.
45 def __init__( 46 self, 47 name: str, 48 event_types: list[str], 49 priority: int = 100, 50 ) -> None: 51 """ 52 Initialize the hook. 53 54 Args: 55 name: Unique name for this hook. 56 event_types: List of event types to handle. 57 priority: Execution priority (lower = earlier). 58 """ 59 self._name = name 60 self._event_types = event_types 61 self._priority = priority
Initialize the hook.
Args: name: Unique name for this hook. event_types: List of event types to handle. priority: Execution priority (lower = earlier).
68 @property 69 def event_types(self) -> list[str]: 70 """List of event types this hook handles.""" 71 return self._event_types
List of event types this hook handles.
83class FunctionHook(BaseHook): 84 """ 85 Hook that wraps a simple async function. 86 87 Useful for creating hooks without subclassing. 88 """ 89 90 def __init__( 91 self, 92 name: str, 93 event_types: list[str], 94 handler: Callable[[HookEvent], Awaitable[None]], 95 priority: int = 100, 96 ) -> None: 97 """ 98 Initialize the function hook. 99 100 Args: 101 name: Unique name for this hook. 102 event_types: List of event types to handle. 103 handler: Async function to call when events fire. 104 priority: Execution priority (lower = earlier). 105 """ 106 super().__init__(name, event_types, priority) 107 self._handler = handler 108 109 async def execute(self, event: HookEvent) -> None: 110 """Execute the handler function.""" 111 await self._handler(event)
Hook that wraps a simple async function.
Useful for creating hooks without subclassing.
90 def __init__( 91 self, 92 name: str, 93 event_types: list[str], 94 handler: Callable[[HookEvent], Awaitable[None]], 95 priority: int = 100, 96 ) -> None: 97 """ 98 Initialize the function hook. 99 100 Args: 101 name: Unique name for this hook. 102 event_types: List of event types to handle. 103 handler: Async function to call when events fire. 104 priority: Execution priority (lower = earlier). 105 """ 106 super().__init__(name, event_types, priority) 107 self._handler = handler
Initialize the function hook.
Args: name: Unique name for this hook. event_types: List of event types to handle. handler: Async function to call when events fire. priority: Execution priority (lower = earlier).
114def hook( 115 name: str, 116 event_types: list[str], 117 priority: int = 100, 118) -> Callable[[Callable[[HookEvent], Awaitable[None]]], FunctionHook]: 119 """ 120 Decorator to create a hook from an async function. 121 122 Usage: 123 @hook("my_hook", ["session:start"]) 124 async def on_session_start(event: HookEvent) -> None: 125 print(f"Session started: {event}") 126 """ 127 128 def decorator( 129 func: Callable[[HookEvent], Awaitable[None]], 130 ) -> FunctionHook: 131 return FunctionHook( 132 name=name, 133 event_types=event_types, 134 handler=func, 135 priority=priority, 136 ) 137 138 return decorator
Decorator to create a hook from an async function.
Usage: @hook("my_hook", ["session:start"]) async def on_session_start(event: HookEvent) -> None: print(f"Session started: {event}")
15class HookEvent(BaseModel): 16 """Base class for all hook events.""" 17 18 type: str 19 timestamp: datetime = Field(default_factory=datetime.utcnow)
Base class for all hook events.
22class GatewayBootEvent(HookEvent): 23 """Emitted when the gateway starts up.""" 24 25 type: Literal["gateway:boot"] = "gateway:boot" 26 config: dict[str, Any] = Field(default_factory=dict)
Emitted when the gateway starts up.
29class GatewayShutdownEvent(HookEvent): 30 """Emitted when the gateway is shutting down.""" 31 32 type: Literal["gateway:shutdown"] = "gateway:shutdown"
Emitted when the gateway is shutting down.
35class SessionStartEvent(HookEvent): 36 """Emitted when a new session starts.""" 37 38 type: Literal["session:start"] = "session:start" 39 session_key: str 40 agent_id: str 41 channel: str | None = None
Emitted when a new session starts.
44class SessionEndEvent(HookEvent): 45 """Emitted when a session ends.""" 46 47 type: Literal["session:end"] = "session:end" 48 session_key: str 49 duration_ms: int = 0
Emitted when a session ends.
52class MessageReceivedEvent(HookEvent): 53 """Emitted when a message is received from a channel.""" 54 55 type: Literal["message:received"] = "message:received" 56 message_id: str 57 channel: str 58 sender_id: str 59 content: str
Emitted when a message is received from a channel.
62class MessageSentEvent(HookEvent): 63 """Emitted when a message is sent to a channel.""" 64 65 type: Literal["message:sent"] = "message:sent" 66 message_id: str | None = None 67 channel: str 68 content: str
Emitted when a message is sent to a channel.
71class ToolExecuteEvent(HookEvent): 72 """Emitted before a tool is executed.""" 73 74 type: Literal["tool:execute"] = "tool:execute" 75 tool_name: str 76 arguments: dict[str, Any] = Field(default_factory=dict) 77 agent_id: str
Emitted before a tool is executed.
80class ToolCompleteEvent(HookEvent): 81 """Emitted after a tool completes execution.""" 82 83 type: Literal["tool:complete"] = "tool:complete" 84 tool_name: str 85 success: bool 86 result: str 87 duration_ms: int = 0 88 agent_id: str
Emitted after a tool completes execution.
91class AgentThinkEvent(HookEvent): 92 """Emitted when agent produces thinking/reasoning.""" 93 94 type: Literal["agent:think"] = "agent:think" 95 agent_id: str 96 content: str
Emitted when agent produces thinking/reasoning.
99class AgentRespondEvent(HookEvent): 100 """Emitted when agent produces a response.""" 101 102 type: Literal["agent:respond"] = "agent:respond" 103 agent_id: str 104 content: str 105 is_final: bool = False
Emitted when agent produces a response.
108class PluginLoadEvent(HookEvent): 109 """Emitted when a plugin is loaded.""" 110 111 type: Literal["plugin:load"] = "plugin:load" 112 plugin_name: str 113 plugin_path: str | None = None
Emitted when a plugin is loaded.
116class PluginUnloadEvent(HookEvent): 117 """Emitted when a plugin is unloaded.""" 118 119 type: Literal["plugin:unload"] = "plugin:unload" 120 plugin_name: str
Emitted when a plugin is unloaded.
123class ErrorEvent(HookEvent): 124 """Emitted when an error occurs.""" 125 126 type: Literal["error"] = "error" 127 message: str 128 error_type: str | None = None 129 context: dict[str, Any] = Field(default_factory=dict)
Emitted when an error occurs.
19class HookManager: 20 """ 21 Manages lifecycle hooks. 22 23 Handles registration, execution, and ordering of hooks. 24 """ 25 26 def __init__(self) -> None: 27 """Initialize the hook manager.""" 28 self._hooks: dict[str, list[Hook]] = defaultdict(list) 29 self._hook_names: set[str] = set() 30 31 def register(self, hook: Hook) -> None: 32 """ 33 Register a hook. 34 35 Args: 36 hook: The hook to register. 37 38 Raises: 39 ValueError: If a hook with the same name is already registered. 40 """ 41 if hook.name in self._hook_names: 42 raise ValueError(f"Hook '{hook.name}' is already registered") 43 44 self._hook_names.add(hook.name) 45 46 # Register for each event type 47 for event_type in hook.event_types: 48 self._hooks[event_type].append(hook) 49 # Sort by priority 50 self._hooks[event_type].sort(key=lambda h: h.priority) 51 52 def unregister(self, name: str) -> bool: 53 """ 54 Unregister a hook by name. 55 56 Args: 57 name: Name of the hook to unregister. 58 59 Returns: 60 True if the hook was unregistered, False if not found. 61 """ 62 if name not in self._hook_names: 63 return False 64 65 self._hook_names.discard(name) 66 67 # Remove from all event types 68 for event_type in list(self._hooks.keys()): 69 self._hooks[event_type] = [h for h in self._hooks[event_type] if h.name != name] 70 71 return True 72 73 def get_hooks(self, event_type: str) -> list[Hook]: 74 """ 75 Get all hooks registered for an event type. 76 77 Args: 78 event_type: The event type to get hooks for. 79 80 Returns: 81 List of hooks sorted by priority. 82 """ 83 return self._hooks.get(event_type, []) 84 85 def list_hooks(self) -> list[str]: 86 """List all registered hook names.""" 87 return list(self._hook_names) 88 89 async def emit(self, event: HookEvent) -> None: 90 """ 91 Emit an event to all registered hooks. 92 93 Hooks are executed in priority order. 94 Errors in individual hooks are logged but don't stop other hooks. 95 96 Args: 97 event: The event to emit. 98 """ 99 hooks = self._hooks.get(event.type, []) 100 101 if not hooks: 102 return 103 104 await logger.adebug( 105 "Emitting hook event", 106 event_type=event.type, 107 hook_count=len(hooks), 108 ) 109 110 for hook in hooks: 111 try: 112 await hook.execute(event) 113 except Exception as e: 114 await logger.aerror( 115 "Hook execution failed", 116 hook=hook.name, 117 event_type=event.type, 118 error=str(e), 119 ) 120 121 async def emit_parallel(self, event: HookEvent) -> None: 122 """ 123 Emit an event to all hooks in parallel. 124 125 Unlike emit(), this runs all hooks concurrently. 126 Use when hook order doesn't matter and you want speed. 127 128 Args: 129 event: The event to emit. 130 """ 131 hooks = self._hooks.get(event.type, []) 132 133 if not hooks: 134 return 135 136 async def safe_execute(hook: Hook) -> None: 137 try: 138 await hook.execute(event) 139 except Exception as e: 140 await logger.aerror( 141 "Hook execution failed", 142 hook=hook.name, 143 event_type=event.type, 144 error=str(e), 145 ) 146 147 await asyncio.gather(*[safe_execute(hook) for hook in hooks]) 148 149 def clear(self) -> int: 150 """ 151 Clear all registered hooks. 152 153 Returns: 154 Number of hooks cleared. 155 """ 156 count = len(self._hook_names) 157 self._hooks.clear() 158 self._hook_names.clear() 159 return count
Manages lifecycle hooks.
Handles registration, execution, and ordering of hooks.
26 def __init__(self) -> None: 27 """Initialize the hook manager.""" 28 self._hooks: dict[str, list[Hook]] = defaultdict(list) 29 self._hook_names: set[str] = set()
Initialize the hook manager.
31 def register(self, hook: Hook) -> None: 32 """ 33 Register a hook. 34 35 Args: 36 hook: The hook to register. 37 38 Raises: 39 ValueError: If a hook with the same name is already registered. 40 """ 41 if hook.name in self._hook_names: 42 raise ValueError(f"Hook '{hook.name}' is already registered") 43 44 self._hook_names.add(hook.name) 45 46 # Register for each event type 47 for event_type in hook.event_types: 48 self._hooks[event_type].append(hook) 49 # Sort by priority 50 self._hooks[event_type].sort(key=lambda h: h.priority)
Register a hook.
Args: hook: The hook to register.
Raises: ValueError: If a hook with the same name is already registered.
52 def unregister(self, name: str) -> bool: 53 """ 54 Unregister a hook by name. 55 56 Args: 57 name: Name of the hook to unregister. 58 59 Returns: 60 True if the hook was unregistered, False if not found. 61 """ 62 if name not in self._hook_names: 63 return False 64 65 self._hook_names.discard(name) 66 67 # Remove from all event types 68 for event_type in list(self._hooks.keys()): 69 self._hooks[event_type] = [h for h in self._hooks[event_type] if h.name != name] 70 71 return True
Unregister a hook by name.
Args: name: Name of the hook to unregister.
Returns: True if the hook was unregistered, False if not found.
73 def get_hooks(self, event_type: str) -> list[Hook]: 74 """ 75 Get all hooks registered for an event type. 76 77 Args: 78 event_type: The event type to get hooks for. 79 80 Returns: 81 List of hooks sorted by priority. 82 """ 83 return self._hooks.get(event_type, [])
Get all hooks registered for an event type.
Args: event_type: The event type to get hooks for.
Returns: List of hooks sorted by priority.
85 def list_hooks(self) -> list[str]: 86 """List all registered hook names.""" 87 return list(self._hook_names)
List all registered hook names.
89 async def emit(self, event: HookEvent) -> None: 90 """ 91 Emit an event to all registered hooks. 92 93 Hooks are executed in priority order. 94 Errors in individual hooks are logged but don't stop other hooks. 95 96 Args: 97 event: The event to emit. 98 """ 99 hooks = self._hooks.get(event.type, []) 100 101 if not hooks: 102 return 103 104 await logger.adebug( 105 "Emitting hook event", 106 event_type=event.type, 107 hook_count=len(hooks), 108 ) 109 110 for hook in hooks: 111 try: 112 await hook.execute(event) 113 except Exception as e: 114 await logger.aerror( 115 "Hook execution failed", 116 hook=hook.name, 117 event_type=event.type, 118 error=str(e), 119 )
Emit an event to all registered hooks.
Hooks are executed in priority order. Errors in individual hooks are logged but don't stop other hooks.
Args: event: The event to emit.
121 async def emit_parallel(self, event: HookEvent) -> None: 122 """ 123 Emit an event to all hooks in parallel. 124 125 Unlike emit(), this runs all hooks concurrently. 126 Use when hook order doesn't matter and you want speed. 127 128 Args: 129 event: The event to emit. 130 """ 131 hooks = self._hooks.get(event.type, []) 132 133 if not hooks: 134 return 135 136 async def safe_execute(hook: Hook) -> None: 137 try: 138 await hook.execute(event) 139 except Exception as e: 140 await logger.aerror( 141 "Hook execution failed", 142 hook=hook.name, 143 event_type=event.type, 144 error=str(e), 145 ) 146 147 await asyncio.gather(*[safe_execute(hook) for hook in hooks])
Emit an event to all hooks in parallel.
Unlike emit(), this runs all hooks concurrently. Use when hook order doesn't matter and you want speed.
Args: event: The event to emit.
149 def clear(self) -> int: 150 """ 151 Clear all registered hooks. 152 153 Returns: 154 Number of hooks cleared. 155 """ 156 count = len(self._hook_names) 157 self._hooks.clear() 158 self._hook_names.clear() 159 return count
Clear all registered hooks.
Returns: Number of hooks cleared.
166def get_hook_manager() -> HookManager: 167 """ 168 Get the global hook manager. 169 170 .. deprecated:: 171 Use AppContext.hook_manager instead. This function will be removed 172 in a future version. 173 174 Returns: 175 The global HookManager instance. 176 """ 177 import warnings 178 179 from tiphys.context import get_context, has_context 180 181 # If AppContext is available, use it (preferred path) 182 if has_context(): 183 warnings.warn( 184 "get_hook_manager() is deprecated. Use AppContext.hook_manager instead.", 185 DeprecationWarning, 186 stacklevel=2, 187 ) 188 return get_context().hook_manager 189 190 # Fallback to global singleton for backwards compatibility 191 global _hook_manager 192 if _hook_manager is None: 193 _hook_manager = HookManager() 194 return _hook_manager
Get the global hook manager.
Deprecated since version : Use AppContext.hook_manager instead. This function will be removed in a future version.
Returns: The global HookManager instance.