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,120 @@
1
+ # src/aiagent_runner/prompt_builder.py
2
+ # Build prompts for CLI execution from task information
3
+ # Reference: docs/plan/PHASE3_PULL_ARCHITECTURE.md - Phase 3-5
4
+
5
+ from typing import Optional
6
+ from aiagent_runner.mcp_client import TaskInfo
7
+
8
+
9
+ class PromptBuilder:
10
+ """Builds prompts for CLI execution from task information."""
11
+
12
+ def __init__(self, agent_id: str, agent_name: Optional[str] = None):
13
+ """Initialize prompt builder.
14
+
15
+ Args:
16
+ agent_id: Agent ID executing the task
17
+ agent_name: Human-readable agent name (optional)
18
+ """
19
+ self.agent_id = agent_id
20
+ self.agent_name = agent_name or agent_id
21
+
22
+ def build(
23
+ self,
24
+ task: TaskInfo,
25
+ context: Optional[dict] = None,
26
+ handoff: Optional[dict] = None
27
+ ) -> str:
28
+ """Build a prompt from task information.
29
+
30
+ Args:
31
+ task: Task information
32
+ context: Previous context (optional, overrides task.context)
33
+ handoff: Handoff information (optional, overrides task.handoff)
34
+
35
+ Returns:
36
+ Complete prompt string for CLI execution
37
+ """
38
+ # Use provided context/handoff or fall back to task's
39
+ ctx = context or task.context
40
+ ho = handoff or task.handoff
41
+
42
+ sections = [
43
+ self._build_header(task),
44
+ self._build_identification(task),
45
+ self._build_description(task),
46
+ ]
47
+
48
+ if task.working_directory:
49
+ sections.append(self._build_working_directory(task))
50
+
51
+ if ctx:
52
+ sections.append(self._build_context(ctx))
53
+
54
+ if ho:
55
+ sections.append(self._build_handoff(ho))
56
+
57
+ sections.append(self._build_instructions(task))
58
+
59
+ return "\n\n".join(sections)
60
+
61
+ def _build_header(self, task: TaskInfo) -> str:
62
+ """Build header section."""
63
+ return f"# Task: {task.title}"
64
+
65
+ def _build_identification(self, task: TaskInfo) -> str:
66
+ """Build identification section with IDs."""
67
+ return f"""## Identification
68
+ - Task ID: {task.task_id}
69
+ - Project ID: {task.project_id}
70
+ - Agent ID: {self.agent_id}
71
+ - Agent Name: {self.agent_name}
72
+ - Priority: {task.priority}"""
73
+
74
+ def _build_description(self, task: TaskInfo) -> str:
75
+ """Build description section."""
76
+ return f"""## Description
77
+ {task.description}"""
78
+
79
+ def _build_working_directory(self, task: TaskInfo) -> str:
80
+ """Build working directory section."""
81
+ return f"""## Working Directory
82
+ Path: {task.working_directory}"""
83
+
84
+ def _build_context(self, context: dict) -> str:
85
+ """Build previous context section."""
86
+ lines = ["## Previous Context"]
87
+ if context.get("progress"):
88
+ lines.append(f"**Progress**: {context['progress']}")
89
+ if context.get("findings"):
90
+ lines.append(f"**Findings**: {context['findings']}")
91
+ if context.get("blockers"):
92
+ lines.append(f"**Blockers**: {context['blockers']}")
93
+ if context.get("next_steps"):
94
+ lines.append(f"**Next Steps**: {context['next_steps']}")
95
+ return "\n".join(lines)
96
+
97
+ def _build_handoff(self, handoff: dict) -> str:
98
+ """Build handoff information section."""
99
+ lines = ["## Handoff Information"]
100
+ if handoff.get("from_agent") or handoff.get("from_agent_id"):
101
+ from_agent = handoff.get("from_agent") or handoff.get("from_agent_id")
102
+ lines.append(f"**From Agent**: {from_agent}")
103
+ if handoff.get("summary"):
104
+ lines.append(f"**Summary**: {handoff['summary']}")
105
+ if handoff.get("context"):
106
+ lines.append(f"**Context**: {handoff['context']}")
107
+ if handoff.get("recommendations"):
108
+ lines.append(f"**Recommendations**: {handoff['recommendations']}")
109
+ return "\n".join(lines)
110
+
111
+ def _build_instructions(self, task: TaskInfo) -> str:
112
+ """Build instructions section."""
113
+ return f"""## Instructions
114
+ 1. Complete the task as described above
115
+ 2. Save your progress regularly using:
116
+ save_context(task_id="{task.task_id}", progress="...", findings="...", next_steps="...")
117
+ 3. When done, update the task status using:
118
+ update_task_status(task_id="{task.task_id}", status="done")
119
+ 4. If you need to hand off to another agent, use:
120
+ create_handoff(task_id="{task.task_id}", from_agent_id="{self.agent_id}", summary="...", recommendations="...")"""
@@ -0,0 +1,236 @@
1
+ # src/aiagent_runner/runner.py
2
+ # Main runner loop for AI Agent PM
3
+ # Reference: docs/plan/PHASE3_PULL_ARCHITECTURE.md - Phase 3-5
4
+
5
+ import asyncio
6
+ import logging
7
+ import os
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from aiagent_runner.config import RunnerConfig
13
+ from aiagent_runner.executor import CLIExecutor, ExecutionResult
14
+ from aiagent_runner.mcp_client import (
15
+ AuthenticationError,
16
+ MCPClient,
17
+ MCPError,
18
+ SessionExpiredError,
19
+ TaskInfo,
20
+ )
21
+ from aiagent_runner.prompt_builder import PromptBuilder
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class Runner:
27
+ """Main runner that polls for tasks and executes them.
28
+
29
+ The runner operates in a polling loop:
30
+ 1. Authenticate with MCP server
31
+ 2. Poll for pending tasks
32
+ 3. Execute tasks using CLI
33
+ 4. Report execution results
34
+ 5. Wait for polling interval
35
+ 6. Repeat
36
+ """
37
+
38
+ def __init__(self, config: RunnerConfig):
39
+ """Initialize runner.
40
+
41
+ Args:
42
+ config: Runner configuration
43
+ """
44
+ self.config = config
45
+ self.mcp_client = MCPClient(config.mcp_socket_path)
46
+ self.executor = CLIExecutor(config.cli_command, config.cli_args)
47
+ self.prompt_builder: Optional[PromptBuilder] = None
48
+
49
+ self._running = False
50
+ self._authenticated = False
51
+ self._agent_name: Optional[str] = None
52
+
53
+ @property
54
+ def log_directory(self) -> Path:
55
+ """Get log directory, creating if needed."""
56
+ if self.config.log_directory:
57
+ log_dir = Path(self.config.log_directory)
58
+ else:
59
+ log_dir = Path.home() / ".aiagent-runner" / "logs"
60
+ log_dir.mkdir(parents=True, exist_ok=True)
61
+ return log_dir
62
+
63
+ @property
64
+ def working_directory(self) -> str:
65
+ """Get working directory."""
66
+ return self.config.working_directory or os.getcwd()
67
+
68
+ async def start(self) -> None:
69
+ """Start the runner loop.
70
+
71
+ Runs until stop() is called or an unrecoverable error occurs.
72
+ """
73
+ logger.info(
74
+ f"Starting runner for agent {self.config.agent_id}, "
75
+ f"polling every {self.config.polling_interval}s"
76
+ )
77
+
78
+ self._running = True
79
+
80
+ while self._running:
81
+ try:
82
+ await self._run_once()
83
+ except AuthenticationError as e:
84
+ logger.error(f"Authentication failed: {e}")
85
+ self._authenticated = False
86
+ # Wait before retrying
87
+ await asyncio.sleep(self.config.polling_interval)
88
+ except MCPError as e:
89
+ logger.error(f"MCP error: {e}")
90
+ # Wait before retrying
91
+ await asyncio.sleep(self.config.polling_interval)
92
+ except Exception as e:
93
+ logger.exception(f"Unexpected error: {e}")
94
+ # Wait before retrying
95
+ await asyncio.sleep(self.config.polling_interval)
96
+
97
+ if self._running:
98
+ await asyncio.sleep(self.config.polling_interval)
99
+
100
+ def stop(self) -> None:
101
+ """Stop the runner loop."""
102
+ logger.info("Stopping runner")
103
+ self._running = False
104
+
105
+ async def _run_once(self) -> None:
106
+ """Run one iteration of the polling loop."""
107
+ # Ensure authenticated
108
+ await self._ensure_authenticated()
109
+
110
+ # Get pending tasks (agent_id is derived from session token)
111
+ try:
112
+ tasks = await self.mcp_client.get_pending_tasks()
113
+ except SessionExpiredError:
114
+ logger.info("Session expired, re-authenticating")
115
+ self._authenticated = False
116
+ await self._ensure_authenticated()
117
+ tasks = await self.mcp_client.get_pending_tasks()
118
+
119
+ if not tasks:
120
+ logger.debug("No pending tasks")
121
+ return
122
+
123
+ logger.info(f"Found {len(tasks)} pending task(s)")
124
+
125
+ # Process first task (one at a time)
126
+ task = tasks[0]
127
+ await self._process_task(task)
128
+
129
+ async def _ensure_authenticated(self) -> None:
130
+ """Ensure we have a valid session."""
131
+ if self._authenticated:
132
+ return
133
+
134
+ logger.info(f"Authenticating agent {self.config.agent_id} for project {self.config.project_id}")
135
+ result = await self.mcp_client.authenticate(
136
+ self.config.agent_id,
137
+ self.config.passkey,
138
+ self.config.project_id
139
+ )
140
+ self._authenticated = True
141
+ self._agent_name = result.agent_name
142
+ self.prompt_builder = PromptBuilder(
143
+ self.config.agent_id,
144
+ self._agent_name
145
+ )
146
+ logger.info(
147
+ f"Authenticated as {self._agent_name or self.config.agent_id}, "
148
+ f"session expires in {result.expires_in}s"
149
+ )
150
+
151
+ async def _process_task(self, task: TaskInfo) -> None:
152
+ """Process a single task.
153
+
154
+ Args:
155
+ task: Task to process
156
+ """
157
+ logger.info(f"Processing task {task.task_id}: {task.title}")
158
+
159
+ # Report execution start (agent_id is derived from session token)
160
+ try:
161
+ start_result = await self.mcp_client.report_execution_start(
162
+ task.task_id
163
+ )
164
+ execution_id = start_result.execution_id
165
+ except MCPError as e:
166
+ logger.error(f"Failed to report execution start: {e}")
167
+ return
168
+
169
+ # Build prompt
170
+ prompt = self.prompt_builder.build(task)
171
+
172
+ # Determine working directory
173
+ work_dir = task.working_directory or self.working_directory
174
+
175
+ # Generate log file path
176
+ log_file = self._generate_log_path(task.task_id)
177
+
178
+ # Execute CLI
179
+ logger.info(f"Executing {self.config.cli_command} for task {task.task_id}")
180
+ result = self.executor.execute(prompt, work_dir, log_file)
181
+
182
+ # Report execution complete
183
+ error_message = None
184
+ if result.exit_code != 0:
185
+ error_message = f"CLI exited with code {result.exit_code}"
186
+ logger.warning(
187
+ f"Task {task.task_id} execution failed: {error_message}"
188
+ )
189
+ else:
190
+ logger.info(
191
+ f"Task {task.task_id} execution completed in "
192
+ f"{result.duration_seconds:.1f}s"
193
+ )
194
+
195
+ try:
196
+ await self.mcp_client.report_execution_complete(
197
+ execution_id,
198
+ result.exit_code,
199
+ result.duration_seconds,
200
+ result.log_file,
201
+ error_message
202
+ )
203
+ except MCPError as e:
204
+ logger.error(f"Failed to report execution complete: {e}")
205
+
206
+ def _generate_log_path(self, task_id: str) -> str:
207
+ """Generate a log file path for a task execution.
208
+
209
+ Args:
210
+ task_id: Task ID
211
+
212
+ Returns:
213
+ Path to log file
214
+ """
215
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
216
+ filename = f"{task_id}_{timestamp}.log"
217
+ return str(self.log_directory / filename)
218
+
219
+
220
+ async def run_async(config: RunnerConfig) -> None:
221
+ """Run the runner asynchronously.
222
+
223
+ Args:
224
+ config: Runner configuration
225
+ """
226
+ runner = Runner(config)
227
+ await runner.start()
228
+
229
+
230
+ def run(config: RunnerConfig) -> None:
231
+ """Run the runner synchronously.
232
+
233
+ Args:
234
+ config: Runner configuration
235
+ """
236
+ asyncio.run(run_async(config))
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiagent-runner
3
+ Version: 0.1.3
4
+ Summary: Runner for AI Agent PM - executes tasks via MCP and CLI
5
+ Author: AI Agent PM Team
6
+ License-Expression: MIT
7
+ Keywords: agent,ai,automation,mcp,orchestration,task-management
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.9
18
+ Requires-Dist: pyyaml>=6.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: aiohttp>=3.9; extra == 'dev'
21
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
22
+ Requires-Dist: pytest-cov>=4.1; extra == 'dev'
23
+ Requires-Dist: pytest>=8.0; extra == 'dev'
24
+ Provides-Extra: http
25
+ Requires-Dist: aiohttp>=3.9; extra == 'http'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # AI Agent PM Coordinator
29
+
30
+ 全エージェントのタスク実行を統合管理するオーケストレーションデーモン。
31
+
32
+ ---
33
+
34
+ ## 🚨 致命的な設計違反警告 (2026-01-09)
35
+
36
+ **現在の実装には致命的な設計違反があります。修正が必要です。**
37
+
38
+ ### 問題点
39
+
40
+ Coordinator/Runnerは**純粋なMCPクライアント**であるべきですが、現在の実装は以下の不正な情報を保持しています:
41
+
42
+ | 設定項目 | 問題 | 対応 |
43
+ |---------|------|------|
44
+ | `mcp_server_command` | サーバー起動コマンドを保持 | **削除必須** |
45
+ | `mcp_database_path` | DBパス(内部実装詳細)を保持 | **削除必須** |
46
+
47
+ ### 設計原則
48
+
49
+ ```
50
+ ✅ 正しい設計: Coordinatorはソケット接続のみ行う純粋なクライアント
51
+ ❌ 現在の実装: Coordinatorがサーバー起動やDB接続情報を管理
52
+ ```
53
+
54
+ ### 影響
55
+
56
+ - Agent Instanceごとに新しいMCPサーバーをstdio transportで起動
57
+ - 複数のMCPサーバーが同じDBに同時アクセス → データ整合性の問題
58
+ - テストと本番で異なるDB設定が必要 → 設定の複雑化
59
+
60
+ ### 正しいアーキテクチャ
61
+
62
+ ```
63
+ [アプリ] → MCPデーモン起動(唯一のサーバー)
64
+ [Coordinator] → Unix Socketで接続(クライアントのみ)
65
+ [Agent Instance] → 同じUnix Socketで接続(クライアントのみ)
66
+ ```
67
+
68
+ 詳細は `docs/plan/PHASE4_COORDINATOR_ARCHITECTURE.md` の警告セクションを参照。
69
+
70
+ ---
71
+
72
+ ## クイックスタート
73
+
74
+ ```bash
75
+ cd runner
76
+ pip install -e .
77
+
78
+ # デフォルト設定で起動(config/coordinator_default.yaml を使用)
79
+ python -m aiagent_runner --coordinator
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Coordinatorモード(推奨)
85
+
86
+ 単一のCoordinatorが全ての(agent_id, project_id)ペアを管理します。
87
+
88
+ ### 基本起動
89
+
90
+ ```bash
91
+ # デフォルト設定で起動
92
+ python -m aiagent_runner --coordinator
93
+
94
+ # 詳細ログ出力
95
+ python -m aiagent_runner --coordinator -v
96
+ ```
97
+
98
+ ### カスタム設定ファイル
99
+
100
+ ```bash
101
+ python -m aiagent_runner --coordinator -c /path/to/config.yaml
102
+ ```
103
+
104
+ ### 設定ファイル例
105
+
106
+ ```yaml
107
+ # config/coordinator_default.yaml がデフォルトで読み込まれます
108
+ # カスタム設定で上書き可能
109
+
110
+ polling_interval: 10
111
+ max_concurrent: 3
112
+
113
+ # AI providers
114
+ ai_providers:
115
+ claude:
116
+ cli_command: claude
117
+ cli_args:
118
+ - "--dangerously-skip-permissions"
119
+ - "--max-turns"
120
+ - "50"
121
+
122
+ # Agents (passkeyのみ - ai_type等はMCPから取得)
123
+ agents:
124
+ agt_developer:
125
+ passkey: secret123
126
+ agt_reviewer:
127
+ passkey: ${REVIEWER_PASSKEY} # 環境変数展開対応
128
+
129
+ log_directory: /tmp/coordinator_logs
130
+ ```
131
+
132
+ ### バックグラウンド実行
133
+
134
+ ```bash
135
+ nohup python -m aiagent_runner --coordinator -v > coordinator.log 2>&1 &
136
+ ```
137
+
138
+ ---
139
+
140
+ ## 設定の優先順位
141
+
142
+ 1. **コマンドライン引数** (`--polling-interval` 等)
143
+ 2. **指定した設定ファイル** (`-c /path/to/config.yaml`)
144
+ 3. **デフォルト設定** (`runner/config/coordinator_default.yaml`)
145
+ 4. **組み込みデフォルト値**
146
+
147
+ ---
148
+
149
+ ## 動作フロー
150
+
151
+ 1. MCPサーバーに接続(Unixソケット)
152
+ 2. `list_active_projects_with_agents()` で全プロジェクト・エージェントを取得
153
+ 3. 各(agent_id, project_id)ペアに対して `get_agent_action()` を呼び出し
154
+ 4. 作業が必要な場合、Agent Instance(Claude CLI等)をスポーン
155
+ 5. Agent Instanceが `authenticate` → `get_my_task` → 実行 → `report_completed`
156
+ 6. 待機して2に戻る
157
+
158
+ ---
159
+
160
+ ## 前提条件
161
+
162
+ - MCPサーバーが起動していること
163
+ - エージェントがアプリで登録済みで、passkeyが設定されていること
164
+ - 該当エージェントがプロジェクトに割り当てられていること
165
+ - タスクが `in_progress` ステータスであること
166
+
167
+ ---
168
+
169
+ ## Legacy Runnerモード(非推奨)
170
+
171
+ 1エージェント = 1デーモン の旧アーキテクチャ。
172
+
173
+ ```bash
174
+ # 非推奨: Coordinatorモードを使用してください
175
+ aiagent-runner --agent-id <AGENT_ID> --passkey <PASSKEY> --project-id <PROJECT_ID>
176
+ ```
177
+
178
+ ---
179
+
180
+ ## 開発
181
+
182
+ ```bash
183
+ pip install -e ".[dev]"
184
+ pytest
185
+ ```
@@ -0,0 +1,13 @@
1
+ aiagent_runner/__init__.py,sha256=dniIduQc_cgSMFCXg283ytOOFUxrlO5imMKOxLkTCT0,314
2
+ aiagent_runner/__main__.py,sha256=xxBGs0gZdMEVSh21UOWVJg6ekud5OcwmCIflH0eSYcI,8711
3
+ aiagent_runner/config.py,sha256=3Wu5XPuKjPpj6SJcGDoycrK7DkA2bWZ_1OiEUJm2l7g,3539
4
+ aiagent_runner/coordinator.py,sha256=Gg3CbWGe2fH1dqVobqSMGGgXHQ1UrOHqcWZuKQHl43E,26338
5
+ aiagent_runner/coordinator_config.py,sha256=DVGleEeGU93xLSSOONSgFLckwV-3bc53WiwWz6GIIAU,7208
6
+ aiagent_runner/executor.py,sha256=kUyXFb0j6Qw7P41S8KkznI3JSYJT0dxL8gGQdk7kkcA,2914
7
+ aiagent_runner/mcp_client.py,sha256=Y70DDzhQXBirui8OQ_MmOTwBZLWlRK7c3TSHHAYsT4o,23360
8
+ aiagent_runner/prompt_builder.py,sha256=IwPOeXDqYED6iRkLETlQg6tarG-U2VUtnaQaiGwgEQk,4402
9
+ aiagent_runner/runner.py,sha256=0re8QZXrbqYLWGo2mTh15R9PSvg5d7xTnHtIuJHu1Rg,7369
10
+ aiagent_runner-0.1.3.dist-info/METADATA,sha256=GHUpqrzBmmm3rIUsgYczgT18uWA1PvcLrd9vv8-S02w,5179
11
+ aiagent_runner-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ aiagent_runner-0.1.3.dist-info/entry_points.txt,sha256=UwbGpHt0NTjnTFlVQhcZLR2GWgG15v2r1n-OYvxHzK4,64
13
+ aiagent_runner-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aiagent-runner = aiagent_runner.__main__:main