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.
- aiagent_runner/__init__.py +9 -0
- aiagent_runner/__main__.py +282 -0
- aiagent_runner/config.py +99 -0
- aiagent_runner/coordinator.py +687 -0
- aiagent_runner/coordinator_config.py +203 -0
- aiagent_runner/executor.py +99 -0
- aiagent_runner/mcp_client.py +698 -0
- aiagent_runner/prompt_builder.py +120 -0
- aiagent_runner/runner.py +236 -0
- aiagent_runner-0.1.3.dist-info/METADATA +185 -0
- aiagent_runner-0.1.3.dist-info/RECORD +13 -0
- aiagent_runner-0.1.3.dist-info/WHEEL +4 -0
- aiagent_runner-0.1.3.dist-info/entry_points.txt +2 -0
|
@@ -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())
|
aiagent_runner/config.py
ADDED
|
@@ -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)
|