multiagentos 0.4.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.
- agents/__init__.py +1 -0
- agents/catalog.py +52 -0
- agents/registry.py +11 -0
- core/__init__.py +38 -0
- core/artifacts.py +47 -0
- core/chat_agent_bridge.py +280 -0
- core/chat_agent_registry.py +61 -0
- core/chat_agent_router.py +85 -0
- core/chat_session.py +62 -0
- core/contracts/__init__.py +22 -0
- core/contracts/agent.py +21 -0
- core/contracts/ai.py +25 -0
- core/contracts/capability.py +44 -0
- core/contracts/chat_agent.py +34 -0
- core/contracts/checkpoint.py +78 -0
- core/contracts/discovery.py +30 -0
- core/contracts/execution.py +28 -0
- core/contracts/github.py +74 -0
- core/contracts/handoff.py +56 -0
- core/contracts/health.py +206 -0
- core/contracts/human_review.py +20 -0
- core/contracts/ide.py +113 -0
- core/contracts/mcp.py +76 -0
- core/contracts/model_runtime.py +25 -0
- core/contracts/planning.py +25 -0
- core/contracts/profile.py +66 -0
- core/contracts/quota.py +62 -0
- core/contracts/repository.py +34 -0
- core/contracts/resume.py +55 -0
- core/contracts/runtime.py +19 -0
- core/contracts/vyrelon_runtime.py +118 -0
- core/contracts/work_unit.py +48 -0
- core/delegation.py +33 -0
- core/handoff.py +68 -0
- core/lifecycle.py +85 -0
- core/multi_agent_workflow.py +574 -0
- core/orchestrator.py +51 -0
- core/planning.py +22 -0
- core/registry.py +44 -0
- core/routing.py +195 -0
- core/state.py +155 -0
- installer/__init__.py +1 -0
- installer/init.py +71 -0
- integrations/__init__.py +0 -0
- integrations/github/__init__.py +3 -0
- integrations/github/gateway.py +164 -0
- integrations/openai/__init__.py +1 -0
- integrations/openai/chat_agent.py +147 -0
- multiagentos/__init__.py +1 -0
- multiagentos/cli.py +759 -0
- multiagentos-0.4.0.dist-info/METADATA +7 -0
- multiagentos-0.4.0.dist-info/RECORD +122 -0
- multiagentos-0.4.0.dist-info/WHEEL +5 -0
- multiagentos-0.4.0.dist-info/entry_points.txt +2 -0
- multiagentos-0.4.0.dist-info/top_level.txt +7 -0
- profiles/__init__.py +1 -0
- profiles/common.py +11 -0
- profiles/detector.py +67 -0
- profiles/registry.py +46 -0
- profiles/resolver.py +63 -0
- runtime/__init__.py +5 -0
- runtime/agent/__init__.py +1 -0
- runtime/agent/ide.py +74 -0
- runtime/agent/model.py +197 -0
- runtime/agent/model_backed.py +49 -0
- runtime/agent/process.py +21 -0
- runtime/builtin_tools.py +57 -0
- runtime/capability.py +143 -0
- runtime/chat_adapter_registry.py +43 -0
- runtime/chat_config.py +57 -0
- runtime/chat_execution.py +52 -0
- runtime/config/__init__.py +1 -0
- runtime/config/loader.py +67 -0
- runtime/discovery.py +334 -0
- runtime/execution_config.py +71 -0
- runtime/execution_registry.py +50 -0
- runtime/git.py +84 -0
- runtime/github.py +56 -0
- runtime/github_probe.py +23 -0
- runtime/health.py +14 -0
- runtime/ide/__init__.py +6 -0
- runtime/ide/bridge.py +198 -0
- runtime/ide/registry.py +24 -0
- runtime/ide/runtime.py +35 -0
- runtime/local/__init__.py +18 -0
- runtime/local/filesystem.py +58 -0
- runtime/local/patch.py +79 -0
- runtime/local/path_security.py +31 -0
- runtime/local/permissions.py +20 -0
- runtime/local/process.py +56 -0
- runtime/local/shell.py +97 -0
- runtime/mcp/__init__.py +7 -0
- runtime/mcp/client.py +120 -0
- runtime/mcp/config.py +36 -0
- runtime/mcp/policy.py +57 -0
- runtime/mcp/proxy.py +30 -0
- runtime/mcp/server.py +137 -0
- runtime/mcp/session.py +19 -0
- runtime/model/__init__.py +5 -0
- runtime/model/ai_runtime.py +165 -0
- runtime/model/cli.py +49 -0
- runtime/model/config.py +98 -0
- runtime/model/credentials.py +42 -0
- runtime/model/factory.py +82 -0
- runtime/model/http.py +78 -0
- runtime/model/invoker.py +16 -0
- runtime/model/materializer.py +101 -0
- runtime/model/native.py +244 -0
- runtime/model/providers.py +55 -0
- runtime/model/registry.py +26 -0
- runtime/model/runtime_loader.py +51 -0
- runtime/model_control.py +136 -0
- runtime/multi_agent.py +142 -0
- runtime/policy.py +28 -0
- runtime/process.py +41 -0
- runtime/quota.py +259 -0
- runtime/repository.py +103 -0
- runtime/repository_tools.py +62 -0
- runtime/status.py +87 -0
- runtime/tool_calling.py +134 -0
- runtime/validation.py +121 -0
- runtime/vyrelon.py +1557 -0
runtime/discovery.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Provider-neutral discovery adapters for model capabilities and quota."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import urllib.request
|
|
8
|
+
from urllib.error import HTTPError, URLError
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
from core.contracts.ai import AIProvider, ModelSpec
|
|
13
|
+
from core.contracts.capability import CapabilityConfidence, ModelCapabilityProfile
|
|
14
|
+
from core.contracts.discovery import DiscoveryError, ProviderDiscoveryResult
|
|
15
|
+
from core.contracts.quota import QuotaConfidence, QuotaDimension, QuotaSnapshot
|
|
16
|
+
from runtime.policy import ExecutionPolicy
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _path(value: object, path: tuple[str, ...]) -> object:
|
|
20
|
+
current = value
|
|
21
|
+
for part in path:
|
|
22
|
+
if not isinstance(current, dict):
|
|
23
|
+
return None
|
|
24
|
+
current = current.get(part)
|
|
25
|
+
return current
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _string_list(value: object) -> frozenset[str]:
|
|
29
|
+
if not isinstance(value, list):
|
|
30
|
+
return frozenset()
|
|
31
|
+
return frozenset(str(item) for item in value if isinstance(item, (str, int, float)))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _now() -> datetime:
|
|
35
|
+
return datetime.now(timezone.utc)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ProviderDiscoveryAdapter:
|
|
39
|
+
"""Discover provider metadata using declarative provider configuration."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
policy: ExecutionPolicy | None = None,
|
|
44
|
+
fetch: Callable[[str, dict[str, str]], dict[str, Any] | tuple[dict[str, Any], dict[str, str]]] | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
self.policy = policy or ExecutionPolicy()
|
|
47
|
+
self.fetch = fetch or self._fetch
|
|
48
|
+
|
|
49
|
+
def discover(
|
|
50
|
+
self,
|
|
51
|
+
provider: AIProvider,
|
|
52
|
+
*,
|
|
53
|
+
models: tuple[ModelSpec, ...] | None = None,
|
|
54
|
+
) -> ProviderDiscoveryResult:
|
|
55
|
+
metadata = provider.metadata
|
|
56
|
+
discovery = dict(metadata.get("discovery", {}) or {})
|
|
57
|
+
self._apply_builtin_defaults(provider, discovery)
|
|
58
|
+
if not isinstance(discovery, dict):
|
|
59
|
+
return ProviderDiscoveryResult(provider.id, source="model_spec")
|
|
60
|
+
|
|
61
|
+
endpoint = discovery.get("endpoint")
|
|
62
|
+
if not isinstance(endpoint, str) or not endpoint:
|
|
63
|
+
return ProviderDiscoveryResult(provider.id, source="model_spec")
|
|
64
|
+
|
|
65
|
+
headers = self._headers(discovery)
|
|
66
|
+
self._query_env = discovery.get("query_env", {})
|
|
67
|
+
fetched = self.fetch(endpoint, headers)
|
|
68
|
+
response_headers = {}
|
|
69
|
+
if isinstance(fetched, tuple):
|
|
70
|
+
payload, response_headers = fetched
|
|
71
|
+
else:
|
|
72
|
+
payload = fetched
|
|
73
|
+
model_payload = self._extract(payload, discovery.get("models_path", ["data"]))
|
|
74
|
+
if provider.kind.lower() == "gemini" and isinstance(model_payload, list):
|
|
75
|
+
model_payload = [self._normalize_gemini_model(item) for item in model_payload]
|
|
76
|
+
quota_payload = self._extract(payload, discovery.get("quota_path", []))
|
|
77
|
+
|
|
78
|
+
configured = {model.id: model for model in (models or provider.models)}
|
|
79
|
+
profiles = self._capabilities(provider, model_payload, configured)
|
|
80
|
+
quotas = self._quotas(provider, quota_payload, configured)
|
|
81
|
+
quotas.extend(self._header_quotas(provider, response_headers, configured))
|
|
82
|
+
return ProviderDiscoveryResult(
|
|
83
|
+
provider_id=provider.id,
|
|
84
|
+
capabilities=tuple(profiles),
|
|
85
|
+
quotas=tuple(quotas),
|
|
86
|
+
source="provider_discovery",
|
|
87
|
+
metadata={"endpoint": endpoint},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def discover_safe(
|
|
91
|
+
self,
|
|
92
|
+
provider: AIProvider,
|
|
93
|
+
*,
|
|
94
|
+
models: tuple[ModelSpec, ...] | None = None,
|
|
95
|
+
) -> tuple[ProviderDiscoveryResult | None, DiscoveryError | None]:
|
|
96
|
+
try:
|
|
97
|
+
return self.discover(provider, models=models), None
|
|
98
|
+
except (OSError, ValueError, RuntimeError) as exc:
|
|
99
|
+
return None, DiscoveryError(
|
|
100
|
+
provider_id=provider.id,
|
|
101
|
+
operation="discover",
|
|
102
|
+
message=str(exc),
|
|
103
|
+
retryable=isinstance(exc, OSError),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _apply_builtin_defaults(provider: AIProvider, discovery: dict[str, object]) -> None:
|
|
108
|
+
if discovery.get("endpoint"):
|
|
109
|
+
return
|
|
110
|
+
presets = {
|
|
111
|
+
"openai": ("https://api.openai.com/v1/models", "OPENAI_API_KEY", "openai"),
|
|
112
|
+
"anthropic": ("https://api.anthropic.com/v1/models", "ANTHROPIC_API_KEY", "anthropic"),
|
|
113
|
+
"gemini": ("https://generativelanguage.googleapis.com/v1beta/models", "GEMINI_API_KEY", "gemini"),
|
|
114
|
+
}
|
|
115
|
+
preset = presets.get(provider.kind.lower()) or presets.get(provider.id.lower())
|
|
116
|
+
if not preset:
|
|
117
|
+
return
|
|
118
|
+
endpoint, key_env, kind = preset
|
|
119
|
+
discovery["endpoint"] = endpoint
|
|
120
|
+
discovery["models_path"] = ["models"] if kind == "gemini" else ["data"]
|
|
121
|
+
if kind == "gemini":
|
|
122
|
+
discovery["query_env"] = {"key": key_env}
|
|
123
|
+
elif kind == "openai":
|
|
124
|
+
discovery["header_env"] = {"Authorization": key_env}
|
|
125
|
+
discovery["header_prefix"] = {"Authorization": "Bearer "}
|
|
126
|
+
else:
|
|
127
|
+
discovery["header_env"] = {"x-api-key": key_env}
|
|
128
|
+
discovery["headers"] = {"anthropic-version": "2023-06-01"}
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _header_quotas(provider: AIProvider, headers: dict[str, str], configured: dict[str, ModelSpec]) -> list[QuotaSnapshot]:
|
|
132
|
+
mappings = {
|
|
133
|
+
"openai": (
|
|
134
|
+
("requests", "x-ratelimit-limit-requests", "x-ratelimit-remaining-requests"),
|
|
135
|
+
("tokens", "x-ratelimit-limit-tokens", "x-ratelimit-remaining-tokens"),
|
|
136
|
+
),
|
|
137
|
+
"anthropic": (
|
|
138
|
+
("requests", "anthropic-ratelimit-requests-limit", "anthropic-ratelimit-requests-remaining"),
|
|
139
|
+
("input_tokens", "anthropic-ratelimit-input-tokens-limit", "anthropic-ratelimit-input-tokens-remaining"),
|
|
140
|
+
("output_tokens", "anthropic-ratelimit-output-tokens-limit", "anthropic-ratelimit-output-tokens-remaining"),
|
|
141
|
+
),
|
|
142
|
+
}
|
|
143
|
+
rows = mappings.get(provider.kind.lower(), ())
|
|
144
|
+
dimensions = []
|
|
145
|
+
for name, limit_key, remaining_key in rows:
|
|
146
|
+
limit, remaining = _as_int(headers.get(limit_key)), _as_int(headers.get(remaining_key))
|
|
147
|
+
if limit is None and remaining is None:
|
|
148
|
+
continue
|
|
149
|
+
dimensions.append(QuotaDimension(name=name, limit=limit, used=(limit - remaining) if limit is not None and remaining is not None else None,
|
|
150
|
+
remaining=remaining, confidence=QuotaConfidence.ACTUAL, source="provider_response_headers"))
|
|
151
|
+
if not dimensions:
|
|
152
|
+
return []
|
|
153
|
+
return [QuotaSnapshot(model_id=model_id, provider_id=provider.id, observed_at=_now(), dimensions=tuple(dimensions),
|
|
154
|
+
scope="provider", metadata={"source": "provider_response_headers"}) for model_id in configured]
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _normalize_gemini_model(item: object) -> object:
|
|
158
|
+
if not isinstance(item, dict):
|
|
159
|
+
return item
|
|
160
|
+
normalized = dict(item)
|
|
161
|
+
name = normalized.get("name")
|
|
162
|
+
if isinstance(name, str) and name.startswith("models/"):
|
|
163
|
+
normalized["id"] = name[len("models/"):]
|
|
164
|
+
methods = normalized.get("supportedGenerationMethods")
|
|
165
|
+
if isinstance(methods, list):
|
|
166
|
+
normalized["capabilities"] = ["chat" if str(method) == "generateContent" else str(method) for method in methods]
|
|
167
|
+
return normalized
|
|
168
|
+
|
|
169
|
+
def _capabilities(
|
|
170
|
+
self,
|
|
171
|
+
provider: AIProvider,
|
|
172
|
+
value: object,
|
|
173
|
+
configured: dict[str, ModelSpec],
|
|
174
|
+
) -> list[ModelCapabilityProfile]:
|
|
175
|
+
if not isinstance(value, list):
|
|
176
|
+
return []
|
|
177
|
+
profiles: list[ModelCapabilityProfile] = []
|
|
178
|
+
for item in value:
|
|
179
|
+
if not isinstance(item, dict):
|
|
180
|
+
continue
|
|
181
|
+
model_id = str(item.get("id", ""))
|
|
182
|
+
if not model_id:
|
|
183
|
+
continue
|
|
184
|
+
base = configured.get(model_id)
|
|
185
|
+
capabilities = set(base.capabilities if base else ())
|
|
186
|
+
capabilities.update(_normalize_capabilities(item))
|
|
187
|
+
profiles.append(
|
|
188
|
+
ModelCapabilityProfile(
|
|
189
|
+
model_id=model_id,
|
|
190
|
+
provider_id=provider.id,
|
|
191
|
+
capabilities=frozenset(capabilities),
|
|
192
|
+
confidence=CapabilityConfidence.DECLARED,
|
|
193
|
+
source="provider_discovery",
|
|
194
|
+
metadata={"provider_metadata": item},
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
return profiles
|
|
198
|
+
|
|
199
|
+
def _quotas(
|
|
200
|
+
self,
|
|
201
|
+
provider: AIProvider,
|
|
202
|
+
value: object,
|
|
203
|
+
configured: dict[str, ModelSpec],
|
|
204
|
+
) -> list[QuotaSnapshot]:
|
|
205
|
+
if not isinstance(value, list):
|
|
206
|
+
return []
|
|
207
|
+
snapshots: list[QuotaSnapshot] = []
|
|
208
|
+
for item in value:
|
|
209
|
+
if not isinstance(item, dict):
|
|
210
|
+
continue
|
|
211
|
+
model_id = str(item.get("model_id", item.get("model", "")))
|
|
212
|
+
if not model_id or model_id not in configured:
|
|
213
|
+
continue
|
|
214
|
+
raw_dimensions = item.get("dimensions", [])
|
|
215
|
+
dimensions: list[QuotaDimension] = []
|
|
216
|
+
if isinstance(raw_dimensions, list):
|
|
217
|
+
for raw in raw_dimensions:
|
|
218
|
+
if not isinstance(raw, dict) or not raw.get("name"):
|
|
219
|
+
continue
|
|
220
|
+
dimensions.append(
|
|
221
|
+
QuotaDimension(
|
|
222
|
+
name=str(raw["name"]),
|
|
223
|
+
limit=raw.get("limit"),
|
|
224
|
+
used=raw.get("used"),
|
|
225
|
+
remaining=raw.get("remaining"),
|
|
226
|
+
reset_at=_parse_datetime(raw.get("reset_at")),
|
|
227
|
+
window_seconds=_as_int(raw.get("window_seconds")),
|
|
228
|
+
confidence=QuotaConfidence.ACTUAL,
|
|
229
|
+
source="provider_discovery",
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
snapshots.append(
|
|
233
|
+
QuotaSnapshot(
|
|
234
|
+
model_id=model_id,
|
|
235
|
+
provider_id=provider.id,
|
|
236
|
+
observed_at=_now(),
|
|
237
|
+
dimensions=tuple(dimensions),
|
|
238
|
+
scope=str(item.get("scope", "model")),
|
|
239
|
+
metadata={"provider_metadata": item},
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
return snapshots
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def _extract(payload: dict[str, Any], value: object) -> object:
|
|
246
|
+
if value in (None, [], ""):
|
|
247
|
+
return None
|
|
248
|
+
if not isinstance(value, list):
|
|
249
|
+
return None
|
|
250
|
+
return _path(payload, tuple(str(item) for item in value))
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def _headers(discovery: dict[str, object]) -> dict[str, str]:
|
|
254
|
+
result = {str(k): str(v) for k, v in (discovery.get("headers", {}) or {}).items()}
|
|
255
|
+
prefixes = discovery.get("header_prefix", {}) or {}
|
|
256
|
+
for header, env_name in (discovery.get("header_env", {}) or {}).items():
|
|
257
|
+
if isinstance(env_name, str) and os.environ.get(env_name):
|
|
258
|
+
prefix = prefixes.get(header, "") if isinstance(prefixes, dict) else ""
|
|
259
|
+
result[str(header)] = f"{prefix}{os.environ[env_name]}"
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
def _fetch(self, endpoint: str, headers: dict[str, str]) -> tuple[dict[str, Any], dict[str, str]]:
|
|
263
|
+
if not self.policy.permits("network"):
|
|
264
|
+
raise PermissionError("Network discovery is disabled by policy")
|
|
265
|
+
query_env = getattr(self, "_query_env", {})
|
|
266
|
+
if query_env:
|
|
267
|
+
from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
268
|
+
parts = urlsplit(endpoint)
|
|
269
|
+
query = {key: os.environ.get(env, "") for key, env in query_env.items()}
|
|
270
|
+
endpoint = urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment))
|
|
271
|
+
request = urllib.request.Request(endpoint, headers=headers, method="GET")
|
|
272
|
+
try:
|
|
273
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
274
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
275
|
+
response_headers = {str(k).lower(): str(v) for k, v in response.headers.items()}
|
|
276
|
+
except HTTPError as exc:
|
|
277
|
+
raise RuntimeError(f"provider discovery failed with HTTP {exc.code}") from exc
|
|
278
|
+
except URLError as exc:
|
|
279
|
+
raise RuntimeError("provider discovery failed: network error") from exc
|
|
280
|
+
if not isinstance(payload, dict):
|
|
281
|
+
raise ValueError("provider discovery response must be a JSON object")
|
|
282
|
+
return payload, response_headers
|
|
283
|
+
|
|
284
|
+
def _normalize_capabilities(item: dict[str, object]) -> frozenset[str]:
|
|
285
|
+
"""Normalize provider-specific metadata to VYRELON routing vocabulary."""
|
|
286
|
+
aliases = {
|
|
287
|
+
"generatecontent": "chat",
|
|
288
|
+
"generate_content": "chat",
|
|
289
|
+
"chat": "chat",
|
|
290
|
+
"embedcontent": "embeddings",
|
|
291
|
+
"embed_content": "embeddings",
|
|
292
|
+
"embeddings": "embeddings",
|
|
293
|
+
"predictlongrunning": "predict",
|
|
294
|
+
"counttokens": "token_count",
|
|
295
|
+
"function_calling": "tools",
|
|
296
|
+
"function-calling": "tools",
|
|
297
|
+
"structured-outputs": "structured_output",
|
|
298
|
+
}
|
|
299
|
+
result: set[str] = set()
|
|
300
|
+
for field in ("capabilities", "supportedGenerationMethods"):
|
|
301
|
+
for value in _string_list(item.get(field)):
|
|
302
|
+
key = value.strip().lower().replace(" ", "_")
|
|
303
|
+
result.add(aliases.get(key, key))
|
|
304
|
+
for field in ("input_modalities", "output_modalities", "modalities"):
|
|
305
|
+
for value in _string_list(item.get(field)):
|
|
306
|
+
key = value.strip().lower()
|
|
307
|
+
result.add(aliases.get(key, key))
|
|
308
|
+
for field, capability in (
|
|
309
|
+
("supports_tools", "tools"),
|
|
310
|
+
("supports_reasoning", "reasoning"),
|
|
311
|
+
("supports_vision", "vision"),
|
|
312
|
+
("supports_audio", "audio"),
|
|
313
|
+
("supports_image", "image"),
|
|
314
|
+
("supports_structured_output", "structured_output"),
|
|
315
|
+
):
|
|
316
|
+
if item.get(field) is True:
|
|
317
|
+
result.add(capability)
|
|
318
|
+
return frozenset(result)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _as_int(value: object) -> int | None:
|
|
322
|
+
try:
|
|
323
|
+
return int(value) if value is not None else None
|
|
324
|
+
except (TypeError, ValueError):
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _parse_datetime(value: object) -> datetime | None:
|
|
329
|
+
if not value:
|
|
330
|
+
return None
|
|
331
|
+
try:
|
|
332
|
+
return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
333
|
+
except ValueError:
|
|
334
|
+
return None
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Project-scoped execution configuration for VYRELON."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ExecutionConfig:
|
|
12
|
+
version: int
|
|
13
|
+
runtime: str
|
|
14
|
+
agent_id: str
|
|
15
|
+
model_id: str
|
|
16
|
+
|
|
17
|
+
def validate(self) -> None:
|
|
18
|
+
if self.version != 1:
|
|
19
|
+
raise ValueError(f"unsupported execution config version: {self.version}")
|
|
20
|
+
if not self.runtime.strip():
|
|
21
|
+
raise ValueError("execution runtime must not be empty")
|
|
22
|
+
if not self.agent_id.strip():
|
|
23
|
+
raise ValueError("execution agent id must not be empty")
|
|
24
|
+
if not self.model_id.strip():
|
|
25
|
+
raise ValueError("execution model id must not be empty")
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict[str, object]:
|
|
28
|
+
self.validate()
|
|
29
|
+
return {
|
|
30
|
+
"version": self.version,
|
|
31
|
+
"runtime": self.runtime,
|
|
32
|
+
"agent_id": self.agent_id,
|
|
33
|
+
"model_id": self.model_id,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
DEFAULT_EXECUTION_CONFIG = ExecutionConfig(
|
|
38
|
+
version=1,
|
|
39
|
+
runtime="process",
|
|
40
|
+
agent_id="cli-executor",
|
|
41
|
+
model_id="local-process",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_execution_config(project_root: Path) -> ExecutionConfig:
|
|
46
|
+
path = project_root / ".multiagentos" / "execution.json"
|
|
47
|
+
if not path.is_file():
|
|
48
|
+
raise FileNotFoundError(f"VYRELON execution config not found: {path}")
|
|
49
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
50
|
+
if not isinstance(data, dict):
|
|
51
|
+
raise ValueError("execution config must be a JSON object")
|
|
52
|
+
config = ExecutionConfig(
|
|
53
|
+
version=int(data.get("version", 0)),
|
|
54
|
+
runtime=str(data.get("runtime", "")),
|
|
55
|
+
agent_id=str(data.get("agent_id", "")),
|
|
56
|
+
model_id=str(data.get("model_id", "")),
|
|
57
|
+
)
|
|
58
|
+
config.validate()
|
|
59
|
+
return config
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def write_default_execution_config(project_root: Path) -> Path:
|
|
63
|
+
target = project_root / ".multiagentos"
|
|
64
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
path = target / "execution.json"
|
|
66
|
+
if not path.exists():
|
|
67
|
+
path.write_text(
|
|
68
|
+
json.dumps(DEFAULT_EXECUTION_CONFIG.to_dict(), indent=2) + "\n",
|
|
69
|
+
encoding="utf-8",
|
|
70
|
+
)
|
|
71
|
+
return path
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Built-in execution contracts used by the VYRELON CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from core.contracts import AgentContract, AIProvider, ModelSpec
|
|
6
|
+
from core.registry import AgentRegistry, AIRegistry
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def default_execution_registries() -> tuple[AgentRegistry, AIRegistry]:
|
|
10
|
+
agents = AgentRegistry()
|
|
11
|
+
agents.register(
|
|
12
|
+
AgentContract(
|
|
13
|
+
id="cli-executor",
|
|
14
|
+
role="executor",
|
|
15
|
+
capabilities=frozenset({"process"}),
|
|
16
|
+
tools=frozenset({"process"}),
|
|
17
|
+
kind="runtime",
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
models = AIRegistry()
|
|
22
|
+
models.register(
|
|
23
|
+
AIProvider(
|
|
24
|
+
id="local",
|
|
25
|
+
kind="runtime",
|
|
26
|
+
models=(
|
|
27
|
+
ModelSpec(
|
|
28
|
+
id="local-process",
|
|
29
|
+
provider_id="local",
|
|
30
|
+
capabilities=frozenset({"process"}),
|
|
31
|
+
metadata={"runtime": "process"},
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
return agents, models
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_execution_contracts(
|
|
40
|
+
agent_id: str,
|
|
41
|
+
model_id: str,
|
|
42
|
+
) -> tuple[AgentContract, ModelSpec]:
|
|
43
|
+
agents, models = default_execution_registries()
|
|
44
|
+
agent = agents.get(agent_id)
|
|
45
|
+
model = models.model(model_id)
|
|
46
|
+
if not agent.capabilities.issubset(model.capabilities):
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"execution model {model.id!r} is incompatible with agent {agent.id!r}"
|
|
49
|
+
)
|
|
50
|
+
return agent, model
|
runtime/git.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Policy-controlled Git runtime for VYRELON."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from runtime.process import ProcessRuntime
|
|
6
|
+
from runtime.policy import ExecutionPolicy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GitRuntime:
|
|
10
|
+
def __init__(self, process: ProcessRuntime | None = None, policy: ExecutionPolicy | None = None):
|
|
11
|
+
self.policy = policy or ExecutionPolicy()
|
|
12
|
+
self.process = process or ProcessRuntime(self.policy)
|
|
13
|
+
|
|
14
|
+
def status(self, cwd: str):
|
|
15
|
+
return self.process.run(["git", "status", "--short", "--branch"], cwd)
|
|
16
|
+
|
|
17
|
+
def diff(self, cwd: str, *, staged: bool = False, path: str | None = None):
|
|
18
|
+
command = ["git", "diff"]
|
|
19
|
+
if staged:
|
|
20
|
+
command.append("--staged")
|
|
21
|
+
if path:
|
|
22
|
+
command.extend(["--", path])
|
|
23
|
+
else:
|
|
24
|
+
command.append("--")
|
|
25
|
+
return self.process.run(command, cwd)
|
|
26
|
+
|
|
27
|
+
def log(self, cwd: str, count: int = 10):
|
|
28
|
+
return self.process.run(["git", "log", "--oneline", "-n", str(count)], cwd)
|
|
29
|
+
|
|
30
|
+
def add(self, cwd: str, paths: list[str] | None = None):
|
|
31
|
+
self._require_git_write()
|
|
32
|
+
command = ["git", "add"]
|
|
33
|
+
command.extend(paths if paths else ["-A"])
|
|
34
|
+
return self.process.run(command, cwd)
|
|
35
|
+
|
|
36
|
+
def checkout_branch(self, cwd: str, branch: str):
|
|
37
|
+
self._require_git_write()
|
|
38
|
+
return self.process.run(["git", "switch", branch], cwd)
|
|
39
|
+
|
|
40
|
+
def restore(self, cwd: str, paths: list[str], source: str = "HEAD"):
|
|
41
|
+
self._require_git_write()
|
|
42
|
+
return self.process.run(["git", "restore", "--source", source, "--", *paths], cwd)
|
|
43
|
+
|
|
44
|
+
def stash(self, cwd: str, action: str = "list", message: str | None = None):
|
|
45
|
+
command = ["git", "stash"]
|
|
46
|
+
if action == "list":
|
|
47
|
+
command.append("list")
|
|
48
|
+
else:
|
|
49
|
+
self._require_git_write()
|
|
50
|
+
command.append(action)
|
|
51
|
+
if action == "push" and message:
|
|
52
|
+
command.extend(["-m", message])
|
|
53
|
+
return self.process.run(command, cwd)
|
|
54
|
+
|
|
55
|
+
def commit(self, cwd: str, message: str, approved: bool = False):
|
|
56
|
+
self._require_git_write()
|
|
57
|
+
self._require_approval("git.commit", approved)
|
|
58
|
+
added = self.process.run(["git", "add", "-A"], cwd)
|
|
59
|
+
if added.returncode != 0:
|
|
60
|
+
return added
|
|
61
|
+
return self.process.run(["git", "commit", "-m", message], cwd)
|
|
62
|
+
|
|
63
|
+
def push(self, cwd: str, remote: str = "origin", branch: str | None = None, approved: bool = False):
|
|
64
|
+
self._require_git_write()
|
|
65
|
+
self._require_approval("git.push", approved)
|
|
66
|
+
command = ["git", "push", remote]
|
|
67
|
+
if branch:
|
|
68
|
+
command.append(branch)
|
|
69
|
+
return self.process.run(command, cwd)
|
|
70
|
+
|
|
71
|
+
def pull(self, cwd: str, remote: str = "origin", branch: str | None = None):
|
|
72
|
+
self._require_git_write()
|
|
73
|
+
command = ["git", "pull", remote]
|
|
74
|
+
if branch:
|
|
75
|
+
command.append(branch)
|
|
76
|
+
return self.process.run(command, cwd)
|
|
77
|
+
|
|
78
|
+
def _require_git_write(self):
|
|
79
|
+
if not self.policy.permits("git.write"):
|
|
80
|
+
raise PermissionError("Git write operations are disabled by policy")
|
|
81
|
+
|
|
82
|
+
def _require_approval(self, action: str, approved: bool):
|
|
83
|
+
if self.policy.requires_approval(action) and not approved:
|
|
84
|
+
raise PermissionError(f"Explicit approval required for: {action}")
|
runtime/github.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Policy-controlled GitHub runtime for VYRELON."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from core.contracts.github import GitHubGateway
|
|
6
|
+
from runtime.policy import ExecutionPolicy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GitHubRuntime:
|
|
10
|
+
def __init__(self, gateway: GitHubGateway, policy: ExecutionPolicy | None = None) -> None:
|
|
11
|
+
self.gateway = gateway
|
|
12
|
+
self.policy = policy or ExecutionPolicy()
|
|
13
|
+
|
|
14
|
+
def _require_write(self) -> None:
|
|
15
|
+
if not self.policy.permits("github.write"):
|
|
16
|
+
raise PermissionError("GitHub write operations are disabled by policy")
|
|
17
|
+
|
|
18
|
+
def create_branch(self, repository: str, branch: str, base: str):
|
|
19
|
+
self._require_write()
|
|
20
|
+
return self.gateway.create_branch(repository, branch, base)
|
|
21
|
+
|
|
22
|
+
def create_file(self, repository: str, path: str, content: str, branch: str, message: str):
|
|
23
|
+
self._require_write()
|
|
24
|
+
return self.gateway.create_file(repository, path, content, branch, message)
|
|
25
|
+
|
|
26
|
+
def update_file(self, repository: str, path: str, content: str, sha: str, branch: str, message: str):
|
|
27
|
+
self._require_write()
|
|
28
|
+
return self.gateway.update_file(repository, path, content, sha, branch, message)
|
|
29
|
+
|
|
30
|
+
def create_issue(self, repository: str, title: str, body: str = ""):
|
|
31
|
+
self._require_write()
|
|
32
|
+
return self.gateway.create_issue(repository, title, body)
|
|
33
|
+
|
|
34
|
+
def create_pull_request(
|
|
35
|
+
self, repository: str, title: str, body: str, head: str, base: str,
|
|
36
|
+
draft: bool = False, approved: bool = False,
|
|
37
|
+
):
|
|
38
|
+
self._require_write()
|
|
39
|
+
self._require_approval("github.pr", approved)
|
|
40
|
+
return self.gateway.create_pull_request(repository, title, body, head, base, draft)
|
|
41
|
+
|
|
42
|
+
def review_pull_request(self, repository: str, number: int, action: str, body: str = ""):
|
|
43
|
+
self._require_write()
|
|
44
|
+
return self.gateway.review_pull_request(repository, number, action, body)
|
|
45
|
+
|
|
46
|
+
def merge_pull_request(
|
|
47
|
+
self, repository: str, number: int, method: str = "squash",
|
|
48
|
+
approved: bool = False,
|
|
49
|
+
):
|
|
50
|
+
self._require_write()
|
|
51
|
+
self._require_approval("github.merge", approved)
|
|
52
|
+
return self.gateway.merge_pull_request(repository, number, method)
|
|
53
|
+
|
|
54
|
+
def _require_approval(self, action: str, approved: bool) -> None:
|
|
55
|
+
if self.policy.requires_approval(action) and not approved:
|
|
56
|
+
raise PermissionError(f"Explicit approval required for: {action}")
|
runtime/github_probe.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Probe VYRELON's local GitHub access."""
|
|
2
|
+
|
|
3
|
+
from integrations.github.gateway import GitHubGatewayClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def probe(repository: str) -> dict:
|
|
7
|
+
client = GitHubGatewayClient()
|
|
8
|
+
repo = client.get_repository(repository)
|
|
9
|
+
branch = client.get_branch(repository, repo.default_branch)
|
|
10
|
+
return {
|
|
11
|
+
"repository": repo.full_name,
|
|
12
|
+
"private": repo.private,
|
|
13
|
+
"default_branch": repo.default_branch,
|
|
14
|
+
"default_branch_sha": branch.sha,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
import argparse
|
|
20
|
+
parser = argparse.ArgumentParser()
|
|
21
|
+
parser.add_argument("repository")
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
print(probe(args.repository))
|
runtime/health.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Runtime health registry facade."""
|
|
2
|
+
from core.contracts.health import (
|
|
3
|
+
HealthStatus,
|
|
4
|
+
ModelHealth,
|
|
5
|
+
ModelHealthRegistry,
|
|
6
|
+
ModelHealthStore,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"HealthStatus",
|
|
11
|
+
"ModelHealth",
|
|
12
|
+
"ModelHealthRegistry",
|
|
13
|
+
"ModelHealthStore",
|
|
14
|
+
]
|