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,95 @@
1
+ """Credential resolution (spec section 5).
2
+
3
+ Credentials never live in jambu.yaml. They are resolved from, in order:
4
+ environment variables, a project-local ``.env``, then ``~/.gpu/credentials``.
5
+ The resolution strategy can grow (keychain, secret managers, CI secrets)
6
+ without touching jambu.yaml or any adapter.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ from .paths import gpu_home
16
+
17
+ _DOTENV_CACHE: dict[Path, dict[str, str]] = {}
18
+
19
+
20
+ def _parse_dotenv(path: Path) -> dict[str, str]:
21
+ if path in _DOTENV_CACHE:
22
+ return _DOTENV_CACHE[path]
23
+ values: dict[str, str] = {}
24
+ if path.is_file():
25
+ for raw in path.read_text().splitlines():
26
+ line = raw.strip()
27
+ if not line or line.startswith("#") or "=" not in line:
28
+ continue
29
+ if line.startswith("export "):
30
+ line = line[len("export ") :]
31
+ key, _, value = line.partition("=")
32
+ value = value.strip().strip('"').strip("'")
33
+ values[key.strip()] = value
34
+ _DOTENV_CACHE[path] = values
35
+ return values
36
+
37
+
38
+ class CredentialResolver:
39
+ """Looks up named credentials without ever writing them to disk."""
40
+
41
+ def __init__(
42
+ self,
43
+ project_dir: Optional[Path] = None,
44
+ profile: str = "default",
45
+ extra_sources: Optional[list[tuple[str, Path]]] = None,
46
+ ) -> None:
47
+ self.project_dir = Path(project_dir or Path.cwd())
48
+ self.profile = profile
49
+ self.sources: list[tuple[str, Path]] = [
50
+ ("project .env", self.project_dir / ".env"),
51
+ ("user credentials", gpu_home() / "credentials"),
52
+ *(extra_sources or []),
53
+ ]
54
+
55
+ def get(self, name: str) -> Optional[str]:
56
+ value = os.environ.get(name)
57
+ if value:
58
+ return value.strip()
59
+ # Profile-scoped override: VAST_API_KEY__staging
60
+ for _, path in self.sources:
61
+ values = _parse_dotenv(path)
62
+ scoped = values.get(f"{name}__{self.profile}")
63
+ if scoped:
64
+ return scoped
65
+ if values.get(name):
66
+ return values[name]
67
+ return None
68
+
69
+ def file_source(self, name: str) -> Optional[str]:
70
+ """Which file (not the environment) provides ``name``, if any.
71
+
72
+ A systemd user unit does not inherit the shell's environment, so
73
+ anything scheduled must find its credentials in one of these files.
74
+ """
75
+ for label, path in self.sources:
76
+ values = _parse_dotenv(path)
77
+ if values.get(f"{name}__{self.profile}") or values.get(name):
78
+ return f"{label} ({path})"
79
+ return None
80
+
81
+ def require(self, name: str) -> str:
82
+ value = self.get(name)
83
+ if not value:
84
+ raise KeyError(name)
85
+ return value
86
+
87
+ def missing(self, names: list[str]) -> list[str]:
88
+ return [name for name in names if not self.get(name)]
89
+
90
+ def describe_sources(self) -> list[str]:
91
+ out = ["environment variables"]
92
+ for label, path in self.sources:
93
+ if path.is_file():
94
+ out.append(f"{label} ({path})")
95
+ return out
@@ -0,0 +1,47 @@
1
+ """Duration parsing/formatting shared by config and CLI flags."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ _UNITS = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}
8
+ _TOKEN = re.compile(r"(\d+(?:\.\d+)?)\s*([smhdw])", re.IGNORECASE)
9
+
10
+
11
+ def parse_duration(value: "str | int | float | None") -> "float | None":
12
+ """Parse ``6h``, ``15m``, ``1h30m``, ``90`` (seconds) into seconds.
13
+
14
+ ``None`` and the literal strings ``none``/``never``/``off`` mean "no limit".
15
+ """
16
+ if value is None:
17
+ return None
18
+ if isinstance(value, (int, float)):
19
+ return float(value)
20
+
21
+ text = str(value).strip().lower()
22
+ if not text or text in {"none", "never", "off", "unlimited", "0s"}:
23
+ return None
24
+ if re.fullmatch(r"\d+(\.\d+)?", text):
25
+ return float(text)
26
+
27
+ matches = _TOKEN.findall(text)
28
+ if not matches or "".join(f"{n}{u}" for n, u in matches).replace(" ", "") != text.replace(" ", ""):
29
+ raise ValueError(f"invalid duration: {value!r} (expected e.g. 30s, 15m, 6h, 1h30m)")
30
+ return float(sum(float(n) * _UNITS[u.lower()] for n, u in matches))
31
+
32
+
33
+ def format_duration(seconds: "float | None") -> str:
34
+ """Render seconds as a compact human string (``1h30m``)."""
35
+ if seconds is None:
36
+ return "-"
37
+ seconds = int(max(0, round(seconds)))
38
+ if seconds < 60:
39
+ return f"{seconds}s"
40
+ parts: list[str] = []
41
+ for unit, size in (("d", 86400), ("h", 3600), ("m", 60), ("s", 1)):
42
+ if seconds >= size:
43
+ amount, seconds = divmod(seconds, size)
44
+ parts.append(f"{amount}{unit}")
45
+ if len(parts) == 2:
46
+ break
47
+ return "".join(parts)