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.
Files changed (120) hide show
  1. monoco/core/automation/__init__.py +0 -11
  2. monoco/core/automation/handlers.py +108 -26
  3. monoco/core/config.py +28 -10
  4. monoco/core/daemon/__init__.py +5 -0
  5. monoco/core/daemon/pid.py +290 -0
  6. monoco/core/injection.py +86 -8
  7. monoco/core/integrations.py +0 -24
  8. monoco/core/router/__init__.py +1 -39
  9. monoco/core/router/action.py +3 -142
  10. monoco/core/scheduler/events.py +28 -2
  11. monoco/core/setup.py +9 -0
  12. monoco/core/sync.py +199 -4
  13. monoco/core/watcher/__init__.py +6 -0
  14. monoco/core/watcher/base.py +18 -1
  15. monoco/core/watcher/im.py +460 -0
  16. monoco/core/watcher/memo.py +40 -48
  17. monoco/daemon/app.py +3 -60
  18. monoco/daemon/commands.py +459 -25
  19. monoco/daemon/scheduler.py +1 -16
  20. monoco/daemon/services.py +15 -0
  21. monoco/features/agent/resources/en/AGENTS.md +14 -14
  22. monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
  23. monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
  24. monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
  25. monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
  26. monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
  27. monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
  28. monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
  29. monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
  30. monoco/features/hooks/__init__.py +61 -6
  31. monoco/features/hooks/commands.py +281 -271
  32. monoco/features/hooks/dispatchers/__init__.py +23 -0
  33. monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
  34. monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
  35. monoco/features/hooks/manager.py +357 -0
  36. monoco/features/hooks/models.py +262 -0
  37. monoco/features/hooks/parser.py +322 -0
  38. monoco/features/hooks/universal_interceptor.py +503 -0
  39. monoco/features/im/__init__.py +67 -0
  40. monoco/features/im/core.py +782 -0
  41. monoco/features/im/models.py +311 -0
  42. monoco/features/issue/commands.py +65 -50
  43. monoco/features/issue/core.py +199 -99
  44. monoco/features/issue/domain_commands.py +0 -19
  45. monoco/features/issue/resources/en/AGENTS.md +17 -122
  46. monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
  47. monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
  48. monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
  49. monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
  50. monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
  51. monoco/features/issue/resources/zh/AGENTS.md +18 -123
  52. monoco/features/memo/cli.py +15 -64
  53. monoco/features/memo/core.py +6 -34
  54. monoco/features/memo/models.py +24 -15
  55. monoco/features/memo/resources/en/AGENTS.md +31 -0
  56. monoco/features/memo/resources/zh/AGENTS.md +28 -5
  57. monoco/main.py +5 -3
  58. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
  59. monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
  60. monoco/core/automation/config.py +0 -338
  61. monoco/core/execution.py +0 -67
  62. monoco/core/executor/__init__.py +0 -38
  63. monoco/core/executor/agent_action.py +0 -254
  64. monoco/core/executor/git_action.py +0 -303
  65. monoco/core/executor/im_action.py +0 -309
  66. monoco/core/executor/pytest_action.py +0 -218
  67. monoco/core/router/router.py +0 -392
  68. monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
  69. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
  70. monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
  71. monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
  72. monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
  73. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  74. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
  75. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
  76. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
  77. monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
  78. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
  79. monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
  80. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
  81. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
  82. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
  83. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
  84. monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
  85. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  86. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
  87. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
  88. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
  89. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
  90. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
  91. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
  92. monoco/features/hooks/adapter.py +0 -67
  93. monoco/features/hooks/core.py +0 -441
  94. monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
  95. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  96. monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
  97. monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  98. monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
  99. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  100. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  101. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  102. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  103. monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
  104. monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
  105. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  106. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  107. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  108. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  109. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
  110. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  111. monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
  112. monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  113. monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
  114. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
  115. monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
  116. monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
  117. monoco_toolkit-0.3.12.dist-info/RECORD +0 -202
  118. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
  119. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
  120. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,338 +0,0 @@
1
- """
2
- Automation Configuration - YAML/JSON configuration for triggers.
3
-
4
- Part of the Event Automation Framework.
5
- Provides configuration schema and loading for automation triggers.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import json
11
- import logging
12
- from dataclasses import dataclass, field as dataclass_field
13
- from pathlib import Path
14
- from typing import Any, Dict, List, Optional, Union
15
-
16
- import yaml
17
-
18
- from monoco.core.scheduler import AgentEventType
19
-
20
- logger = logging.getLogger(__name__)
21
-
22
-
23
- @dataclass
24
- class ActionConfig:
25
- """Configuration for an action."""
26
- type: str
27
- params: Dict[str, Any] = dataclass_field(default_factory=dict)
28
-
29
- @classmethod
30
- def from_dict(cls, data: Dict[str, Any]) -> "ActionConfig":
31
- """Create from dict."""
32
- return cls(
33
- type=data.get("type", ""),
34
- params=data.get("params", {}),
35
- )
36
-
37
-
38
- @dataclass
39
- class TriggerConfig:
40
- """
41
- Configuration for a trigger.
42
-
43
- Attributes:
44
- name: Unique trigger name
45
- watcher: Watcher type (IssueWatcher, MemoWatcher, etc.)
46
- event_type: Event type to listen for
47
- condition: Optional condition expression
48
- field: Optional field to watch (for field-level triggers)
49
- actions: List of actions to execute
50
- enabled: Whether trigger is enabled
51
- """
52
- name: str
53
- watcher: str
54
- event_type: Optional[str] = None
55
- condition: Optional[str] = None
56
- field: Optional[str] = None
57
- actions: List[ActionConfig] = dataclass_field(default_factory=list)
58
- enabled: bool = True
59
- priority: int = 0
60
-
61
- @classmethod
62
- def from_dict(cls, data: Dict[str, Any]) -> "TriggerConfig":
63
- """Create from dict."""
64
- actions = [
65
- ActionConfig.from_dict(a) if isinstance(a, dict) else ActionConfig(type=a)
66
- for a in data.get("actions", [])
67
- ]
68
-
69
- return cls(
70
- name=data.get("name", "unnamed"),
71
- watcher=data.get("watcher", ""),
72
- event_type=data.get("event_type"),
73
- condition=data.get("condition"),
74
- field=data.get("field"),
75
- actions=actions,
76
- enabled=data.get("enabled", True),
77
- priority=data.get("priority", 0),
78
- )
79
-
80
- def to_agent_event_type(self) -> Optional[AgentEventType]:
81
- """Convert event_type string to AgentEventType."""
82
- if not self.event_type:
83
- return None
84
-
85
- try:
86
- return AgentEventType(self.event_type)
87
- except ValueError:
88
- # Try to map common patterns
89
- mapping = {
90
- "issue.created": AgentEventType.ISSUE_CREATED,
91
- "issue.updated": AgentEventType.ISSUE_UPDATED,
92
- "issue.stage_changed": AgentEventType.ISSUE_STAGE_CHANGED,
93
- "issue.status_changed": AgentEventType.ISSUE_STATUS_CHANGED,
94
- "memo.created": AgentEventType.MEMO_CREATED,
95
- "memo.threshold": AgentEventType.MEMO_THRESHOLD,
96
- "session.completed": AgentEventType.SESSION_COMPLETED,
97
- "session.failed": AgentEventType.SESSION_FAILED,
98
- "pr.created": AgentEventType.PR_CREATED,
99
- }
100
- return mapping.get(self.event_type)
101
-
102
-
103
- @dataclass
104
- class AutomationConfig:
105
- """
106
- Complete automation configuration.
107
-
108
- Attributes:
109
- version: Configuration version
110
- triggers: List of trigger configurations
111
- settings: Global settings
112
- """
113
- version: str = "1.0"
114
- triggers: List[TriggerConfig] = dataclass_field(default_factory=list)
115
- settings: Dict[str, Any] = dataclass_field(default_factory=dict)
116
-
117
- @classmethod
118
- def from_dict(cls, data: Dict[str, Any]) -> "AutomationConfig":
119
- """Create from dict."""
120
- triggers = [
121
- TriggerConfig.from_dict(t)
122
- for t in data.get("triggers", [])
123
- ]
124
-
125
- return cls(
126
- version=data.get("version", "1.0"),
127
- triggers=triggers,
128
- settings=data.get("settings", {}),
129
- )
130
-
131
- @classmethod
132
- def from_yaml(cls, yaml_content: str) -> "AutomationConfig":
133
- """Load from YAML string."""
134
- data = yaml.safe_load(yaml_content)
135
- return cls.from_dict(data or {})
136
-
137
- @classmethod
138
- def from_json(cls, json_content: str) -> "AutomationConfig":
139
- """Load from JSON string."""
140
- data = json.loads(json_content)
141
- return cls.from_dict(data)
142
-
143
- def to_yaml(self) -> str:
144
- """Export to YAML string."""
145
- data = {
146
- "version": self.version,
147
- "triggers": [
148
- {
149
- "name": t.name,
150
- "watcher": t.watcher,
151
- "event_type": t.event_type,
152
- "condition": t.condition,
153
- "field": t.field,
154
- "actions": [
155
- {"type": a.type, "params": a.params}
156
- for a in t.actions
157
- ],
158
- "enabled": t.enabled,
159
- "priority": t.priority,
160
- }
161
- for t in self.triggers
162
- ],
163
- "settings": self.settings,
164
- }
165
- return yaml.dump(data, default_flow_style=False, sort_keys=False)
166
-
167
- def to_json(self) -> str:
168
- """Export to JSON string."""
169
- data = {
170
- "version": self.version,
171
- "triggers": [
172
- {
173
- "name": t.name,
174
- "watcher": t.watcher,
175
- "event_type": t.event_type,
176
- "condition": t.condition,
177
- "field": t.field,
178
- "actions": [
179
- {"type": a.type, "params": a.params}
180
- for a in t.actions
181
- ],
182
- "enabled": t.enabled,
183
- "priority": t.priority,
184
- }
185
- for t in self.triggers
186
- ],
187
- "settings": self.settings,
188
- }
189
- return json.dumps(data, indent=2)
190
-
191
- def get_enabled_triggers(self) -> List[TriggerConfig]:
192
- """Get all enabled triggers."""
193
- return [t for t in self.triggers if t.enabled]
194
-
195
- def get_trigger(self, name: str) -> Optional[TriggerConfig]:
196
- """Get trigger by name."""
197
- for trigger in self.triggers:
198
- if trigger.name == name:
199
- return trigger
200
- return None
201
-
202
- def add_trigger(self, trigger: TriggerConfig) -> None:
203
- """Add a trigger."""
204
- # Remove existing trigger with same name
205
- self.triggers = [t for t in self.triggers if t.name != trigger.name]
206
- self.triggers.append(trigger)
207
-
208
- def remove_trigger(self, name: str) -> bool:
209
- """Remove a trigger by name."""
210
- original_count = len(self.triggers)
211
- self.triggers = [t for t in self.triggers if t.name != name]
212
- return len(self.triggers) < original_count
213
-
214
-
215
- def load_automation_config(
216
- path: Union[str, Path],
217
- create_default: bool = False,
218
- ) -> AutomationConfig:
219
- """
220
- Load automation configuration from file.
221
-
222
- Supports .yaml, .yml, and .json files.
223
-
224
- Args:
225
- path: Path to configuration file
226
- create_default: If True and file doesn't exist, create default config
227
-
228
- Returns:
229
- AutomationConfig instance
230
- """
231
- path = Path(path)
232
-
233
- if not path.exists():
234
- if create_default:
235
- default_config = create_default_config()
236
- path.write_text(default_config.to_yaml())
237
- logger.info(f"Created default automation config at {path}")
238
- return default_config
239
- else:
240
- logger.warning(f"Config file not found: {path}")
241
- return AutomationConfig()
242
-
243
- content = path.read_text(encoding="utf-8")
244
-
245
- if path.suffix in (".yaml", ".yml"):
246
- return AutomationConfig.from_yaml(content)
247
- elif path.suffix == ".json":
248
- return AutomationConfig.from_json(content)
249
- else:
250
- # Try YAML first, then JSON
251
- try:
252
- return AutomationConfig.from_yaml(content)
253
- except yaml.YAMLError:
254
- return AutomationConfig.from_json(content)
255
-
256
-
257
- def create_default_config() -> AutomationConfig:
258
- """Create a default automation configuration."""
259
- return AutomationConfig(
260
- version="1.0",
261
- triggers=[
262
- TriggerConfig(
263
- name="memo_threshold",
264
- watcher="MemoWatcher",
265
- event_type="memo.threshold",
266
- condition="pending_count >= 5",
267
- actions=[
268
- ActionConfig(
269
- type="SpawnAgentAction",
270
- params={"role": "Architect"},
271
- ),
272
- ],
273
- ),
274
- TriggerConfig(
275
- name="issue_doing",
276
- watcher="IssueWatcher",
277
- event_type="issue.stage_changed",
278
- field="stage",
279
- condition="value == 'doing'",
280
- actions=[
281
- ActionConfig(
282
- type="SpawnAgentAction",
283
- params={"role": "Engineer"},
284
- ),
285
- ],
286
- ),
287
- TriggerConfig(
288
- name="issue_completed",
289
- watcher="IssueWatcher",
290
- event_type="issue.stage_changed",
291
- field="stage",
292
- condition="value == 'done'",
293
- actions=[
294
- ActionConfig(
295
- type="SendIMAction",
296
- params={
297
- "channel": "console",
298
- "message_template": "Issue {issue_id} completed!",
299
- },
300
- ),
301
- ],
302
- ),
303
- ],
304
- settings={
305
- "default_poll_interval": 5.0,
306
- "max_concurrent_actions": 10,
307
- "action_timeout": 300,
308
- },
309
- )
310
-
311
-
312
- def save_automation_config(
313
- config: AutomationConfig,
314
- path: Union[str, Path],
315
- format: str = "yaml",
316
- ) -> None:
317
- """
318
- Save automation configuration to file.
319
-
320
- Args:
321
- config: Configuration to save
322
- path: Path to save to
323
- format: "yaml" or "json"
324
- """
325
- path = Path(path)
326
- path.parent.mkdir(parents=True, exist_ok=True)
327
-
328
- if format == "yaml":
329
- content = config.to_yaml()
330
- if path.suffix not in (".yaml", ".yml"):
331
- path = path.with_suffix(".yaml")
332
- else:
333
- content = config.to_json()
334
- if path.suffix != ".json":
335
- path = path.with_suffix(".json")
336
-
337
- path.write_text(content, encoding="utf-8")
338
- logger.info(f"Saved automation config to {path}")
monoco/core/execution.py DELETED
@@ -1,67 +0,0 @@
1
- from pathlib import Path
2
- from typing import List, Optional
3
- from pydantic import BaseModel
4
-
5
-
6
- class ExecutionProfile(BaseModel):
7
- name: str
8
- source: str # "Global" or "Project"
9
- path: str
10
- content: Optional[str] = None
11
-
12
-
13
- def scan_execution_profiles(
14
- project_root: Optional[Path] = None,
15
- ) -> List[ExecutionProfile]:
16
- """
17
- Scan for execution profiles (SOPs) in global and project scopes.
18
- """
19
- profiles = []
20
-
21
- # 1. Global Scope
22
- global_path = Path.home() / ".monoco" / "execution"
23
- if global_path.exists():
24
- profiles.extend(_scan_dir(global_path, "Global"))
25
-
26
- # 2. Project Scope
27
- if project_root:
28
- project_path = project_root / ".monoco" / "execution"
29
- if project_path.exists():
30
- profiles.extend(_scan_dir(project_path, "Project"))
31
-
32
- return profiles
33
-
34
-
35
- def _scan_dir(base_path: Path, source: str) -> List[ExecutionProfile]:
36
- profiles = []
37
- if not base_path.is_dir():
38
- return profiles
39
-
40
- for item in base_path.iterdir():
41
- if item.is_dir():
42
- sop_path = item / "SOP.md"
43
- if sop_path.exists():
44
- profiles.append(
45
- ExecutionProfile(
46
- name=item.name, source=source, path=str(sop_path.absolute())
47
- )
48
- )
49
- return profiles
50
-
51
-
52
- def get_profile_detail(profile_path: str) -> Optional[ExecutionProfile]:
53
- path = Path(profile_path)
54
- if not path.exists():
55
- return None
56
-
57
- # Determine source (rough heuristic)
58
- source = "Project"
59
- if str(path).startswith(str(Path.home() / ".monoco")):
60
- source = "Global"
61
-
62
- return ExecutionProfile(
63
- name=path.parent.name,
64
- source=source,
65
- path=str(path.absolute()),
66
- content=path.read_text(encoding="utf-8"),
67
- )
@@ -1,38 +0,0 @@
1
- """
2
- Executor Module - Layer 3 of the Event Automation Framework.
3
-
4
- This module provides concrete Action implementations for various execution types.
5
- It is part of the three-layer architecture:
6
- - Layer 1: File Watcher
7
- - Layer 2: Action Router
8
- - Layer 3: Action Executor (this module)
9
-
10
- Available Actions:
11
- - SpawnAgentAction: Spawn agent sessions
12
- - RunPytestAction: Execute pytest tests
13
- - GitCommitAction: Perform git commits
14
- - GitPushAction: Push to remote
15
- - SendIMAction: Send notifications
16
-
17
- Example Usage:
18
- >>> from monoco.core.executor import SpawnAgentAction
19
- >>> from monoco.core.scheduler import AgentEventType
20
- >>> from monoco.core.router import ActionRouter
21
- >>>
22
- >>> action = SpawnAgentAction(role="Engineer")
23
- >>> router = ActionRouter()
24
- >>> router.register(AgentEventType.ISSUE_STAGE_CHANGED, action)
25
- """
26
-
27
- from .agent_action import SpawnAgentAction
28
- from .pytest_action import RunPytestAction
29
- from .git_action import GitCommitAction, GitPushAction
30
- from .im_action import SendIMAction
31
-
32
- __all__ = [
33
- "SpawnAgentAction",
34
- "RunPytestAction",
35
- "GitCommitAction",
36
- "GitPushAction",
37
- "SendIMAction",
38
- ]
@@ -1,254 +0,0 @@
1
- """
2
- SpawnAgentAction - Action for spawning agent sessions.
3
-
4
- Part of Layer 3 (Action Executor) in the event automation framework.
5
- Uses AgentScheduler to spawn and manage agent sessions.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import logging
11
- from typing import Any, Dict, List, Optional
12
-
13
- from monoco.core.scheduler import AgentEvent, AgentEventType, AgentScheduler, AgentTask
14
- from monoco.core.router import Action, ActionResult
15
- from monoco.features.agent.models import RoleTemplate
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class SpawnAgentAction(Action):
21
- """
22
- Action that spawns an agent session.
23
-
24
- This action creates and starts a new agent session based on the event.
25
- It supports different roles (Architect, Engineer, Reviewer, etc.)
26
- and integrates with the AgentScheduler for lifecycle management.
27
-
28
- Example:
29
- >>> action = SpawnAgentAction(
30
- ... role="Engineer",
31
- ... scheduler=scheduler,
32
- ... )
33
- >>> result = await action(event)
34
- """
35
-
36
- ROLE_TEMPLATES = {
37
- "Architect": {
38
- "description": "System Architect",
39
- "trigger": "memo.accumulation",
40
- "goal": "Process memo inbox and create issues.",
41
- "system_prompt": "You are the Architect. Process the Memo inbox.",
42
- "engine": "gemini",
43
- },
44
- "Engineer": {
45
- "description": "Software Engineer",
46
- "trigger": "issue.stage_changed",
47
- "goal": "Implement feature requirements.",
48
- "system_prompt": "You are a Software Engineer. Read the issue and implement requirements.",
49
- "engine": "gemini",
50
- },
51
- "Reviewer": {
52
- "description": "Code Reviewer",
53
- "trigger": "pr.created",
54
- "goal": "Review code changes thoroughly.",
55
- "system_prompt": "You are a Code Reviewer. Review the code changes thoroughly.",
56
- "engine": "gemini",
57
- },
58
- "Coroner": {
59
- "description": "Session Autopsy",
60
- "trigger": "session.failed",
61
- "goal": "Analyze failed session and create incident report.",
62
- "system_prompt": "You are the Coroner. Analyze the failed session.",
63
- "engine": "gemini",
64
- },
65
- }
66
-
67
- def __init__(
68
- self,
69
- role: str,
70
- scheduler: AgentScheduler,
71
- config: Optional[Dict[str, Any]] = None,
72
- custom_role_template: Optional[Dict[str, str]] = None,
73
- ):
74
- super().__init__(config)
75
- self.role = role
76
- self.scheduler = scheduler
77
- self.custom_role_template = custom_role_template
78
- self._spawned_sessions: List[str] = []
79
-
80
- @property
81
- def name(self) -> str:
82
- return f"SpawnAgentAction({self.role})"
83
-
84
- async def can_execute(self, event: AgentEvent) -> bool:
85
- """
86
- Check if we should spawn an agent.
87
-
88
- Conditions:
89
- - Scheduler has capacity for new tasks
90
- """
91
- # Check scheduler capacity
92
- stats = self.scheduler.get_stats()
93
- active_tasks = stats.get("active_tasks", 0)
94
- max_concurrent = stats.get("max_concurrent", 5)
95
-
96
- if active_tasks >= max_concurrent:
97
- logger.warning(f"Scheduler at capacity ({active_tasks}/{max_concurrent}), skipping")
98
- return False
99
-
100
- return True
101
-
102
- async def execute(self, event: AgentEvent) -> ActionResult:
103
- """Spawn an agent session."""
104
- issue_id = event.payload.get("issue_id", "unknown")
105
- issue_title = event.payload.get("issue_title", "Unknown")
106
-
107
- logger.info(f"Spawning {self.role} agent for {issue_id}")
108
-
109
- try:
110
- # Create AgentTask
111
- task = AgentTask(
112
- task_id=f"{self.role.lower()}-{issue_id}-{event.timestamp.timestamp()}",
113
- role_name=self.role,
114
- issue_id=issue_id,
115
- prompt=self._build_prompt(issue_id, issue_title),
116
- engine=self._get_engine(),
117
- timeout=self.config.get("timeout", 1800),
118
- metadata={
119
- "trigger": event.type.value,
120
- "issue_title": issue_title,
121
- },
122
- )
123
-
124
- # Schedule task
125
- session_id = await self.scheduler.schedule(task)
126
-
127
- # Track spawned session
128
- self._spawned_sessions.append(session_id)
129
-
130
- logger.info(f"{self.role} scheduled: session={session_id}")
131
-
132
- return ActionResult.success_result(
133
- output={
134
- "session_id": session_id,
135
- "issue_id": issue_id,
136
- "role": self.role,
137
- },
138
- metadata={
139
- "task_id": task.task_id,
140
- },
141
- )
142
-
143
- except Exception as e:
144
- logger.error(f"Failed to spawn {self.role} for {issue_id}: {e}")
145
- return ActionResult.failure_result(
146
- error=str(e),
147
- metadata={
148
- "issue_id": issue_id,
149
- "role": self.role,
150
- },
151
- )
152
-
153
- def _build_prompt(self, issue_id: str, issue_title: str) -> str:
154
- """Build the prompt for the agent."""
155
- template = self.ROLE_TEMPLATES.get(self.role, self.ROLE_TEMPLATES["Engineer"])
156
-
157
- return f"""You are a {template['description']}.
158
-
159
- Issue: {issue_id} - {issue_title}
160
-
161
- Goal: {template['goal']}
162
-
163
- {template['system_prompt']}
164
- """
165
-
166
- def _get_engine(self) -> str:
167
- """Get the engine for this role."""
168
- if self.custom_role_template:
169
- return self.custom_role_template.get("engine", "gemini")
170
- template = self.ROLE_TEMPLATES.get(self.role, self.ROLE_TEMPLATES["Engineer"])
171
- return template["engine"]
172
-
173
- def get_spawned_sessions(self) -> List[str]:
174
- """Get list of session IDs spawned by this action."""
175
- return self._spawned_sessions.copy()
176
-
177
- def get_stats(self) -> Dict[str, Any]:
178
- """Get action statistics."""
179
- stats = super().get_stats()
180
- stats.update({
181
- "role": self.role,
182
- "spawned_sessions": len(self._spawned_sessions),
183
- })
184
- return stats
185
-
186
-
187
- class SpawnArchitectAction(SpawnAgentAction):
188
- """Convenience action for spawning Architect agents."""
189
-
190
- def __init__(
191
- self,
192
- scheduler: AgentScheduler,
193
- config: Optional[Dict[str, Any]] = None,
194
- ):
195
- super().__init__(
196
- role="Architect",
197
- scheduler=scheduler,
198
- config=config,
199
- )
200
-
201
- async def can_execute(self, event: AgentEvent) -> bool:
202
- """Only execute for MEMO_THRESHOLD events."""
203
- if event.type != AgentEventType.MEMO_THRESHOLD:
204
- return False
205
- return await super().can_execute(event)
206
-
207
-
208
- class SpawnEngineerAction(SpawnAgentAction):
209
- """Convenience action for spawning Engineer agents."""
210
-
211
- def __init__(
212
- self,
213
- scheduler: AgentScheduler,
214
- config: Optional[Dict[str, Any]] = None,
215
- ):
216
- super().__init__(
217
- role="Engineer",
218
- scheduler=scheduler,
219
- config=config,
220
- )
221
-
222
- async def can_execute(self, event: AgentEvent) -> bool:
223
- """Only execute for ISSUE_STAGE_CHANGED to 'doing' events."""
224
- if event.type != AgentEventType.ISSUE_STAGE_CHANGED:
225
- return False
226
-
227
- new_stage = event.payload.get("new_stage")
228
- issue_status = event.payload.get("issue_status")
229
-
230
- if new_stage != "doing" or issue_status != "open":
231
- return False
232
-
233
- return await super().can_execute(event)
234
-
235
-
236
- class SpawnReviewerAction(SpawnAgentAction):
237
- """Convenience action for spawning Reviewer agents."""
238
-
239
- def __init__(
240
- self,
241
- scheduler: AgentScheduler,
242
- config: Optional[Dict[str, Any]] = None,
243
- ):
244
- super().__init__(
245
- role="Reviewer",
246
- scheduler=scheduler,
247
- config=config,
248
- )
249
-
250
- async def can_execute(self, event: AgentEvent) -> bool:
251
- """Only execute for PR_CREATED events."""
252
- if event.type != AgentEventType.PR_CREATED:
253
- return False
254
- return await super().can_execute(event)