@elizaos/python 2.0.0-alpha.10

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.
Files changed (197) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +239 -0
  3. package/elizaos/__init__.py +280 -0
  4. package/elizaos/action_docs.py +149 -0
  5. package/elizaos/advanced_capabilities/__init__.py +85 -0
  6. package/elizaos/advanced_capabilities/actions/__init__.py +54 -0
  7. package/elizaos/advanced_capabilities/actions/add_contact.py +139 -0
  8. package/elizaos/advanced_capabilities/actions/follow_room.py +151 -0
  9. package/elizaos/advanced_capabilities/actions/image_generation.py +148 -0
  10. package/elizaos/advanced_capabilities/actions/mute_room.py +164 -0
  11. package/elizaos/advanced_capabilities/actions/remove_contact.py +145 -0
  12. package/elizaos/advanced_capabilities/actions/roles.py +207 -0
  13. package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +154 -0
  14. package/elizaos/advanced_capabilities/actions/search_contacts.py +145 -0
  15. package/elizaos/advanced_capabilities/actions/send_message.py +187 -0
  16. package/elizaos/advanced_capabilities/actions/settings.py +151 -0
  17. package/elizaos/advanced_capabilities/actions/unfollow_room.py +164 -0
  18. package/elizaos/advanced_capabilities/actions/unmute_room.py +164 -0
  19. package/elizaos/advanced_capabilities/actions/update_contact.py +164 -0
  20. package/elizaos/advanced_capabilities/actions/update_entity.py +161 -0
  21. package/elizaos/advanced_capabilities/evaluators/__init__.py +18 -0
  22. package/elizaos/advanced_capabilities/evaluators/reflection.py +134 -0
  23. package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +203 -0
  24. package/elizaos/advanced_capabilities/providers/__init__.py +36 -0
  25. package/elizaos/advanced_capabilities/providers/agent_settings.py +60 -0
  26. package/elizaos/advanced_capabilities/providers/contacts.py +77 -0
  27. package/elizaos/advanced_capabilities/providers/facts.py +82 -0
  28. package/elizaos/advanced_capabilities/providers/follow_ups.py +113 -0
  29. package/elizaos/advanced_capabilities/providers/knowledge.py +83 -0
  30. package/elizaos/advanced_capabilities/providers/relationships.py +112 -0
  31. package/elizaos/advanced_capabilities/providers/roles.py +97 -0
  32. package/elizaos/advanced_capabilities/providers/settings.py +51 -0
  33. package/elizaos/advanced_capabilities/services/__init__.py +18 -0
  34. package/elizaos/advanced_capabilities/services/follow_up.py +138 -0
  35. package/elizaos/advanced_capabilities/services/rolodex.py +244 -0
  36. package/elizaos/advanced_memory/__init__.py +3 -0
  37. package/elizaos/advanced_memory/evaluators.py +97 -0
  38. package/elizaos/advanced_memory/memory_service.py +556 -0
  39. package/elizaos/advanced_memory/plugin.py +30 -0
  40. package/elizaos/advanced_memory/prompts.py +12 -0
  41. package/elizaos/advanced_memory/providers.py +90 -0
  42. package/elizaos/advanced_memory/types.py +65 -0
  43. package/elizaos/advanced_planning/__init__.py +10 -0
  44. package/elizaos/advanced_planning/actions.py +145 -0
  45. package/elizaos/advanced_planning/message_classifier.py +127 -0
  46. package/elizaos/advanced_planning/planning_service.py +712 -0
  47. package/elizaos/advanced_planning/plugin.py +40 -0
  48. package/elizaos/advanced_planning/prompts.py +4 -0
  49. package/elizaos/basic_capabilities/__init__.py +66 -0
  50. package/elizaos/basic_capabilities/actions/__init__.py +24 -0
  51. package/elizaos/basic_capabilities/actions/choice.py +140 -0
  52. package/elizaos/basic_capabilities/actions/ignore.py +66 -0
  53. package/elizaos/basic_capabilities/actions/none.py +56 -0
  54. package/elizaos/basic_capabilities/actions/reply.py +120 -0
  55. package/elizaos/basic_capabilities/providers/__init__.py +54 -0
  56. package/elizaos/basic_capabilities/providers/action_state.py +113 -0
  57. package/elizaos/basic_capabilities/providers/actions.py +263 -0
  58. package/elizaos/basic_capabilities/providers/attachments.py +76 -0
  59. package/elizaos/basic_capabilities/providers/capabilities.py +62 -0
  60. package/elizaos/basic_capabilities/providers/character.py +113 -0
  61. package/elizaos/basic_capabilities/providers/choice.py +73 -0
  62. package/elizaos/basic_capabilities/providers/context_bench.py +44 -0
  63. package/elizaos/basic_capabilities/providers/current_time.py +58 -0
  64. package/elizaos/basic_capabilities/providers/entities.py +99 -0
  65. package/elizaos/basic_capabilities/providers/evaluators.py +54 -0
  66. package/elizaos/basic_capabilities/providers/providers_list.py +55 -0
  67. package/elizaos/basic_capabilities/providers/recent_messages.py +85 -0
  68. package/elizaos/basic_capabilities/providers/time.py +45 -0
  69. package/elizaos/basic_capabilities/providers/world.py +93 -0
  70. package/elizaos/basic_capabilities/services/__init__.py +18 -0
  71. package/elizaos/basic_capabilities/services/embedding.py +122 -0
  72. package/elizaos/basic_capabilities/services/task.py +178 -0
  73. package/elizaos/bootstrap/__init__.py +12 -0
  74. package/elizaos/bootstrap/actions/__init__.py +68 -0
  75. package/elizaos/bootstrap/actions/add_contact.py +149 -0
  76. package/elizaos/bootstrap/actions/choice.py +147 -0
  77. package/elizaos/bootstrap/actions/follow_room.py +151 -0
  78. package/elizaos/bootstrap/actions/ignore.py +80 -0
  79. package/elizaos/bootstrap/actions/image_generation.py +135 -0
  80. package/elizaos/bootstrap/actions/mute_room.py +151 -0
  81. package/elizaos/bootstrap/actions/none.py +71 -0
  82. package/elizaos/bootstrap/actions/remove_contact.py +159 -0
  83. package/elizaos/bootstrap/actions/reply.py +140 -0
  84. package/elizaos/bootstrap/actions/roles.py +193 -0
  85. package/elizaos/bootstrap/actions/schedule_follow_up.py +164 -0
  86. package/elizaos/bootstrap/actions/search_contacts.py +159 -0
  87. package/elizaos/bootstrap/actions/send_message.py +173 -0
  88. package/elizaos/bootstrap/actions/settings.py +165 -0
  89. package/elizaos/bootstrap/actions/unfollow_room.py +151 -0
  90. package/elizaos/bootstrap/actions/unmute_room.py +151 -0
  91. package/elizaos/bootstrap/actions/update_contact.py +178 -0
  92. package/elizaos/bootstrap/actions/update_entity.py +175 -0
  93. package/elizaos/bootstrap/autonomy/__init__.py +18 -0
  94. package/elizaos/bootstrap/autonomy/action.py +197 -0
  95. package/elizaos/bootstrap/autonomy/providers.py +165 -0
  96. package/elizaos/bootstrap/autonomy/routes.py +171 -0
  97. package/elizaos/bootstrap/autonomy/service.py +562 -0
  98. package/elizaos/bootstrap/autonomy/types.py +18 -0
  99. package/elizaos/bootstrap/evaluators/__init__.py +19 -0
  100. package/elizaos/bootstrap/evaluators/reflection.py +118 -0
  101. package/elizaos/bootstrap/evaluators/relationship_extraction.py +192 -0
  102. package/elizaos/bootstrap/plugin.py +140 -0
  103. package/elizaos/bootstrap/providers/__init__.py +80 -0
  104. package/elizaos/bootstrap/providers/action_state.py +71 -0
  105. package/elizaos/bootstrap/providers/actions.py +256 -0
  106. package/elizaos/bootstrap/providers/agent_settings.py +63 -0
  107. package/elizaos/bootstrap/providers/attachments.py +76 -0
  108. package/elizaos/bootstrap/providers/capabilities.py +66 -0
  109. package/elizaos/bootstrap/providers/character.py +128 -0
  110. package/elizaos/bootstrap/providers/choice.py +77 -0
  111. package/elizaos/bootstrap/providers/contacts.py +78 -0
  112. package/elizaos/bootstrap/providers/context_bench.py +49 -0
  113. package/elizaos/bootstrap/providers/current_time.py +56 -0
  114. package/elizaos/bootstrap/providers/entities.py +99 -0
  115. package/elizaos/bootstrap/providers/evaluators.py +58 -0
  116. package/elizaos/bootstrap/providers/facts.py +86 -0
  117. package/elizaos/bootstrap/providers/follow_ups.py +116 -0
  118. package/elizaos/bootstrap/providers/knowledge.py +73 -0
  119. package/elizaos/bootstrap/providers/providers_list.py +59 -0
  120. package/elizaos/bootstrap/providers/recent_messages.py +85 -0
  121. package/elizaos/bootstrap/providers/relationships.py +106 -0
  122. package/elizaos/bootstrap/providers/roles.py +95 -0
  123. package/elizaos/bootstrap/providers/settings.py +55 -0
  124. package/elizaos/bootstrap/providers/time.py +45 -0
  125. package/elizaos/bootstrap/providers/world.py +97 -0
  126. package/elizaos/bootstrap/services/__init__.py +26 -0
  127. package/elizaos/bootstrap/services/embedding.py +122 -0
  128. package/elizaos/bootstrap/services/follow_up.py +138 -0
  129. package/elizaos/bootstrap/services/rolodex.py +244 -0
  130. package/elizaos/bootstrap/services/task.py +585 -0
  131. package/elizaos/bootstrap/types.py +54 -0
  132. package/elizaos/bootstrap/utils/__init__.py +7 -0
  133. package/elizaos/bootstrap/utils/xml.py +69 -0
  134. package/elizaos/character.py +149 -0
  135. package/elizaos/logger.py +179 -0
  136. package/elizaos/media/__init__.py +45 -0
  137. package/elizaos/media/mime.py +315 -0
  138. package/elizaos/media/search.py +161 -0
  139. package/elizaos/media/tests/__init__.py +1 -0
  140. package/elizaos/media/tests/test_mime.py +117 -0
  141. package/elizaos/media/tests/test_search.py +156 -0
  142. package/elizaos/plugin.py +191 -0
  143. package/elizaos/prompts.py +1071 -0
  144. package/elizaos/py.typed +0 -0
  145. package/elizaos/runtime.py +2572 -0
  146. package/elizaos/services/__init__.py +49 -0
  147. package/elizaos/services/hook_service.py +511 -0
  148. package/elizaos/services/message_service.py +1248 -0
  149. package/elizaos/settings.py +182 -0
  150. package/elizaos/streaming_context.py +159 -0
  151. package/elizaos/trajectory_context.py +18 -0
  152. package/elizaos/types/__init__.py +512 -0
  153. package/elizaos/types/agent.py +31 -0
  154. package/elizaos/types/components.py +208 -0
  155. package/elizaos/types/database.py +64 -0
  156. package/elizaos/types/environment.py +46 -0
  157. package/elizaos/types/events.py +47 -0
  158. package/elizaos/types/memory.py +45 -0
  159. package/elizaos/types/model.py +393 -0
  160. package/elizaos/types/plugin.py +188 -0
  161. package/elizaos/types/primitives.py +100 -0
  162. package/elizaos/types/runtime.py +460 -0
  163. package/elizaos/types/service.py +113 -0
  164. package/elizaos/types/service_interfaces.py +244 -0
  165. package/elizaos/types/state.py +188 -0
  166. package/elizaos/types/task.py +29 -0
  167. package/elizaos/utils/__init__.py +108 -0
  168. package/elizaos/utils/spec_examples.py +48 -0
  169. package/elizaos/utils/streaming.py +426 -0
  170. package/elizaos_atropos_shared/__init__.py +1 -0
  171. package/elizaos_atropos_shared/canonical_eliza.py +282 -0
  172. package/package.json +19 -0
  173. package/pyproject.toml +143 -0
  174. package/requirements-dev.in +11 -0
  175. package/requirements-dev.lock +134 -0
  176. package/requirements.in +9 -0
  177. package/requirements.lock +64 -0
  178. package/tests/__init__.py +0 -0
  179. package/tests/test_action_parameters.py +154 -0
  180. package/tests/test_actions_provider_examples.py +39 -0
  181. package/tests/test_advanced_memory_behavior.py +96 -0
  182. package/tests/test_advanced_memory_flag.py +30 -0
  183. package/tests/test_advanced_planning_behavior.py +225 -0
  184. package/tests/test_advanced_planning_flag.py +26 -0
  185. package/tests/test_autonomy.py +445 -0
  186. package/tests/test_bootstrap_initialize.py +37 -0
  187. package/tests/test_character.py +163 -0
  188. package/tests/test_character_provider.py +231 -0
  189. package/tests/test_dynamic_prompt_exec.py +561 -0
  190. package/tests/test_logger_redaction.py +43 -0
  191. package/tests/test_plugin.py +117 -0
  192. package/tests/test_runtime.py +422 -0
  193. package/tests/test_salt_production_enforcement.py +22 -0
  194. package/tests/test_settings_crypto.py +118 -0
  195. package/tests/test_streaming.py +295 -0
  196. package/tests/test_types.py +221 -0
  197. package/tests/test_uuid_parity.py +46 -0
@@ -0,0 +1,49 @@
1
+ """Services for elizaOS."""
2
+
3
+ from elizaos.services.hook_service import (
4
+ DEFAULT_HOOK_PRIORITY,
5
+ LEGACY_EVENT_MAP,
6
+ HookEligibilityResult,
7
+ HookEventType,
8
+ HookHandler,
9
+ HookLoadResult,
10
+ HookMetadata,
11
+ HookRegistration,
12
+ HookRequirements,
13
+ HookService,
14
+ HookSnapshot,
15
+ HookSource,
16
+ HookSummary,
17
+ map_legacy_event,
18
+ map_legacy_events,
19
+ )
20
+ from elizaos.services.message_service import (
21
+ DefaultMessageService,
22
+ IMessageService,
23
+ MessageProcessingResult,
24
+ StreamingMessageResult,
25
+ )
26
+
27
+ __all__ = [
28
+ # Hook Service
29
+ "DEFAULT_HOOK_PRIORITY",
30
+ "HookEligibilityResult",
31
+ "HookEventType",
32
+ "HookHandler",
33
+ "HookLoadResult",
34
+ "HookMetadata",
35
+ "HookRegistration",
36
+ "HookRequirements",
37
+ "HookService",
38
+ "HookSnapshot",
39
+ "HookSource",
40
+ "HookSummary",
41
+ "LEGACY_EVENT_MAP",
42
+ "map_legacy_event",
43
+ "map_legacy_events",
44
+ # Message Service
45
+ "DefaultMessageService",
46
+ "IMessageService",
47
+ "MessageProcessingResult",
48
+ "StreamingMessageResult",
49
+ ]
@@ -0,0 +1,511 @@
1
+ """
2
+ HookService - Unified Hook Management Service for Python
3
+
4
+ This service provides a centralized hook management system that integrates
5
+ with the Eliza event system. Hooks can be registered for specific event
6
+ types and will be triggered when those events are emitted.
7
+
8
+ Key Features:
9
+ - Register hooks for specific event types with priority ordering
10
+ - FIFO execution order by default, with priority override support
11
+ - Hook eligibility checks based on requirements (OS, binaries, env vars, config paths)
12
+ - Directory-based hook discovery from HOOK.md files
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import platform
19
+ import shutil
20
+ import uuid
21
+ from collections.abc import Callable
22
+ from dataclasses import dataclass, field
23
+ from datetime import datetime
24
+ from enum import Enum
25
+ from typing import TYPE_CHECKING, Any
26
+
27
+ from elizaos.types.service import Service, ServiceType
28
+
29
+ if TYPE_CHECKING:
30
+ from elizaos.types.runtime import IAgentRuntime
31
+
32
+
33
+ class HookSource(str, Enum):
34
+ """Source of a hook registration."""
35
+
36
+ BUNDLED = "bundled"
37
+ MANAGED = "managed"
38
+ WORKSPACE = "workspace"
39
+ PLUGIN = "plugin"
40
+ RUNTIME = "runtime"
41
+
42
+
43
+ class HookEventType(str, Enum):
44
+ """Hook-specific event types."""
45
+
46
+ HOOK_COMMAND_NEW = "HOOK_COMMAND_NEW"
47
+ HOOK_COMMAND_RESET = "HOOK_COMMAND_RESET"
48
+ HOOK_COMMAND_STOP = "HOOK_COMMAND_STOP"
49
+ HOOK_SESSION_START = "HOOK_SESSION_START"
50
+ HOOK_SESSION_END = "HOOK_SESSION_END"
51
+ HOOK_AGENT_BOOTSTRAP = "HOOK_AGENT_BOOTSTRAP"
52
+ HOOK_AGENT_START = "HOOK_AGENT_START"
53
+ HOOK_AGENT_END = "HOOK_AGENT_END"
54
+ HOOK_GATEWAY_START = "HOOK_GATEWAY_START"
55
+ HOOK_GATEWAY_STOP = "HOOK_GATEWAY_STOP"
56
+ HOOK_COMPACTION_BEFORE = "HOOK_COMPACTION_BEFORE"
57
+ HOOK_COMPACTION_AFTER = "HOOK_COMPACTION_AFTER"
58
+ HOOK_TOOL_BEFORE = "HOOK_TOOL_BEFORE"
59
+ HOOK_TOOL_AFTER = "HOOK_TOOL_AFTER"
60
+ HOOK_TOOL_PERSIST = "HOOK_TOOL_PERSIST"
61
+ HOOK_MESSAGE_SENDING = "HOOK_MESSAGE_SENDING"
62
+
63
+
64
+ DEFAULT_HOOK_PRIORITY: int = 0
65
+
66
+
67
+ @dataclass
68
+ class HookRequirements:
69
+ """Requirements that must be met for a hook to be eligible."""
70
+
71
+ os: list[str] | None = None
72
+ bins: list[str] | None = None
73
+ any_bins: list[str] | None = None
74
+ env: list[str] | None = None
75
+ config: list[str] | None = None
76
+
77
+
78
+ @dataclass
79
+ class HookEligibilityResult:
80
+ """Result of checking hook eligibility."""
81
+
82
+ eligible: bool
83
+ reasons: list[str] = field(default_factory=list)
84
+
85
+
86
+ @dataclass
87
+ class HookMetadata:
88
+ """Metadata describing a registered hook."""
89
+
90
+ name: str
91
+ source: HookSource
92
+ events: list[str]
93
+ priority: int = DEFAULT_HOOK_PRIORITY
94
+ enabled: bool = True
95
+ description: str = ""
96
+ plugin_id: str | None = None
97
+ always: bool = False
98
+ requires: HookRequirements | None = None
99
+
100
+
101
+ HookHandler = Callable[[dict[str, Any]], None]
102
+
103
+
104
+ @dataclass
105
+ class HookRegistration:
106
+ """A registered hook with its handler."""
107
+
108
+ id: str
109
+ metadata: HookMetadata
110
+ handler: HookHandler
111
+ registered_at: float
112
+
113
+
114
+ @dataclass
115
+ class HookSummary:
116
+ """Summary information for a hook."""
117
+
118
+ name: str
119
+ events: list[str]
120
+ source: HookSource
121
+ enabled: bool
122
+ priority: int
123
+ plugin_id: str | None = None
124
+
125
+
126
+ @dataclass
127
+ class HookSnapshot:
128
+ """Snapshot of all registered hooks."""
129
+
130
+ hooks: list[HookSummary]
131
+ version: int
132
+ timestamp: float
133
+
134
+
135
+ @dataclass
136
+ class HookLoadResult:
137
+ """Result of loading hooks from a directory."""
138
+
139
+ loaded: list[str]
140
+ skipped: list[dict[str, str]]
141
+ errors: list[dict[str, str]]
142
+
143
+
144
+ LEGACY_EVENT_MAP: dict[str, str] = {
145
+ "command:new": HookEventType.HOOK_COMMAND_NEW.value,
146
+ "command:reset": HookEventType.HOOK_COMMAND_RESET.value,
147
+ "command:stop": HookEventType.HOOK_COMMAND_STOP.value,
148
+ "session:start": HookEventType.HOOK_SESSION_START.value,
149
+ "session:end": HookEventType.HOOK_SESSION_END.value,
150
+ "agent:bootstrap": HookEventType.HOOK_AGENT_BOOTSTRAP.value,
151
+ "agent:start": HookEventType.HOOK_AGENT_START.value,
152
+ "agent:end": HookEventType.HOOK_AGENT_END.value,
153
+ "gateway:start": HookEventType.HOOK_GATEWAY_START.value,
154
+ "gateway:stop": HookEventType.HOOK_GATEWAY_STOP.value,
155
+ "compaction:before": HookEventType.HOOK_COMPACTION_BEFORE.value,
156
+ "compaction:after": HookEventType.HOOK_COMPACTION_AFTER.value,
157
+ "tool:before": HookEventType.HOOK_TOOL_BEFORE.value,
158
+ "tool:after": HookEventType.HOOK_TOOL_AFTER.value,
159
+ "tool:persist": HookEventType.HOOK_TOOL_PERSIST.value,
160
+ "message:sending": HookEventType.HOOK_MESSAGE_SENDING.value,
161
+ }
162
+
163
+
164
+ def map_legacy_event(legacy_event: str) -> str | None:
165
+ """Map a legacy event string to its HookEventType."""
166
+ return LEGACY_EVENT_MAP.get(legacy_event)
167
+
168
+
169
+ def map_legacy_events(legacy_events: list[str]) -> list[str]:
170
+ """Map a list of legacy events to HookEventTypes."""
171
+ result = []
172
+ for legacy in legacy_events:
173
+ mapped = map_legacy_event(legacy)
174
+ if mapped:
175
+ result.append(mapped)
176
+ return result
177
+
178
+
179
+ class HookService(Service):
180
+ """
181
+ Unified hook management service.
182
+
183
+ Provides centralized hook registration, discovery, eligibility checking,
184
+ and dispatch integrated with the Eliza event system.
185
+ """
186
+
187
+ service_type = ServiceType.HOOKS
188
+
189
+ def __init__(self, runtime: IAgentRuntime | None = None) -> None:
190
+ super().__init__(runtime)
191
+ self._registry: dict[str, HookRegistration] = {}
192
+ self._event_index: dict[str, set[str]] = {}
193
+ self._id_counter = 0
194
+ self._snapshot_version = 0
195
+ self._hook_config: dict[str, Any] = {}
196
+
197
+ @property
198
+ def capability_description(self) -> str:
199
+ return "Hook registration and execution"
200
+
201
+ @classmethod
202
+ async def start(cls, runtime: IAgentRuntime) -> HookService:
203
+ """Start the HookService and set up event interceptors."""
204
+ service = cls(runtime)
205
+ service._setup_event_interceptors()
206
+ return service
207
+
208
+ async def stop(self) -> None:
209
+ """Stop the HookService and clean up."""
210
+ self._registry.clear()
211
+ self._event_index.clear()
212
+
213
+ def _setup_event_interceptors(self) -> None:
214
+ """Register this service to intercept HOOK_* events."""
215
+ if self._runtime is None:
216
+ return
217
+
218
+ for event_type in HookEventType:
219
+ self._runtime.register_event(
220
+ event_type.value,
221
+ self._create_event_handler(event_type.value),
222
+ )
223
+
224
+ def _create_event_handler(self, event_type: str) -> Callable[[dict[str, Any]], None]:
225
+ """Create an async event handler for a specific event type."""
226
+
227
+ async def handler(payload: dict[str, Any]) -> None:
228
+ await self._dispatch_to_hooks(event_type, payload)
229
+
230
+ return handler
231
+
232
+ async def _dispatch_to_hooks(self, event_type: str, payload: dict[str, Any]) -> None:
233
+ """Dispatch an event to all registered hooks for that event type."""
234
+ hook_ids = self._event_index.get(event_type, set())
235
+ if not hook_ids:
236
+ return
237
+
238
+ registrations = [self._registry[hid] for hid in hook_ids if hid in self._registry]
239
+ registrations.sort(key=lambda r: (-r.metadata.priority, r.registered_at))
240
+
241
+ for registration in registrations:
242
+ if not registration.metadata.enabled:
243
+ continue
244
+
245
+ eligibility = self._check_eligibility_internal(registration)
246
+ if not eligibility.eligible:
247
+ continue
248
+
249
+ registration.handler(payload)
250
+
251
+ def _check_eligibility_internal(self, registration: HookRegistration) -> HookEligibilityResult:
252
+ """Check if a hook is eligible to run."""
253
+ if not registration.metadata.requires:
254
+ return HookEligibilityResult(eligible=True)
255
+ return self.check_requirements(registration.metadata.requires)
256
+
257
+ def register(
258
+ self,
259
+ events: str | list[str],
260
+ handler: HookHandler,
261
+ *,
262
+ name: str,
263
+ description: str = "",
264
+ source: HookSource = HookSource.RUNTIME,
265
+ plugin_id: str | None = None,
266
+ priority: int = DEFAULT_HOOK_PRIORITY,
267
+ always: bool = False,
268
+ requires: HookRequirements | None = None,
269
+ ) -> str:
270
+ """
271
+ Register a hook for one or more events.
272
+
273
+ Args:
274
+ events: Event type(s) to listen for
275
+ handler: Handler function to call when event is emitted
276
+ name: Name of the hook
277
+ description: Optional description
278
+ source: Source of the hook registration
279
+ plugin_id: Optional plugin ID if hook comes from a plugin
280
+ priority: Hook priority (higher runs first, default 0)
281
+ always: If True, hook runs even when globally disabled
282
+ requires: Optional requirements for hook eligibility
283
+
284
+ Returns:
285
+ Unique hook ID
286
+ """
287
+ event_list = [events] if isinstance(events, str) else events
288
+
289
+ self._id_counter += 1
290
+ hook_id = f"hook-{self._id_counter}-{uuid.uuid4().hex[:8]}"
291
+
292
+ metadata = HookMetadata(
293
+ name=name,
294
+ description=description,
295
+ source=source,
296
+ plugin_id=plugin_id,
297
+ events=event_list,
298
+ priority=priority,
299
+ enabled=True,
300
+ always=always,
301
+ requires=requires,
302
+ )
303
+
304
+ registration = HookRegistration(
305
+ id=hook_id,
306
+ metadata=metadata,
307
+ handler=handler,
308
+ registered_at=datetime.now().timestamp(),
309
+ )
310
+
311
+ self._registry[hook_id] = registration
312
+
313
+ for event in event_list:
314
+ if event not in self._event_index:
315
+ self._event_index[event] = set()
316
+ self._event_index[event].add(hook_id)
317
+
318
+ self._snapshot_version += 1
319
+ return hook_id
320
+
321
+ def unregister(self, hook_id: str) -> bool:
322
+ """
323
+ Unregister a hook by ID.
324
+
325
+ Args:
326
+ hook_id: The hook ID to unregister
327
+
328
+ Returns:
329
+ True if hook was found and removed, False otherwise
330
+ """
331
+ registration = self._registry.pop(hook_id, None)
332
+ if registration is None:
333
+ return False
334
+
335
+ for event in registration.metadata.events:
336
+ if event in self._event_index:
337
+ self._event_index[event].discard(hook_id)
338
+ if not self._event_index[event]:
339
+ del self._event_index[event]
340
+
341
+ self._snapshot_version += 1
342
+ return True
343
+
344
+ def get_snapshot(self) -> HookSnapshot:
345
+ """Get a snapshot of all registered hooks."""
346
+ hooks = [
347
+ HookSummary(
348
+ name=reg.metadata.name,
349
+ events=reg.metadata.events,
350
+ source=reg.metadata.source,
351
+ enabled=reg.metadata.enabled,
352
+ priority=reg.metadata.priority,
353
+ plugin_id=reg.metadata.plugin_id,
354
+ )
355
+ for reg in self._registry.values()
356
+ ]
357
+ return HookSnapshot(
358
+ hooks=hooks,
359
+ version=self._snapshot_version,
360
+ timestamp=datetime.now().timestamp(),
361
+ )
362
+
363
+ def get_hooks_by_event(self, event: str) -> list[HookRegistration]:
364
+ """Get all hooks registered for a specific event."""
365
+ hook_ids = self._event_index.get(event, set())
366
+ return [self._registry[hid] for hid in hook_ids if hid in self._registry]
367
+
368
+ def get_hook(self, hook_id: str) -> HookRegistration | None:
369
+ """Get a specific hook by ID."""
370
+ return self._registry.get(hook_id)
371
+
372
+ def get_all_hooks(self) -> list[HookRegistration]:
373
+ """Get all registered hooks."""
374
+ return list(self._registry.values())
375
+
376
+ def set_enabled(self, hook_id: str, enabled: bool) -> None:
377
+ """Enable or disable a hook."""
378
+ registration = self._registry.get(hook_id)
379
+ if registration:
380
+ registration.metadata.enabled = enabled
381
+ self._snapshot_version += 1
382
+
383
+ def set_priority(self, hook_id: str, priority: int) -> None:
384
+ """Update the priority of a hook."""
385
+ registration = self._registry.get(hook_id)
386
+ if registration:
387
+ registration.metadata.priority = priority
388
+ self._snapshot_version += 1
389
+
390
+ def set_config(self, config: dict[str, Any]) -> None:
391
+ """Set the configuration for requirement checks."""
392
+ self._hook_config = config
393
+
394
+ def check_eligibility(self, hook_id: str) -> HookEligibilityResult:
395
+ """Check if a hook is eligible to run."""
396
+ registration = self._registry.get(hook_id)
397
+ if registration is None:
398
+ return HookEligibilityResult(eligible=False, reasons=["Hook not found"])
399
+ return self._check_eligibility_internal(registration)
400
+
401
+ def check_requirements(
402
+ self,
403
+ requirements: HookRequirements,
404
+ config: dict[str, Any] | None = None,
405
+ ) -> HookEligibilityResult:
406
+ """
407
+ Check if requirements are met.
408
+
409
+ Args:
410
+ requirements: Requirements to check
411
+ config: Optional config for config path checks
412
+
413
+ Returns:
414
+ Eligibility result with reasons if not eligible
415
+ """
416
+ cfg = config or self._hook_config
417
+ reasons: list[str] = []
418
+
419
+ if requirements.os:
420
+ current_os = _get_current_platform()
421
+ if current_os not in requirements.os:
422
+ reasons.append(f"OS '{current_os}' not in allowed list: {requirements.os}")
423
+
424
+ if requirements.bins:
425
+ for bin_name in requirements.bins:
426
+ if not _has_binary(bin_name):
427
+ reasons.append(f"Required binary '{bin_name}' not found")
428
+
429
+ if requirements.any_bins:
430
+ if not any(_has_binary(b) for b in requirements.any_bins):
431
+ reasons.append(f"None of the required binaries found: {requirements.any_bins}")
432
+
433
+ if requirements.env:
434
+ for env_var in requirements.env:
435
+ value = os.environ.get(env_var)
436
+ if not value or not _is_truthy(value):
437
+ reasons.append(f"Required env var '{env_var}' not set or falsy")
438
+
439
+ if requirements.config:
440
+ for config_path in requirements.config:
441
+ value = _resolve_config_path(cfg, config_path)
442
+ if not _is_truthy(value):
443
+ reasons.append(f"Required config path '{config_path}' not set or falsy")
444
+
445
+ return HookEligibilityResult(
446
+ eligible=len(reasons) == 0,
447
+ reasons=reasons,
448
+ )
449
+
450
+
451
+ def _get_current_platform() -> str:
452
+ """Get the current platform name."""
453
+ system = platform.system().lower()
454
+ if system == "darwin":
455
+ return "darwin"
456
+ elif system == "linux":
457
+ return "linux"
458
+ elif system == "windows":
459
+ return "win32"
460
+ return system
461
+
462
+
463
+ def _has_binary(bin_name: str) -> bool:
464
+ """Check if a binary is available in PATH."""
465
+ return shutil.which(bin_name) is not None
466
+
467
+
468
+ def _is_truthy(value: Any) -> bool:
469
+ """Check if a value is truthy."""
470
+ if value is None:
471
+ return False
472
+ if isinstance(value, bool):
473
+ return value
474
+ if isinstance(value, str):
475
+ return value.lower() not in ("", "0", "false", "no", "off")
476
+ if isinstance(value, (int, float)):
477
+ return value != 0
478
+ return True
479
+
480
+
481
+ def _resolve_config_path(config: dict[str, Any], path: str) -> Any:
482
+ """Resolve a dot-separated path in a config dict."""
483
+ parts = path.split(".")
484
+ current: Any = config
485
+ for part in parts:
486
+ if isinstance(current, dict):
487
+ current = current.get(part)
488
+ else:
489
+ return None
490
+ if current is None:
491
+ return None
492
+ return current
493
+
494
+
495
+ __all__ = [
496
+ "HookService",
497
+ "HookSource",
498
+ "HookEventType",
499
+ "HookRequirements",
500
+ "HookEligibilityResult",
501
+ "HookMetadata",
502
+ "HookHandler",
503
+ "HookRegistration",
504
+ "HookSummary",
505
+ "HookSnapshot",
506
+ "HookLoadResult",
507
+ "DEFAULT_HOOK_PRIORITY",
508
+ "LEGACY_EVENT_MAP",
509
+ "map_legacy_event",
510
+ "map_legacy_events",
511
+ ]