monoco-toolkit 0.3.2__py3-none-any.whl → 0.3.3__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.
@@ -0,0 +1,129 @@
1
+ from typing import Optional
2
+ from .models import RoleTemplate
3
+
4
+
5
+ class Worker:
6
+ """
7
+ Represents an active or pending agent session assigned to a specific role and issue.
8
+ """
9
+
10
+ def __init__(self, role: RoleTemplate, issue_id: str):
11
+ self.role = role
12
+ self.issue_id = issue_id
13
+ self.status = "pending" # pending, running, suspended, terminated
14
+ self.process_id: Optional[int] = None
15
+ self._process = None
16
+
17
+ def start(self, context: Optional[dict] = None):
18
+ """
19
+ Start the worker session asynchronously.
20
+ """
21
+ # Allow restart if not currently running
22
+ if self.status == "running":
23
+ return
24
+
25
+ print(f"Starting worker {self.role.name} for issue {self.issue_id}")
26
+
27
+ try:
28
+ self._execute_work(context)
29
+ self.status = "running"
30
+ except Exception as e:
31
+ print(f"Worker failed to start: {e}")
32
+ self.status = "failed"
33
+ raise
34
+
35
+ def _execute_work(self, context: Optional[dict] = None):
36
+ import subprocess
37
+ import sys
38
+
39
+ # Prepare the prompt
40
+ if self.role.name == "drafter" and context:
41
+ issue_type = context.get("type", "feature")
42
+ description = context.get("description", "No description")
43
+ prompt = (
44
+ f"You are a Drafter in the Monoco project.\n\n"
45
+ f"Task: Create a new {issue_type} issue based on this request: {description}\n\n"
46
+ "Constraints:\n"
47
+ "1. Use 'monoco issue create' to generate the file.\n"
48
+ "2. Use 'monoco issue update' or direct file editing to enrich Objective and Tasks.\n"
49
+ "3. IMPORTANT: Once the issue file is created and filled with high-quality content, EXIT search or interactive mode immediately.\n"
50
+ "4. Do not perform any other development tasks."
51
+ )
52
+ else:
53
+ prompt = (
54
+ f"{self.role.system_prompt}\n\n"
55
+ f"Issue context: {self.issue_id}\n"
56
+ f"Goal: {self.role.goal}\n"
57
+ )
58
+ if context and "description" in context:
59
+ prompt += f"Specific Task: {context['description']}"
60
+
61
+ engine = self.role.engine
62
+
63
+ print(f"[{self.role.name}] Engine: {engine}")
64
+ print(f"[{self.role.name}] Goal: {self.role.goal}")
65
+
66
+ try:
67
+ # Execute CLI agent with YOLO mode
68
+ engine_args = (
69
+ [engine, "-y", prompt] if engine == "gemini" else [engine, prompt]
70
+ )
71
+
72
+ self._process = subprocess.Popen(
73
+ engine_args, stdout=sys.stdout, stderr=sys.stderr, text=True
74
+ )
75
+ self.process_id = self._process.pid
76
+
77
+ # DO NOT WAIT HERE.
78
+ # The scheduler/monitoring loop is responsible for checking status.
79
+
80
+ except FileNotFoundError:
81
+ raise RuntimeError(
82
+ f"Agent engine '{engine}' not found. Please ensure it is installed and in PATH."
83
+ )
84
+ except Exception as e:
85
+ print(f"[{self.role.name}] Process Error: {e}")
86
+ raise
87
+
88
+ def poll(self) -> str:
89
+ """
90
+ Check process status. Returns current worker status.
91
+ Updates self.status if process has finished.
92
+ """
93
+ if not self._process:
94
+ return self.status
95
+
96
+ returncode = self._process.poll()
97
+ if returncode is None:
98
+ return "running"
99
+
100
+ if returncode == 0:
101
+ self.status = "completed"
102
+ else:
103
+ self.status = "failed"
104
+
105
+ return self.status
106
+
107
+ def wait(self):
108
+ """
109
+ Block until process finishes.
110
+ """
111
+ if self._process:
112
+ self._process.wait()
113
+ self.poll() # Update status
114
+
115
+ def stop(self):
116
+ """
117
+ Stop the worker session.
118
+ """
119
+ if self.status == "terminated":
120
+ return
121
+
122
+ print(f"Stopping worker {self.role.name} for issue {self.issue_id}")
123
+ self.status = "terminated"
124
+ self.process_id = None
125
+
126
+ def __repr__(self):
127
+ return (
128
+ f"<Worker role={self.role.name} issue={self.issue_id} status={self.status}>"
129
+ )
monoco/main.py CHANGED
@@ -166,6 +166,10 @@ app.add_typer(config_cmd.app, name="config", help="Manage configuration")
166
166
  app.add_typer(project_cmd.app, name="project", help="Manage projects")
167
167
  app.add_typer(workspace_cmd.app, name="workspace", help="Manage workspace")
168
168
 
169
+ from monoco.features.scheduler import cli as scheduler_cmd
170
+
171
+ app.add_typer(scheduler_cmd.app, name="agent", help="Manage agent sessions")
172
+
169
173
 
170
174
  from monoco.daemon.commands import serve
171
175
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: monoco-toolkit
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Agent Native Toolkit for Monoco - Task Management & Kanban for AI Agents
5
5
  Project-URL: Homepage, https://monoco.io
6
6
  Project-URL: Repository, https://github.com/IndenScale/Monoco
@@ -1,29 +1,24 @@
1
- monoco/main.py,sha256=_ZhokFhmYF1NNHubuVr9gZ_HLg6AaZPWMppnUESbpwA,5346
1
+ monoco/main.py,sha256=YGQ-A3UlD12cyvJscFfV_BFpeHxlm_LJn_WVIaLrINc,5484
2
2
  monoco/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  monoco/cli/project.py,sha256=FqLaDD3hiWxa0_TKzxEF7PYH9RPsvmLyjO3NYVckgGs,2737
4
4
  monoco/cli/workspace.py,sha256=1TVVS835XyirLDvBGQXSblIaYVhe2Pk9EpORDvcktyk,1538
5
5
  monoco/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- monoco/core/config.py,sha256=DsxkUaHbKK17JuMzcYLVBWnLhhs78HC_gday2fyx6sw,12250
6
+ monoco/core/config.py,sha256=M9F5De2FUgaIxFwqZBoJUXsMsph31rNh-4U-k_9rUHA,13543
7
7
  monoco/core/execution.py,sha256=7522s7uVWiNdMKtWlIb8JHzbAcJgLVuxUFp_6VmhWNQ,1801
8
8
  monoco/core/feature.py,sha256=bcK0C1CaQgNLcdMF0RuniaaQRCYSA-0JdOpQz9W_1xM,1968
9
9
  monoco/core/git.py,sha256=d3A8L30nlEj0F1pwSHk_cOoRYi1dMCiZc7KD70ViMyw,8335
10
10
  monoco/core/hooks.py,sha256=QY4c74LIYDtcjtWYax1PrK5CtVNKYTfVVVBOj-SCulo,1826
11
11
  monoco/core/injection.py,sha256=zs9RpXaBfwP-fbwOU_X2nm-AH94tQN9tVWXNtV4tCYM,6887
12
- monoco/core/integrations.py,sha256=GOWwDceEuhU6Q1nX8vvyGQ7s_kzuW0lKK23nEFkzCT0,7912
12
+ monoco/core/integrations.py,sha256=ifSz7I87DXHF-eHLyXRN3pY1-Kh8IjvMyIrG9iogGpw,7744
13
13
  monoco/core/lsp.py,sha256=GOrHzubkMWS3iBpP4H2fTFW-0JXa6D2YlU0ZEhp5zmY,2018
14
14
  monoco/core/output.py,sha256=ZlUydZ65FIP5a32_eESIIvMrhcSwghIqZ3N23OSx6Ss,3878
15
15
  monoco/core/registry.py,sha256=1m9wvkcqWqrTcARt-jUGHH1BEYMvp0NJGRkq-uduJeg,1167
16
16
  monoco/core/setup.py,sha256=YUIhzqZwjEeH7zGZm9SPA-OhgTISJtA6ByNFCGBznUU,11487
17
17
  monoco/core/skills.py,sha256=KeVN09KIcRiksjJ91AggJbN_EvjRQfMGqd_hUQfUjes,16168
18
18
  monoco/core/state.py,sha256=dfoTH1Sj_TSjtTEXuv4w0ZOp_Fd30XNymVSZo30Xshg,1820
19
- monoco/core/sync.py,sha256=DJXnWM_LxN36VniTUS5axFiAhVXYHbxLbI1uaUvV1JU,8750
19
+ monoco/core/sync.py,sha256=1p37pTfGBanogzcLhdTfMTxDqq79B5iiwiPS4fiuQaA,8378
20
20
  monoco/core/telemetry.py,sha256=GQDbtgrZwAL1ZpjgbJZuawbTyH6J0NjMXMi4ogq-Ang,2915
21
21
  monoco/core/workspace.py,sha256=H_PHD5A0HZFq841u1JtLoFjkXdQg9D6x6I7QcFtJge4,3000
22
- monoco/core/agent/__init__.py,sha256=iU3wac9iYecA1M2jaYnnPjU9wANdAS7B-jRAuT5gCo4,38
23
- monoco/core/agent/action.py,sha256=qkQGJERhL4N5MSuwDivn8NAw4sMhEa346huGtqNyFTM,5295
24
- monoco/core/agent/adapters.py,sha256=q6obVcXve46mVNBaSCBRFRYs8gImv6Y2gAJVv173Z6w,4376
25
- monoco/core/agent/protocol.py,sha256=eB0OiMjDQu1DOoeVqb_PpHsMyPy2LdNenNyt4vJ9qbQ,798
26
- monoco/core/agent/state.py,sha256=Z8qqY7loms5nt3524_TrEMmlpnurxf0aHfiCT1FgvDI,3385
27
22
  monoco/core/resources/en/AGENTS.md,sha256=vf9z43UU-LPwYKPWCrtw8TpWrmkeZ6zfMyHP4Q9JqdQ,1178
28
23
  monoco/core/resources/en/SKILL.md,sha256=wBXNpPqATOxO8TGiOM8S091gMNZ8du_WhQubO6TfNyg,3504
29
24
  monoco/core/resources/zh/AGENTS.md,sha256=KBNZSCPBIals6jDdvG5wJgjZuswi_1nKljggJSMtfy8,1156
@@ -46,16 +41,18 @@ monoco/features/i18n/resources/en/SKILL.md,sha256=Z8fAAqeMvpLDw1D_9AzZIykS5-HLVM
46
41
  monoco/features/i18n/resources/zh/AGENTS.md,sha256=lKkwLLCADRH7UDq9no4eQY2sRfJrb64JoZ_HNved8vA,175
47
42
  monoco/features/i18n/resources/zh/SKILL.md,sha256=Wynk7zVBW7CO0_6AEQNUvlD_3eMW_7EPnzEn9QUaiDs,1884
48
43
  monoco/features/issue/adapter.py,sha256=4dzKg4-0XH63uORoh8qcolvKxJR6McBDIYxYEcZJJkA,1204
49
- monoco/features/issue/commands.py,sha256=ndBE_3tP4CZK45lqAbsGpeopzvebMZKeIezZchTGIsc,37353
50
- monoco/features/issue/core.py,sha256=WoWZRpw7o9f6ERqqIgD35c-SYD7Jv1aTsJVj6ljPz9E,50369
51
- monoco/features/issue/linter.py,sha256=DQ3UruGUUMZu63EFn71eczoFDoqj31JxHHaRR3Ej6O8,21067
44
+ monoco/features/issue/commands.py,sha256=V5EVdmin-JPoqdV9-vrbKcZ7HXWHZt-zq_mM1kqFVu0,38163
45
+ monoco/features/issue/core.py,sha256=kRyUbYqEBno0Zoogq7dMIcQv5M0QIRSlptvzp0pfImE,51302
46
+ monoco/features/issue/domain_commands.py,sha256=eatSF_uZp4nGpVr2PIgb00MWfEBm0OnyAd4JvUJEAAA,1535
47
+ monoco/features/issue/domain_service.py,sha256=bEs_WXOWmotgIR-lGwyWekF4nonvjsgrK1YG3pyVfwk,2564
48
+ monoco/features/issue/linter.py,sha256=lBXWj3T9OK4ysU33Mr-gzO7wcxXxl4Mu_IMgv4ePwjQ,26502
52
49
  monoco/features/issue/migration.py,sha256=i0xlxZjrpmuHGHOAIN4iu31EwwVIvZn7yjveS-kU22c,4896
53
50
  monoco/features/issue/models.py,sha256=7oIMxvUEfe00n7wni9bZgKU2e9404flvArixbLQ95Dg,5902
54
51
  monoco/features/issue/monitor.py,sha256=vZN0TbR3V5fHKHRGkIhimO6UwWcwYjDHQs2qzjEG174,3549
55
- monoco/features/issue/validator.py,sha256=Rbn5M_RLEHVPi5n0Y19-VCWZvVlRWcCRFYRbsw-Oc-I,24732
52
+ monoco/features/issue/validator.py,sha256=8N921_3B2Cd3hFjRRMFPwCZh9Pbc6CsGXFRt28X9KLQ,26798
56
53
  monoco/features/issue/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
54
  monoco/features/issue/domain/lifecycle.py,sha256=oIuLYTVy1RRHGngAzbNU4kTWOHMBhjuw_TuYCwNqMJw,4991
58
- monoco/features/issue/domain/models.py,sha256=eo5Ks7VzWDQsqzuu9gcBuxIafvfqnzUDNDG9wbaghsc,5797
55
+ monoco/features/issue/domain/models.py,sha256=2vuSAGaFrbAEkQapwkGG_T8cHlsmARN71fTA44PIVSM,5850
59
56
  monoco/features/issue/domain/parser.py,sha256=axfB9BjY-m4DQqYwTQBm_38plOidZaDqFtsIejW_kYI,9095
60
57
  monoco/features/issue/domain/workspace.py,sha256=nyTEFNmpLPuX7nssMCBiFI1XBhY9zbqG877Ppp_s0Fg,3658
61
58
  monoco/features/issue/engine/__init__.py,sha256=-SBvzjlZJz0O-IB1c95d9pbhr3lydJNLq2vhnfkZ8PQ,747
@@ -68,6 +65,15 @@ monoco/features/issue/resources/en/AGENTS.md,sha256=7lMJdLhokispHVTrViIyMaz5TOJ8
68
65
  monoco/features/issue/resources/en/SKILL.md,sha256=M5khTJdraQenX0tf7RHOVcqyKHC285qwDzWdsCJ7XPI,4032
69
66
  monoco/features/issue/resources/zh/AGENTS.md,sha256=veb22lU0qindYcsNsuGLqzpBEjExCPDOkX5Q39OjZz0,1113
70
67
  monoco/features/issue/resources/zh/SKILL.md,sha256=XFE33cCCos0U1IVNkMhU7fGuqVqfxo_FNKdp1aScaoM,5055
68
+ monoco/features/scheduler/__init__.py,sha256=hvE9A14qFTeTYbWlK9iMRdXIFLMBuG8724GEZzFTHB8,483
69
+ monoco/features/scheduler/cli.py,sha256=peRfALi_4etXpyn9SKl98lNJhUdNWlgX1GVo0xXSlLI,5859
70
+ monoco/features/scheduler/config.py,sha256=4kC9ll-bFd8jldzxF29D94inn4HR2DkDEO-4u0LqjXA,1175
71
+ monoco/features/scheduler/defaults.py,sha256=4Ne3RpF7gZ46BmHtF0hC8wYkuCIkDSuKB7hGnHhGXms,2208
72
+ monoco/features/scheduler/manager.py,sha256=PmNKqfktgrdpUwE-jPE_WX3IvzZat0JKdEcmzse08-0,1680
73
+ monoco/features/scheduler/models.py,sha256=rb8rx8t_TD5ilUC6xU3jvK0sud7YUZE_iEdes9Np8R0,877
74
+ monoco/features/scheduler/reliability.py,sha256=AGs87DPkAa0IMYtQ5x7dpAOeTB4NTRh37FJsiqZY9Y4,3624
75
+ monoco/features/scheduler/session.py,sha256=V1xFXA65BaERhBwRYIecaye2TjDtFvAzuIy2gZ68ag8,2954
76
+ monoco/features/scheduler/worker.py,sha256=4eFyC3118ZuUnmiFVe3nSTUsIOn-N2uPXpCP_1FSTRg,4238
71
77
  monoco/features/skills/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
78
  monoco/features/skills/core.py,sha256=BXVvMot8E457-pj9U19lPHypBVhdLQRsx0zEAXJxpGY,3436
73
79
  monoco/features/spike/adapter.py,sha256=ZIpw-xNVoA2IIP18dbR-dmQGbQmK5ua_P-uGXuDjCeI,1021
@@ -77,8 +83,8 @@ monoco/features/spike/resources/en/AGENTS.md,sha256=NG3CMnlDk_0J8hnRUcueAM9lgIQr
77
83
  monoco/features/spike/resources/en/SKILL.md,sha256=qKDcVh0D3pDRvfNLh1Bzo4oQU3obpl4tqdlzxeiWYMk,1911
78
84
  monoco/features/spike/resources/zh/AGENTS.md,sha256=5RHNl7fc3RdYYTFH483ojJl_arGPKkyYziOuGgFbqqg,290
79
85
  monoco/features/spike/resources/zh/SKILL.md,sha256=Q82e9lCQOAYIwBs5rGnvlVUDq7bp0pz8yvO10KTWFYQ,1710
80
- monoco_toolkit-0.3.2.dist-info/METADATA,sha256=9Xyk-ImLkR4WvBAz6eQb8_N-QZiJ7TPu27P3u48Y35c,4866
81
- monoco_toolkit-0.3.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
82
- monoco_toolkit-0.3.2.dist-info/entry_points.txt,sha256=iYj7FWYBdtClU15-Du1skqD0s6SFSIhJvxJ29VWp8ng,43
83
- monoco_toolkit-0.3.2.dist-info/licenses/LICENSE,sha256=ACAGGjV6aod4eIlVUTx1q9PZbnZGN5bBwkSs9RHj83s,1071
84
- monoco_toolkit-0.3.2.dist-info/RECORD,,
86
+ monoco_toolkit-0.3.3.dist-info/METADATA,sha256=GXPy1vr5MxL9afZc7r-vvUUOofmjiShBZe3qA9X9MHc,4866
87
+ monoco_toolkit-0.3.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
88
+ monoco_toolkit-0.3.3.dist-info/entry_points.txt,sha256=iYj7FWYBdtClU15-Du1skqD0s6SFSIhJvxJ29VWp8ng,43
89
+ monoco_toolkit-0.3.3.dist-info/licenses/LICENSE,sha256=ACAGGjV6aod4eIlVUTx1q9PZbnZGN5bBwkSs9RHj83s,1071
90
+ monoco_toolkit-0.3.3.dist-info/RECORD,,
@@ -1,3 +0,0 @@
1
- """
2
- Monoco Agent Execution Layer.
3
- """
@@ -1,168 +0,0 @@
1
- import re
2
- import yaml
3
- from pathlib import Path
4
- from typing import Dict, List, Optional, Any
5
- from pydantic import BaseModel
6
-
7
-
8
- class ActionContext(BaseModel):
9
- """Context information for matching actions."""
10
-
11
- id: Optional[str] = None
12
- type: Optional[str] = None
13
- stage: Optional[str] = None
14
- status: Optional[str] = None
15
- file_path: Optional[str] = None
16
- project_id: Optional[str] = None
17
-
18
-
19
- class ActionWhen(BaseModel):
20
- """Conditions under which an action should be displayed/active."""
21
-
22
- idMatch: Optional[str] = None
23
- typeMatch: Optional[str] = None
24
- stageMatch: Optional[str] = None
25
- statusMatch: Optional[str] = None
26
- fileMatch: Optional[str] = None
27
-
28
- def matches(self, context: ActionContext) -> bool:
29
- """Evaluate if the context matches these criteria."""
30
- if self.idMatch and context.id and not re.match(self.idMatch, context.id):
31
- return False
32
- if (
33
- self.typeMatch
34
- and context.type
35
- and not re.match(self.typeMatch, context.type)
36
- ):
37
- return False
38
- if (
39
- self.stageMatch
40
- and context.stage
41
- and not re.match(self.stageMatch, context.stage)
42
- ):
43
- return False
44
- if (
45
- self.statusMatch
46
- and context.status
47
- and not re.match(self.statusMatch, context.status)
48
- ):
49
- return False
50
- if (
51
- self.fileMatch
52
- and context.file_path
53
- and not re.match(self.fileMatch, context.file_path)
54
- ):
55
- return False
56
- return True
57
-
58
-
59
- class PromptyAction(BaseModel):
60
- name: str
61
- description: str
62
- version: Optional[str] = "1.0.0"
63
- authors: List[str] = []
64
- model: Dict[str, Any] = {}
65
- inputs: Dict[str, Any] = {}
66
- outputs: Dict[str, Any] = {}
67
- template: str
68
- when: Optional[ActionWhen] = None
69
-
70
- # Monoco specific metadata
71
- path: Optional[str] = None
72
- provider: Optional[str] = None # Derived from model.api or explicitly set
73
-
74
-
75
- class ActionRegistry:
76
- def __init__(self, project_root: Optional[Path] = None):
77
- self.project_root = project_root
78
- self._actions: List[PromptyAction] = []
79
-
80
- def scan(self) -> List[PromptyAction]:
81
- """Scan user global and project local directories for .prompty files."""
82
- self._actions = []
83
-
84
- # 1. User Global: ~/.monoco/actions/
85
- user_dir = Path.home() / ".monoco" / "actions"
86
- self._scan_dir(user_dir)
87
-
88
- # 2. Project Local: {project_root}/.monoco/actions/
89
- if self.project_root:
90
- project_dir = self.project_root / ".monoco" / "actions"
91
- self._scan_dir(project_dir)
92
-
93
- return self._actions
94
-
95
- def _scan_dir(self, directory: Path):
96
- if not directory.exists():
97
- return
98
-
99
- for prompty_file in directory.glob("*.prompty"):
100
- try:
101
- action = self._load_action(prompty_file)
102
- if action:
103
- self._actions.append(action)
104
- except Exception as e:
105
- print(f"Failed to load action {prompty_file}: {e}")
106
-
107
- def _load_action(self, file_path: Path) -> Optional[PromptyAction]:
108
- content = file_path.read_text(encoding="utf-8")
109
-
110
- # Prompty Parser (Standard YAML Frontmatter + Body)
111
- # We look for the first --- and the second ---
112
- parts = re.split(r"^---\s*$", content, maxsplit=2, flags=re.MULTILINE)
113
-
114
- if len(parts) < 3:
115
- return None
116
-
117
- frontmatter_raw = parts[1]
118
- body = parts[2].strip()
119
-
120
- try:
121
- meta = yaml.safe_load(frontmatter_raw)
122
- if not meta or "name" not in meta:
123
- # Use filename as fallback name if missing? Prompty usually requires name.
124
- if not meta:
125
- meta = {}
126
- meta["name"] = meta.get("name", file_path.stem)
127
-
128
- # Map Prompty 'when' if present
129
- when_data = meta.get("when")
130
- when = ActionWhen(**when_data) if when_data else None
131
-
132
- action = PromptyAction(
133
- name=meta["name"],
134
- description=meta.get("description", ""),
135
- version=meta.get("version"),
136
- authors=meta.get("authors", []),
137
- model=meta.get("model", {}),
138
- inputs=meta.get("inputs", {}),
139
- outputs=meta.get("outputs", {}),
140
- template=body,
141
- when=when,
142
- path=str(file_path.absolute()),
143
- provider=meta.get("provider") or meta.get("model", {}).get("api"),
144
- )
145
- return action
146
-
147
- except Exception as e:
148
- print(f"Invalid Prompty in {file_path}: {e}")
149
- return None
150
-
151
- def list_available(
152
- self, context: Optional[ActionContext] = None
153
- ) -> List[PromptyAction]:
154
- if not self._actions:
155
- self.scan()
156
-
157
- if not context:
158
- return self._actions
159
-
160
- return [a for a in self._actions if not a.when or a.when.matches(context)]
161
-
162
- def get(self, name: str) -> Optional[PromptyAction]:
163
- if not self._actions:
164
- self.scan()
165
- for a in self._actions:
166
- if a.name == name:
167
- return a
168
- return None
@@ -1,133 +0,0 @@
1
- """
2
- CLI Adapters for Agent Frameworks.
3
- """
4
-
5
- import shutil
6
- from typing import List
7
- from pathlib import Path
8
- from .protocol import AgentClient
9
-
10
-
11
- class BaseCLIClient:
12
- def __init__(self, executable: str):
13
- self._executable = executable
14
-
15
- @property
16
- def name(self) -> str:
17
- return self._executable
18
-
19
- async def available(self) -> bool:
20
- return shutil.which(self._executable) is not None
21
-
22
- def _build_prompt(self, prompt: str, context_files: List[Path]) -> str:
23
- """Concatenate prompt and context files."""
24
- # Inject Language Rule
25
- try:
26
- from monoco.core.config import get_config
27
-
28
- settings = get_config()
29
- lang = settings.i18n.source_lang
30
- if lang:
31
- prompt = f"{prompt}\n\n[SYSTEM: LANGUAGE CONSTRAINT]\nThe project source language is '{lang}'. You MUST use '{lang}' for all thinking and reporting unless explicitly instructed otherwise."
32
- except Exception:
33
- pass
34
-
35
- full_prompt = [prompt]
36
- if context_files:
37
- full_prompt.append("\n\n--- CONTEXT FILES ---")
38
- for file_path in context_files:
39
- try:
40
- full_prompt.append(f"\nFile: {file_path}")
41
- full_prompt.append("```")
42
- # Read file content safely
43
- full_prompt.append(
44
- file_path.read_text(encoding="utf-8", errors="replace")
45
- )
46
- full_prompt.append("```")
47
- except Exception as e:
48
- full_prompt.append(f"Error reading {file_path}: {e}")
49
- full_prompt.append("--- END CONTEXT ---\n")
50
- return "\n".join(full_prompt)
51
-
52
- async def _run_command(self, args: List[str]) -> str:
53
- """Run the CLI command and return stdout."""
54
- # Using synchronous subprocess in async function for now
55
- # Ideally this should use asyncio.create_subprocess_exec
56
- import asyncio
57
-
58
- proc = await asyncio.create_subprocess_exec(
59
- *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
60
- )
61
-
62
- stdout, stderr = await proc.communicate()
63
-
64
- if proc.returncode != 0:
65
- error_msg = stderr.decode().strip()
66
- raise RuntimeError(
67
- f"Agent CLI failed (code {proc.returncode}): {error_msg}"
68
- )
69
-
70
- return stdout.decode().strip()
71
-
72
-
73
- class GeminiClient(BaseCLIClient, AgentClient):
74
- """Adapter for Google Gemini CLI."""
75
-
76
- def __init__(self):
77
- super().__init__("gemini")
78
-
79
- async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
80
- full_prompt = self._build_prompt(prompt, context_files)
81
- # Usage: gemini "prompt"
82
- return await self._run_command([self._executable, full_prompt])
83
-
84
-
85
- class ClaudeClient(BaseCLIClient, AgentClient):
86
- """Adapter for Anthropic Claude CLI."""
87
-
88
- def __init__(self):
89
- super().__init__("claude")
90
-
91
- async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
92
- full_prompt = self._build_prompt(prompt, context_files)
93
- # Usage: claude -p "prompt"
94
- return await self._run_command([self._executable, "-p", full_prompt])
95
-
96
-
97
- class QwenClient(BaseCLIClient, AgentClient):
98
- """Adapter for Alibaba Qwen CLI."""
99
-
100
- def __init__(self):
101
- super().__init__("qwen")
102
-
103
- async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
104
- full_prompt = self._build_prompt(prompt, context_files)
105
- # Usage: qwen "prompt"
106
- return await self._run_command([self._executable, full_prompt])
107
-
108
-
109
- class KimiClient(BaseCLIClient, AgentClient):
110
- """Adapter for Moonshot Kimi CLI."""
111
-
112
- def __init__(self):
113
- super().__init__("kimi")
114
-
115
- async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
116
- full_prompt = self._build_prompt(prompt, context_files)
117
- # Usage: kimi "prompt"
118
- return await self._run_command([self._executable, full_prompt])
119
-
120
-
121
- _ADAPTERS = {
122
- "gemini": GeminiClient,
123
- "claude": ClaudeClient,
124
- "qwen": QwenClient,
125
- "kimi": KimiClient,
126
- }
127
-
128
-
129
- def get_agent_client(name: str) -> AgentClient:
130
- """Factory to get agent client by name."""
131
- if name not in _ADAPTERS:
132
- raise ValueError(f"Unknown agent provider: {name}")
133
- return _ADAPTERS[name]()
@@ -1,32 +0,0 @@
1
- """
2
- Protocol definition for Agent Clients.
3
- """
4
-
5
- from typing import Protocol, List
6
- from pathlib import Path
7
-
8
-
9
- class AgentClient(Protocol):
10
- """Protocol for interacting with CLI-based agents."""
11
-
12
- @property
13
- def name(self) -> str:
14
- """Name of the agent provider (e.g. 'gemini', 'claude')."""
15
- ...
16
-
17
- async def available(self) -> bool:
18
- """Check if the agent is available in the current environment."""
19
- ...
20
-
21
- async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
22
- """
23
- Execute a prompt against the agent.
24
-
25
- Args:
26
- prompt: The main instructions.
27
- context_files: List of files to provide as context.
28
-
29
- Returns:
30
- The raw string response from the agent.
31
- """
32
- ...
@@ -1,106 +0,0 @@
1
- """
2
- Agent State Management.
3
-
4
- Handles persistence and retrieval of agent availability state.
5
- """
6
-
7
- import yaml
8
- import logging
9
- from pathlib import Path
10
- from datetime import datetime, timezone
11
- from typing import Dict, Optional
12
- from pydantic import BaseModel
13
-
14
- logger = logging.getLogger("monoco.core.agent.state")
15
-
16
-
17
- class AgentProviderState(BaseModel):
18
- available: bool
19
- path: Optional[str] = None
20
- error: Optional[str] = None
21
- latency_ms: Optional[int] = None
22
-
23
-
24
- class AgentState(BaseModel):
25
- last_checked: datetime
26
- providers: Dict[str, AgentProviderState]
27
-
28
- @property
29
- def is_stale(self) -> bool:
30
- """Check if the state is older than 7 days."""
31
- delta = datetime.now(timezone.utc) - self.last_checked
32
- return delta.days > 7
33
-
34
-
35
- class AgentStateManager:
36
- def __init__(self, state_path: Path = Path.home() / ".monoco" / "agent_state.yaml"):
37
- self.state_path = state_path
38
- self._state: Optional[AgentState] = None
39
-
40
- def load(self) -> Optional[AgentState]:
41
- """Load state from file, returning None if missing or invalid."""
42
- if not self.state_path.exists():
43
- return None
44
-
45
- try:
46
- with open(self.state_path, "r") as f:
47
- data = yaml.safe_load(f)
48
- if not data:
49
- return None
50
- # Handle ISO string to datetime conversion if needed provided by Pydantic mostly
51
- return AgentState(**data)
52
- except Exception as e:
53
- logger.warning(f"Failed to load agent state: {e}")
54
- return None
55
-
56
- def get_or_refresh(self, force: bool = False) -> AgentState:
57
- """Get current state, refreshing if missing, stale, or forced."""
58
- if not force:
59
- self._state = self.load()
60
- if self._state and not self._state.is_stale:
61
- return self._state
62
-
63
- return self.refresh()
64
-
65
- def refresh(self) -> AgentState:
66
- """Run diagnostics on all integrations and update state."""
67
- logger.info("Refreshing agent state...")
68
-
69
- from monoco.core.integrations import get_all_integrations
70
- from monoco.core.config import get_config
71
-
72
- # Load config to get possible overrides
73
- # Determine root (hacky for now, should be passed)
74
- root = Path.cwd()
75
- config = get_config(str(root))
76
-
77
- integrations = get_all_integrations(
78
- config_overrides=config.agent.integrations, enabled_only=True
79
- )
80
-
81
- providers = {}
82
- for key, integration in integrations.items():
83
- if not integration.bin_name:
84
- continue # Skip integrations that don't have a binary component
85
-
86
- health = integration.check_health()
87
- providers[key] = AgentProviderState(
88
- available=health.available,
89
- path=health.path,
90
- error=health.error,
91
- latency_ms=health.latency_ms,
92
- )
93
-
94
- state = AgentState(last_checked=datetime.now(timezone.utc), providers=providers)
95
-
96
- # Save state
97
- self.state_path.parent.mkdir(parents=True, exist_ok=True)
98
- with open(self.state_path, "w") as f:
99
- yaml.dump(state.model_dump(mode="json"), f)
100
-
101
- self._state = state
102
- return state
103
-
104
- def _find_script(self) -> Optional[Path]:
105
- """[Deprecated] No longer used."""
106
- return None