monoco-toolkit 0.3.12__py3-none-any.whl → 0.4.0__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/core/automation/__init__.py +0 -11
- monoco/core/automation/handlers.py +108 -26
- monoco/core/config.py +28 -10
- monoco/core/daemon/__init__.py +5 -0
- monoco/core/daemon/pid.py +290 -0
- monoco/core/injection.py +86 -8
- monoco/core/integrations.py +0 -24
- monoco/core/router/__init__.py +1 -39
- monoco/core/router/action.py +3 -142
- monoco/core/scheduler/events.py +28 -2
- monoco/core/setup.py +9 -0
- monoco/core/sync.py +199 -4
- monoco/core/watcher/__init__.py +6 -0
- monoco/core/watcher/base.py +18 -1
- monoco/core/watcher/im.py +460 -0
- monoco/core/watcher/memo.py +40 -48
- monoco/daemon/app.py +3 -60
- monoco/daemon/commands.py +459 -25
- monoco/daemon/scheduler.py +1 -16
- monoco/daemon/services.py +15 -0
- monoco/features/agent/resources/en/AGENTS.md +14 -14
- monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/hooks/__init__.py +61 -6
- monoco/features/hooks/commands.py +281 -271
- monoco/features/hooks/dispatchers/__init__.py +23 -0
- monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
- monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
- monoco/features/hooks/manager.py +357 -0
- monoco/features/hooks/models.py +262 -0
- monoco/features/hooks/parser.py +322 -0
- monoco/features/hooks/universal_interceptor.py +503 -0
- monoco/features/im/__init__.py +67 -0
- monoco/features/im/core.py +782 -0
- monoco/features/im/models.py +311 -0
- monoco/features/issue/commands.py +65 -50
- monoco/features/issue/core.py +199 -99
- monoco/features/issue/domain_commands.py +0 -19
- monoco/features/issue/resources/en/AGENTS.md +17 -122
- monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
- monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
- monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
- monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
- monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
- monoco/features/issue/resources/zh/AGENTS.md +18 -123
- monoco/features/memo/cli.py +15 -64
- monoco/features/memo/core.py +6 -34
- monoco/features/memo/models.py +24 -15
- monoco/features/memo/resources/en/AGENTS.md +31 -0
- monoco/features/memo/resources/zh/AGENTS.md +28 -5
- monoco/main.py +5 -3
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
- monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
- monoco/core/automation/config.py +0 -338
- monoco/core/execution.py +0 -67
- monoco/core/executor/__init__.py +0 -38
- monoco/core/executor/agent_action.py +0 -254
- monoco/core/executor/git_action.py +0 -303
- monoco/core/executor/im_action.py +0 -309
- monoco/core/executor/pytest_action.py +0 -218
- monoco/core/router/router.py +0 -392
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
- monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
- monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
- monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
- monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
- monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
- monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
- monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
- monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/hooks/adapter.py +0 -67
- monoco/features/hooks/core.py +0 -441
- monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
- monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco_toolkit-0.3.12.dist-info/RECORD +0 -202
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
RunPytestAction - Action for running pytest tests.
|
|
3
|
-
|
|
4
|
-
Part of Layer 3 (Action Executor) in the event automation framework.
|
|
5
|
-
Executes pytest and parses results.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import asyncio
|
|
11
|
-
import json
|
|
12
|
-
import logging
|
|
13
|
-
import subprocess
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Any, Dict, List, Optional
|
|
16
|
-
|
|
17
|
-
from monoco.core.scheduler import AgentEvent
|
|
18
|
-
from monoco.core.router import Action, ActionResult
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class PytestResult:
|
|
24
|
-
"""Result of a pytest execution."""
|
|
25
|
-
|
|
26
|
-
def __init__(
|
|
27
|
-
self,
|
|
28
|
-
returncode: int,
|
|
29
|
-
stdout: str,
|
|
30
|
-
stderr: str,
|
|
31
|
-
summary: Optional[Dict[str, Any]] = None,
|
|
32
|
-
):
|
|
33
|
-
self.returncode = returncode
|
|
34
|
-
self.stdout = stdout
|
|
35
|
-
self.stderr = stderr
|
|
36
|
-
self.summary = summary or {}
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def passed(self) -> bool:
|
|
40
|
-
return self.returncode == 0
|
|
41
|
-
|
|
42
|
-
@property
|
|
43
|
-
def failed_count(self) -> int:
|
|
44
|
-
return self.summary.get("failed", 0)
|
|
45
|
-
|
|
46
|
-
@property
|
|
47
|
-
def passed_count(self) -> int:
|
|
48
|
-
return self.summary.get("passed", 0)
|
|
49
|
-
|
|
50
|
-
@property
|
|
51
|
-
def total_count(self) -> int:
|
|
52
|
-
return self.summary.get("total", 0)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class RunPytestAction(Action):
|
|
56
|
-
"""
|
|
57
|
-
Action that runs pytest tests.
|
|
58
|
-
|
|
59
|
-
This action executes pytest with configurable options and
|
|
60
|
-
parses the results for downstream processing.
|
|
61
|
-
|
|
62
|
-
Example:
|
|
63
|
-
>>> action = RunPytestAction(
|
|
64
|
-
... test_path="tests/",
|
|
65
|
-
... markers=["unit"],
|
|
66
|
-
... cov=True,
|
|
67
|
-
... )
|
|
68
|
-
>>> result = await action(event)
|
|
69
|
-
"""
|
|
70
|
-
|
|
71
|
-
def __init__(
|
|
72
|
-
self,
|
|
73
|
-
test_path: Optional[str] = None,
|
|
74
|
-
markers: Optional[List[str]] = None,
|
|
75
|
-
cov: bool = False,
|
|
76
|
-
cov_report: Optional[str] = None,
|
|
77
|
-
verbose: bool = True,
|
|
78
|
-
timeout: int = 300,
|
|
79
|
-
config: Optional[Dict[str, Any]] = None,
|
|
80
|
-
):
|
|
81
|
-
super().__init__(config)
|
|
82
|
-
self.test_path = test_path or "."
|
|
83
|
-
self.markers = markers or []
|
|
84
|
-
self.cov = cov
|
|
85
|
-
self.cov_report = cov_report
|
|
86
|
-
self.verbose = verbose
|
|
87
|
-
self.timeout = timeout
|
|
88
|
-
self._last_result: Optional[PytestResult] = None
|
|
89
|
-
|
|
90
|
-
@property
|
|
91
|
-
def name(self) -> str:
|
|
92
|
-
return "RunPytestAction"
|
|
93
|
-
|
|
94
|
-
async def can_execute(self, event: AgentEvent) -> bool:
|
|
95
|
-
"""Always can execute (no preconditions)."""
|
|
96
|
-
return True
|
|
97
|
-
|
|
98
|
-
async def execute(self, event: AgentEvent) -> ActionResult:
|
|
99
|
-
"""Run pytest tests."""
|
|
100
|
-
logger.info(f"Running pytest for {self.test_path}")
|
|
101
|
-
|
|
102
|
-
try:
|
|
103
|
-
result = await self._run_pytest()
|
|
104
|
-
self._last_result = result
|
|
105
|
-
|
|
106
|
-
if result.passed:
|
|
107
|
-
return ActionResult.success_result(
|
|
108
|
-
output={
|
|
109
|
-
"passed": result.passed_count,
|
|
110
|
-
"failed": result.failed_count,
|
|
111
|
-
"total": result.total_count,
|
|
112
|
-
},
|
|
113
|
-
metadata={
|
|
114
|
-
"stdout_preview": result.stdout[:500] if result.stdout else None,
|
|
115
|
-
},
|
|
116
|
-
)
|
|
117
|
-
else:
|
|
118
|
-
return ActionResult.failure_result(
|
|
119
|
-
error=f"Tests failed: {result.failed_count} failures",
|
|
120
|
-
metadata={
|
|
121
|
-
"passed": result.passed_count,
|
|
122
|
-
"failed": result.failed_count,
|
|
123
|
-
"total": result.total_count,
|
|
124
|
-
"stderr_preview": result.stderr[:500] if result.stderr else None,
|
|
125
|
-
},
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
except Exception as e:
|
|
129
|
-
logger.error(f"Pytest execution failed: {e}")
|
|
130
|
-
return ActionResult.failure_result(error=str(e))
|
|
131
|
-
|
|
132
|
-
async def _run_pytest(self) -> PytestResult:
|
|
133
|
-
"""Execute pytest subprocess."""
|
|
134
|
-
cmd = ["python", "-m", "pytest"]
|
|
135
|
-
|
|
136
|
-
# Add test path
|
|
137
|
-
cmd.append(self.test_path)
|
|
138
|
-
|
|
139
|
-
# Add markers
|
|
140
|
-
if self.markers:
|
|
141
|
-
marker_expr = " and ".join(self.markers)
|
|
142
|
-
cmd.extend(["-m", marker_expr])
|
|
143
|
-
|
|
144
|
-
# Add coverage
|
|
145
|
-
if self.cov:
|
|
146
|
-
cmd.append("--cov")
|
|
147
|
-
if self.cov_report:
|
|
148
|
-
cmd.extend(["--cov-report", self.cov_report])
|
|
149
|
-
|
|
150
|
-
# Add verbosity
|
|
151
|
-
if self.verbose:
|
|
152
|
-
cmd.append("-v")
|
|
153
|
-
|
|
154
|
-
# Add JSON report for parsing
|
|
155
|
-
cmd.extend(["--tb=short", "-q"])
|
|
156
|
-
|
|
157
|
-
logger.debug(f"Running command: {' '.join(cmd)}")
|
|
158
|
-
|
|
159
|
-
# Run subprocess
|
|
160
|
-
process = await asyncio.create_subprocess_exec(
|
|
161
|
-
*cmd,
|
|
162
|
-
stdout=asyncio.subprocess.PIPE,
|
|
163
|
-
stderr=asyncio.subprocess.PIPE,
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
try:
|
|
167
|
-
stdout, stderr = await asyncio.wait_for(
|
|
168
|
-
process.communicate(),
|
|
169
|
-
timeout=self.timeout,
|
|
170
|
-
)
|
|
171
|
-
except asyncio.TimeoutError:
|
|
172
|
-
process.kill()
|
|
173
|
-
raise RuntimeError(f"Pytest timed out after {self.timeout}s")
|
|
174
|
-
|
|
175
|
-
stdout_str = stdout.decode("utf-8", errors="replace")
|
|
176
|
-
stderr_str = stderr.decode("utf-8", errors="replace")
|
|
177
|
-
|
|
178
|
-
# Parse summary
|
|
179
|
-
summary = self._parse_summary(stdout_str)
|
|
180
|
-
|
|
181
|
-
return PytestResult(
|
|
182
|
-
returncode=process.returncode,
|
|
183
|
-
stdout=stdout_str,
|
|
184
|
-
stderr=stderr_str,
|
|
185
|
-
summary=summary,
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
def _parse_summary(self, output: str) -> Dict[str, int]:
|
|
189
|
-
"""Parse pytest summary from output."""
|
|
190
|
-
summary = {"passed": 0, "failed": 0, "error": 0, "skipped": 0, "total": 0}
|
|
191
|
-
|
|
192
|
-
# Look for summary line like "5 passed, 2 failed in 0.5s"
|
|
193
|
-
import re
|
|
194
|
-
pattern = r"(\d+)\s+(passed|failed|error|skipped)"
|
|
195
|
-
matches = re.findall(pattern, output)
|
|
196
|
-
|
|
197
|
-
for count, status in matches:
|
|
198
|
-
summary[status] = int(count)
|
|
199
|
-
summary["total"] += int(count)
|
|
200
|
-
|
|
201
|
-
return summary
|
|
202
|
-
|
|
203
|
-
def get_last_result(self) -> Optional[PytestResult]:
|
|
204
|
-
"""Get the last pytest result."""
|
|
205
|
-
return self._last_result
|
|
206
|
-
|
|
207
|
-
def get_stats(self) -> Dict[str, Any]:
|
|
208
|
-
"""Get action statistics."""
|
|
209
|
-
stats = super().get_stats()
|
|
210
|
-
stats.update({
|
|
211
|
-
"test_path": self.test_path,
|
|
212
|
-
"markers": self.markers,
|
|
213
|
-
"last_result": {
|
|
214
|
-
"passed": self._last_result.passed if self._last_result else None,
|
|
215
|
-
"failed": self._last_result.failed_count if self._last_result else None,
|
|
216
|
-
} if self._last_result else None,
|
|
217
|
-
})
|
|
218
|
-
return stats
|
monoco/core/router/router.py
DELETED
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ActionRouter - Layer 2 of the Event Automation Framework.
|
|
3
|
-
|
|
4
|
-
This module implements the ActionRouter which routes events to appropriate actions.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import asyncio
|
|
10
|
-
import inspect
|
|
11
|
-
import logging
|
|
12
|
-
from collections import defaultdict
|
|
13
|
-
from typing import Any, Callable, Dict, List, Optional, Set, Union
|
|
14
|
-
|
|
15
|
-
from monoco.core.scheduler import AgentEvent, AgentEventType, EventBus, event_bus
|
|
16
|
-
|
|
17
|
-
from .action import Action, ActionChain, ActionRegistry, ActionResult, ActionStatus
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class RoutingRule:
|
|
23
|
-
"""
|
|
24
|
-
A routing rule that maps events to actions.
|
|
25
|
-
|
|
26
|
-
Attributes:
|
|
27
|
-
event_types: Event types this rule matches
|
|
28
|
-
action: Action to execute
|
|
29
|
-
condition: Optional additional condition
|
|
30
|
-
priority: Rule priority (higher = executed first)
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
def __init__(
|
|
34
|
-
self,
|
|
35
|
-
event_types: List[AgentEventType],
|
|
36
|
-
action: Union[Action, ActionChain],
|
|
37
|
-
condition: Optional[Callable[[AgentEvent], bool]] = None,
|
|
38
|
-
priority: int = 0,
|
|
39
|
-
):
|
|
40
|
-
self.event_types = event_types
|
|
41
|
-
self.action = action
|
|
42
|
-
self.condition = condition
|
|
43
|
-
self.priority = priority
|
|
44
|
-
|
|
45
|
-
def matches(self, event: AgentEvent) -> bool:
|
|
46
|
-
"""Check if this rule matches the event."""
|
|
47
|
-
if event.type not in self.event_types:
|
|
48
|
-
return False
|
|
49
|
-
|
|
50
|
-
if self.condition is not None:
|
|
51
|
-
if inspect.iscoroutinefunction(self.condition):
|
|
52
|
-
# Note: This is called from sync context, so we can't await
|
|
53
|
-
# For async conditions, use Action.can_execute instead
|
|
54
|
-
logger.warning("Async conditions in RoutingRule are not supported")
|
|
55
|
-
return False
|
|
56
|
-
return self.condition(event)
|
|
57
|
-
|
|
58
|
-
return True
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class ActionRouter:
|
|
62
|
-
"""
|
|
63
|
-
Event router that maps events to actions (Layer 2).
|
|
64
|
-
|
|
65
|
-
Responsibilities:
|
|
66
|
-
- Subscribe to EventBus events
|
|
67
|
-
- Route events to registered actions
|
|
68
|
-
- Support conditional routing
|
|
69
|
-
- Support action chains
|
|
70
|
-
- Manage action lifecycle
|
|
71
|
-
|
|
72
|
-
Example:
|
|
73
|
-
>>> router = ActionRouter()
|
|
74
|
-
>>>
|
|
75
|
-
>>> # Register simple action
|
|
76
|
-
>>> router.register(AgentEventType.ISSUE_CREATED, my_action)
|
|
77
|
-
>>>
|
|
78
|
-
>>> # Register with condition
|
|
79
|
-
>>> router.register(
|
|
80
|
-
... AgentEventType.ISSUE_STAGE_CHANGED,
|
|
81
|
-
... engineer_action,
|
|
82
|
-
... condition=lambda e: e.payload.get("new_stage") == "doing"
|
|
83
|
-
... )
|
|
84
|
-
>>>
|
|
85
|
-
>>> await router.start()
|
|
86
|
-
"""
|
|
87
|
-
|
|
88
|
-
def __init__(
|
|
89
|
-
self,
|
|
90
|
-
event_bus: Optional[EventBus] = None,
|
|
91
|
-
name: str = "ActionRouter",
|
|
92
|
-
):
|
|
93
|
-
self.event_bus = event_bus or event_bus
|
|
94
|
-
self.name = name
|
|
95
|
-
self._rules: List[RoutingRule] = []
|
|
96
|
-
self._registry = ActionRegistry()
|
|
97
|
-
self._running = False
|
|
98
|
-
self._event_handler: Optional[Callable[[AgentEvent], Any]] = None
|
|
99
|
-
|
|
100
|
-
# Statistics
|
|
101
|
-
self._event_count = 0
|
|
102
|
-
self._routed_count = 0
|
|
103
|
-
self._execution_results: List[ActionResult] = []
|
|
104
|
-
self._max_results_history = 100
|
|
105
|
-
|
|
106
|
-
def register(
|
|
107
|
-
self,
|
|
108
|
-
event_types: Union[AgentEventType, List[AgentEventType]],
|
|
109
|
-
action: Union[Action, ActionChain],
|
|
110
|
-
condition: Optional[Callable[[AgentEvent], bool]] = None,
|
|
111
|
-
priority: int = 0,
|
|
112
|
-
) -> "ActionRouter":
|
|
113
|
-
"""
|
|
114
|
-
Register an action for event types.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
event_types: Event type(s) to subscribe to
|
|
118
|
-
action: Action or ActionChain to execute
|
|
119
|
-
condition: Optional condition function
|
|
120
|
-
priority: Rule priority (higher = executed first)
|
|
121
|
-
|
|
122
|
-
Returns:
|
|
123
|
-
Self for chaining
|
|
124
|
-
"""
|
|
125
|
-
if isinstance(event_types, AgentEventType):
|
|
126
|
-
event_types = [event_types]
|
|
127
|
-
|
|
128
|
-
rule = RoutingRule(
|
|
129
|
-
event_types=event_types,
|
|
130
|
-
action=action,
|
|
131
|
-
condition=condition,
|
|
132
|
-
priority=priority,
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
self._rules.append(rule)
|
|
136
|
-
|
|
137
|
-
# Sort rules by priority (descending)
|
|
138
|
-
self._rules.sort(key=lambda r: r.priority, reverse=True)
|
|
139
|
-
|
|
140
|
-
# Register action in registry
|
|
141
|
-
if isinstance(action, Action):
|
|
142
|
-
self._registry.register(action)
|
|
143
|
-
elif isinstance(action, ActionChain):
|
|
144
|
-
for a in action.actions:
|
|
145
|
-
self._registry.register(a)
|
|
146
|
-
|
|
147
|
-
logger.info(
|
|
148
|
-
f"Registered {action.name if hasattr(action, 'name') else type(action).__name__} "
|
|
149
|
-
f"for events: {[t.value for t in event_types]}"
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
return self
|
|
153
|
-
|
|
154
|
-
def unregister(self, action_name: str) -> bool:
|
|
155
|
-
"""
|
|
156
|
-
Unregister an action by name.
|
|
157
|
-
|
|
158
|
-
Args:
|
|
159
|
-
action_name: Name of the action to unregister
|
|
160
|
-
|
|
161
|
-
Returns:
|
|
162
|
-
True if action was found and removed
|
|
163
|
-
"""
|
|
164
|
-
# Remove from rules
|
|
165
|
-
original_count = len(self._rules)
|
|
166
|
-
self._rules = [
|
|
167
|
-
r for r in self._rules
|
|
168
|
-
if not (
|
|
169
|
-
(hasattr(r.action, 'name') and r.action.name == action_name) or
|
|
170
|
-
(isinstance(r.action, ActionChain) and action_name in [a.name for a in r.action.actions])
|
|
171
|
-
)
|
|
172
|
-
]
|
|
173
|
-
|
|
174
|
-
# Remove from registry
|
|
175
|
-
self._registry.unregister(action_name)
|
|
176
|
-
|
|
177
|
-
removed = len(self._rules) < original_count
|
|
178
|
-
if removed:
|
|
179
|
-
logger.info(f"Unregistered action: {action_name}")
|
|
180
|
-
|
|
181
|
-
return removed
|
|
182
|
-
|
|
183
|
-
async def start(self) -> None:
|
|
184
|
-
"""Start the router and subscribe to events."""
|
|
185
|
-
if self._running:
|
|
186
|
-
return
|
|
187
|
-
|
|
188
|
-
self._running = True
|
|
189
|
-
|
|
190
|
-
# Create event handler
|
|
191
|
-
self._event_handler = self._handle_event
|
|
192
|
-
|
|
193
|
-
# Subscribe to all event types mentioned in rules
|
|
194
|
-
event_types = set()
|
|
195
|
-
for rule in self._rules:
|
|
196
|
-
event_types.update(rule.event_types)
|
|
197
|
-
|
|
198
|
-
for event_type in event_types:
|
|
199
|
-
self.event_bus.subscribe(event_type, self._event_handler)
|
|
200
|
-
|
|
201
|
-
logger.info(f"Started ActionRouter with {len(self._rules)} rules")
|
|
202
|
-
|
|
203
|
-
async def stop(self) -> None:
|
|
204
|
-
"""Stop the router and unsubscribe from events."""
|
|
205
|
-
if not self._running:
|
|
206
|
-
return
|
|
207
|
-
|
|
208
|
-
self._running = False
|
|
209
|
-
|
|
210
|
-
# Unsubscribe from all event types
|
|
211
|
-
if self._event_handler:
|
|
212
|
-
for event_type in AgentEventType:
|
|
213
|
-
self.event_bus.unsubscribe(event_type, self._event_handler)
|
|
214
|
-
|
|
215
|
-
logger.info("Stopped ActionRouter")
|
|
216
|
-
|
|
217
|
-
async def _handle_event(self, event: AgentEvent) -> None:
|
|
218
|
-
"""
|
|
219
|
-
Handle incoming events.
|
|
220
|
-
|
|
221
|
-
Args:
|
|
222
|
-
event: The event to route
|
|
223
|
-
"""
|
|
224
|
-
self._event_count += 1
|
|
225
|
-
|
|
226
|
-
logger.debug(f"Routing event: {event.type.value}")
|
|
227
|
-
|
|
228
|
-
# Find matching rules
|
|
229
|
-
matching_rules = [r for r in self._rules if r.matches(event)]
|
|
230
|
-
|
|
231
|
-
if not matching_rules:
|
|
232
|
-
logger.debug(f"No matching rules for event: {event.type.value}")
|
|
233
|
-
return
|
|
234
|
-
|
|
235
|
-
# Execute actions
|
|
236
|
-
for rule in matching_rules:
|
|
237
|
-
try:
|
|
238
|
-
if isinstance(rule.action, ActionChain):
|
|
239
|
-
results = await rule.action.execute(event)
|
|
240
|
-
for result in results:
|
|
241
|
-
self._record_result(result)
|
|
242
|
-
else:
|
|
243
|
-
result = await rule.action(event)
|
|
244
|
-
self._record_result(result)
|
|
245
|
-
|
|
246
|
-
self._routed_count += 1
|
|
247
|
-
|
|
248
|
-
except Exception as e:
|
|
249
|
-
logger.error(f"Error executing action for {event.type.value}: {e}")
|
|
250
|
-
self._record_result(ActionResult.failure_result(error=str(e)))
|
|
251
|
-
|
|
252
|
-
def _record_result(self, result: ActionResult) -> None:
|
|
253
|
-
"""Record execution result."""
|
|
254
|
-
self._execution_results.append(result)
|
|
255
|
-
|
|
256
|
-
# Trim history
|
|
257
|
-
if len(self._execution_results) > self._max_results_history:
|
|
258
|
-
self._execution_results = self._execution_results[-self._max_results_history:]
|
|
259
|
-
|
|
260
|
-
def route(self, event: AgentEvent) -> List[ActionResult]:
|
|
261
|
-
"""
|
|
262
|
-
Manually route an event (synchronous version).
|
|
263
|
-
|
|
264
|
-
Args:
|
|
265
|
-
event: The event to route
|
|
266
|
-
|
|
267
|
-
Returns:
|
|
268
|
-
List of action results
|
|
269
|
-
"""
|
|
270
|
-
results = []
|
|
271
|
-
matching_rules = [r for r in self._rules if r.matches(event)]
|
|
272
|
-
|
|
273
|
-
for rule in matching_rules:
|
|
274
|
-
try:
|
|
275
|
-
if isinstance(rule.action, ActionChain):
|
|
276
|
-
# For chains, we need to run async
|
|
277
|
-
loop = asyncio.get_event_loop()
|
|
278
|
-
chain_results = loop.run_until_complete(rule.action.execute(event))
|
|
279
|
-
results.extend(chain_results)
|
|
280
|
-
else:
|
|
281
|
-
loop = asyncio.get_event_loop()
|
|
282
|
-
result = loop.run_until_complete(rule.action(event))
|
|
283
|
-
results.append(result)
|
|
284
|
-
|
|
285
|
-
except Exception as e:
|
|
286
|
-
logger.error(f"Error in manual route: {e}")
|
|
287
|
-
results.append(ActionResult.failure_result(error=str(e)))
|
|
288
|
-
|
|
289
|
-
return results
|
|
290
|
-
|
|
291
|
-
def get_rules(self) -> List[Dict[str, Any]]:
|
|
292
|
-
"""Get all routing rules as dicts."""
|
|
293
|
-
return [
|
|
294
|
-
{
|
|
295
|
-
"event_types": [t.value for t in r.event_types],
|
|
296
|
-
"action": r.action.name if hasattr(r.action, 'name') else str(type(r.action)),
|
|
297
|
-
"priority": r.priority,
|
|
298
|
-
"has_condition": r.condition is not None,
|
|
299
|
-
}
|
|
300
|
-
for r in self._rules
|
|
301
|
-
]
|
|
302
|
-
|
|
303
|
-
def get_stats(self) -> Dict[str, Any]:
|
|
304
|
-
"""Get router statistics."""
|
|
305
|
-
success_count = sum(
|
|
306
|
-
1 for r in self._execution_results
|
|
307
|
-
if r.success and r.status == ActionStatus.SUCCESS
|
|
308
|
-
)
|
|
309
|
-
failed_count = sum(
|
|
310
|
-
1 for r in self._execution_results
|
|
311
|
-
if not r.success
|
|
312
|
-
)
|
|
313
|
-
skipped_count = sum(
|
|
314
|
-
1 for r in self._execution_results
|
|
315
|
-
if r.status == ActionStatus.SKIPPED
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
return {
|
|
319
|
-
"name": self.name,
|
|
320
|
-
"running": self._running,
|
|
321
|
-
"rules": len(self._rules),
|
|
322
|
-
"registered_actions": self._registry.list_actions(),
|
|
323
|
-
"events_received": self._event_count,
|
|
324
|
-
"events_routed": self._routed_count,
|
|
325
|
-
"results": {
|
|
326
|
-
"total": len(self._execution_results),
|
|
327
|
-
"success": success_count,
|
|
328
|
-
"failed": failed_count,
|
|
329
|
-
"skipped": skipped_count,
|
|
330
|
-
},
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
class ConditionalRouter(ActionRouter):
|
|
335
|
-
"""
|
|
336
|
-
Router with advanced conditional routing capabilities.
|
|
337
|
-
|
|
338
|
-
Supports complex routing logic based on event payload.
|
|
339
|
-
"""
|
|
340
|
-
|
|
341
|
-
def register_field_condition(
|
|
342
|
-
self,
|
|
343
|
-
event_types: Union[AgentEventType, List[AgentEventType]],
|
|
344
|
-
action: Union[Action, ActionChain],
|
|
345
|
-
field: str,
|
|
346
|
-
expected_value: Any,
|
|
347
|
-
priority: int = 0,
|
|
348
|
-
) -> "ConditionalRouter":
|
|
349
|
-
"""
|
|
350
|
-
Register action with a field value condition.
|
|
351
|
-
|
|
352
|
-
Args:
|
|
353
|
-
event_types: Event type(s) to subscribe to
|
|
354
|
-
action: Action to execute
|
|
355
|
-
field: Payload field to check
|
|
356
|
-
expected_value: Expected value of the field
|
|
357
|
-
priority: Rule priority
|
|
358
|
-
|
|
359
|
-
Returns:
|
|
360
|
-
Self for chaining
|
|
361
|
-
"""
|
|
362
|
-
def condition(event: AgentEvent) -> bool:
|
|
363
|
-
return event.payload.get(field) == expected_value
|
|
364
|
-
|
|
365
|
-
return self.register(event_types, action, condition, priority)
|
|
366
|
-
|
|
367
|
-
def register_payload_condition(
|
|
368
|
-
self,
|
|
369
|
-
event_types: Union[AgentEventType, List[AgentEventType]],
|
|
370
|
-
action: Union[Action, ActionChain],
|
|
371
|
-
payload_matcher: Dict[str, Any],
|
|
372
|
-
priority: int = 0,
|
|
373
|
-
) -> "ConditionalRouter":
|
|
374
|
-
"""
|
|
375
|
-
Register action with a payload matching condition.
|
|
376
|
-
|
|
377
|
-
Args:
|
|
378
|
-
event_types: Event type(s) to subscribe to
|
|
379
|
-
action: Action to execute
|
|
380
|
-
payload_matcher: Dict of field -> expected value
|
|
381
|
-
priority: Rule priority
|
|
382
|
-
|
|
383
|
-
Returns:
|
|
384
|
-
Self for chaining
|
|
385
|
-
"""
|
|
386
|
-
def condition(event: AgentEvent) -> bool:
|
|
387
|
-
return all(
|
|
388
|
-
event.payload.get(k) == v
|
|
389
|
-
for k, v in payload_matcher.items()
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
return self.register(event_types, action, condition, priority)
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: atom-code-dev
|
|
3
|
-
type: atom
|
|
4
|
-
domain: code
|
|
5
|
-
description: 代码开发的原子操作 - 调研、实现、测试、文档
|
|
6
|
-
version: 1.0.0
|
|
7
|
-
author: Monoco Toolkit
|
|
8
|
-
|
|
9
|
-
# 系统级合规规则
|
|
10
|
-
compliance_rules:
|
|
11
|
-
- rule: "遵循项目代码规范"
|
|
12
|
-
severity: warning
|
|
13
|
-
|
|
14
|
-
- rule: "优先编写测试"
|
|
15
|
-
severity: warning
|
|
16
|
-
mindset: "TDD"
|
|
17
|
-
|
|
18
|
-
- rule: "保持代码简单直观"
|
|
19
|
-
severity: info
|
|
20
|
-
mindset: "KISS"
|
|
21
|
-
|
|
22
|
-
# 原子操作定义
|
|
23
|
-
operations:
|
|
24
|
-
- name: investigate
|
|
25
|
-
description: 理解需求与上下文,识别相关代码和依赖
|
|
26
|
-
reminder: "理解需求后再开始编码,识别相关文件和依赖 Issue"
|
|
27
|
-
checkpoints:
|
|
28
|
-
- "阅读并理解 Issue 描述"
|
|
29
|
-
- "识别相关代码文件"
|
|
30
|
-
- "检查依赖 Issue 状态"
|
|
31
|
-
- "评估技术可行性"
|
|
32
|
-
output: "技术方案草稿、风险清单"
|
|
33
|
-
|
|
34
|
-
- name: implement
|
|
35
|
-
description: 实现代码变更
|
|
36
|
-
reminder: "遵循项目代码规范,小步提交,处理边界情况"
|
|
37
|
-
checkpoints:
|
|
38
|
-
- "遵循项目代码规范"
|
|
39
|
-
- "编写/更新必要的文档"
|
|
40
|
-
- "处理边界情况"
|
|
41
|
-
- "保持代码可审查性(单次提交 < 400 行)"
|
|
42
|
-
|
|
43
|
-
- name: test
|
|
44
|
-
description: 运行测试确保代码质量
|
|
45
|
-
reminder: "所有测试必须通过,检查测试覆盖率"
|
|
46
|
-
checkpoints:
|
|
47
|
-
- "编写/更新单元测试"
|
|
48
|
-
- "运行测试套件 (pytest, cargo test, etc.)"
|
|
49
|
-
- "修复失败的测试"
|
|
50
|
-
- "检查测试覆盖率"
|
|
51
|
-
compliance_rules:
|
|
52
|
-
- rule: "所有单元测试必须通过后才能提交"
|
|
53
|
-
severity: error
|
|
54
|
-
|
|
55
|
-
- name: document
|
|
56
|
-
description: 更新文档
|
|
57
|
-
reminder: "代码变更必须同步更新文档"
|
|
58
|
-
checkpoints:
|
|
59
|
-
- "更新代码注释"
|
|
60
|
-
- "更新相关文档"
|
|
61
|
-
- "更新 CHANGELOG(如适用)"
|