monoco-toolkit 0.3.10__py3-none-any.whl → 0.3.12__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.
- monoco/__main__.py +8 -0
- monoco/core/artifacts/__init__.py +16 -0
- monoco/core/artifacts/manager.py +575 -0
- monoco/core/artifacts/models.py +161 -0
- monoco/core/automation/__init__.py +51 -0
- monoco/core/automation/config.py +338 -0
- monoco/core/automation/field_watcher.py +296 -0
- monoco/core/automation/handlers.py +723 -0
- monoco/core/config.py +31 -4
- monoco/core/executor/__init__.py +38 -0
- monoco/core/executor/agent_action.py +254 -0
- monoco/core/executor/git_action.py +303 -0
- monoco/core/executor/im_action.py +309 -0
- monoco/core/executor/pytest_action.py +218 -0
- monoco/core/git.py +38 -0
- monoco/core/hooks/context.py +74 -13
- monoco/core/ingestion/__init__.py +20 -0
- monoco/core/ingestion/discovery.py +248 -0
- monoco/core/ingestion/watcher.py +343 -0
- monoco/core/ingestion/worker.py +436 -0
- monoco/core/loader.py +633 -0
- monoco/core/registry.py +34 -25
- monoco/core/router/__init__.py +55 -0
- monoco/core/router/action.py +341 -0
- monoco/core/router/router.py +392 -0
- monoco/core/scheduler/__init__.py +63 -0
- monoco/core/scheduler/base.py +152 -0
- monoco/core/scheduler/engines.py +175 -0
- monoco/core/scheduler/events.py +171 -0
- monoco/core/scheduler/local.py +377 -0
- monoco/core/skills.py +119 -80
- monoco/core/watcher/__init__.py +57 -0
- monoco/core/watcher/base.py +365 -0
- monoco/core/watcher/dropzone.py +152 -0
- monoco/core/watcher/issue.py +303 -0
- monoco/core/watcher/memo.py +200 -0
- monoco/core/watcher/task.py +238 -0
- monoco/daemon/app.py +77 -1
- monoco/daemon/commands.py +10 -0
- monoco/daemon/events.py +34 -0
- monoco/daemon/mailroom_service.py +196 -0
- monoco/daemon/models.py +1 -0
- monoco/daemon/scheduler.py +207 -0
- monoco/daemon/services.py +27 -58
- monoco/daemon/triggers.py +55 -0
- monoco/features/agent/__init__.py +25 -7
- monoco/features/agent/adapter.py +17 -7
- monoco/features/agent/cli.py +91 -57
- monoco/features/agent/engines.py +31 -170
- monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
- monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
- monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
- monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
- monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
- monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
- monoco/features/agent/worker.py +1 -1
- monoco/features/artifact/__init__.py +0 -0
- monoco/features/artifact/adapter.py +33 -0
- monoco/features/artifact/resources/zh/AGENTS.md +14 -0
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
- monoco/features/glossary/adapter.py +18 -7
- monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
- monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
- monoco/features/hooks/__init__.py +11 -0
- monoco/features/hooks/adapter.py +67 -0
- monoco/features/hooks/commands.py +309 -0
- monoco/features/hooks/core.py +441 -0
- monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
- monoco/features/i18n/adapter.py +18 -5
- monoco/features/i18n/core.py +482 -17
- monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
- monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
- monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
- monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
- monoco/features/issue/adapter.py +19 -6
- monoco/features/issue/commands.py +352 -20
- monoco/features/issue/core.py +475 -16
- monoco/features/issue/engine/machine.py +114 -4
- monoco/features/issue/linter.py +60 -5
- monoco/features/issue/models.py +2 -2
- monoco/features/issue/resources/en/AGENTS.md +109 -0
- monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
- monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
- monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
- monoco/features/issue/resources/hooks/pre-push.sh +35 -0
- monoco/features/issue/resources/zh/AGENTS.md +109 -0
- monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
- monoco/features/issue/validator.py +101 -1
- monoco/features/memo/adapter.py +21 -8
- monoco/features/memo/cli.py +103 -10
- monoco/features/memo/core.py +178 -92
- monoco/features/memo/models.py +53 -0
- monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
- monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
- monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
- monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
- monoco/features/spike/adapter.py +18 -5
- monoco/features/spike/commands.py +5 -3
- monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
- monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
- monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
- monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
- monoco/main.py +38 -1
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +7 -1
- monoco_toolkit-0.3.12.dist-info/RECORD +202 -0
- monoco/features/agent/apoptosis.py +0 -44
- monoco/features/agent/manager.py +0 -91
- monoco/features/agent/session.py +0 -121
- monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
- /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
- /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Engine Adapters for Monoco Scheduler.
|
|
3
|
+
|
|
4
|
+
This module provides a unified interface for different AI agent execution engines,
|
|
5
|
+
allowing the Worker to seamlessly switch between Gemini, Claude, and future engines.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EngineAdapter(ABC):
|
|
13
|
+
"""
|
|
14
|
+
Abstract base class for agent engine adapters.
|
|
15
|
+
|
|
16
|
+
Each adapter is responsible for:
|
|
17
|
+
1. Constructing the correct CLI command for its engine
|
|
18
|
+
2. Handling engine-specific error scenarios
|
|
19
|
+
3. Providing metadata about the engine's capabilities
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
24
|
+
"""
|
|
25
|
+
Build the CLI command to execute the agent with the given prompt.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
prompt: The instruction/context to send to the agent
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
List of command arguments (e.g., ["gemini", "-y", "prompt text"])
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def name(self) -> str:
|
|
38
|
+
"""Return the canonical name of this engine."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def supports_yolo_mode(self) -> bool:
|
|
43
|
+
"""Whether this engine supports auto-approval mode."""
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GeminiAdapter(EngineAdapter):
|
|
48
|
+
"""
|
|
49
|
+
Adapter for Google Gemini CLI.
|
|
50
|
+
|
|
51
|
+
Command format: gemini -p <prompt> -y
|
|
52
|
+
The -y flag enables "YOLO mode" (auto-approval of actions).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
56
|
+
# Based on Gemini CLI help: -p <prompt> for non-interactive
|
|
57
|
+
return ["gemini", "-p", prompt, "-y"]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def name(self) -> str:
|
|
61
|
+
return "gemini"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def supports_yolo_mode(self) -> bool:
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ClaudeAdapter(EngineAdapter):
|
|
69
|
+
"""
|
|
70
|
+
Adapter for Anthropic Claude CLI.
|
|
71
|
+
|
|
72
|
+
Command format: claude -p <prompt>
|
|
73
|
+
The -p/--print flag enables non-interactive mode.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
77
|
+
# Based on Claude CLI help: -p <prompt> is NOT standard, usually -p means print/non-interactive.
|
|
78
|
+
# But for one-shot execution, we do passing prompt as argument with -p flag.
|
|
79
|
+
return ["claude", "-p", prompt]
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def name(self) -> str:
|
|
83
|
+
return "claude"
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def supports_yolo_mode(self) -> bool:
|
|
87
|
+
# Claude uses -p for non-interactive mode, similar concept
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class QwenAdapter(EngineAdapter):
|
|
92
|
+
"""
|
|
93
|
+
Adapter for Qwen Code CLI.
|
|
94
|
+
|
|
95
|
+
Command format: qwen -p <prompt> -y
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
99
|
+
# Assuming Qwen follows similar patterns (based on user feedback)
|
|
100
|
+
return ["qwen", "-p", prompt, "-y"]
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def name(self) -> str:
|
|
104
|
+
return "qwen"
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def supports_yolo_mode(self) -> bool:
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class KimiAdapter(EngineAdapter):
|
|
112
|
+
"""
|
|
113
|
+
Adapter for Kimi CLI (Moonshot AI).
|
|
114
|
+
|
|
115
|
+
Command format: kimi -p <prompt> --print
|
|
116
|
+
Note: --print implicitly adds --yolo.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
120
|
+
# Based on Kimi CLI help: -p, --prompt TEXT.
|
|
121
|
+
# Also using --print for non-interactive mode (which enables yolo).
|
|
122
|
+
return ["kimi", "-p", prompt, "--print"]
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def name(self) -> str:
|
|
126
|
+
return "kimi"
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def supports_yolo_mode(self) -> bool:
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class EngineFactory:
|
|
134
|
+
"""
|
|
135
|
+
Factory for creating engine adapter instances.
|
|
136
|
+
|
|
137
|
+
Usage:
|
|
138
|
+
adapter = EngineFactory.create("gemini")
|
|
139
|
+
command = adapter.build_command("Write a test")
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
_adapters = {
|
|
143
|
+
"gemini": GeminiAdapter,
|
|
144
|
+
"claude": ClaudeAdapter,
|
|
145
|
+
"qwen": QwenAdapter,
|
|
146
|
+
"kimi": KimiAdapter,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def create(cls, engine_name: str) -> EngineAdapter:
|
|
151
|
+
"""
|
|
152
|
+
Create an adapter instance for the specified engine.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
engine_name: Name of the engine (e.g., "gemini", "claude")
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
An instance of the appropriate EngineAdapter
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
ValueError: If the engine is not supported
|
|
162
|
+
"""
|
|
163
|
+
adapter_class = cls._adapters.get(engine_name.lower())
|
|
164
|
+
if not adapter_class:
|
|
165
|
+
supported = ", ".join(cls._adapters.keys())
|
|
166
|
+
raise ValueError(
|
|
167
|
+
f"Unsupported engine: '{engine_name}'. "
|
|
168
|
+
f"Supported engines: {supported}"
|
|
169
|
+
)
|
|
170
|
+
return adapter_class()
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def supported_engines(cls) -> List[str]:
|
|
174
|
+
"""Return a list of all supported engine names."""
|
|
175
|
+
return list(cls._adapters.keys())
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EventBus - Central event system for Agent scheduling (FEAT-0155).
|
|
3
|
+
|
|
4
|
+
Provides async event publishing/subscription mechanism for decoupled
|
|
5
|
+
Agent lifecycle management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import inspect
|
|
10
|
+
import logging
|
|
11
|
+
from enum import Enum, auto
|
|
12
|
+
from typing import Dict, List, Callable, Any, Optional
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("monoco.core.scheduler.events")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentEventType(Enum):
|
|
20
|
+
"""Event types for Agent lifecycle and triggers."""
|
|
21
|
+
# Memo events
|
|
22
|
+
MEMO_CREATED = "memo.created"
|
|
23
|
+
MEMO_THRESHOLD = "memo.threshold"
|
|
24
|
+
|
|
25
|
+
# Issue events
|
|
26
|
+
ISSUE_CREATED = "issue.created"
|
|
27
|
+
ISSUE_UPDATED = "issue.updated"
|
|
28
|
+
ISSUE_STAGE_CHANGED = "issue.stage_changed"
|
|
29
|
+
ISSUE_STATUS_CHANGED = "issue.status_changed"
|
|
30
|
+
|
|
31
|
+
# Session events
|
|
32
|
+
SESSION_STARTED = "session.started"
|
|
33
|
+
SESSION_COMPLETED = "session.completed"
|
|
34
|
+
SESSION_FAILED = "session.failed"
|
|
35
|
+
SESSION_CRASHED = "session.crashed"
|
|
36
|
+
SESSION_TERMINATED = "session.terminated"
|
|
37
|
+
|
|
38
|
+
# PR events (for Reviewer trigger)
|
|
39
|
+
PR_CREATED = "pr.created"
|
|
40
|
+
PR_UPDATED = "pr.updated"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class AgentEvent:
|
|
45
|
+
"""Event data structure."""
|
|
46
|
+
type: AgentEventType
|
|
47
|
+
payload: Dict[str, Any]
|
|
48
|
+
timestamp: datetime = None
|
|
49
|
+
source: str = None
|
|
50
|
+
|
|
51
|
+
def __post_init__(self):
|
|
52
|
+
if self.timestamp is None:
|
|
53
|
+
self.timestamp = datetime.now()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
EventHandler = Callable[[AgentEvent], Any]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class EventBus:
|
|
60
|
+
"""
|
|
61
|
+
Central async event bus for Agent scheduling.
|
|
62
|
+
|
|
63
|
+
Supports:
|
|
64
|
+
- Subscribe/unsubscribe handlers for specific event types
|
|
65
|
+
- Publish events to all subscribed handlers
|
|
66
|
+
- Async handler execution
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self._handlers: Dict[AgentEventType, List[EventHandler]] = {
|
|
71
|
+
event_type: [] for event_type in AgentEventType
|
|
72
|
+
}
|
|
73
|
+
self._lock = asyncio.Lock()
|
|
74
|
+
self._event_queue: asyncio.Queue = asyncio.Queue()
|
|
75
|
+
self._dispatch_task: Optional[asyncio.Task] = None
|
|
76
|
+
self._running = False
|
|
77
|
+
|
|
78
|
+
async def start(self):
|
|
79
|
+
"""Start the event dispatch loop."""
|
|
80
|
+
if self._running:
|
|
81
|
+
return
|
|
82
|
+
self._running = True
|
|
83
|
+
self._dispatch_task = asyncio.create_task(self._dispatch_loop())
|
|
84
|
+
logger.info("EventBus started")
|
|
85
|
+
|
|
86
|
+
async def stop(self):
|
|
87
|
+
"""Stop the event dispatch loop."""
|
|
88
|
+
if not self._running:
|
|
89
|
+
return
|
|
90
|
+
self._running = False
|
|
91
|
+
if self._dispatch_task:
|
|
92
|
+
self._dispatch_task.cancel()
|
|
93
|
+
try:
|
|
94
|
+
await self._dispatch_task
|
|
95
|
+
except asyncio.CancelledError:
|
|
96
|
+
pass
|
|
97
|
+
logger.info("EventBus stopped")
|
|
98
|
+
|
|
99
|
+
async def _dispatch_loop(self):
|
|
100
|
+
"""Background loop to dispatch events."""
|
|
101
|
+
while self._running:
|
|
102
|
+
try:
|
|
103
|
+
event = await asyncio.wait_for(self._event_queue.get(), timeout=1.0)
|
|
104
|
+
await self._dispatch_event(event)
|
|
105
|
+
except asyncio.TimeoutError:
|
|
106
|
+
continue
|
|
107
|
+
except asyncio.CancelledError:
|
|
108
|
+
break
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Error dispatching event: {e}")
|
|
111
|
+
|
|
112
|
+
async def _dispatch_event(self, event: AgentEvent):
|
|
113
|
+
"""Dispatch event to all subscribed handlers."""
|
|
114
|
+
handlers = self._handlers.get(event.type, [])
|
|
115
|
+
if not handlers:
|
|
116
|
+
logger.debug(f"No handlers for event {event.type.value}")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
logger.debug(f"Dispatching {event.type.value} to {len(handlers)} handlers")
|
|
120
|
+
|
|
121
|
+
# Execute handlers concurrently
|
|
122
|
+
tasks = []
|
|
123
|
+
for handler in handlers:
|
|
124
|
+
try:
|
|
125
|
+
if inspect.iscoroutinefunction(handler):
|
|
126
|
+
tasks.append(asyncio.create_task(handler(event)))
|
|
127
|
+
else:
|
|
128
|
+
handler(event)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(f"Handler error for {event.type.value}: {e}")
|
|
131
|
+
|
|
132
|
+
if tasks:
|
|
133
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
134
|
+
|
|
135
|
+
def subscribe(self, event_type: AgentEventType, handler: EventHandler):
|
|
136
|
+
"""Subscribe a handler to an event type."""
|
|
137
|
+
if handler not in self._handlers[event_type]:
|
|
138
|
+
self._handlers[event_type].append(handler)
|
|
139
|
+
logger.debug(f"Handler subscribed to {event_type.value}")
|
|
140
|
+
|
|
141
|
+
def unsubscribe(self, event_type: AgentEventType, handler: EventHandler):
|
|
142
|
+
"""Unsubscribe a handler from an event type."""
|
|
143
|
+
if handler in self._handlers[event_type]:
|
|
144
|
+
self._handlers[event_type].remove(handler)
|
|
145
|
+
logger.debug(f"Handler unsubscribed from {event_type.value}")
|
|
146
|
+
|
|
147
|
+
async def publish(self, event_type: AgentEventType, payload: Dict[str, Any], source: str = None):
|
|
148
|
+
"""Publish an event to the bus."""
|
|
149
|
+
event = AgentEvent(type=event_type, payload=payload, source=source)
|
|
150
|
+
await self._event_queue.put(event)
|
|
151
|
+
logger.debug(f"Published event {event_type.value}")
|
|
152
|
+
|
|
153
|
+
def get_subscriber_count(self, event_type: AgentEventType) -> int:
|
|
154
|
+
"""Get number of subscribers for an event type."""
|
|
155
|
+
return len(self._handlers.get(event_type, []))
|
|
156
|
+
|
|
157
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
158
|
+
"""Get event bus statistics."""
|
|
159
|
+
return {
|
|
160
|
+
"running": self._running,
|
|
161
|
+
"queue_size": self._event_queue.qsize(),
|
|
162
|
+
"subscribers": {
|
|
163
|
+
event_type.value: len(handlers)
|
|
164
|
+
for event_type, handlers in self._handlers.items()
|
|
165
|
+
if handlers
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Global event bus instance
|
|
171
|
+
event_bus = EventBus()
|