bamboo-coding 0.1.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.
- agent/__init__.py +0 -0
- agent/bamboo_git_agent/__init__.py +0 -0
- agent/bamboo_git_agent/capabilities/__init__.py +0 -0
- agent/bamboo_git_agent/capabilities/repo.py +189 -0
- agent/bamboo_git_agent/config.py +76 -0
- agent/bamboo_git_agent/controller_client.py +73 -0
- agent/bamboo_git_agent/journal.py +209 -0
- agent/bamboo_git_agent/main.py +164 -0
- agent/bamboo_git_agent/protocol.py +21 -0
- agent/bamboo_git_agent/status.py +59 -0
- api/__init__.py +2 -0
- api/commit.py +82 -0
- api/repository.py +592 -0
- bamboo_coding/__init__.py +3 -0
- bamboo_coding/client/__init__.py +3 -0
- bamboo_coding/client/capabilities/__init__.py +1 -0
- bamboo_coding/client/capabilities/repo.py +279 -0
- bamboo_coding/client/config.py +179 -0
- bamboo_coding/client/controller_client.py +112 -0
- bamboo_coding/client/journal.py +1 -0
- bamboo_coding/client/main.py +425 -0
- bamboo_coding/client/status.py +59 -0
- bamboo_coding/client/terminal_runtime.py +251 -0
- bamboo_coding/server/__init__.py +3 -0
- bamboo_coding/server/core/__init__.py +1 -0
- bamboo_coding/server/core/config.py +1 -0
- bamboo_coding/server/main.py +39 -0
- bamboo_coding/shared/__init__.py +39 -0
- bamboo_coding/shared/protocol.py +1 -0
- bamboo_coding-0.1.0.dist-info/METADATA +284 -0
- bamboo_coding-0.1.0.dist-info/RECORD +62 -0
- bamboo_coding-0.1.0.dist-info/WHEEL +5 -0
- bamboo_coding-0.1.0.dist-info/entry_points.txt +3 -0
- bamboo_coding-0.1.0.dist-info/top_level.txt +6 -0
- controller/__init__.py +0 -0
- controller/app/__init__.py +0 -0
- controller/app/api/__init__.py +0 -0
- controller/app/api/agents/__init__.py +3 -0
- controller/app/api/agents/ws.py +133 -0
- controller/app/api/public/__init__.py +11 -0
- controller/app/api/public/repos.py +207 -0
- controller/app/api/public/terminals.py +64 -0
- controller/app/core/__init__.py +0 -0
- controller/app/core/config.py +20 -0
- controller/app/core/errors.py +14 -0
- controller/app/db/__init__.py +0 -0
- controller/app/db/models.py +42 -0
- controller/app/db/session.py +74 -0
- controller/app/main.py +71 -0
- controller/app/schemas/__init__.py +23 -0
- controller/app/schemas/agent_messages.py +21 -0
- controller/app/schemas/public.py +3 -0
- controller/app/services/__init__.py +0 -0
- controller/app/services/agents.py +108 -0
- controller/app/services/registrations.py +17 -0
- controller/app/services/repositories.py +85 -0
- controller/app/services/router.py +140 -0
- controller/app/services/tasks.py +104 -0
- controller/app/services/terminals.py +148 -0
- git_utils.py +2238 -0
- shared/__init__.py +39 -0
- shared/protocol.py +253 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import signal
|
|
9
|
+
import sys
|
|
10
|
+
from dataclasses import asdict
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Awaitable, Callable
|
|
13
|
+
|
|
14
|
+
from bamboo_coding.client.config import AgentConfig, load_config
|
|
15
|
+
from bamboo_coding.client.controller_client import ControllerConnection
|
|
16
|
+
from bamboo_coding.client.terminal_runtime import ClientTerminalRuntime
|
|
17
|
+
from bamboo_coding.client.journal import RuntimeJournal
|
|
18
|
+
from bamboo_coding.client.status import build_register_message, build_status_message
|
|
19
|
+
from bamboo_coding.shared.protocol import (
|
|
20
|
+
TaskEventMessage,
|
|
21
|
+
TaskMessage,
|
|
22
|
+
TaskResultMessage,
|
|
23
|
+
TerminalCloseMessage,
|
|
24
|
+
TerminalInputMessage,
|
|
25
|
+
TerminalOpenMessage,
|
|
26
|
+
TerminalResizeMessage,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
LOGGER = logging.getLogger(__name__)
|
|
31
|
+
INITIAL_RETRY_DELAY = 1
|
|
32
|
+
MAX_RETRY_DELAY = 8
|
|
33
|
+
TaskExecutor = Callable[[TaskMessage], Awaitable[dict]]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConfigError(RuntimeError):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AgentRuntime:
|
|
41
|
+
def __init__(self, executor: TaskExecutor, config: AgentConfig | None = None):
|
|
42
|
+
self.config = config or load_config()
|
|
43
|
+
self.executor = executor
|
|
44
|
+
self.journal = RuntimeJournal(self.config.journal_path)
|
|
45
|
+
self.connection = ControllerConnection(self.config.controller_url, token=self.config.token)
|
|
46
|
+
self.terminals = ClientTerminalRuntime(
|
|
47
|
+
send_status=self.connection.send_terminal_status,
|
|
48
|
+
send_output=self.connection.send_terminal_output,
|
|
49
|
+
send_closed=self.connection.send_terminal_closed,
|
|
50
|
+
send_heartbeat=self.connection.send_terminal_heartbeat,
|
|
51
|
+
)
|
|
52
|
+
self.last_registration_message = ""
|
|
53
|
+
|
|
54
|
+
def capabilities(self) -> list[dict]:
|
|
55
|
+
return [
|
|
56
|
+
{"capability": "fs.tree", "actions": ["get"]},
|
|
57
|
+
{"capability": "fs.browse", "actions": ["get"]},
|
|
58
|
+
{"capability": "fs.file.read", "actions": ["get"]},
|
|
59
|
+
{"capability": "fs.file.read_raw", "actions": ["get"]},
|
|
60
|
+
{"capability": "fs.file.write", "actions": ["put"]},
|
|
61
|
+
{"capability": "fs.folder.create", "actions": ["post"]},
|
|
62
|
+
{"capability": "fs.path.delete", "actions": ["delete"]},
|
|
63
|
+
{"capability": "fs.path.rename", "actions": ["put"]},
|
|
64
|
+
{"capability": "git.diff", "actions": ["get"]},
|
|
65
|
+
{"capability": "git.status", "actions": ["get"]},
|
|
66
|
+
{"capability": "git.commits", "actions": ["list"]},
|
|
67
|
+
{"capability": "git.commit_details", "actions": ["get"]},
|
|
68
|
+
{"capability": "git.stage", "actions": ["post"]},
|
|
69
|
+
{"capability": "git.unstage", "actions": ["post"]},
|
|
70
|
+
{"capability": "git.branches.list", "actions": ["get"]},
|
|
71
|
+
{"capability": "git.branches.create", "actions": ["post"]},
|
|
72
|
+
{"capability": "git.branches.checkout", "actions": ["put"]},
|
|
73
|
+
{"capability": "git.branches.delete", "actions": ["delete"]},
|
|
74
|
+
{"capability": "git.branches.merge", "actions": ["post"]},
|
|
75
|
+
{"capability": "git.commit.create", "actions": ["post"]},
|
|
76
|
+
{"capability": "git.remotes.list", "actions": ["get"]},
|
|
77
|
+
{"capability": "git.remotes.add", "actions": ["post"]},
|
|
78
|
+
{"capability": "git.remotes.remove", "actions": ["delete"]},
|
|
79
|
+
{"capability": "git.remotes.push", "actions": ["post"]},
|
|
80
|
+
{"capability": "git.remotes.pull", "actions": ["post"]},
|
|
81
|
+
{"capability": "repo.tree", "actions": ["get"]},
|
|
82
|
+
{"capability": "repo.browse", "actions": ["get"]},
|
|
83
|
+
{"capability": "repo.diff", "actions": ["get"]},
|
|
84
|
+
{"capability": "repo.status", "actions": ["get"]},
|
|
85
|
+
{"capability": "repo.commits", "actions": ["list"]},
|
|
86
|
+
{"capability": "repo.commit_details", "actions": ["get"]},
|
|
87
|
+
{"capability": "repo.file.read", "actions": ["get"]},
|
|
88
|
+
{"capability": "repo.file.write", "actions": ["put"]},
|
|
89
|
+
{"capability": "repo.stage", "actions": ["post"]},
|
|
90
|
+
{"capability": "repo.unstage", "actions": ["post"]},
|
|
91
|
+
{"capability": "repo.branches.list", "actions": ["get"]},
|
|
92
|
+
{"capability": "repo.branches.create", "actions": ["post"]},
|
|
93
|
+
{"capability": "repo.branches.checkout", "actions": ["put"]},
|
|
94
|
+
{"capability": "repo.branches.delete", "actions": ["delete"]},
|
|
95
|
+
{"capability": "repo.branches.merge", "actions": ["post"]},
|
|
96
|
+
{"capability": "repo.commit.create", "actions": ["post"]},
|
|
97
|
+
{"capability": "terminal.open", "actions": ["post"]},
|
|
98
|
+
{"capability": "terminal.input", "actions": ["post"]},
|
|
99
|
+
{"capability": "terminal.resize", "actions": ["post"]},
|
|
100
|
+
{"capability": "terminal.close", "actions": ["post"]},
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
def roots(self) -> list[dict]:
|
|
104
|
+
return [{"path": item.path, "name": item.name} for item in self.config.repositories]
|
|
105
|
+
|
|
106
|
+
def repositories(self) -> list[dict]:
|
|
107
|
+
return [asdict(item) for item in self.config.repositories]
|
|
108
|
+
|
|
109
|
+
def runtime_health(self) -> tuple[int, int, int]:
|
|
110
|
+
active = len(self.journal.list_active_tasks())
|
|
111
|
+
queued = 0
|
|
112
|
+
return active, queued, self.config.max_concurrent_tasks
|
|
113
|
+
|
|
114
|
+
def _persist_token(self, token: str) -> None:
|
|
115
|
+
self.config.token_path.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
self.config.token_path.write_text(json.dumps({"token": token}), encoding="utf-8")
|
|
117
|
+
self.connection.token = token
|
|
118
|
+
|
|
119
|
+
def validate_config(self) -> None:
|
|
120
|
+
errors = validate_runtime_config(self.config)
|
|
121
|
+
if errors:
|
|
122
|
+
raise ConfigError("\n".join(errors))
|
|
123
|
+
|
|
124
|
+
def prepare_storage(self) -> None:
|
|
125
|
+
self.config.token_path.parent.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
self.config.journal_path.parent.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
|
|
128
|
+
async def register_once(self) -> bool:
|
|
129
|
+
await self.connection.connect()
|
|
130
|
+
active, queued, max_concurrent = self.runtime_health()
|
|
131
|
+
register = build_register_message(
|
|
132
|
+
client_id=self.config.client_id,
|
|
133
|
+
hostname=self.config.hostname,
|
|
134
|
+
version=self.config.version,
|
|
135
|
+
platform_name="python",
|
|
136
|
+
capabilities=self.capabilities(),
|
|
137
|
+
roots=self.roots(),
|
|
138
|
+
active_tasks=active,
|
|
139
|
+
queued_tasks=queued,
|
|
140
|
+
max_concurrent_tasks=max_concurrent,
|
|
141
|
+
connected=True,
|
|
142
|
+
)
|
|
143
|
+
registration = await self.connection.send_register(register)
|
|
144
|
+
self.last_registration_message = str(registration.get("message") or "")
|
|
145
|
+
approved = bool(registration.get("approved", False))
|
|
146
|
+
token = registration.get("token")
|
|
147
|
+
if approved and isinstance(token, str) and token:
|
|
148
|
+
self._persist_token(token)
|
|
149
|
+
return approved
|
|
150
|
+
|
|
151
|
+
async def _sleep_before_retry(self, delay: int, reason: str, stop_event: asyncio.Event | None = None) -> bool:
|
|
152
|
+
LOGGER.warning("%s; retrying in %ss", reason, delay)
|
|
153
|
+
if stop_event is None:
|
|
154
|
+
await asyncio.sleep(delay)
|
|
155
|
+
return False
|
|
156
|
+
try:
|
|
157
|
+
await asyncio.wait_for(stop_event.wait(), timeout=delay)
|
|
158
|
+
return True
|
|
159
|
+
except asyncio.TimeoutError:
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
async def run(self, stop_event: asyncio.Event | None = None) -> None:
|
|
163
|
+
self.validate_config()
|
|
164
|
+
self.prepare_storage()
|
|
165
|
+
retry_delay = INITIAL_RETRY_DELAY
|
|
166
|
+
|
|
167
|
+
while True:
|
|
168
|
+
if stop_event is not None and stop_event.is_set():
|
|
169
|
+
break
|
|
170
|
+
try:
|
|
171
|
+
approved = await self.register_once()
|
|
172
|
+
if not approved:
|
|
173
|
+
await self.connection.close()
|
|
174
|
+
reason = self.last_registration_message or "Client approval is still pending"
|
|
175
|
+
if await self._sleep_before_retry(retry_delay, reason, stop_event):
|
|
176
|
+
break
|
|
177
|
+
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
|
|
178
|
+
continue
|
|
179
|
+
retry_delay = INITIAL_RETRY_DELAY
|
|
180
|
+
await self.connection.listen(self.handle_task, self.handle_terminal_message)
|
|
181
|
+
await self.terminals.close_all(reason="controller_disconnected")
|
|
182
|
+
await self.connection.close()
|
|
183
|
+
if stop_event is not None and stop_event.is_set():
|
|
184
|
+
break
|
|
185
|
+
if await self._sleep_before_retry(retry_delay, "Controller connection closed", stop_event):
|
|
186
|
+
break
|
|
187
|
+
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
|
|
188
|
+
except asyncio.CancelledError:
|
|
189
|
+
await self.terminals.close_all(reason="cancelled")
|
|
190
|
+
await self.connection.close()
|
|
191
|
+
raise
|
|
192
|
+
except ConnectionRefusedError as exc:
|
|
193
|
+
await self.terminals.close_all(reason="connection_failed")
|
|
194
|
+
await self.connection.close()
|
|
195
|
+
if await self._sleep_before_retry(retry_delay, f"Controller connection failed: {exc}", stop_event):
|
|
196
|
+
break
|
|
197
|
+
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
await self.terminals.close_all(reason="runtime_error")
|
|
200
|
+
await self.connection.close()
|
|
201
|
+
if await self._sleep_before_retry(retry_delay, f"Client runtime error: {exc}", stop_event):
|
|
202
|
+
break
|
|
203
|
+
retry_delay = min(retry_delay * 2, MAX_RETRY_DELAY)
|
|
204
|
+
|
|
205
|
+
await self.terminals.close_all(reason="stopped")
|
|
206
|
+
await self.connection.close()
|
|
207
|
+
|
|
208
|
+
async def handle_terminal_message(
|
|
209
|
+
self,
|
|
210
|
+
message: TerminalOpenMessage | TerminalInputMessage | TerminalResizeMessage | TerminalCloseMessage,
|
|
211
|
+
) -> None:
|
|
212
|
+
if isinstance(message, TerminalOpenMessage):
|
|
213
|
+
await self.terminals.open_terminal(message)
|
|
214
|
+
elif isinstance(message, TerminalInputMessage):
|
|
215
|
+
await self.terminals.input_terminal(message)
|
|
216
|
+
elif isinstance(message, TerminalResizeMessage):
|
|
217
|
+
await self.terminals.resize_terminal(message.terminal_id, cols=message.cols, rows=message.rows)
|
|
218
|
+
elif isinstance(message, TerminalCloseMessage):
|
|
219
|
+
await self.terminals.close_terminal(message.terminal_id, reason=message.reason or "controller_closed")
|
|
220
|
+
|
|
221
|
+
async def handle_task(self, task: TaskMessage) -> None:
|
|
222
|
+
self.journal.record_task_received(
|
|
223
|
+
task_id=task.task_id,
|
|
224
|
+
repo_id=task.repo_id,
|
|
225
|
+
capability=task.capability,
|
|
226
|
+
action=task.action,
|
|
227
|
+
params=task.params,
|
|
228
|
+
)
|
|
229
|
+
self.journal.append_event(task_id=task.task_id, event_type="accepted", status="running", payload={"message": "started"})
|
|
230
|
+
await self.connection.send_event(TaskEventMessage(task_id=task.task_id, status="running", message="started"))
|
|
231
|
+
try:
|
|
232
|
+
result = await self.executor(task)
|
|
233
|
+
self.journal.record_terminal_result(task_id=task.task_id, status="succeeded", result=result)
|
|
234
|
+
await self.connection.send_result(TaskResultMessage(task_id=task.task_id, success=True, result=result))
|
|
235
|
+
except Exception as exc:
|
|
236
|
+
self.journal.record_terminal_result(task_id=task.task_id, status="failed", error=str(exc))
|
|
237
|
+
await self.connection.send_result(TaskResultMessage(task_id=task.task_id, success=False, error=str(exc)))
|
|
238
|
+
active, queued, max_concurrent = self.runtime_health()
|
|
239
|
+
await self.connection.send_status(
|
|
240
|
+
build_status_message(
|
|
241
|
+
client_id=self.config.client_id,
|
|
242
|
+
hostname=self.config.hostname,
|
|
243
|
+
version=self.config.version,
|
|
244
|
+
platform_name="python",
|
|
245
|
+
reason="task_update",
|
|
246
|
+
capabilities=self.capabilities(),
|
|
247
|
+
roots=self.roots(),
|
|
248
|
+
active_tasks=active,
|
|
249
|
+
queued_tasks=queued,
|
|
250
|
+
max_concurrent_tasks=max_concurrent,
|
|
251
|
+
connected=True,
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def validate_runtime_config(config: AgentConfig) -> list[str]:
|
|
257
|
+
errors: list[str] = []
|
|
258
|
+
if not config.config_path.exists():
|
|
259
|
+
errors.append(f"Config file not found at {config.config_path}. Run `bamboo-coding setup` first.")
|
|
260
|
+
if not config.allowed_roots:
|
|
261
|
+
errors.append(
|
|
262
|
+
f"No allowed roots configured in {config.config_path}. Configure [client].allowed_roots or run `bamboo-coding setup`."
|
|
263
|
+
)
|
|
264
|
+
return errors
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _display_path(path: Path) -> str:
|
|
268
|
+
resolved = path.expanduser().resolve()
|
|
269
|
+
home = Path.home().expanduser().resolve()
|
|
270
|
+
try:
|
|
271
|
+
relative = resolved.relative_to(home)
|
|
272
|
+
except ValueError:
|
|
273
|
+
return str(resolved)
|
|
274
|
+
return f"~/{relative.as_posix()}"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _toml_string(value: str) -> str:
|
|
278
|
+
return json.dumps(value)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _render_config_text(
|
|
282
|
+
*,
|
|
283
|
+
controller_url: str,
|
|
284
|
+
client_id: str,
|
|
285
|
+
hostname: str,
|
|
286
|
+
allowed_roots: list[str],
|
|
287
|
+
max_concurrent_tasks: int,
|
|
288
|
+
token_path: Path,
|
|
289
|
+
journal_path: Path,
|
|
290
|
+
) -> str:
|
|
291
|
+
roots_value = ", ".join(_toml_string(root) for root in allowed_roots)
|
|
292
|
+
return "\n".join(
|
|
293
|
+
[
|
|
294
|
+
"[client]",
|
|
295
|
+
f"controller_url = {_toml_string(controller_url)}",
|
|
296
|
+
f"client_id = {_toml_string(client_id)}",
|
|
297
|
+
f"hostname = {_toml_string(hostname)}",
|
|
298
|
+
f"allowed_roots = [{roots_value}]",
|
|
299
|
+
f"max_concurrent_tasks = {max_concurrent_tasks}",
|
|
300
|
+
"",
|
|
301
|
+
"[storage]",
|
|
302
|
+
f"token_path = {_toml_string(_display_path(token_path))}",
|
|
303
|
+
f"journal_path = {_toml_string(_display_path(journal_path))}",
|
|
304
|
+
"",
|
|
305
|
+
]
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
310
|
+
parser = argparse.ArgumentParser(description="Bamboo Coding client")
|
|
311
|
+
parser.add_argument("--config", help="Path to bamboo-coding.toml")
|
|
312
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
313
|
+
|
|
314
|
+
setup_parser = subparsers.add_parser("setup", help="Create the default Bamboo Coding client config")
|
|
315
|
+
setup_parser.add_argument("--controller-url", help="Controller websocket URL")
|
|
316
|
+
setup_parser.add_argument("--root", action="append", dest="roots", help="Allowed root path; pass more than once for multiple roots")
|
|
317
|
+
setup_parser.add_argument("--client-id", help="Client identifier advertised to the controller")
|
|
318
|
+
setup_parser.add_argument("--hostname", help="Hostname advertised to the controller")
|
|
319
|
+
setup_parser.add_argument("--force", action="store_true", help="Overwrite the config file if it already exists")
|
|
320
|
+
|
|
321
|
+
subparsers.add_parser("run", help="Run the Bamboo Coding client")
|
|
322
|
+
return parser
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
326
|
+
parser = build_parser()
|
|
327
|
+
args = parser.parse_args(argv)
|
|
328
|
+
if args.command is None:
|
|
329
|
+
args.command = "run"
|
|
330
|
+
return args
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def setup_command(args: argparse.Namespace) -> int:
|
|
334
|
+
config = load_config()
|
|
335
|
+
config_path = config.config_path
|
|
336
|
+
|
|
337
|
+
if config_path.exists() and not args.force:
|
|
338
|
+
print(f"Config already exists at {config_path}. Use --force to overwrite it.", file=sys.stderr)
|
|
339
|
+
return 1
|
|
340
|
+
|
|
341
|
+
roots = [str(Path(root).expanduser().resolve()) for root in (args.roots or list(config.allowed_roots) or [str(Path.cwd().resolve())])]
|
|
342
|
+
client_id = args.client_id or config.client_id
|
|
343
|
+
hostname = args.hostname or config.hostname or client_id
|
|
344
|
+
controller_url = args.controller_url or config.controller_url
|
|
345
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
346
|
+
config_path.write_text(
|
|
347
|
+
_render_config_text(
|
|
348
|
+
controller_url=controller_url,
|
|
349
|
+
client_id=client_id,
|
|
350
|
+
hostname=hostname,
|
|
351
|
+
allowed_roots=roots,
|
|
352
|
+
max_concurrent_tasks=config.max_concurrent_tasks,
|
|
353
|
+
token_path=config.token_path,
|
|
354
|
+
journal_path=config.journal_path,
|
|
355
|
+
),
|
|
356
|
+
encoding="utf-8",
|
|
357
|
+
)
|
|
358
|
+
print(f"Wrote config to {config_path}")
|
|
359
|
+
print("Next: bamboo-coding run")
|
|
360
|
+
return 0
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
async def _run_runtime(runtime: AgentRuntime) -> None:
|
|
364
|
+
stop_event = asyncio.Event()
|
|
365
|
+
loop = asyncio.get_running_loop()
|
|
366
|
+
registered_signals: list[signal.Signals] = []
|
|
367
|
+
|
|
368
|
+
for signum in (signal.SIGINT, signal.SIGTERM):
|
|
369
|
+
try:
|
|
370
|
+
loop.add_signal_handler(signum, stop_event.set)
|
|
371
|
+
registered_signals.append(signum)
|
|
372
|
+
except (NotImplementedError, RuntimeError):
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
await runtime.run(stop_event=stop_event)
|
|
377
|
+
finally:
|
|
378
|
+
for signum in registered_signals:
|
|
379
|
+
try:
|
|
380
|
+
loop.remove_signal_handler(signum)
|
|
381
|
+
except RuntimeError:
|
|
382
|
+
continue
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def run_command(_args: argparse.Namespace) -> int:
|
|
386
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
|
387
|
+
|
|
388
|
+
from bamboo_coding.client.capabilities.repo import execute_task
|
|
389
|
+
|
|
390
|
+
config = load_config()
|
|
391
|
+
errors = validate_runtime_config(config)
|
|
392
|
+
if errors:
|
|
393
|
+
for error in errors:
|
|
394
|
+
print(error, file=sys.stderr)
|
|
395
|
+
return 1
|
|
396
|
+
|
|
397
|
+
repositories = {item.repo_id: item.path for item in config.repositories}
|
|
398
|
+
|
|
399
|
+
async def _execute(task: TaskMessage) -> dict:
|
|
400
|
+
return execute_task(task, repositories)
|
|
401
|
+
|
|
402
|
+
runtime = AgentRuntime(_execute, config=config)
|
|
403
|
+
try:
|
|
404
|
+
asyncio.run(_run_runtime(runtime))
|
|
405
|
+
except KeyboardInterrupt:
|
|
406
|
+
LOGGER.info("Client interrupted")
|
|
407
|
+
return 0
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def main(argv: list[str] | None = None) -> None:
|
|
411
|
+
args = parse_args(argv)
|
|
412
|
+
if args.config:
|
|
413
|
+
os.environ["BAMBOO_CODING_CONFIG_PATH"] = args.config
|
|
414
|
+
|
|
415
|
+
if args.command == "setup":
|
|
416
|
+
code = setup_command(args)
|
|
417
|
+
else:
|
|
418
|
+
code = run_command(args)
|
|
419
|
+
|
|
420
|
+
if code:
|
|
421
|
+
raise SystemExit(code)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
if __name__ == "__main__":
|
|
425
|
+
main()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from bamboo_coding.shared.protocol import AgentRegisterMessage, ClientStatusMessage
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _build_common_payload(*, client_id: str, hostname: str, version: str, platform_name: str, capabilities: list[dict], roots: list[dict] | None, repositories: list[dict] | None, active_tasks: int, queued_tasks: int, max_concurrent_tasks: int, connected: bool) -> dict:
|
|
7
|
+
root_payload = roots if roots is not None else (repositories or [])
|
|
8
|
+
return {
|
|
9
|
+
"protocol_version": 2,
|
|
10
|
+
"client_id": client_id,
|
|
11
|
+
"client_info": {
|
|
12
|
+
"hostname": hostname,
|
|
13
|
+
"version": version,
|
|
14
|
+
"platform": platform_name,
|
|
15
|
+
},
|
|
16
|
+
"capabilities": capabilities,
|
|
17
|
+
"roots": root_payload,
|
|
18
|
+
"connectivity": {"controller": {"connected": connected}},
|
|
19
|
+
"runtime_health": {
|
|
20
|
+
"active_tasks": active_tasks,
|
|
21
|
+
"queued_tasks": queued_tasks,
|
|
22
|
+
"max_concurrent_tasks": max_concurrent_tasks,
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_register_message(*, client_id: str, hostname: str, version: str, platform_name: str, capabilities: list[dict], roots: list[dict] | None = None, repositories: list[dict] | None = None, active_tasks: int, queued_tasks: int, max_concurrent_tasks: int, connected: bool = True) -> AgentRegisterMessage:
|
|
28
|
+
payload = _build_common_payload(
|
|
29
|
+
client_id=client_id,
|
|
30
|
+
hostname=hostname,
|
|
31
|
+
version=version,
|
|
32
|
+
platform_name=platform_name,
|
|
33
|
+
capabilities=capabilities,
|
|
34
|
+
roots=roots,
|
|
35
|
+
repositories=repositories,
|
|
36
|
+
active_tasks=active_tasks,
|
|
37
|
+
queued_tasks=queued_tasks,
|
|
38
|
+
max_concurrent_tasks=max_concurrent_tasks,
|
|
39
|
+
connected=connected,
|
|
40
|
+
)
|
|
41
|
+
return AgentRegisterMessage(**payload)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_status_message(*, client_id: str, hostname: str, version: str, platform_name: str, reason: str, capabilities: list[dict], roots: list[dict] | None = None, repositories: list[dict] | None = None, active_tasks: int, queued_tasks: int, max_concurrent_tasks: int, connected: bool) -> ClientStatusMessage:
|
|
45
|
+
payload = _build_common_payload(
|
|
46
|
+
client_id=client_id,
|
|
47
|
+
hostname=hostname,
|
|
48
|
+
version=version,
|
|
49
|
+
platform_name=platform_name,
|
|
50
|
+
capabilities=capabilities,
|
|
51
|
+
roots=roots,
|
|
52
|
+
repositories=repositories,
|
|
53
|
+
active_tasks=active_tasks,
|
|
54
|
+
queued_tasks=queued_tasks,
|
|
55
|
+
max_concurrent_tasks=max_concurrent_tasks,
|
|
56
|
+
connected=connected,
|
|
57
|
+
)
|
|
58
|
+
payload["reason"] = reason
|
|
59
|
+
return ClientStatusMessage(**payload)
|