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
core/registry.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Registries for agents and AI providers."""
|
|
2
|
+
|
|
3
|
+
from core.contracts.agent import AgentContract
|
|
4
|
+
from core.contracts.ai import AIProvider, ModelSpec
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AgentRegistry:
|
|
8
|
+
def __init__(self) -> None:
|
|
9
|
+
self._agents: dict[str, AgentContract] = {}
|
|
10
|
+
|
|
11
|
+
def register(self, agent: AgentContract) -> None:
|
|
12
|
+
if agent.id in self._agents:
|
|
13
|
+
raise ValueError(f"Agent already registered: {agent.id}")
|
|
14
|
+
self._agents[agent.id] = agent
|
|
15
|
+
|
|
16
|
+
def get(self, agent_id: str) -> AgentContract:
|
|
17
|
+
return self._agents[agent_id]
|
|
18
|
+
|
|
19
|
+
def list(self) -> tuple[AgentContract, ...]:
|
|
20
|
+
return tuple(self._agents.values())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AIRegistry:
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self._providers: dict[str, AIProvider] = {}
|
|
26
|
+
self._models: dict[str, ModelSpec] = {}
|
|
27
|
+
|
|
28
|
+
def register(self, provider: AIProvider) -> None:
|
|
29
|
+
if provider.id in self._providers:
|
|
30
|
+
raise ValueError(f"Provider already registered: {provider.id}")
|
|
31
|
+
self._providers[provider.id] = provider
|
|
32
|
+
for model in provider.models:
|
|
33
|
+
if model.id in self._models:
|
|
34
|
+
raise ValueError(f"Model already registered: {model.id}")
|
|
35
|
+
self._models[model.id] = model
|
|
36
|
+
|
|
37
|
+
def provider(self, provider_id: str) -> AIProvider:
|
|
38
|
+
return self._providers[provider_id]
|
|
39
|
+
|
|
40
|
+
def model(self, model_id: str) -> ModelSpec:
|
|
41
|
+
return self._models[model_id]
|
|
42
|
+
|
|
43
|
+
def models(self) -> tuple[ModelSpec, ...]:
|
|
44
|
+
return tuple(self._models.values())
|
core/routing.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from core.contracts.agent import AgentContract
|
|
8
|
+
from core.contracts.ai import ModelSpec
|
|
9
|
+
from core.contracts.health import ModelHealth
|
|
10
|
+
from core.contracts.quota import QuotaSnapshot
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from runtime.capability import CapabilityRegistry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RoutingStrategy(str, Enum):
|
|
16
|
+
EXPLICIT = "explicit"
|
|
17
|
+
POOL = "pool"
|
|
18
|
+
AUTO = "auto"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Assignment:
|
|
23
|
+
agent_id: str
|
|
24
|
+
model_id: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class RoutingCandidate:
|
|
29
|
+
model_id: str
|
|
30
|
+
provider_id: str
|
|
31
|
+
selected: bool
|
|
32
|
+
compatible: bool
|
|
33
|
+
health_available: bool
|
|
34
|
+
quota_available: bool
|
|
35
|
+
capability_score: float
|
|
36
|
+
quota_score: float
|
|
37
|
+
missing_capabilities: frozenset[str]
|
|
38
|
+
rejection_reasons: tuple[str, ...]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class RoutingExplanation:
|
|
43
|
+
agent_id: str
|
|
44
|
+
strategy: RoutingStrategy
|
|
45
|
+
selected_model_id: str | None
|
|
46
|
+
candidates: tuple[RoutingCandidate, ...]
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def assignment(self) -> Assignment:
|
|
50
|
+
if self.selected_model_id is None:
|
|
51
|
+
raise LookupError(f"No compatible available model for agent: {self.agent_id}")
|
|
52
|
+
return Assignment(self.agent_id, self.selected_model_id)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AIRouter:
|
|
56
|
+
def assign(
|
|
57
|
+
self,
|
|
58
|
+
agent: AgentContract,
|
|
59
|
+
models: list[ModelSpec],
|
|
60
|
+
preferred_model_ids: list[str] | None = None,
|
|
61
|
+
strategy: RoutingStrategy | str = RoutingStrategy.POOL,
|
|
62
|
+
quota_snapshots: dict[str, QuotaSnapshot] | None = None,
|
|
63
|
+
health_snapshots: dict[str, ModelHealth] | None = None,
|
|
64
|
+
capability_registry: CapabilityRegistry | None = None,
|
|
65
|
+
) -> Assignment:
|
|
66
|
+
return self.explain(
|
|
67
|
+
agent,
|
|
68
|
+
models,
|
|
69
|
+
preferred_model_ids=preferred_model_ids,
|
|
70
|
+
strategy=strategy,
|
|
71
|
+
quota_snapshots=quota_snapshots,
|
|
72
|
+
health_snapshots=health_snapshots,
|
|
73
|
+
capability_registry=capability_registry,
|
|
74
|
+
).assignment
|
|
75
|
+
|
|
76
|
+
def explain(
|
|
77
|
+
self,
|
|
78
|
+
agent: AgentContract,
|
|
79
|
+
models: list[ModelSpec],
|
|
80
|
+
preferred_model_ids: list[str] | None = None,
|
|
81
|
+
strategy: RoutingStrategy | str = RoutingStrategy.POOL,
|
|
82
|
+
quota_snapshots: dict[str, QuotaSnapshot] | None = None,
|
|
83
|
+
health_snapshots: dict[str, ModelHealth] | None = None,
|
|
84
|
+
capability_registry: CapabilityRegistry | None = None,
|
|
85
|
+
) -> RoutingExplanation:
|
|
86
|
+
"""Explain model eligibility, rejection reasons, and the selected model."""
|
|
87
|
+
from runtime.quota import quota_available, quota_score
|
|
88
|
+
|
|
89
|
+
strategy = RoutingStrategy(strategy)
|
|
90
|
+
preferred = preferred_model_ids if preferred_model_ids is not None else list(agent.model_ids)
|
|
91
|
+
preferred_set = set(preferred)
|
|
92
|
+
quota_snapshots = quota_snapshots or {}
|
|
93
|
+
health_snapshots = health_snapshots or {}
|
|
94
|
+
requirements = frozenset(agent.capabilities)
|
|
95
|
+
|
|
96
|
+
def capability_match(model: ModelSpec):
|
|
97
|
+
if capability_registry is None:
|
|
98
|
+
matched = requirements & model.capabilities
|
|
99
|
+
return matched, requirements - model.capabilities, (
|
|
100
|
+
len(matched) / len(requirements) if requirements else 1.0
|
|
101
|
+
)
|
|
102
|
+
match = capability_registry.match(model, requirements)
|
|
103
|
+
return match.matched, match.missing, match.score
|
|
104
|
+
|
|
105
|
+
candidates = []
|
|
106
|
+
for model in models:
|
|
107
|
+
_, missing, cap_score = capability_match(model)
|
|
108
|
+
health = health_snapshots.get(model.id)
|
|
109
|
+
health_ok = health is None or health.available
|
|
110
|
+
quota = quota_snapshots.get(model.id)
|
|
111
|
+
quota_ok = quota is None or quota_available(quota)
|
|
112
|
+
reasons = []
|
|
113
|
+
if missing:
|
|
114
|
+
reasons.append("missing_capabilities")
|
|
115
|
+
if not health_ok:
|
|
116
|
+
reasons.append("unhealthy")
|
|
117
|
+
if not quota_ok:
|
|
118
|
+
reasons.append("quota_unavailable")
|
|
119
|
+
if strategy is RoutingStrategy.EXPLICIT and model.id not in preferred_set:
|
|
120
|
+
reasons.append("not_explicitly_selected")
|
|
121
|
+
candidates.append(
|
|
122
|
+
RoutingCandidate(
|
|
123
|
+
model_id=model.id,
|
|
124
|
+
provider_id=model.provider_id,
|
|
125
|
+
selected=False,
|
|
126
|
+
compatible=not missing,
|
|
127
|
+
health_available=health_ok,
|
|
128
|
+
quota_available=quota_ok,
|
|
129
|
+
capability_score=cap_score,
|
|
130
|
+
quota_score=quota_score(quota) if quota is not None else 0.5,
|
|
131
|
+
missing_capabilities=frozenset(missing),
|
|
132
|
+
rejection_reasons=tuple(reasons),
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
by_id = {model.id: model for model in models}
|
|
137
|
+
selected_model_id = None
|
|
138
|
+
|
|
139
|
+
if strategy in {RoutingStrategy.EXPLICIT, RoutingStrategy.POOL}:
|
|
140
|
+
for model_id in preferred:
|
|
141
|
+
model = by_id.get(model_id)
|
|
142
|
+
if model is None:
|
|
143
|
+
continue
|
|
144
|
+
candidate = next(item for item in candidates if item.model_id == model.id)
|
|
145
|
+
if candidate.compatible and candidate.health_available and candidate.quota_available:
|
|
146
|
+
selected_model_id = model.id
|
|
147
|
+
break
|
|
148
|
+
if selected_model_id is None and strategy is RoutingStrategy.EXPLICIT:
|
|
149
|
+
raise LookupError(f"No explicitly assigned available model for agent: {agent.id}")
|
|
150
|
+
|
|
151
|
+
if selected_model_id is None and strategy in {RoutingStrategy.POOL, RoutingStrategy.AUTO}:
|
|
152
|
+
eligible = [
|
|
153
|
+
item for item in candidates
|
|
154
|
+
if item.compatible and item.health_available and item.quota_available
|
|
155
|
+
]
|
|
156
|
+
eligible.sort(
|
|
157
|
+
key=lambda item: (item.capability_score, item.quota_score),
|
|
158
|
+
reverse=True,
|
|
159
|
+
)
|
|
160
|
+
if eligible:
|
|
161
|
+
selected_model_id = eligible[0].model_id
|
|
162
|
+
|
|
163
|
+
if selected_model_id is None:
|
|
164
|
+
raise LookupError(f"No compatible available model for agent: {agent.id}")
|
|
165
|
+
|
|
166
|
+
explained = tuple(
|
|
167
|
+
RoutingCandidate(
|
|
168
|
+
model_id=item.model_id,
|
|
169
|
+
provider_id=item.provider_id,
|
|
170
|
+
selected=item.model_id == selected_model_id,
|
|
171
|
+
compatible=item.compatible,
|
|
172
|
+
health_available=item.health_available,
|
|
173
|
+
quota_available=item.quota_available,
|
|
174
|
+
capability_score=item.capability_score,
|
|
175
|
+
quota_score=item.quota_score,
|
|
176
|
+
missing_capabilities=item.missing_capabilities,
|
|
177
|
+
rejection_reasons=tuple(
|
|
178
|
+
reason for reason in item.rejection_reasons
|
|
179
|
+
if reason != "not_explicitly_selected" or item.model_id != selected_model_id
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
for item in candidates
|
|
183
|
+
)
|
|
184
|
+
return RoutingExplanation(
|
|
185
|
+
agent_id=agent.id,
|
|
186
|
+
strategy=strategy,
|
|
187
|
+
selected_model_id=selected_model_id,
|
|
188
|
+
candidates=explained,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def compatible(agent: AgentContract, model: ModelSpec) -> bool:
|
|
193
|
+
return not agent.capabilities or agent.capabilities.issubset(model.capabilities)
|
|
194
|
+
|
|
195
|
+
_compatible = compatible
|
core/state.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Persistent WorkUnit state store."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from core.contracts.checkpoint import WorkflowCheckpoint
|
|
9
|
+
from core.contracts.work_unit import WorkStatus, WorkUnit
|
|
10
|
+
from core.contracts.vyrelon_runtime import SessionSpec, SessionState
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WorkStateStore:
|
|
14
|
+
def __init__(self, root: Path) -> None:
|
|
15
|
+
self.root = root
|
|
16
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
self.checkpoint_root = self.root.parent / "checkpoints"
|
|
18
|
+
self.checkpoint_root.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
def save(self, work_unit: WorkUnit) -> Path:
|
|
21
|
+
path = self.root / f"{work_unit.id}.json"
|
|
22
|
+
payload = {
|
|
23
|
+
"id": work_unit.id,
|
|
24
|
+
"objective": work_unit.objective,
|
|
25
|
+
"status": work_unit.status.value,
|
|
26
|
+
"inputs": work_unit.inputs,
|
|
27
|
+
"artifacts": work_unit.artifacts,
|
|
28
|
+
"assigned_agents": work_unit.assigned_agents,
|
|
29
|
+
"metadata": work_unit.metadata,
|
|
30
|
+
}
|
|
31
|
+
path.write_text(
|
|
32
|
+
json.dumps(payload, indent=2, default=str) + "\n",
|
|
33
|
+
encoding="utf-8",
|
|
34
|
+
)
|
|
35
|
+
return path
|
|
36
|
+
|
|
37
|
+
def exists(self, work_unit_id: str) -> bool:
|
|
38
|
+
return (self.root / f"{work_unit_id}.json").exists()
|
|
39
|
+
|
|
40
|
+
def list_ids(self) -> tuple[str, ...]:
|
|
41
|
+
return tuple(sorted(path.stem for path in self.root.glob("*.json")))
|
|
42
|
+
|
|
43
|
+
def load(self, work_unit_id: str) -> WorkUnit:
|
|
44
|
+
path = self.root / f"{work_unit_id}.json"
|
|
45
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
46
|
+
return WorkUnit(
|
|
47
|
+
id=data["id"],
|
|
48
|
+
objective=data["objective"],
|
|
49
|
+
status=WorkStatus(data["status"]),
|
|
50
|
+
inputs=dict(data.get("inputs", {})),
|
|
51
|
+
artifacts=list(data.get("artifacts", [])),
|
|
52
|
+
assigned_agents=list(data.get("assigned_agents", [])),
|
|
53
|
+
metadata=dict(data.get("metadata", {})),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def save_checkpoint(self, checkpoint: WorkflowCheckpoint) -> Path:
|
|
57
|
+
"""Persist a durable checkpoint independently of transient model output."""
|
|
58
|
+
checkpoint.validate()
|
|
59
|
+
path = self.checkpoint_root / f"{checkpoint.work_unit_id}.json"
|
|
60
|
+
path.write_text(
|
|
61
|
+
json.dumps(checkpoint.to_dict(), indent=2, ensure_ascii=False) + "\n",
|
|
62
|
+
encoding="utf-8",
|
|
63
|
+
)
|
|
64
|
+
return path
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_checkpoint(self, work_unit_id: str) -> WorkflowCheckpoint:
|
|
68
|
+
"""Load the latest durable checkpoint for a WorkUnit."""
|
|
69
|
+
path = self.checkpoint_root / f"{work_unit_id}.json"
|
|
70
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
71
|
+
return WorkflowCheckpoint.from_dict(data)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def checkpoint(
|
|
75
|
+
self,
|
|
76
|
+
work_unit: WorkUnit,
|
|
77
|
+
*,
|
|
78
|
+
workflow: str,
|
|
79
|
+
stage: str,
|
|
80
|
+
sequence: int = 0,
|
|
81
|
+
next_action: str | None = None,
|
|
82
|
+
agent_ids: tuple[str, ...] = (),
|
|
83
|
+
model_ids: tuple[str, ...] = (),
|
|
84
|
+
resumable: bool = True,
|
|
85
|
+
metadata: dict[str, object] | None = None,
|
|
86
|
+
) -> WorkflowCheckpoint:
|
|
87
|
+
"""Persist WorkUnit state and a normalized checkpoint atomically at the API level."""
|
|
88
|
+
self.save(work_unit)
|
|
89
|
+
checkpoint = WorkflowCheckpoint(
|
|
90
|
+
schema_version=1,
|
|
91
|
+
work_unit_id=work_unit.id,
|
|
92
|
+
workflow=workflow,
|
|
93
|
+
status=work_unit.status.value,
|
|
94
|
+
stage=stage,
|
|
95
|
+
sequence=sequence,
|
|
96
|
+
next_action=next_action,
|
|
97
|
+
agent_ids=agent_ids,
|
|
98
|
+
model_ids=model_ids,
|
|
99
|
+
artifact_ids=tuple(work_unit.artifacts),
|
|
100
|
+
findings=tuple(
|
|
101
|
+
str(value) for value in work_unit.metadata.get("findings", ())
|
|
102
|
+
),
|
|
103
|
+
resumable=resumable,
|
|
104
|
+
metadata=dict(metadata or {}),
|
|
105
|
+
)
|
|
106
|
+
self.save_checkpoint(checkpoint)
|
|
107
|
+
return checkpoint
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class SessionStateStore:
|
|
112
|
+
"""Persist SessionState independently from WorkUnit state."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, root: Path) -> None:
|
|
115
|
+
self.root = root
|
|
116
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
def save(self, state: SessionState) -> Path:
|
|
119
|
+
path = self.root / f"{state.spec.id}.json"
|
|
120
|
+
payload = {
|
|
121
|
+
"spec": {
|
|
122
|
+
"id": state.spec.id,
|
|
123
|
+
"project_root": state.spec.project_root,
|
|
124
|
+
"agent_id": state.spec.agent_id,
|
|
125
|
+
"model_id": state.spec.model_id,
|
|
126
|
+
"checkpoint_id": state.spec.checkpoint_id,
|
|
127
|
+
"metadata": state.spec.metadata,
|
|
128
|
+
},
|
|
129
|
+
"work_unit_ids": state.work_unit_ids,
|
|
130
|
+
"status": state.status,
|
|
131
|
+
"metadata": state.metadata,
|
|
132
|
+
}
|
|
133
|
+
path.write_text(json.dumps(payload, indent=2, default=str) + "\n", encoding="utf-8")
|
|
134
|
+
return path
|
|
135
|
+
|
|
136
|
+
def exists(self, session_id: str) -> bool:
|
|
137
|
+
return (self.root / f"{session_id}.json").exists()
|
|
138
|
+
|
|
139
|
+
def load(self, session_id: str) -> SessionState:
|
|
140
|
+
data = json.loads((self.root / f"{session_id}.json").read_text(encoding="utf-8"))
|
|
141
|
+
raw = data["spec"]
|
|
142
|
+
spec = SessionSpec(
|
|
143
|
+
id=raw["id"],
|
|
144
|
+
project_root=raw["project_root"],
|
|
145
|
+
agent_id=raw.get("agent_id"),
|
|
146
|
+
model_id=raw.get("model_id"),
|
|
147
|
+
checkpoint_id=raw.get("checkpoint_id"),
|
|
148
|
+
metadata=dict(raw.get("metadata", {})),
|
|
149
|
+
)
|
|
150
|
+
return SessionState(
|
|
151
|
+
spec=spec,
|
|
152
|
+
work_unit_ids=list(data.get("work_unit_ids", [])),
|
|
153
|
+
status=data.get("status", "active"),
|
|
154
|
+
metadata=dict(data.get("metadata", {})),
|
|
155
|
+
)
|
installer/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""VYRELON project installer."""
|
installer/init.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Apply VYRELON project bootstrap files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from agents.catalog import build_agent_catalog
|
|
9
|
+
from core.contracts.profile import DetectionResult
|
|
10
|
+
from runtime.execution_config import write_default_execution_config
|
|
11
|
+
from runtime.chat_config import write_default_chat_config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProjectInitializer:
|
|
15
|
+
COMPONENTS = frozenset({"vyrelon", "multi-agent", "all"})
|
|
16
|
+
|
|
17
|
+
def apply(
|
|
18
|
+
self,
|
|
19
|
+
project_root: Path,
|
|
20
|
+
detections: tuple[DetectionResult, ...],
|
|
21
|
+
component: str = "all",
|
|
22
|
+
) -> Path:
|
|
23
|
+
if component not in self.COMPONENTS:
|
|
24
|
+
raise ValueError(f"unsupported component: {component}")
|
|
25
|
+
target = project_root / ".multiagentos"
|
|
26
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
payload = {
|
|
28
|
+
"version": 1,
|
|
29
|
+
"profiles": [
|
|
30
|
+
{
|
|
31
|
+
"id": result.profile_id,
|
|
32
|
+
"confidence": result.confidence,
|
|
33
|
+
"evidence": list(result.evidence),
|
|
34
|
+
}
|
|
35
|
+
for result in detections
|
|
36
|
+
],
|
|
37
|
+
}
|
|
38
|
+
config = target / "profile.json"
|
|
39
|
+
config.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
40
|
+
(target / "components.json").write_text(
|
|
41
|
+
json.dumps({"version": 1, "components": [component]}, indent=2) + "\n",
|
|
42
|
+
encoding="utf-8",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if component in {"vyrelon", "all"}:
|
|
46
|
+
write_default_execution_config(project_root)
|
|
47
|
+
write_default_chat_config(project_root)
|
|
48
|
+
|
|
49
|
+
if component == "vyrelon":
|
|
50
|
+
return config
|
|
51
|
+
|
|
52
|
+
agents = build_agent_catalog(tuple(result.profile_id for result in detections))
|
|
53
|
+
agent_payload = {
|
|
54
|
+
"version": 1,
|
|
55
|
+
"agents": [
|
|
56
|
+
{
|
|
57
|
+
"id": agent.id,
|
|
58
|
+
"role": agent.role,
|
|
59
|
+
"kind": agent.kind,
|
|
60
|
+
"capabilities": sorted(agent.capabilities),
|
|
61
|
+
"tools": sorted(agent.tools),
|
|
62
|
+
"permissions": sorted(agent.permissions),
|
|
63
|
+
"models": list(agent.model_ids),
|
|
64
|
+
}
|
|
65
|
+
for agent in agents
|
|
66
|
+
],
|
|
67
|
+
}
|
|
68
|
+
(target / "agents.json").write_text(
|
|
69
|
+
json.dumps(agent_payload, indent=2) + "\n", encoding="utf-8"
|
|
70
|
+
)
|
|
71
|
+
return config
|
integrations/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""GitHub adapter using the locally authenticated GitHub CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from core.contracts.github import (
|
|
11
|
+
GitHubBranch,
|
|
12
|
+
GitHubFile,
|
|
13
|
+
GitHubGateway,
|
|
14
|
+
GitHubIssue,
|
|
15
|
+
GitHubRepository,
|
|
16
|
+
MergeResult,
|
|
17
|
+
PullRequest,
|
|
18
|
+
ReviewResult,
|
|
19
|
+
WorkflowRun,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GitHubGatewayClient:
|
|
24
|
+
def __init__(self, gh_binary: str = "gh") -> None:
|
|
25
|
+
self.gh_binary = gh_binary
|
|
26
|
+
|
|
27
|
+
def _run(self, *args: str) -> Any:
|
|
28
|
+
result = subprocess.run(
|
|
29
|
+
[self.gh_binary, *args],
|
|
30
|
+
check=False,
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
)
|
|
34
|
+
if result.returncode != 0:
|
|
35
|
+
raise RuntimeError(
|
|
36
|
+
f"gh command failed ({result.returncode}): "
|
|
37
|
+
f"{' '.join(args)}\n{result.stderr.strip()}"
|
|
38
|
+
)
|
|
39
|
+
output = result.stdout.strip()
|
|
40
|
+
return json.loads(output) if output else None
|
|
41
|
+
|
|
42
|
+
def get_repository(self, full_name: str) -> GitHubRepository:
|
|
43
|
+
data = self._run(
|
|
44
|
+
"repo", "view", full_name,
|
|
45
|
+
"--json", "nameWithOwner,defaultBranchRef,isPrivate",
|
|
46
|
+
)
|
|
47
|
+
return GitHubRepository(
|
|
48
|
+
full_name=data["nameWithOwner"],
|
|
49
|
+
default_branch=data["defaultBranchRef"]["name"],
|
|
50
|
+
private=data["isPrivate"],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def get_branch(self, full_name: str, branch: str) -> GitHubBranch:
|
|
54
|
+
data = self._run(
|
|
55
|
+
"api", f"repos/{full_name}/branches/{branch}",
|
|
56
|
+
"--jq", "{name: .name, sha: .commit.sha}",
|
|
57
|
+
)
|
|
58
|
+
return GitHubBranch(name=data["name"], sha=data["sha"])
|
|
59
|
+
|
|
60
|
+
def create_branch(self, full_name: str, branch: str, base: str) -> GitHubBranch:
|
|
61
|
+
base_ref = self.get_branch(full_name, base)
|
|
62
|
+
self._run(
|
|
63
|
+
"api", f"repos/{full_name}/git/refs", "--method", "POST",
|
|
64
|
+
"-f", f"ref=refs/heads/{branch}", "-f", f"sha={base_ref.sha}",
|
|
65
|
+
)
|
|
66
|
+
return GitHubBranch(name=branch, sha=base_ref.sha)
|
|
67
|
+
|
|
68
|
+
def get_file(self, full_name: str, path: str, ref: str) -> GitHubFile:
|
|
69
|
+
data = self._run(
|
|
70
|
+
"api", f"repos/{full_name}/contents/{path}",
|
|
71
|
+
"-f", f"ref={ref}",
|
|
72
|
+
)
|
|
73
|
+
content = base64.b64decode(data["content"]).decode("utf-8")
|
|
74
|
+
return GitHubFile(path=data["path"], content=content, sha=data["sha"])
|
|
75
|
+
|
|
76
|
+
def create_file(
|
|
77
|
+
self, full_name: str, path: str, content: str,
|
|
78
|
+
branch: str, message: str,
|
|
79
|
+
) -> str:
|
|
80
|
+
encoded = base64.b64encode(content.encode()).decode()
|
|
81
|
+
data = self._run(
|
|
82
|
+
"api", f"repos/{full_name}/contents/{path}", "--method", "PUT",
|
|
83
|
+
"-f", f"message={message}", "-f", f"content={encoded}",
|
|
84
|
+
"-f", f"branch={branch}",
|
|
85
|
+
)
|
|
86
|
+
return data["commit"]["sha"]
|
|
87
|
+
|
|
88
|
+
def update_file(
|
|
89
|
+
self, full_name: str, path: str, content: str, sha: str,
|
|
90
|
+
branch: str, message: str,
|
|
91
|
+
) -> str:
|
|
92
|
+
encoded = base64.b64encode(content.encode()).decode()
|
|
93
|
+
data = self._run(
|
|
94
|
+
"api", f"repos/{full_name}/contents/{path}", "--method", "PUT",
|
|
95
|
+
"-f", f"message={message}", "-f", f"content={encoded}",
|
|
96
|
+
"-f", f"sha={sha}", "-f", f"branch={branch}",
|
|
97
|
+
)
|
|
98
|
+
return data["commit"]["sha"]
|
|
99
|
+
|
|
100
|
+
def create_issue(self, full_name: str, title: str, body: str = "") -> GitHubIssue:
|
|
101
|
+
data = self._run(
|
|
102
|
+
"api", f"repos/{full_name}/issues", "--method", "POST",
|
|
103
|
+
"-f", f"title={title}", "-f", f"body={body}",
|
|
104
|
+
)
|
|
105
|
+
return GitHubIssue(number=data["number"], title=data["title"], url=data["html_url"])
|
|
106
|
+
|
|
107
|
+
def create_pull_request(
|
|
108
|
+
self, full_name: str, title: str, body: str,
|
|
109
|
+
head: str, base: str, draft: bool = False,
|
|
110
|
+
) -> PullRequest:
|
|
111
|
+
args = [
|
|
112
|
+
"pr", "create", "--repo", full_name,
|
|
113
|
+
"--title", title, "--body", body,
|
|
114
|
+
"--head", head, "--base", base,
|
|
115
|
+
]
|
|
116
|
+
if draft:
|
|
117
|
+
args.append("--draft")
|
|
118
|
+
data = self._run(*args, "--json", "number,title,url,headRefName,baseRefName")
|
|
119
|
+
return PullRequest(
|
|
120
|
+
number=data["number"],
|
|
121
|
+
title=data["title"],
|
|
122
|
+
url=data["url"],
|
|
123
|
+
head=data["headRefName"],
|
|
124
|
+
base=data["baseRefName"],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def review_pull_request(
|
|
128
|
+
self, full_name: str, number: int, action: str, body: str = "",
|
|
129
|
+
) -> ReviewResult:
|
|
130
|
+
flags = {"approve": "--approve", "comment": "--comment", "request_changes": "--request-changes"}
|
|
131
|
+
try:
|
|
132
|
+
flag = flags[action]
|
|
133
|
+
except KeyError as exc:
|
|
134
|
+
raise ValueError(f"Unsupported review action: {action}") from exc
|
|
135
|
+
args = ["pr", "review", str(number), "--repo", full_name, flag]
|
|
136
|
+
if body:
|
|
137
|
+
args.extend(["--body", body])
|
|
138
|
+
self._run(*args)
|
|
139
|
+
return ReviewResult(success=True, action=action)
|
|
140
|
+
|
|
141
|
+
def merge_pull_request(
|
|
142
|
+
self, full_name: str, number: int, method: str = "squash",
|
|
143
|
+
) -> MergeResult:
|
|
144
|
+
if method not in {"merge", "squash", "rebase"}:
|
|
145
|
+
raise ValueError(f"Unsupported merge method: {method}")
|
|
146
|
+
data = self._run(
|
|
147
|
+
"api", f"repos/{full_name}/pulls/{number}/merge",
|
|
148
|
+
"--method", "PUT", "-f", f"merge_method={method}",
|
|
149
|
+
)
|
|
150
|
+
return MergeResult(
|
|
151
|
+
merged=bool(data.get("merged")),
|
|
152
|
+
sha=data.get("sha"),
|
|
153
|
+
message=data.get("message", ""),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def list_workflows(self, full_name: str, ref: str) -> list[WorkflowRun]:
|
|
157
|
+
data = self._run(
|
|
158
|
+
"run", "list", "--repo", full_name, "--branch", ref,
|
|
159
|
+
"--json", "databaseId,status,conclusion",
|
|
160
|
+
)
|
|
161
|
+
return [
|
|
162
|
+
WorkflowRun(item["databaseId"], item["status"], item["conclusion"])
|
|
163
|
+
for item in data
|
|
164
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""OpenAI provider adapters for VYRELON Chat Agents."""
|