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.
Files changed (49) hide show
  1. by_framework/client/client.py +15 -7
  2. by_framework/core/__init__.py +69 -0
  3. by_framework/core/availability.py +495 -0
  4. by_framework/core/delivery_gate.py +60 -0
  5. by_framework/core/discovery.py +359 -0
  6. by_framework/core/extensions/__init__.py +29 -0
  7. by_framework/core/extensions/agent_config.py +64 -0
  8. by_framework/core/extensions/plugin.py +312 -0
  9. by_framework/core/extensions/registry.py +704 -0
  10. by_framework/core/extensions/trace_provider.py +20 -0
  11. by_framework/core/protocol/__init__.py +99 -0
  12. by_framework/core/protocol/action_type.py +33 -0
  13. by_framework/core/protocol/agent_state.py +78 -0
  14. by_framework/core/protocol/byai_codec.py +96 -0
  15. by_framework/core/protocol/byai_command.py +53 -0
  16. by_framework/core/protocol/byai_types.py +7 -0
  17. by_framework/core/protocol/commands.py +285 -0
  18. by_framework/core/protocol/content_codec.py +17 -0
  19. by_framework/core/protocol/content_type.py +38 -0
  20. by_framework/core/protocol/data_message.py +45 -0
  21. by_framework/core/protocol/data_shapes.py +83 -0
  22. by_framework/core/protocol/event_type.py +34 -0
  23. by_framework/core/protocol/events.py +69 -0
  24. by_framework/core/protocol/message.py +99 -0
  25. by_framework/core/protocol/message_header.py +78 -0
  26. by_framework/core/protocol/responses.py +94 -0
  27. by_framework/core/protocol/results.py +149 -0
  28. by_framework/core/registry.py +1102 -0
  29. by_framework/core/runtime/__init__.py +27 -0
  30. by_framework/core/runtime/agent_config_manager.py +283 -0
  31. by_framework/core/runtime/agent_runtime_state.py +75 -0
  32. by_framework/core/runtime/file_manager.py +434 -0
  33. by_framework/core/runtime/file_paths.py +76 -0
  34. by_framework/core/runtime/file_permissions.py +71 -0
  35. by_framework/core/runtime/filestore/__init__.py +15 -0
  36. by_framework/core/runtime/filestore/base.py +140 -0
  37. by_framework/core/runtime/filestore/local.py +313 -0
  38. by_framework/core/runtime/history/__init__.py +10 -0
  39. by_framework/core/runtime/history/base.py +57 -0
  40. by_framework/core/runtime/history/history_manager.py +55 -0
  41. by_framework/core/runtime/history/in_memory.py +58 -0
  42. by_framework/core/runtime/session_manager.py +118 -0
  43. by_framework/core/wakeup_controller.py +149 -0
  44. by_framework/core/workspace.py +126 -0
  45. {by_framework-0.2.2.dev2.dist-info → by_framework-0.2.2.dev4.dist-info}/METADATA +1 -1
  46. by_framework-0.2.2.dev4.dist-info/RECORD +92 -0
  47. by_framework-0.2.2.dev2.dist-info/RECORD +0 -49
  48. {by_framework-0.2.2.dev2.dist-info → by_framework-0.2.2.dev4.dist-info}/WHEEL +0 -0
  49. {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
+ )