@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.
- package/LICENSE +26 -0
- package/README.md +239 -0
- package/elizaos/__init__.py +280 -0
- package/elizaos/action_docs.py +149 -0
- package/elizaos/advanced_capabilities/__init__.py +85 -0
- package/elizaos/advanced_capabilities/actions/__init__.py +54 -0
- package/elizaos/advanced_capabilities/actions/add_contact.py +139 -0
- package/elizaos/advanced_capabilities/actions/follow_room.py +151 -0
- package/elizaos/advanced_capabilities/actions/image_generation.py +148 -0
- package/elizaos/advanced_capabilities/actions/mute_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/remove_contact.py +145 -0
- package/elizaos/advanced_capabilities/actions/roles.py +207 -0
- package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +154 -0
- package/elizaos/advanced_capabilities/actions/search_contacts.py +145 -0
- package/elizaos/advanced_capabilities/actions/send_message.py +187 -0
- package/elizaos/advanced_capabilities/actions/settings.py +151 -0
- package/elizaos/advanced_capabilities/actions/unfollow_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/unmute_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/update_contact.py +164 -0
- package/elizaos/advanced_capabilities/actions/update_entity.py +161 -0
- package/elizaos/advanced_capabilities/evaluators/__init__.py +18 -0
- package/elizaos/advanced_capabilities/evaluators/reflection.py +134 -0
- package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +203 -0
- package/elizaos/advanced_capabilities/providers/__init__.py +36 -0
- package/elizaos/advanced_capabilities/providers/agent_settings.py +60 -0
- package/elizaos/advanced_capabilities/providers/contacts.py +77 -0
- package/elizaos/advanced_capabilities/providers/facts.py +82 -0
- package/elizaos/advanced_capabilities/providers/follow_ups.py +113 -0
- package/elizaos/advanced_capabilities/providers/knowledge.py +83 -0
- package/elizaos/advanced_capabilities/providers/relationships.py +112 -0
- package/elizaos/advanced_capabilities/providers/roles.py +97 -0
- package/elizaos/advanced_capabilities/providers/settings.py +51 -0
- package/elizaos/advanced_capabilities/services/__init__.py +18 -0
- package/elizaos/advanced_capabilities/services/follow_up.py +138 -0
- package/elizaos/advanced_capabilities/services/rolodex.py +244 -0
- package/elizaos/advanced_memory/__init__.py +3 -0
- package/elizaos/advanced_memory/evaluators.py +97 -0
- package/elizaos/advanced_memory/memory_service.py +556 -0
- package/elizaos/advanced_memory/plugin.py +30 -0
- package/elizaos/advanced_memory/prompts.py +12 -0
- package/elizaos/advanced_memory/providers.py +90 -0
- package/elizaos/advanced_memory/types.py +65 -0
- package/elizaos/advanced_planning/__init__.py +10 -0
- package/elizaos/advanced_planning/actions.py +145 -0
- package/elizaos/advanced_planning/message_classifier.py +127 -0
- package/elizaos/advanced_planning/planning_service.py +712 -0
- package/elizaos/advanced_planning/plugin.py +40 -0
- package/elizaos/advanced_planning/prompts.py +4 -0
- package/elizaos/basic_capabilities/__init__.py +66 -0
- package/elizaos/basic_capabilities/actions/__init__.py +24 -0
- package/elizaos/basic_capabilities/actions/choice.py +140 -0
- package/elizaos/basic_capabilities/actions/ignore.py +66 -0
- package/elizaos/basic_capabilities/actions/none.py +56 -0
- package/elizaos/basic_capabilities/actions/reply.py +120 -0
- package/elizaos/basic_capabilities/providers/__init__.py +54 -0
- package/elizaos/basic_capabilities/providers/action_state.py +113 -0
- package/elizaos/basic_capabilities/providers/actions.py +263 -0
- package/elizaos/basic_capabilities/providers/attachments.py +76 -0
- package/elizaos/basic_capabilities/providers/capabilities.py +62 -0
- package/elizaos/basic_capabilities/providers/character.py +113 -0
- package/elizaos/basic_capabilities/providers/choice.py +73 -0
- package/elizaos/basic_capabilities/providers/context_bench.py +44 -0
- package/elizaos/basic_capabilities/providers/current_time.py +58 -0
- package/elizaos/basic_capabilities/providers/entities.py +99 -0
- package/elizaos/basic_capabilities/providers/evaluators.py +54 -0
- package/elizaos/basic_capabilities/providers/providers_list.py +55 -0
- package/elizaos/basic_capabilities/providers/recent_messages.py +85 -0
- package/elizaos/basic_capabilities/providers/time.py +45 -0
- package/elizaos/basic_capabilities/providers/world.py +93 -0
- package/elizaos/basic_capabilities/services/__init__.py +18 -0
- package/elizaos/basic_capabilities/services/embedding.py +122 -0
- package/elizaos/basic_capabilities/services/task.py +178 -0
- package/elizaos/bootstrap/__init__.py +12 -0
- package/elizaos/bootstrap/actions/__init__.py +68 -0
- package/elizaos/bootstrap/actions/add_contact.py +149 -0
- package/elizaos/bootstrap/actions/choice.py +147 -0
- package/elizaos/bootstrap/actions/follow_room.py +151 -0
- package/elizaos/bootstrap/actions/ignore.py +80 -0
- package/elizaos/bootstrap/actions/image_generation.py +135 -0
- package/elizaos/bootstrap/actions/mute_room.py +151 -0
- package/elizaos/bootstrap/actions/none.py +71 -0
- package/elizaos/bootstrap/actions/remove_contact.py +159 -0
- package/elizaos/bootstrap/actions/reply.py +140 -0
- package/elizaos/bootstrap/actions/roles.py +193 -0
- package/elizaos/bootstrap/actions/schedule_follow_up.py +164 -0
- package/elizaos/bootstrap/actions/search_contacts.py +159 -0
- package/elizaos/bootstrap/actions/send_message.py +173 -0
- package/elizaos/bootstrap/actions/settings.py +165 -0
- package/elizaos/bootstrap/actions/unfollow_room.py +151 -0
- package/elizaos/bootstrap/actions/unmute_room.py +151 -0
- package/elizaos/bootstrap/actions/update_contact.py +178 -0
- package/elizaos/bootstrap/actions/update_entity.py +175 -0
- package/elizaos/bootstrap/autonomy/__init__.py +18 -0
- package/elizaos/bootstrap/autonomy/action.py +197 -0
- package/elizaos/bootstrap/autonomy/providers.py +165 -0
- package/elizaos/bootstrap/autonomy/routes.py +171 -0
- package/elizaos/bootstrap/autonomy/service.py +562 -0
- package/elizaos/bootstrap/autonomy/types.py +18 -0
- package/elizaos/bootstrap/evaluators/__init__.py +19 -0
- package/elizaos/bootstrap/evaluators/reflection.py +118 -0
- package/elizaos/bootstrap/evaluators/relationship_extraction.py +192 -0
- package/elizaos/bootstrap/plugin.py +140 -0
- package/elizaos/bootstrap/providers/__init__.py +80 -0
- package/elizaos/bootstrap/providers/action_state.py +71 -0
- package/elizaos/bootstrap/providers/actions.py +256 -0
- package/elizaos/bootstrap/providers/agent_settings.py +63 -0
- package/elizaos/bootstrap/providers/attachments.py +76 -0
- package/elizaos/bootstrap/providers/capabilities.py +66 -0
- package/elizaos/bootstrap/providers/character.py +128 -0
- package/elizaos/bootstrap/providers/choice.py +77 -0
- package/elizaos/bootstrap/providers/contacts.py +78 -0
- package/elizaos/bootstrap/providers/context_bench.py +49 -0
- package/elizaos/bootstrap/providers/current_time.py +56 -0
- package/elizaos/bootstrap/providers/entities.py +99 -0
- package/elizaos/bootstrap/providers/evaluators.py +58 -0
- package/elizaos/bootstrap/providers/facts.py +86 -0
- package/elizaos/bootstrap/providers/follow_ups.py +116 -0
- package/elizaos/bootstrap/providers/knowledge.py +73 -0
- package/elizaos/bootstrap/providers/providers_list.py +59 -0
- package/elizaos/bootstrap/providers/recent_messages.py +85 -0
- package/elizaos/bootstrap/providers/relationships.py +106 -0
- package/elizaos/bootstrap/providers/roles.py +95 -0
- package/elizaos/bootstrap/providers/settings.py +55 -0
- package/elizaos/bootstrap/providers/time.py +45 -0
- package/elizaos/bootstrap/providers/world.py +97 -0
- package/elizaos/bootstrap/services/__init__.py +26 -0
- package/elizaos/bootstrap/services/embedding.py +122 -0
- package/elizaos/bootstrap/services/follow_up.py +138 -0
- package/elizaos/bootstrap/services/rolodex.py +244 -0
- package/elizaos/bootstrap/services/task.py +585 -0
- package/elizaos/bootstrap/types.py +54 -0
- package/elizaos/bootstrap/utils/__init__.py +7 -0
- package/elizaos/bootstrap/utils/xml.py +69 -0
- package/elizaos/character.py +149 -0
- package/elizaos/logger.py +179 -0
- package/elizaos/media/__init__.py +45 -0
- package/elizaos/media/mime.py +315 -0
- package/elizaos/media/search.py +161 -0
- package/elizaos/media/tests/__init__.py +1 -0
- package/elizaos/media/tests/test_mime.py +117 -0
- package/elizaos/media/tests/test_search.py +156 -0
- package/elizaos/plugin.py +191 -0
- package/elizaos/prompts.py +1071 -0
- package/elizaos/py.typed +0 -0
- package/elizaos/runtime.py +2572 -0
- package/elizaos/services/__init__.py +49 -0
- package/elizaos/services/hook_service.py +511 -0
- package/elizaos/services/message_service.py +1248 -0
- package/elizaos/settings.py +182 -0
- package/elizaos/streaming_context.py +159 -0
- package/elizaos/trajectory_context.py +18 -0
- package/elizaos/types/__init__.py +512 -0
- package/elizaos/types/agent.py +31 -0
- package/elizaos/types/components.py +208 -0
- package/elizaos/types/database.py +64 -0
- package/elizaos/types/environment.py +46 -0
- package/elizaos/types/events.py +47 -0
- package/elizaos/types/memory.py +45 -0
- package/elizaos/types/model.py +393 -0
- package/elizaos/types/plugin.py +188 -0
- package/elizaos/types/primitives.py +100 -0
- package/elizaos/types/runtime.py +460 -0
- package/elizaos/types/service.py +113 -0
- package/elizaos/types/service_interfaces.py +244 -0
- package/elizaos/types/state.py +188 -0
- package/elizaos/types/task.py +29 -0
- package/elizaos/utils/__init__.py +108 -0
- package/elizaos/utils/spec_examples.py +48 -0
- package/elizaos/utils/streaming.py +426 -0
- package/elizaos_atropos_shared/__init__.py +1 -0
- package/elizaos_atropos_shared/canonical_eliza.py +282 -0
- package/package.json +19 -0
- package/pyproject.toml +143 -0
- package/requirements-dev.in +11 -0
- package/requirements-dev.lock +134 -0
- package/requirements.in +9 -0
- package/requirements.lock +64 -0
- package/tests/__init__.py +0 -0
- package/tests/test_action_parameters.py +154 -0
- package/tests/test_actions_provider_examples.py +39 -0
- package/tests/test_advanced_memory_behavior.py +96 -0
- package/tests/test_advanced_memory_flag.py +30 -0
- package/tests/test_advanced_planning_behavior.py +225 -0
- package/tests/test_advanced_planning_flag.py +26 -0
- package/tests/test_autonomy.py +445 -0
- package/tests/test_bootstrap_initialize.py +37 -0
- package/tests/test_character.py +163 -0
- package/tests/test_character_provider.py +231 -0
- package/tests/test_dynamic_prompt_exec.py +561 -0
- package/tests/test_logger_redaction.py +43 -0
- package/tests/test_plugin.py +117 -0
- package/tests/test_runtime.py +422 -0
- package/tests/test_salt_production_enforcement.py +22 -0
- package/tests/test_settings_crypto.py +118 -0
- package/tests/test_streaming.py +295 -0
- package/tests/test_types.py +221 -0
- 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
|
+
]
|