by-framework 0.2.2.dev2__py3-none-any.whl → 0.2.2.dev4__py3-none-any.whl
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.
- by_framework/client/client.py +15 -7
- by_framework/core/__init__.py +69 -0
- by_framework/core/availability.py +495 -0
- by_framework/core/delivery_gate.py +60 -0
- by_framework/core/discovery.py +359 -0
- by_framework/core/extensions/__init__.py +29 -0
- by_framework/core/extensions/agent_config.py +64 -0
- by_framework/core/extensions/plugin.py +312 -0
- by_framework/core/extensions/registry.py +704 -0
- by_framework/core/extensions/trace_provider.py +20 -0
- by_framework/core/protocol/__init__.py +99 -0
- by_framework/core/protocol/action_type.py +33 -0
- by_framework/core/protocol/agent_state.py +78 -0
- by_framework/core/protocol/byai_codec.py +96 -0
- by_framework/core/protocol/byai_command.py +53 -0
- by_framework/core/protocol/byai_types.py +7 -0
- by_framework/core/protocol/commands.py +285 -0
- by_framework/core/protocol/content_codec.py +17 -0
- by_framework/core/protocol/content_type.py +38 -0
- by_framework/core/protocol/data_message.py +45 -0
- by_framework/core/protocol/data_shapes.py +83 -0
- by_framework/core/protocol/event_type.py +34 -0
- by_framework/core/protocol/events.py +69 -0
- by_framework/core/protocol/message.py +99 -0
- by_framework/core/protocol/message_header.py +78 -0
- by_framework/core/protocol/responses.py +94 -0
- by_framework/core/protocol/results.py +149 -0
- by_framework/core/registry.py +1102 -0
- by_framework/core/runtime/__init__.py +27 -0
- by_framework/core/runtime/agent_config_manager.py +283 -0
- by_framework/core/runtime/agent_runtime_state.py +75 -0
- by_framework/core/runtime/file_manager.py +434 -0
- by_framework/core/runtime/file_paths.py +76 -0
- by_framework/core/runtime/file_permissions.py +71 -0
- by_framework/core/runtime/filestore/__init__.py +15 -0
- by_framework/core/runtime/filestore/base.py +140 -0
- by_framework/core/runtime/filestore/local.py +313 -0
- by_framework/core/runtime/history/__init__.py +10 -0
- by_framework/core/runtime/history/base.py +57 -0
- by_framework/core/runtime/history/history_manager.py +55 -0
- by_framework/core/runtime/history/in_memory.py +58 -0
- by_framework/core/runtime/session_manager.py +118 -0
- by_framework/core/wakeup_controller.py +149 -0
- by_framework/core/workspace.py +126 -0
- {by_framework-0.2.2.dev2.dist-info → by_framework-0.2.2.dev4.dist-info}/METADATA +1 -1
- by_framework-0.2.2.dev4.dist-info/RECORD +92 -0
- by_framework-0.2.2.dev2.dist-info/RECORD +0 -49
- {by_framework-0.2.2.dev2.dist-info → by_framework-0.2.2.dev4.dist-info}/WHEEL +0 -0
- {by_framework-0.2.2.dev2.dist-info → by_framework-0.2.2.dev4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plugin registry module.
|
|
3
|
+
|
|
4
|
+
Provides the PluginRegistry class for plugin discovery, registration,
|
|
5
|
+
and lifecycle management in Gateway Workers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import importlib.util
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Tuple
|
|
16
|
+
|
|
17
|
+
import dill
|
|
18
|
+
|
|
19
|
+
from by_framework.common.logger import logger
|
|
20
|
+
|
|
21
|
+
from .agent_config import AgentConfig
|
|
22
|
+
from .plugin import (AgentConfigsSnapshot, Plugin, PluginBuildContext,
|
|
23
|
+
PluginReloadContext, PluginReloadResult)
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from by_framework.core.protocol.commands import (AskAgentCommand,
|
|
27
|
+
CancelTaskCommand,
|
|
28
|
+
ResumeCommand)
|
|
29
|
+
from by_framework.worker.context import AgentContext
|
|
30
|
+
from by_framework.worker.worker import GatewayWorker
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PluginRegistry:
|
|
34
|
+
"""Plugin registry for discovery, registration, and lifecycle management.
|
|
35
|
+
|
|
36
|
+
Plugins are the extension mechanism for Gateway Worker. Each plugin
|
|
37
|
+
registers AgentConfig and can implement lifecycle hooks.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
self.plugins: List[Plugin] = []
|
|
42
|
+
self.log_hook_stats_on_shutdown: bool = True
|
|
43
|
+
self._agent_configs: List[AgentConfig] = []
|
|
44
|
+
self._agent_configs_version: int = 0
|
|
45
|
+
self._initialized_plugins: set[int] = set()
|
|
46
|
+
self.hook_stats: Dict[str, Dict[str, Dict[str, Any]]] = {}
|
|
47
|
+
self._processed_reload_ids: set[str] = set()
|
|
48
|
+
self._reload_status: Dict[str, Dict[str, Any]] = {}
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def agent_configs(self) -> List[AgentConfig]:
|
|
52
|
+
return self._agent_configs
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def agent_configs_version(self) -> int:
|
|
56
|
+
return self._agent_configs_version
|
|
57
|
+
|
|
58
|
+
def get_agent_configs_snapshot(self) -> AgentConfigsSnapshot:
|
|
59
|
+
"""Return an immutable snapshot of the current stable config version."""
|
|
60
|
+
return AgentConfigsSnapshot(
|
|
61
|
+
version=self._agent_configs_version,
|
|
62
|
+
configs=tuple(self._agent_configs),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def serialize_agent_configs_snapshot(snapshot: AgentConfigsSnapshot) -> bytes:
|
|
67
|
+
"""Serialize a snapshot for durable restart recovery."""
|
|
68
|
+
return dill.dumps(snapshot)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def deserialize_agent_configs_snapshot(payload: bytes) -> AgentConfigsSnapshot:
|
|
72
|
+
"""Restore a serialized snapshot payload."""
|
|
73
|
+
restored = dill.loads(payload)
|
|
74
|
+
if not isinstance(restored, AgentConfigsSnapshot):
|
|
75
|
+
raise TypeError(
|
|
76
|
+
"Persisted agent configs snapshot payload did not contain "
|
|
77
|
+
"an AgentConfigsSnapshot"
|
|
78
|
+
)
|
|
79
|
+
return restored
|
|
80
|
+
|
|
81
|
+
def get_reload_status(self, reload_id: str) -> Dict[str, Any] | None:
|
|
82
|
+
"""Return the recorded status for a reload id, if present."""
|
|
83
|
+
status = self._reload_status.get(reload_id)
|
|
84
|
+
return dict(status) if status is not None else None
|
|
85
|
+
|
|
86
|
+
def agent_config(self, agent_id: str) -> AgentConfig | None:
|
|
87
|
+
return next(
|
|
88
|
+
filter(lambda config: config.agent_id == agent_id, self._agent_configs),
|
|
89
|
+
None,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def register_bundle(self, plugin: Plugin) -> None:
|
|
93
|
+
if plugin not in self.plugins:
|
|
94
|
+
if any(existing.name == plugin.name for existing in self.plugins):
|
|
95
|
+
logger.warning("Duplicate plugin name detected: %s", plugin.name)
|
|
96
|
+
self.plugins.append(plugin)
|
|
97
|
+
|
|
98
|
+
def register_bundles(self, plugins: List[Plugin]) -> None:
|
|
99
|
+
for plugin in plugins:
|
|
100
|
+
self.register_bundle(plugin)
|
|
101
|
+
|
|
102
|
+
def get_active_plugins(self) -> List[Plugin]:
|
|
103
|
+
active_plugins = [
|
|
104
|
+
plugin
|
|
105
|
+
for plugin in self.plugins
|
|
106
|
+
if getattr(plugin.manifest, "enabled", True)
|
|
107
|
+
]
|
|
108
|
+
return sorted(
|
|
109
|
+
active_plugins,
|
|
110
|
+
key=lambda plugin: (-getattr(plugin.manifest, "priority", 0), plugin.name),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def get_plugin(self, plugin_id: str) -> Plugin | None:
|
|
114
|
+
return next(filter(lambda p: p.plugin_id == plugin_id, self.plugins), None)
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _extract_context_ids(context: Any) -> Tuple[str, str]:
|
|
118
|
+
session_id = getattr(context, "session_id", "") if context is not None else ""
|
|
119
|
+
trace_id = getattr(context, "trace_id", "") if context is not None else ""
|
|
120
|
+
return session_id, trace_id
|
|
121
|
+
|
|
122
|
+
def _ensure_hook_stats(self, plugin_name: str, hook_name: str) -> Dict[str, Any]:
|
|
123
|
+
plugin_stats = self.hook_stats.setdefault(plugin_name, {})
|
|
124
|
+
return plugin_stats.setdefault(
|
|
125
|
+
hook_name,
|
|
126
|
+
{
|
|
127
|
+
"success": 0,
|
|
128
|
+
"failure": 0,
|
|
129
|
+
"timeout": 0,
|
|
130
|
+
"total_ms": 0.0,
|
|
131
|
+
"last_error": "",
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def _execute_hook(
|
|
136
|
+
self,
|
|
137
|
+
plugin: Plugin,
|
|
138
|
+
hook_name: str,
|
|
139
|
+
coro: Any,
|
|
140
|
+
session_id: str = "",
|
|
141
|
+
trace_id: str = "",
|
|
142
|
+
worker_id: str = "",
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Execute plugin hooks with timeout and error handling."""
|
|
145
|
+
|
|
146
|
+
stat = self._ensure_hook_stats(plugin.name, hook_name)
|
|
147
|
+
started_at = time.perf_counter()
|
|
148
|
+
timeout_seconds = getattr(plugin, "hook_timeout_seconds", None)
|
|
149
|
+
timeout_seconds = (
|
|
150
|
+
timeout_seconds if (timeout_seconds and timeout_seconds > 0) else None
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
logger.info(
|
|
154
|
+
"Executing plugin hook: plugin=%s hook=%s timeout=%s "
|
|
155
|
+
"worker_id=%s session_id=%s trace_id=%s",
|
|
156
|
+
plugin.name,
|
|
157
|
+
hook_name,
|
|
158
|
+
timeout_seconds,
|
|
159
|
+
worker_id,
|
|
160
|
+
session_id,
|
|
161
|
+
trace_id,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
if timeout_seconds:
|
|
166
|
+
await asyncio.wait_for(coro, timeout=timeout_seconds)
|
|
167
|
+
else:
|
|
168
|
+
await coro
|
|
169
|
+
elapsed_ms = (time.perf_counter() - started_at) * 1000
|
|
170
|
+
stat["success"] += 1
|
|
171
|
+
stat["total_ms"] += elapsed_ms
|
|
172
|
+
logger.info(
|
|
173
|
+
"Plugin hook completed: plugin=%s hook=%s elapsed_ms=%.2f",
|
|
174
|
+
plugin.name,
|
|
175
|
+
hook_name,
|
|
176
|
+
elapsed_ms,
|
|
177
|
+
)
|
|
178
|
+
except asyncio.TimeoutError as e:
|
|
179
|
+
stat["failure"] += 1
|
|
180
|
+
stat["timeout"] += 1
|
|
181
|
+
stat["last_error"] = str(e)
|
|
182
|
+
logger.exception(
|
|
183
|
+
"Plugin %s %s timed out (timeout=%ss, worker_id=%s, "
|
|
184
|
+
"session_id=%s, trace_id=%s)",
|
|
185
|
+
plugin.name,
|
|
186
|
+
hook_name,
|
|
187
|
+
timeout_seconds,
|
|
188
|
+
worker_id,
|
|
189
|
+
session_id,
|
|
190
|
+
trace_id,
|
|
191
|
+
)
|
|
192
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
193
|
+
stat["failure"] += 1
|
|
194
|
+
stat["last_error"] = str(e)
|
|
195
|
+
logger.exception(
|
|
196
|
+
"Plugin %s %s failed: %s (worker_id=%s, session_id=%s, trace_id=%s)",
|
|
197
|
+
plugin.name,
|
|
198
|
+
hook_name,
|
|
199
|
+
e,
|
|
200
|
+
worker_id,
|
|
201
|
+
session_id,
|
|
202
|
+
trace_id,
|
|
203
|
+
)
|
|
204
|
+
stat["total_ms"] += (time.perf_counter() - started_at) * 1000
|
|
205
|
+
|
|
206
|
+
def get_hook_stats(self) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
|
207
|
+
"""Get execution statistics for all plugin hooks.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Dictionary containing statistics for each plugin's each hook
|
|
211
|
+
"""
|
|
212
|
+
snapshot: Dict[str, Dict[str, Dict[str, Any]]] = {}
|
|
213
|
+
for plugin_name, plugin_stats in self.hook_stats.items():
|
|
214
|
+
snapshot[plugin_name] = {}
|
|
215
|
+
for hook_name, stat in plugin_stats.items():
|
|
216
|
+
total_runs = stat["success"] + stat["failure"]
|
|
217
|
+
avg_ms = stat["total_ms"] / total_runs if total_runs > 0 else 0.0
|
|
218
|
+
snapshot[plugin_name][hook_name] = {
|
|
219
|
+
**stat,
|
|
220
|
+
"avg_ms": avg_ms,
|
|
221
|
+
"total_runs": total_runs,
|
|
222
|
+
}
|
|
223
|
+
return snapshot
|
|
224
|
+
|
|
225
|
+
def log_hook_stats(self) -> None:
|
|
226
|
+
stats = self.get_hook_stats()
|
|
227
|
+
if not stats:
|
|
228
|
+
logger.info("Plugin hook stats: no data")
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
for plugin_name, plugin_stats in stats.items():
|
|
232
|
+
for hook_name, stat in plugin_stats.items():
|
|
233
|
+
logger.info(
|
|
234
|
+
"Plugin hook stats: plugin=%s hook=%s total_runs=%s "
|
|
235
|
+
"success=%s failure=%s timeout=%s avg_ms=%.2f last_error=%s",
|
|
236
|
+
plugin_name,
|
|
237
|
+
hook_name,
|
|
238
|
+
stat.get("total_runs", 0),
|
|
239
|
+
stat.get("success", 0),
|
|
240
|
+
stat.get("failure", 0),
|
|
241
|
+
stat.get("timeout", 0),
|
|
242
|
+
stat.get("avg_ms", 0.0),
|
|
243
|
+
stat.get("last_error", ""),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def reset_hook_stats(self, plugin_name: str = "", hook_name: str = "") -> None:
|
|
247
|
+
"""Reset hook statistics for a plugin or specific hook."""
|
|
248
|
+
if not plugin_name:
|
|
249
|
+
self.hook_stats.clear()
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
plugin_stats = self.hook_stats.get(plugin_name)
|
|
253
|
+
if not plugin_stats:
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
if not hook_name:
|
|
257
|
+
self.hook_stats.pop(plugin_name, None)
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
plugin_stats.pop(hook_name, None)
|
|
261
|
+
if not plugin_stats:
|
|
262
|
+
self.hook_stats.pop(plugin_name, None)
|
|
263
|
+
|
|
264
|
+
def _validate_agent_config(self, config: AgentConfig) -> None:
|
|
265
|
+
if not config.agent_id:
|
|
266
|
+
raise ValueError("AgentConfig.agent_id must not be empty")
|
|
267
|
+
|
|
268
|
+
def _validate_agent_config_list(self, configs: List[AgentConfig]) -> None:
|
|
269
|
+
for config in configs:
|
|
270
|
+
self._validate_agent_config(config)
|
|
271
|
+
|
|
272
|
+
def _set_agent_configs(self, configs: List[AgentConfig]) -> None:
|
|
273
|
+
self._validate_agent_config_list(configs)
|
|
274
|
+
self._agent_configs = list(configs)
|
|
275
|
+
self._agent_configs_version += 1
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def _extract_reload_configs(
|
|
279
|
+
result: List[AgentConfig] | PluginReloadResult | None,
|
|
280
|
+
fallback: List[AgentConfig],
|
|
281
|
+
) -> List[AgentConfig]:
|
|
282
|
+
if result is None:
|
|
283
|
+
return list(fallback)
|
|
284
|
+
if isinstance(result, PluginReloadResult):
|
|
285
|
+
return list(result.agent_configs)
|
|
286
|
+
return list(result)
|
|
287
|
+
|
|
288
|
+
async def _replay_reload_chain(
|
|
289
|
+
self,
|
|
290
|
+
*,
|
|
291
|
+
base_snapshot: AgentConfigsSnapshot,
|
|
292
|
+
reload_id: str,
|
|
293
|
+
reason: str,
|
|
294
|
+
) -> List[AgentConfig]:
|
|
295
|
+
"""Replay plugin reload hooks in order to build the next config version."""
|
|
296
|
+
stable_configs = base_snapshot.configs
|
|
297
|
+
stable_version = base_snapshot.version
|
|
298
|
+
working_configs = list(stable_configs)
|
|
299
|
+
|
|
300
|
+
for plugin in self.get_active_plugins():
|
|
301
|
+
reload_context = PluginReloadContext(
|
|
302
|
+
plugin_id=plugin.plugin_id,
|
|
303
|
+
reload_id=reload_id,
|
|
304
|
+
reason=reason,
|
|
305
|
+
current_agent_configs=tuple(working_configs),
|
|
306
|
+
previous_stable_agent_configs=stable_configs,
|
|
307
|
+
current_version=stable_version,
|
|
308
|
+
)
|
|
309
|
+
result = await plugin.reload(reload_context)
|
|
310
|
+
working_configs = self._extract_reload_configs(result, working_configs)
|
|
311
|
+
self._validate_agent_config_list(working_configs)
|
|
312
|
+
|
|
313
|
+
return working_configs
|
|
314
|
+
|
|
315
|
+
def _record_reload_success(
|
|
316
|
+
self,
|
|
317
|
+
*,
|
|
318
|
+
reload_id: str,
|
|
319
|
+
reason: str,
|
|
320
|
+
version_before: int,
|
|
321
|
+
version_after: int,
|
|
322
|
+
) -> None:
|
|
323
|
+
self._processed_reload_ids.add(reload_id)
|
|
324
|
+
self._reload_status[reload_id] = {
|
|
325
|
+
"status": "success",
|
|
326
|
+
"reason": reason,
|
|
327
|
+
"version_before": version_before,
|
|
328
|
+
"version_after": version_after,
|
|
329
|
+
"error": "",
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
def _record_reload_failure(
|
|
333
|
+
self,
|
|
334
|
+
*,
|
|
335
|
+
reload_id: str,
|
|
336
|
+
reason: str,
|
|
337
|
+
version_before: int,
|
|
338
|
+
version_after: int,
|
|
339
|
+
error: Exception,
|
|
340
|
+
) -> None:
|
|
341
|
+
self._processed_reload_ids.add(reload_id)
|
|
342
|
+
self._reload_status[reload_id] = {
|
|
343
|
+
"status": "failure",
|
|
344
|
+
"reason": reason,
|
|
345
|
+
"version_before": version_before,
|
|
346
|
+
"version_after": version_after,
|
|
347
|
+
"error": str(error),
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async def _register_plugin_agent_configs(
|
|
351
|
+
self, plugin: Plugin, build_context: PluginBuildContext
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Register agent configs from a plugin into the registry."""
|
|
354
|
+
# Provide the plugin with a read-only snapshot of the previous version
|
|
355
|
+
build_context.freeze_prev_agent_configs()
|
|
356
|
+
new_configs = await plugin.register_agent_configs(build_context)
|
|
357
|
+
if new_configs is not None:
|
|
358
|
+
build_context.set_agent_configs(new_configs)
|
|
359
|
+
|
|
360
|
+
configs = build_context.list_agent_configs()
|
|
361
|
+
if not configs:
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
staged_configs = list(self._agent_configs)
|
|
365
|
+
for config in configs:
|
|
366
|
+
self._validate_agent_config(config)
|
|
367
|
+
agent_id = config.agent_id
|
|
368
|
+
existing = next(
|
|
369
|
+
filter(
|
|
370
|
+
lambda item, target=agent_id: item.agent_id == target,
|
|
371
|
+
staged_configs,
|
|
372
|
+
),
|
|
373
|
+
None,
|
|
374
|
+
)
|
|
375
|
+
if existing:
|
|
376
|
+
if existing is config:
|
|
377
|
+
continue
|
|
378
|
+
if config.on_conflict == "error":
|
|
379
|
+
raise ValueError(f"agent_config '{agent_id}' is already registered")
|
|
380
|
+
if config.on_conflict == "skip":
|
|
381
|
+
logger.warning(
|
|
382
|
+
"Skip duplicate agent_config registration: %s", agent_id
|
|
383
|
+
)
|
|
384
|
+
continue
|
|
385
|
+
logger.warning(
|
|
386
|
+
"Overwrite duplicate agent_config registration: %s", agent_id
|
|
387
|
+
)
|
|
388
|
+
staged_configs.remove(existing)
|
|
389
|
+
|
|
390
|
+
staged_configs.append(config)
|
|
391
|
+
|
|
392
|
+
self._agent_configs = staged_configs
|
|
393
|
+
|
|
394
|
+
async def discover_plugins(self) -> None:
|
|
395
|
+
"""Auto-discover registered plugin classes and instantiate for registration."""
|
|
396
|
+
for cls in Plugin.get_registered_plugins():
|
|
397
|
+
if any(isinstance(p, cls) for p in self.plugins):
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
plugin = cls()
|
|
402
|
+
self.register_bundle(plugin)
|
|
403
|
+
logger.info(
|
|
404
|
+
"Auto-discovered and registered plugin: %s", plugin.plugin_id
|
|
405
|
+
)
|
|
406
|
+
except TypeError as e:
|
|
407
|
+
logger.debug(
|
|
408
|
+
"Skip auto-instantiation for plugin class %s: %s", cls.__name__, e
|
|
409
|
+
)
|
|
410
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
411
|
+
logger.error(
|
|
412
|
+
"Failed to auto-instantiate plugin class %s: %s", cls.__name__, e
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def load_plugins_from_dir(self, directory: str) -> None:
|
|
416
|
+
"""Dynamically load plugin modules from the specified directory.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
directory: Directory path containing plugin Python files
|
|
420
|
+
"""
|
|
421
|
+
if not os.path.isdir(directory):
|
|
422
|
+
logger.warning(
|
|
423
|
+
"Plugin directory not found or not a directory: %s", directory
|
|
424
|
+
)
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
abs_dir = os.path.abspath(directory)
|
|
428
|
+
if abs_dir not in sys.path:
|
|
429
|
+
sys.path.insert(0, abs_dir)
|
|
430
|
+
|
|
431
|
+
logger.info("Scanning directory for plugins: %s", abs_dir)
|
|
432
|
+
|
|
433
|
+
for filename in os.listdir(abs_dir):
|
|
434
|
+
if filename.endswith(".py") and not filename.startswith("__"):
|
|
435
|
+
module_name = filename[:-3]
|
|
436
|
+
file_path = os.path.join(abs_dir, filename)
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
spec = importlib.util.spec_from_file_location(
|
|
440
|
+
module_name, file_path
|
|
441
|
+
)
|
|
442
|
+
if spec and spec.loader:
|
|
443
|
+
module = importlib.util.module_from_spec(spec)
|
|
444
|
+
spec.loader.exec_module(module)
|
|
445
|
+
logger.info("Dynamically loaded plugin module: %s", module_name)
|
|
446
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
447
|
+
logger.error(
|
|
448
|
+
"Failed to load plugin module from %s: %s", file_path, e
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
async def initialize_plugins(
|
|
452
|
+
self, build_context: PluginBuildContext | None = None
|
|
453
|
+
) -> None:
|
|
454
|
+
"""Initialize all discovered plugins.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
build_context: Optional build context
|
|
458
|
+
"""
|
|
459
|
+
previous_configs = list(self._agent_configs)
|
|
460
|
+
if build_context is None:
|
|
461
|
+
build_context = PluginBuildContext(agent_configs=list(self._agent_configs))
|
|
462
|
+
|
|
463
|
+
for plugin in self.get_active_plugins():
|
|
464
|
+
plugin_id = id(plugin)
|
|
465
|
+
if plugin_id in self._initialized_plugins:
|
|
466
|
+
continue
|
|
467
|
+
|
|
468
|
+
before_success = self._ensure_hook_stats(
|
|
469
|
+
plugin.name, "register_agent_configs"
|
|
470
|
+
)["success"]
|
|
471
|
+
await self._execute_hook(
|
|
472
|
+
plugin,
|
|
473
|
+
"register_agent_configs",
|
|
474
|
+
self._register_plugin_agent_configs(
|
|
475
|
+
plugin, build_context=build_context
|
|
476
|
+
),
|
|
477
|
+
)
|
|
478
|
+
after_success = self._ensure_hook_stats(
|
|
479
|
+
plugin.name, "register_agent_configs"
|
|
480
|
+
)["success"]
|
|
481
|
+
if after_success > before_success:
|
|
482
|
+
self._initialized_plugins.add(plugin_id)
|
|
483
|
+
|
|
484
|
+
if self._agent_configs != previous_configs:
|
|
485
|
+
self._agent_configs_version += 1
|
|
486
|
+
|
|
487
|
+
async def reload_plugins(
|
|
488
|
+
self,
|
|
489
|
+
reload_id: str,
|
|
490
|
+
reason: str = "",
|
|
491
|
+
) -> AgentConfigsSnapshot:
|
|
492
|
+
"""Sequentially replay plugin reload hooks over the current stable version.
|
|
493
|
+
|
|
494
|
+
Each plugin receives the current working AgentConfig list and may return
|
|
495
|
+
the next full version. The stable version is only replaced after the
|
|
496
|
+
whole pipeline succeeds.
|
|
497
|
+
"""
|
|
498
|
+
if reload_id in self._processed_reload_ids:
|
|
499
|
+
return self.get_agent_configs_snapshot()
|
|
500
|
+
|
|
501
|
+
base_snapshot = self.get_agent_configs_snapshot()
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
next_configs = await self._replay_reload_chain(
|
|
505
|
+
base_snapshot=base_snapshot,
|
|
506
|
+
reload_id=reload_id,
|
|
507
|
+
reason=reason,
|
|
508
|
+
)
|
|
509
|
+
self._set_agent_configs(next_configs)
|
|
510
|
+
next_snapshot = self.get_agent_configs_snapshot()
|
|
511
|
+
self._record_reload_success(
|
|
512
|
+
reload_id=reload_id,
|
|
513
|
+
reason=reason,
|
|
514
|
+
version_before=base_snapshot.version,
|
|
515
|
+
version_after=next_snapshot.version,
|
|
516
|
+
)
|
|
517
|
+
return next_snapshot
|
|
518
|
+
except Exception as error:
|
|
519
|
+
self._record_reload_failure(
|
|
520
|
+
reload_id=reload_id,
|
|
521
|
+
reason=reason,
|
|
522
|
+
version_before=base_snapshot.version,
|
|
523
|
+
version_after=self._agent_configs_version,
|
|
524
|
+
error=error,
|
|
525
|
+
)
|
|
526
|
+
raise
|
|
527
|
+
|
|
528
|
+
def apply_default_hook_timeout(self, timeout_seconds: float) -> None:
|
|
529
|
+
"""Set default hook timeout for all plugins that don't have timeout set.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
timeout_seconds: Default timeout in seconds
|
|
533
|
+
"""
|
|
534
|
+
if timeout_seconds <= 0:
|
|
535
|
+
return
|
|
536
|
+
for plugin in self.plugins:
|
|
537
|
+
if getattr(plugin, "hook_timeout_seconds", None) is None:
|
|
538
|
+
plugin.hook_timeout_seconds = timeout_seconds
|
|
539
|
+
|
|
540
|
+
async def on_worker_startup(self, worker: "GatewayWorker"):
|
|
541
|
+
await self.discover_plugins()
|
|
542
|
+
await self.initialize_plugins()
|
|
543
|
+
|
|
544
|
+
for plugin in self.get_active_plugins():
|
|
545
|
+
await self._execute_hook(
|
|
546
|
+
plugin,
|
|
547
|
+
"on_worker_startup",
|
|
548
|
+
plugin.on_worker_startup(worker),
|
|
549
|
+
worker_id=getattr(worker, "worker_id", ""),
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
async def on_worker_shutdown(self, worker: "GatewayWorker"):
|
|
553
|
+
for plugin in self.get_active_plugins():
|
|
554
|
+
await self._execute_hook(
|
|
555
|
+
plugin,
|
|
556
|
+
"on_worker_shutdown",
|
|
557
|
+
plugin.on_worker_shutdown(worker),
|
|
558
|
+
worker_id=getattr(worker, "worker_id", ""),
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
async def on_task_start(self, context: "AgentContext"):
|
|
562
|
+
session_id, trace_id = self._extract_context_ids(context)
|
|
563
|
+
for plugin in self.get_active_plugins():
|
|
564
|
+
await self._execute_hook(
|
|
565
|
+
plugin,
|
|
566
|
+
"on_task_start",
|
|
567
|
+
plugin.on_task_start(context),
|
|
568
|
+
session_id=session_id,
|
|
569
|
+
trace_id=trace_id,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
async def on_task_complete(self, context: "AgentContext", result: Any):
|
|
573
|
+
session_id, trace_id = self._extract_context_ids(context)
|
|
574
|
+
for plugin in self.get_active_plugins():
|
|
575
|
+
await self._execute_hook(
|
|
576
|
+
plugin,
|
|
577
|
+
"on_task_complete",
|
|
578
|
+
plugin.on_task_complete(context, result),
|
|
579
|
+
session_id=session_id,
|
|
580
|
+
trace_id=trace_id,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
async def on_task_error(self, context: "AgentContext", error: Exception):
|
|
584
|
+
session_id, trace_id = self._extract_context_ids(context)
|
|
585
|
+
for plugin in self.get_active_plugins():
|
|
586
|
+
await self._execute_hook(
|
|
587
|
+
plugin,
|
|
588
|
+
"on_task_error",
|
|
589
|
+
plugin.on_task_error(context, error),
|
|
590
|
+
session_id=session_id,
|
|
591
|
+
trace_id=trace_id,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
async def on_task_cancel(
|
|
595
|
+
self, context: "AgentContext", command: "CancelTaskCommand"
|
|
596
|
+
):
|
|
597
|
+
session_id, trace_id = self._extract_context_ids(context)
|
|
598
|
+
for plugin in self.get_active_plugins():
|
|
599
|
+
await self._execute_hook(
|
|
600
|
+
plugin,
|
|
601
|
+
"on_task_cancel",
|
|
602
|
+
plugin.on_task_cancel(context, command),
|
|
603
|
+
session_id=session_id,
|
|
604
|
+
trace_id=trace_id,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
async def on_call_agent_start(
|
|
608
|
+
self, context: "AgentContext", command: "AskAgentCommand"
|
|
609
|
+
) -> None:
|
|
610
|
+
session_id, trace_id = self._extract_context_ids(context)
|
|
611
|
+
for plugin in self.get_active_plugins():
|
|
612
|
+
await self._execute_hook(
|
|
613
|
+
plugin,
|
|
614
|
+
"on_call_agent_start",
|
|
615
|
+
plugin.on_call_agent_start(context, command),
|
|
616
|
+
session_id=session_id,
|
|
617
|
+
trace_id=trace_id,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
async def on_call_agent_complete(
|
|
621
|
+
self,
|
|
622
|
+
context: "AgentContext",
|
|
623
|
+
command: "AskAgentCommand",
|
|
624
|
+
result: Any,
|
|
625
|
+
) -> None:
|
|
626
|
+
session_id, trace_id = self._extract_context_ids(context)
|
|
627
|
+
for plugin in self.get_active_plugins():
|
|
628
|
+
await self._execute_hook(
|
|
629
|
+
plugin,
|
|
630
|
+
"on_call_agent_complete",
|
|
631
|
+
plugin.on_call_agent_complete(context, command, result),
|
|
632
|
+
session_id=session_id,
|
|
633
|
+
trace_id=trace_id,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
async def on_call_agent_error(
|
|
637
|
+
self,
|
|
638
|
+
context: "AgentContext",
|
|
639
|
+
command: "AskAgentCommand",
|
|
640
|
+
error: Exception,
|
|
641
|
+
) -> None:
|
|
642
|
+
session_id, trace_id = self._extract_context_ids(context)
|
|
643
|
+
for plugin in self.get_active_plugins():
|
|
644
|
+
await self._execute_hook(
|
|
645
|
+
plugin,
|
|
646
|
+
"on_call_agent_error",
|
|
647
|
+
plugin.on_call_agent_error(context, command, error),
|
|
648
|
+
session_id=session_id,
|
|
649
|
+
trace_id=trace_id,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
async def on_agent_return_start(
|
|
653
|
+
self,
|
|
654
|
+
context: "AgentContext",
|
|
655
|
+
command: "AskAgentCommand",
|
|
656
|
+
callback_command: "ResumeCommand",
|
|
657
|
+
) -> None:
|
|
658
|
+
session_id, trace_id = self._extract_context_ids(context)
|
|
659
|
+
for plugin in self.get_active_plugins():
|
|
660
|
+
await self._execute_hook(
|
|
661
|
+
plugin,
|
|
662
|
+
"on_agent_return_start",
|
|
663
|
+
plugin.on_agent_return_start(context, command, callback_command),
|
|
664
|
+
session_id=session_id,
|
|
665
|
+
trace_id=trace_id,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
async def on_agent_return_complete(
|
|
669
|
+
self,
|
|
670
|
+
context: "AgentContext",
|
|
671
|
+
command: "AskAgentCommand",
|
|
672
|
+
callback_command: "ResumeCommand",
|
|
673
|
+
) -> None:
|
|
674
|
+
session_id, trace_id = self._extract_context_ids(context)
|
|
675
|
+
for plugin in self.get_active_plugins():
|
|
676
|
+
await self._execute_hook(
|
|
677
|
+
plugin,
|
|
678
|
+
"on_agent_return_complete",
|
|
679
|
+
plugin.on_agent_return_complete(context, command, callback_command),
|
|
680
|
+
session_id=session_id,
|
|
681
|
+
trace_id=trace_id,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
async def on_agent_return_error(
|
|
685
|
+
self,
|
|
686
|
+
context: "AgentContext",
|
|
687
|
+
command: "AskAgentCommand",
|
|
688
|
+
callback_command: "ResumeCommand",
|
|
689
|
+
error: Exception,
|
|
690
|
+
) -> None:
|
|
691
|
+
session_id, trace_id = self._extract_context_ids(context)
|
|
692
|
+
for plugin in self.get_active_plugins():
|
|
693
|
+
await self._execute_hook(
|
|
694
|
+
plugin,
|
|
695
|
+
"on_agent_return_error",
|
|
696
|
+
plugin.on_agent_return_error(
|
|
697
|
+
context,
|
|
698
|
+
command,
|
|
699
|
+
callback_command,
|
|
700
|
+
error,
|
|
701
|
+
),
|
|
702
|
+
session_id=session_id,
|
|
703
|
+
trace_id=trace_id,
|
|
704
|
+
)
|