gpu4agents 0.5.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.
Files changed (59) hide show
  1. gpu4agents/__init__.py +5 -0
  2. gpu4agents/__main__.py +4 -0
  3. gpu4agents/agent/__init__.py +1 -0
  4. gpu4agents/agent/install.py +173 -0
  5. gpu4agents/agent/jobs.py +193 -0
  6. gpu4agents/agent/mcp.py +646 -0
  7. gpu4agents/agent/skill/SKILL.md +68 -0
  8. gpu4agents/cli/__init__.py +0 -0
  9. gpu4agents/cli/_common.py +131 -0
  10. gpu4agents/cli/account.py +330 -0
  11. gpu4agents/cli/agent.py +74 -0
  12. gpu4agents/cli/inspect.py +207 -0
  13. gpu4agents/cli/lifecycle.py +127 -0
  14. gpu4agents/cli/main.py +163 -0
  15. gpu4agents/cli/providers.py +96 -0
  16. gpu4agents/cli/run.py +38 -0
  17. gpu4agents/cli/setup.py +164 -0
  18. gpu4agents/cli/status.py +224 -0
  19. gpu4agents/cli/timer.py +143 -0
  20. gpu4agents/cli/validate.py +57 -0
  21. gpu4agents/core/__init__.py +0 -0
  22. gpu4agents/core/beacon.py +133 -0
  23. gpu4agents/core/config.py +647 -0
  24. gpu4agents/core/credentials.py +95 -0
  25. gpu4agents/core/durations.py +47 -0
  26. gpu4agents/core/engine.py +1335 -0
  27. gpu4agents/core/errors.py +94 -0
  28. gpu4agents/core/events.py +117 -0
  29. gpu4agents/core/execution.py +169 -0
  30. gpu4agents/core/health.py +62 -0
  31. gpu4agents/core/hosts.py +58 -0
  32. gpu4agents/core/model_inspect.py +368 -0
  33. gpu4agents/core/models.py +313 -0
  34. gpu4agents/core/paths.py +42 -0
  35. gpu4agents/core/policy.py +264 -0
  36. gpu4agents/core/state.py +406 -0
  37. gpu4agents/core/sweep.py +478 -0
  38. gpu4agents/core/tool_parsers.py +63 -0
  39. gpu4agents/providers/__init__.py +4 -0
  40. gpu4agents/providers/base.py +137 -0
  41. gpu4agents/providers/registry.py +63 -0
  42. gpu4agents/providers/vast/__init__.py +3 -0
  43. gpu4agents/providers/vast/client.py +255 -0
  44. gpu4agents/providers/vast/config.py +56 -0
  45. gpu4agents/providers/vast/mapper.py +292 -0
  46. gpu4agents/providers/vast/provider.py +438 -0
  47. gpu4agents/runtime/__init__.py +6 -0
  48. gpu4agents/runtime/base.py +184 -0
  49. gpu4agents/runtime/vllm.py +295 -0
  50. gpu4agents/watchdog/__init__.py +3 -0
  51. gpu4agents/watchdog/agent_body.py +959 -0
  52. gpu4agents/watchdog/builder.py +248 -0
  53. gpu4agents-0.5.0.dist-info/METADATA +157 -0
  54. gpu4agents-0.5.0.dist-info/RECORD +59 -0
  55. gpu4agents-0.5.0.dist-info/WHEEL +4 -0
  56. gpu4agents-0.5.0.dist-info/entry_points.txt +2 -0
  57. gpu4agents-0.5.0.dist-info/licenses/LICENSE +21 -0
  58. jambu_gpu/__init__.py +9 -0
  59. jambu_gpu/__main__.py +4 -0
@@ -0,0 +1,133 @@
1
+ """Client for the remote lifecycle watchdog.
2
+
3
+ The watchdog is the authority on activity: it holds the heartbeat state and
4
+ performs the shutdown. The CLI only reports activity to it and reads it back.
5
+ Every call is best-effort - losing the watchdog must never break a workload,
6
+ and an unreachable watchdog is surfaced as a warning, not a crash.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Optional
12
+ from urllib.parse import quote
13
+
14
+ import httpx
15
+
16
+ from .errors import JambuError
17
+
18
+
19
+ class WatchdogUnavailable(JambuError):
20
+ """The remote watchdog could not be reached."""
21
+
22
+
23
+ class WatchdogClient:
24
+ def __init__(self, base_url: str, token: Optional[str], timeout: float = 10.0) -> None:
25
+ self.base_url = base_url.rstrip("/")
26
+ self.token = token
27
+ self.timeout = timeout
28
+
29
+ @property
30
+ def headers(self) -> dict[str, str]:
31
+ return {"X-Jambu-Token": self.token} if self.token else {}
32
+
33
+ # -- transport ----------------------------------------------------------
34
+
35
+ def _request(self, method: str, path: str, payload: Optional[dict] = None) -> dict[str, Any]:
36
+ url = f"{self.base_url}{path}"
37
+ try:
38
+ response = httpx.request(
39
+ method,
40
+ url,
41
+ json=payload,
42
+ headers=self.headers,
43
+ timeout=self.timeout,
44
+ )
45
+ except httpx.HTTPError as exc:
46
+ raise WatchdogUnavailable(f"watchdog unreachable at {url}: {exc}") from exc
47
+ if response.status_code == 401:
48
+ raise WatchdogUnavailable("watchdog rejected the control token")
49
+ if response.status_code >= 400:
50
+ raise WatchdogUnavailable(
51
+ f"watchdog returned {response.status_code}: {response.text[:200]}"
52
+ )
53
+ try:
54
+ return response.json()
55
+ except ValueError:
56
+ return {}
57
+
58
+ def try_request(self, method: str, path: str, payload: Optional[dict] = None) -> Optional[dict]:
59
+ try:
60
+ return self._request(method, path, payload)
61
+ except WatchdogUnavailable:
62
+ return None
63
+
64
+ # -- routes -------------------------------------------------------------
65
+
66
+ def ping(self) -> bool:
67
+ try:
68
+ self._request("GET", "/health")
69
+ return True
70
+ except WatchdogUnavailable:
71
+ return False
72
+
73
+ def state(self) -> dict[str, Any]:
74
+ return self._request("GET", "/state")
75
+
76
+ def try_state(self) -> Optional[dict[str, Any]]:
77
+ return self.try_request("GET", "/state")
78
+
79
+ def events(self, limit: int = 100) -> list[dict[str, Any]]:
80
+ data = self.try_request("GET", f"/events?limit={limit}") or {}
81
+ return data.get("events", [])
82
+
83
+ def logs(self, name: str = "runtime", lines: int = 200) -> list[str]:
84
+ """Tail of an instance log (runtime | onstart | watchdog).
85
+
86
+ Raises WatchdogUnavailable, so a caller can tell "empty" from "unreachable".
87
+ """
88
+ data = self._request("GET", f"/logs?name={quote(name)}&lines={int(lines)}")
89
+ return [str(line) for line in data.get("lines") or []]
90
+
91
+ def identify(
92
+ self, instance_id: str, hourly_cost_usd: Optional[float] = None
93
+ ) -> Optional[dict]:
94
+ return self.try_request(
95
+ "POST",
96
+ "/identify",
97
+ {"instance_id": instance_id, "hourly_cost_usd": hourly_cost_usd},
98
+ )
99
+
100
+ def activity(self) -> Optional[dict]:
101
+ return self.try_request("POST", "/activity")
102
+
103
+ def mark_starting(self) -> Optional[dict]:
104
+ return self.try_request("POST", "/starting")
105
+
106
+ def mark_ready(self) -> Optional[dict]:
107
+ return self.try_request("POST", "/ready")
108
+
109
+ def workload_start(self, workload_id: str, label: str = "") -> Optional[dict]:
110
+ return self.try_request(
111
+ "POST", "/workload/start", {"id": workload_id, "label": label}
112
+ )
113
+
114
+ def workload_heartbeat(self, workload_id: str) -> Optional[dict]:
115
+ return self.try_request("POST", "/workload/heartbeat", {"id": workload_id})
116
+
117
+ def workload_end(
118
+ self, workload_id: str, returncode: Optional[int], state: Optional[str] = None
119
+ ) -> Optional[dict]:
120
+ return self.try_request(
121
+ "POST",
122
+ "/workload/end",
123
+ {"id": workload_id, "returncode": returncode, "state": state},
124
+ )
125
+
126
+ def lock(self, ttl_seconds: Optional[float], reason: str = "") -> dict:
127
+ return self._request("POST", "/lock", {"ttl_seconds": ttl_seconds, "reason": reason})
128
+
129
+ def unlock(self, reason: str = "manual") -> dict:
130
+ return self._request("POST", "/unlock", {"reason": reason})
131
+
132
+ def shutdown(self, action: str = "stop", reason: str = "manual") -> Optional[dict]:
133
+ return self.try_request("POST", "/shutdown", {"action": action, "reason": reason})