aiagent-runner 0.1.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,9 @@
1
+ # aiagent_runner - Runner for AI Agent PM
2
+ # Executes tasks via MCP protocol and CLI tools (claude, gemini, etc.)
3
+
4
+ __version__ = "0.1.0"
5
+
6
+ from aiagent_runner.config import RunnerConfig
7
+ from aiagent_runner.runner import Runner, run, run_async
8
+
9
+ __all__ = ["RunnerConfig", "Runner", "run", "run_async", "__version__"]
@@ -0,0 +1,282 @@
1
+ # src/aiagent_runner/__main__.py
2
+ # Entry point for AI Agent PM Runner/Coordinator
3
+ # Reference: docs/plan/PHASE3_PULL_ARCHITECTURE.md - Phase 3-5 (legacy Runner)
4
+ # Reference: docs/plan/PHASE4_COORDINATOR_ARCHITECTURE.md (Coordinator)
5
+
6
+ import argparse
7
+ import logging
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from aiagent_runner.config import RunnerConfig
12
+ from aiagent_runner.coordinator import run_coordinator
13
+ from aiagent_runner.coordinator_config import CoordinatorConfig
14
+ from aiagent_runner.runner import run
15
+
16
+
17
+ def setup_logging(verbose: bool = False) -> None:
18
+ """Configure logging.
19
+
20
+ Args:
21
+ verbose: If True, enable debug logging
22
+ """
23
+ level = logging.DEBUG if verbose else logging.INFO
24
+ logging.basicConfig(
25
+ level=level,
26
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
27
+ datefmt="%Y-%m-%d %H:%M:%S"
28
+ )
29
+
30
+
31
+ def parse_args() -> argparse.Namespace:
32
+ """Parse command line arguments.
33
+
34
+ Returns:
35
+ Parsed arguments
36
+ """
37
+ parser = argparse.ArgumentParser(
38
+ prog="aiagent-runner",
39
+ description="Runner/Coordinator for AI Agent PM - executes tasks via MCP and CLI"
40
+ )
41
+
42
+ # Mode selection
43
+ parser.add_argument(
44
+ "--coordinator",
45
+ action="store_true",
46
+ help="Run in Coordinator mode (Phase 4: single orchestrator for all agents)"
47
+ )
48
+
49
+ # Common arguments
50
+ parser.add_argument(
51
+ "-c", "--config",
52
+ type=Path,
53
+ help="Path to YAML configuration file"
54
+ )
55
+ parser.add_argument(
56
+ "-v", "--verbose",
57
+ action="store_true",
58
+ help="Enable verbose (debug) logging"
59
+ )
60
+
61
+ # Legacy Runner arguments (deprecated, use Coordinator mode instead)
62
+ parser.add_argument(
63
+ "--agent-id",
64
+ help="[Legacy Runner] Agent ID (overrides config/env)"
65
+ )
66
+ parser.add_argument(
67
+ "--passkey",
68
+ help="[Legacy Runner] Agent passkey (overrides config/env)"
69
+ )
70
+ parser.add_argument(
71
+ "--project-id",
72
+ help="[Legacy Runner] Project ID (Phase 4 required)"
73
+ )
74
+ parser.add_argument(
75
+ "--polling-interval",
76
+ type=int,
77
+ help="Polling interval in seconds (default: 5 for Runner, 10 for Coordinator)"
78
+ )
79
+ parser.add_argument(
80
+ "--cli-command",
81
+ help="[Legacy Runner] CLI command to use (default: claude)"
82
+ )
83
+ parser.add_argument(
84
+ "--working-directory",
85
+ type=Path,
86
+ help="[Legacy Runner] Working directory for CLI execution"
87
+ )
88
+ parser.add_argument(
89
+ "--log-directory",
90
+ type=Path,
91
+ help="Directory for execution logs"
92
+ )
93
+
94
+ return parser.parse_args()
95
+
96
+
97
+ def load_runner_config(args: argparse.Namespace) -> RunnerConfig:
98
+ """Load configuration for legacy Runner mode.
99
+
100
+ Priority (highest to lowest):
101
+ 1. CLI arguments
102
+ 2. Config file
103
+ 3. Environment variables
104
+
105
+ Args:
106
+ args: Parsed CLI arguments
107
+
108
+ Returns:
109
+ RunnerConfig instance
110
+ """
111
+ # Start with config file or environment
112
+ if args.config and args.config.exists():
113
+ config = RunnerConfig.from_yaml(args.config)
114
+ else:
115
+ try:
116
+ config = RunnerConfig.from_env()
117
+ except ValueError as e:
118
+ # If no config file and env vars missing, check CLI args
119
+ if args.agent_id and args.passkey and args.project_id:
120
+ config = RunnerConfig(
121
+ agent_id=args.agent_id,
122
+ passkey=args.passkey,
123
+ project_id=args.project_id
124
+ )
125
+ else:
126
+ raise e
127
+
128
+ # Override with CLI arguments
129
+ if args.agent_id:
130
+ config.agent_id = args.agent_id
131
+ if args.passkey:
132
+ config.passkey = args.passkey
133
+ if args.project_id:
134
+ config.project_id = args.project_id
135
+ if args.polling_interval:
136
+ config.polling_interval = args.polling_interval
137
+ if args.cli_command:
138
+ config.cli_command = args.cli_command
139
+ if args.working_directory:
140
+ config.working_directory = str(args.working_directory)
141
+ if args.log_directory:
142
+ config.log_directory = str(args.log_directory)
143
+
144
+ return config
145
+
146
+
147
+ def get_default_config_path() -> Path:
148
+ """Get the default configuration file path.
149
+
150
+ The default config is located at:
151
+ runner/config/coordinator_default.yaml
152
+
153
+ Returns:
154
+ Path to default config file
155
+ """
156
+ # Find the runner package root (where config/ directory is)
157
+ # __file__ is runner/src/aiagent_runner/__main__.py
158
+ # parent = runner/src/aiagent_runner/
159
+ # parent.parent = runner/src/
160
+ # parent.parent.parent = runner/
161
+ runner_root = Path(__file__).parent.parent.parent
162
+ return runner_root / "config" / "coordinator_default.yaml"
163
+
164
+
165
+ def load_coordinator_config(args: argparse.Namespace) -> CoordinatorConfig:
166
+ """Load configuration for Coordinator mode.
167
+
168
+ Configuration loading priority:
169
+ 1. User-specified config file (-c/--config) - full override
170
+ 2. Default config file (runner/config/coordinator_default.yaml)
171
+
172
+ Note: When using default config, agents must be configured via
173
+ environment variables or a separate config file.
174
+
175
+ Args:
176
+ args: Parsed CLI arguments
177
+
178
+ Returns:
179
+ CoordinatorConfig instance
180
+ """
181
+ logger = logging.getLogger(__name__)
182
+
183
+ if args.config and args.config.exists():
184
+ # User specified a config file - use it directly
185
+ logger.info(f"Loading config from: {args.config}")
186
+ config = CoordinatorConfig.from_yaml(args.config)
187
+ else:
188
+ # Try to load default config
189
+ default_config_path = get_default_config_path()
190
+ if default_config_path.exists():
191
+ logger.info(f"Loading default config from: {default_config_path}")
192
+ config = CoordinatorConfig.from_yaml(default_config_path)
193
+ else:
194
+ # No config available - use built-in defaults
195
+ logger.warning(
196
+ "No config file found. Using built-in defaults.\n"
197
+ f"Expected default config at: {default_config_path}\n"
198
+ "Note: Agents must be configured to run tasks."
199
+ )
200
+ config = CoordinatorConfig()
201
+
202
+ # Override with CLI arguments
203
+ if args.polling_interval:
204
+ config.polling_interval = args.polling_interval
205
+ if args.log_directory:
206
+ config.log_directory = str(args.log_directory)
207
+
208
+ return config
209
+
210
+
211
+ def main() -> int:
212
+ """Main entry point.
213
+
214
+ Returns:
215
+ Exit code (0 for success)
216
+ """
217
+ args = parse_args()
218
+ setup_logging(args.verbose)
219
+
220
+ logger = logging.getLogger(__name__)
221
+
222
+ if args.coordinator:
223
+ # Phase 4: Coordinator mode
224
+ logger.info("Running in Coordinator mode (Phase 4)")
225
+ try:
226
+ config = load_coordinator_config(args)
227
+ except ValueError as e:
228
+ logger.error(f"Configuration error: {e}")
229
+ print(f"Error: {e}", file=sys.stderr)
230
+ return 1
231
+
232
+ logger.info(f"Configured agents: {list(config.agents.keys())}")
233
+ logger.info(f"Polling interval: {config.polling_interval}s")
234
+ logger.info(f"Max concurrent: {config.max_concurrent}")
235
+
236
+ try:
237
+ run_coordinator(config)
238
+ except KeyboardInterrupt:
239
+ logger.info("Coordinator stopped by user")
240
+ return 0
241
+ except Exception as e:
242
+ logger.exception(f"Coordinator failed: {e}")
243
+ return 1
244
+ else:
245
+ # Legacy Runner mode (deprecated)
246
+ logger.warning(
247
+ "Running in legacy Runner mode. "
248
+ "Consider using --coordinator mode for Phase 4 architecture."
249
+ )
250
+ try:
251
+ config = load_runner_config(args)
252
+ except ValueError as e:
253
+ logger.error(f"Configuration error: {e}")
254
+ print(f"Error: {e}", file=sys.stderr)
255
+ print(
256
+ "\nProvide configuration via:\n"
257
+ " 1. YAML config file (-c/--config)\n"
258
+ " 2. Environment variables (AGENT_ID, AGENT_PASSKEY, PROJECT_ID)\n"
259
+ " 3. CLI arguments (--agent-id, --passkey, --project-id)",
260
+ file=sys.stderr
261
+ )
262
+ return 1
263
+
264
+ logger.info(f"Starting runner for agent: {config.agent_id}")
265
+ logger.info(f"Project: {config.project_id}")
266
+ logger.info(f"CLI command: {config.cli_command}")
267
+ logger.info(f"Polling interval: {config.polling_interval}s")
268
+
269
+ try:
270
+ run(config)
271
+ except KeyboardInterrupt:
272
+ logger.info("Runner stopped by user")
273
+ return 0
274
+ except Exception as e:
275
+ logger.exception(f"Runner failed: {e}")
276
+ return 1
277
+
278
+ return 0
279
+
280
+
281
+ if __name__ == "__main__":
282
+ sys.exit(main())
@@ -0,0 +1,99 @@
1
+ # src/aiagent_runner/config.py
2
+ # Runner configuration management
3
+ # Reference: docs/plan/PHASE3_PULL_ARCHITECTURE.md - Phase 3-5
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import yaml
11
+
12
+
13
+ @dataclass
14
+ class RunnerConfig:
15
+ """Runner configuration.
16
+
17
+ Can be loaded from environment variables or YAML file.
18
+
19
+ Phase 4: project_id is required for (agent_id, project_id) management unit.
20
+ """
21
+ agent_id: str
22
+ passkey: str
23
+ project_id: str # Phase 4: Required for authenticate
24
+ polling_interval: int = 5
25
+ cli_command: str = "claude"
26
+ cli_args: list[str] = field(default_factory=lambda: ["--dangerously-skip-permissions"])
27
+ working_directory: Optional[str] = None
28
+ log_directory: Optional[str] = None
29
+ mcp_socket_path: Optional[str] = None
30
+
31
+ def __post_init__(self):
32
+ """Validate configuration after initialization."""
33
+ if self.polling_interval <= 0:
34
+ raise ValueError("polling_interval must be positive")
35
+ if not self.agent_id:
36
+ raise ValueError("agent_id is required")
37
+ if not self.passkey:
38
+ raise ValueError("passkey is required")
39
+ if not self.project_id:
40
+ raise ValueError("project_id is required (Phase 4)")
41
+
42
+ @classmethod
43
+ def from_env(cls) -> "RunnerConfig":
44
+ """Load configuration from environment variables.
45
+
46
+ Environment variables:
47
+ AGENT_ID: Agent ID (required)
48
+ AGENT_PASSKEY: Agent passkey (required)
49
+ PROJECT_ID: Project ID (required, Phase 4)
50
+ POLLING_INTERVAL: Polling interval in seconds (default: 5)
51
+ CLI_COMMAND: CLI command to use (default: claude)
52
+ WORKING_DIRECTORY: Default working directory
53
+ LOG_DIRECTORY: Directory for log files
54
+ MCP_SOCKET_PATH: Path to MCP socket
55
+ """
56
+ agent_id = os.environ.get("AGENT_ID")
57
+ passkey = os.environ.get("AGENT_PASSKEY")
58
+ project_id = os.environ.get("PROJECT_ID")
59
+
60
+ if not agent_id:
61
+ raise ValueError("AGENT_ID environment variable is required")
62
+ if not passkey:
63
+ raise ValueError("AGENT_PASSKEY environment variable is required")
64
+ if not project_id:
65
+ raise ValueError("PROJECT_ID environment variable is required (Phase 4)")
66
+
67
+ cli_args_str = os.environ.get("CLI_ARGS", "--dangerously-skip-permissions")
68
+ cli_args = cli_args_str.split() if cli_args_str else ["--dangerously-skip-permissions"]
69
+
70
+ return cls(
71
+ agent_id=agent_id,
72
+ passkey=passkey,
73
+ project_id=project_id,
74
+ polling_interval=int(os.environ.get("POLLING_INTERVAL", "5")),
75
+ cli_command=os.environ.get("CLI_COMMAND", "claude"),
76
+ cli_args=cli_args,
77
+ working_directory=os.environ.get("WORKING_DIRECTORY"),
78
+ log_directory=os.environ.get("LOG_DIRECTORY"),
79
+ mcp_socket_path=os.environ.get("MCP_SOCKET_PATH"),
80
+ )
81
+
82
+ @classmethod
83
+ def from_yaml(cls, path: Path) -> "RunnerConfig":
84
+ """Load configuration from YAML file.
85
+
86
+ Args:
87
+ path: Path to YAML configuration file
88
+
89
+ Returns:
90
+ RunnerConfig instance
91
+ """
92
+ with open(path) as f:
93
+ data = yaml.safe_load(f)
94
+
95
+ # Handle cli_args which might be a string in YAML
96
+ if "cli_args" in data and isinstance(data["cli_args"], str):
97
+ data["cli_args"] = data["cli_args"].split()
98
+
99
+ return cls(**data)