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/vyrelon.py
ADDED
|
@@ -0,0 +1,1557 @@
|
|
|
1
|
+
"""High-level VYRELON runtime facade."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from agents.registry import build_registry
|
|
9
|
+
from core.chat_agent_bridge import ChatAgentBridge, ChatAgentExecutionResult, ChatAgentRequest, ChatAgentResponse
|
|
10
|
+
from core.chat_agent_registry import default_chat_agents
|
|
11
|
+
from core.chat_agent_router import ChatAgentAssignment, ChatAgentRouter, ChatAgentRoutingStrategy
|
|
12
|
+
from core.chat_session import ChatSession, ChatSessionStore
|
|
13
|
+
from core.contracts.human_review import HumanReviewDecision
|
|
14
|
+
from core.contracts.resume import WorkflowResumeContext
|
|
15
|
+
from core.multi_agent_workflow import MultiAgentWorkflow, MultiAgentWorkflowResult
|
|
16
|
+
from core.artifacts import ArtifactStore
|
|
17
|
+
from runtime.chat_config import load_chat_config
|
|
18
|
+
from runtime.chat_adapter_registry import resolve_project_chat_adapter
|
|
19
|
+
from core.contracts.agent import AgentContract
|
|
20
|
+
from core.contracts.ide import IDECommand, IDECommandKind, IDEWorkRequest
|
|
21
|
+
from core.contracts.ai import AIProvider, ModelSpec
|
|
22
|
+
from core.contracts.execution import AgentExecutor, ResultReviewer, ResultVerifier
|
|
23
|
+
from core.contracts.work_unit import WorkStatus, WorkUnit
|
|
24
|
+
from core.contracts.vyrelon_runtime import SessionSpec, SessionState
|
|
25
|
+
from core.handoff import ReviewPanel, ReviewPanelResult
|
|
26
|
+
from core.planning import BasicPlanner
|
|
27
|
+
from core.state import SessionStateStore, WorkStateStore
|
|
28
|
+
from core.orchestrator import OrchestrationResult, Orchestrator
|
|
29
|
+
from core.routing import AIRouter, RoutingStrategy
|
|
30
|
+
from profiles.detector import ProfileDetector
|
|
31
|
+
from profiles.resolver import ProfileResolver
|
|
32
|
+
from integrations.github.gateway import GitHubGatewayClient
|
|
33
|
+
from runtime.github import GitHubRuntime
|
|
34
|
+
from runtime.discovery import ProviderDiscoveryAdapter
|
|
35
|
+
from runtime.github_probe import probe
|
|
36
|
+
from runtime.git import GitRuntime
|
|
37
|
+
from runtime.local.filesystem import FilesystemRuntime
|
|
38
|
+
from runtime.local.patch import PatchRuntime
|
|
39
|
+
from runtime.local.path_security import PathPolicy
|
|
40
|
+
from runtime.local.shell import PersistentShellRuntime
|
|
41
|
+
from runtime.model.config import DEFAULT_CONFIG_PATH, ProviderConfigLoader
|
|
42
|
+
from runtime.model.credentials import EnvironmentCredentialResolver
|
|
43
|
+
from runtime.model.factory import ConfiguredAdapterFactory
|
|
44
|
+
from runtime.model.providers import AIProviderRegistry
|
|
45
|
+
from runtime.model.registry import ModelAdapterRegistry
|
|
46
|
+
from runtime.model.runtime_loader import ProviderRuntimeLoader
|
|
47
|
+
from runtime.mcp.client import MCPClient
|
|
48
|
+
from runtime.mcp.config import MCPConfigLoader
|
|
49
|
+
from runtime.mcp.session import MCPSessionRegistry
|
|
50
|
+
from runtime.mcp.client import MCPClient
|
|
51
|
+
from runtime.mcp.config import MCPConfigLoader
|
|
52
|
+
from runtime.policy import ExecutionPolicy
|
|
53
|
+
from runtime.validation import ValidationReport, ValidationRuntime, ValidationStep
|
|
54
|
+
from runtime.repository import RepositoryRuntime
|
|
55
|
+
from runtime.multi_agent import MultiAgentResult, MultiAgentRuntime
|
|
56
|
+
from runtime.ide.runtime import IDERuntime
|
|
57
|
+
from runtime.ide.bridge import IDEBridge, IDEBridgePolicy, IDEBridgeServer
|
|
58
|
+
from runtime.agent.ide import IDECodingExecutor, IDEValidationVerifier
|
|
59
|
+
from runtime.tool_calling import ToolRuntime
|
|
60
|
+
from runtime.builtin_tools import BuiltinToolBindings
|
|
61
|
+
from runtime.repository_tools import GitToolBindings, MCPToolBindings
|
|
62
|
+
from runtime.quota import QuotaIntelligence, QuotaStore, quota_available
|
|
63
|
+
from runtime.capability import CapabilityRegistry, CapabilityStore
|
|
64
|
+
from runtime.health import ModelHealthRegistry, ModelHealthStore
|
|
65
|
+
from runtime.model_control import ModelControlPlane
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class VYRELONRuntime:
|
|
69
|
+
"""Single entry point for project inspection, local tooling, agent execution, and GitHub access."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
policy: ExecutionPolicy | None = None,
|
|
74
|
+
orchestrator: Orchestrator | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
self.policy = policy or ExecutionPolicy()
|
|
77
|
+
self.orchestrator = orchestrator or Orchestrator()
|
|
78
|
+
self.git = GitRuntime(policy=self.policy)
|
|
79
|
+
self.github = GitHubRuntime(
|
|
80
|
+
gateway=GitHubGatewayClient(),
|
|
81
|
+
policy=self.policy,
|
|
82
|
+
)
|
|
83
|
+
self.providers = AIProviderRegistry()
|
|
84
|
+
self.model_adapters = ModelAdapterRegistry()
|
|
85
|
+
self.provider_config = ProviderConfigLoader()
|
|
86
|
+
self.credentials = EnvironmentCredentialResolver()
|
|
87
|
+
self.adapter_factory = ConfiguredAdapterFactory()
|
|
88
|
+
self.provider_runtime_loader = ProviderRuntimeLoader(policy=self.policy)
|
|
89
|
+
self.mcp_config = MCPConfigLoader()
|
|
90
|
+
self.mcp_sessions = MCPSessionRegistry()
|
|
91
|
+
self.mcp_config = MCPConfigLoader()
|
|
92
|
+
self.mcp_clients: dict[str, MCPClient] = {}
|
|
93
|
+
self.validation = ValidationRuntime(self.policy)
|
|
94
|
+
self.repository = RepositoryRuntime(self.git, self.github, self.policy)
|
|
95
|
+
self.multi_agent = MultiAgentRuntime(self.orchestrator)
|
|
96
|
+
self.ide = IDERuntime()
|
|
97
|
+
self.tool_runtime = ToolRuntime(self.policy)
|
|
98
|
+
self.ide_bridge_server: IDEBridgeServer | None = None
|
|
99
|
+
|
|
100
|
+
def start_ide_bridge(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
token: str,
|
|
104
|
+
host: str = "127.0.0.1",
|
|
105
|
+
port: int = 8787,
|
|
106
|
+
allow_remote: bool = False,
|
|
107
|
+
) -> IDEBridgeServer:
|
|
108
|
+
"""Start the local IDE bridge against this VYRELON runtime."""
|
|
109
|
+
if self.ide_bridge_server is not None:
|
|
110
|
+
return self.ide_bridge_server
|
|
111
|
+
bridge = IDEBridge(
|
|
112
|
+
policy=IDEBridgePolicy(token=token, allow_remote=allow_remote),
|
|
113
|
+
runtime=self.ide,
|
|
114
|
+
work_handler=self.submit_ide_work,
|
|
115
|
+
)
|
|
116
|
+
self.ide_bridge_server = IDEBridgeServer(bridge, host=host, port=port)
|
|
117
|
+
self.ide_bridge_server.start()
|
|
118
|
+
return self.ide_bridge_server
|
|
119
|
+
|
|
120
|
+
def stop_ide_bridge(self) -> None:
|
|
121
|
+
"""Stop the VYRELON IDE bridge when it is running."""
|
|
122
|
+
if self.ide_bridge_server is not None:
|
|
123
|
+
self.ide_bridge_server.stop()
|
|
124
|
+
self.ide_bridge_server = None
|
|
125
|
+
|
|
126
|
+
def submit_ide_work(
|
|
127
|
+
self,
|
|
128
|
+
request: IDEWorkRequest,
|
|
129
|
+
*,
|
|
130
|
+
models: list[ModelSpec] | None = None,
|
|
131
|
+
executor: AgentExecutor | None = None,
|
|
132
|
+
verifier: ResultVerifier | None = None,
|
|
133
|
+
reviewer: ResultReviewer | None = None,
|
|
134
|
+
) -> dict[str, object]:
|
|
135
|
+
"""Turn an explicit IDE work request into a persistent Agent/WorkUnit execution."""
|
|
136
|
+
project_root = Path(request.context.project_root).resolve()
|
|
137
|
+
agent = self.agent_profile(project_root, request.agent_id)
|
|
138
|
+
work_unit = WorkUnit(
|
|
139
|
+
id=f"ide-{uuid4().hex}",
|
|
140
|
+
objective=request.objective,
|
|
141
|
+
inputs={
|
|
142
|
+
"ide_context": {
|
|
143
|
+
"kind": request.context.kind.value,
|
|
144
|
+
"file_path": request.context.file_path,
|
|
145
|
+
"selection_start": request.context.selection_start,
|
|
146
|
+
"selection_end": request.context.selection_end,
|
|
147
|
+
"language_id": request.context.language_id,
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
metadata={
|
|
151
|
+
"source": "ide",
|
|
152
|
+
"ide_kind": request.context.kind.value,
|
|
153
|
+
"agent_id": request.agent_id,
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
effective_verifier = verifier
|
|
157
|
+
if request.validation_commands:
|
|
158
|
+
effective_verifier = IDEValidationVerifier(
|
|
159
|
+
project_root=project_root,
|
|
160
|
+
commands=request.validation_commands,
|
|
161
|
+
policy=self.policy,
|
|
162
|
+
)
|
|
163
|
+
if models is not None and executor is not None:
|
|
164
|
+
tool_runtime = ToolRuntime(self.policy)
|
|
165
|
+
BuiltinToolBindings(str(project_root), tool_runtime)
|
|
166
|
+
GitToolBindings(str(project_root), tool_runtime)
|
|
167
|
+
effective_executor = IDECodingExecutor(
|
|
168
|
+
delegate=executor,
|
|
169
|
+
project_root=project_root,
|
|
170
|
+
apply_changes=request.apply_changes,
|
|
171
|
+
policy=self.policy,
|
|
172
|
+
tool_runtime=tool_runtime,
|
|
173
|
+
)
|
|
174
|
+
result = self.run_persistent(
|
|
175
|
+
project_root=project_root,
|
|
176
|
+
work_unit=work_unit,
|
|
177
|
+
agent=agent,
|
|
178
|
+
models=models,
|
|
179
|
+
executor=effective_executor,
|
|
180
|
+
preferred_model_ids=list(request.model_ids) or None,
|
|
181
|
+
verifier=effective_verifier,
|
|
182
|
+
reviewer=reviewer,
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
result = self.run_persistent_registered_model(
|
|
186
|
+
project_root=project_root,
|
|
187
|
+
work_unit=work_unit,
|
|
188
|
+
agent=agent,
|
|
189
|
+
preferred_model_ids=list(request.model_ids) or None,
|
|
190
|
+
verifier=effective_verifier,
|
|
191
|
+
reviewer=reviewer,
|
|
192
|
+
)
|
|
193
|
+
output = result.output
|
|
194
|
+
text = getattr(output, "text", str(output))
|
|
195
|
+
ide_result = self.ide.execute(
|
|
196
|
+
request.context.kind,
|
|
197
|
+
IDECommand(
|
|
198
|
+
kind=IDECommandKind.SHOW_MESSAGE,
|
|
199
|
+
arguments={
|
|
200
|
+
"text": text,
|
|
201
|
+
"work_unit_id": work_unit.id,
|
|
202
|
+
"status": work_unit.status.value,
|
|
203
|
+
},
|
|
204
|
+
context=request.context,
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
return {
|
|
208
|
+
"work_unit": work_unit,
|
|
209
|
+
"orchestration": result,
|
|
210
|
+
"ide_result": ide_result,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
def load_mcp_config(self, project_root: Path):
|
|
214
|
+
"""Load external MCP server definitions from the project configuration."""
|
|
215
|
+
return self.mcp_config.load(project_root)
|
|
216
|
+
|
|
217
|
+
def connect_mcp(self, project_root: Path, server_id: str):
|
|
218
|
+
"""Connect one configured external MCP server and return its client."""
|
|
219
|
+
specs = {spec.id: spec for spec in self.load_mcp_config(project_root)}
|
|
220
|
+
try:
|
|
221
|
+
spec = specs[server_id]
|
|
222
|
+
except KeyError as exc:
|
|
223
|
+
raise LookupError(f"MCP server not configured: {server_id}") from exc
|
|
224
|
+
client = MCPClient(spec, cwd=str(project_root))
|
|
225
|
+
client.connect()
|
|
226
|
+
self.mcp_clients[server_id] = client
|
|
227
|
+
return client
|
|
228
|
+
|
|
229
|
+
def disconnect_mcp(self, server_id: str) -> None:
|
|
230
|
+
client = self.mcp_clients.pop(server_id, None)
|
|
231
|
+
if client is not None:
|
|
232
|
+
client.close()
|
|
233
|
+
|
|
234
|
+
def load_mcp_config(self, project_root: Path):
|
|
235
|
+
return self.mcp_config.load(project_root)
|
|
236
|
+
|
|
237
|
+
def connect_mcp(self, project_root: Path, server_id: str):
|
|
238
|
+
specs={s.id:s for s in self.load_mcp_config(project_root)}
|
|
239
|
+
if server_id not in specs: raise LookupError(f"MCP server not configured: {server_id}")
|
|
240
|
+
client=MCPClient(specs[server_id],cwd=str(project_root))
|
|
241
|
+
client.connect(); self.mcp_sessions.add(client)
|
|
242
|
+
return client
|
|
243
|
+
|
|
244
|
+
def disconnect_mcp(self, session_id: str) -> None:
|
|
245
|
+
self.mcp_sessions.close(session_id)
|
|
246
|
+
|
|
247
|
+
def local_filesystem(self, project_root: Path) -> FilesystemRuntime:
|
|
248
|
+
return FilesystemRuntime(
|
|
249
|
+
policy=self.policy,
|
|
250
|
+
paths=PathPolicy((str(project_root),)),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def local_shell(self, project_root: Path) -> PersistentShellRuntime:
|
|
254
|
+
return PersistentShellRuntime(
|
|
255
|
+
cwd=str(project_root),
|
|
256
|
+
policy=self.policy,
|
|
257
|
+
paths=PathPolicy((str(project_root),)),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def local_patch(self, project_root: Path) -> PatchRuntime:
|
|
261
|
+
return PatchRuntime(
|
|
262
|
+
policy=self.policy,
|
|
263
|
+
paths=PathPolicy((str(project_root),)),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def run_multi_agent(self, project_root: Path, work_unit: WorkUnit, steps, agents: dict[str, AgentContract], models: list[ModelSpec], executors: dict[str, AgentExecutor], *, preferred_model_ids=None, verifiers=None, reviewers=None) -> MultiAgentResult:
|
|
267
|
+
return self.multi_agent.run(project_root, work_unit, steps, agents, models, executors, preferred_model_ids=preferred_model_ids, verifiers=verifiers, reviewers=reviewers)
|
|
268
|
+
|
|
269
|
+
def repository_checkpoint(self, project_root: Path, *, metadata: dict[str, object] | None = None):
|
|
270
|
+
return self.repository.checkpoint(project_root, metadata=metadata)
|
|
271
|
+
|
|
272
|
+
def repository_recover(self, project_root: Path, checkpoint):
|
|
273
|
+
return self.repository.recover(project_root, checkpoint)
|
|
274
|
+
|
|
275
|
+
def repository_evidence(self, project_root: Path, *, repository: str | None = None, ref: str | None = None, validation: dict[str, object] | None = None):
|
|
276
|
+
return self.repository.evidence(project_root, repository=repository, ref=ref, validation=validation)
|
|
277
|
+
|
|
278
|
+
def validate(self, project_root: Path, steps: list[ValidationStep], *, persist_evidence: bool = True) -> ValidationReport:
|
|
279
|
+
"""Run project validation through the VYRELON local execution boundary."""
|
|
280
|
+
return self.validation.run(project_root, steps, persist_evidence=persist_evidence)
|
|
281
|
+
|
|
282
|
+
def inspect(self, project_root: Path):
|
|
283
|
+
return ProfileDetector().detect(project_root)
|
|
284
|
+
|
|
285
|
+
def plan(self, work_unit: WorkUnit, steps):
|
|
286
|
+
return BasicPlanner().plan(work_unit, steps)
|
|
287
|
+
|
|
288
|
+
def session_store(self, project_root: Path) -> SessionStateStore:
|
|
289
|
+
return SessionStateStore(Path(project_root).resolve() / ".multiagentos" / "sessions")
|
|
290
|
+
|
|
291
|
+
def create_session(
|
|
292
|
+
self,
|
|
293
|
+
project_root: Path,
|
|
294
|
+
*,
|
|
295
|
+
session_id: str | None = None,
|
|
296
|
+
agent_id: str | None = None,
|
|
297
|
+
model_id: str | None = None,
|
|
298
|
+
checkpoint_id: str | None = None,
|
|
299
|
+
metadata: dict[str, object] | None = None,
|
|
300
|
+
) -> SessionState:
|
|
301
|
+
root = Path(project_root).resolve()
|
|
302
|
+
state = SessionState(
|
|
303
|
+
spec=SessionSpec(
|
|
304
|
+
id=session_id or f"session-{uuid4().hex}",
|
|
305
|
+
project_root=str(root),
|
|
306
|
+
agent_id=agent_id,
|
|
307
|
+
model_id=model_id,
|
|
308
|
+
checkpoint_id=checkpoint_id,
|
|
309
|
+
metadata=dict(metadata or {}),
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
self.session_store(root).save(state)
|
|
313
|
+
return state
|
|
314
|
+
|
|
315
|
+
def load_session(self, project_root: Path, session_id: str) -> SessionState:
|
|
316
|
+
return self.session_store(project_root).load(session_id)
|
|
317
|
+
|
|
318
|
+
def attach_work_unit(self, project_root: Path, session_id: str, work_unit_id: str) -> SessionState:
|
|
319
|
+
store = self.session_store(project_root)
|
|
320
|
+
state = store.load(session_id)
|
|
321
|
+
if work_unit_id not in state.work_unit_ids:
|
|
322
|
+
state.work_unit_ids.append(work_unit_id)
|
|
323
|
+
state.metadata["last_work_unit_id"] = work_unit_id
|
|
324
|
+
store.save(state)
|
|
325
|
+
return state
|
|
326
|
+
|
|
327
|
+
def session_checkpoint(self, project_root: Path, session_id: str, *, metadata: dict[str, object] | None = None) -> SessionState:
|
|
328
|
+
root = Path(project_root).resolve()
|
|
329
|
+
state = self.load_session(root, session_id)
|
|
330
|
+
checkpoint = self.repository_checkpoint(root, metadata={"session_id": session_id, **dict(metadata or {})})
|
|
331
|
+
state.spec = SessionSpec(
|
|
332
|
+
id=state.spec.id,
|
|
333
|
+
project_root=state.spec.project_root,
|
|
334
|
+
agent_id=state.spec.agent_id,
|
|
335
|
+
model_id=state.spec.model_id,
|
|
336
|
+
checkpoint_id=checkpoint.id,
|
|
337
|
+
metadata=state.spec.metadata,
|
|
338
|
+
)
|
|
339
|
+
state.metadata["checkpoint_id"] = checkpoint.id
|
|
340
|
+
self.session_store(root).save(state)
|
|
341
|
+
return state
|
|
342
|
+
|
|
343
|
+
def session_recover(self, project_root: Path, session_id: str) -> tuple[SessionState, tuple[WorkUnit, ...]]:
|
|
344
|
+
root = Path(project_root).resolve()
|
|
345
|
+
state = self.load_session(root, session_id)
|
|
346
|
+
recoverable = []
|
|
347
|
+
work_store = self.state_store(root)
|
|
348
|
+
for work_id in state.work_unit_ids:
|
|
349
|
+
if work_store.exists(work_id):
|
|
350
|
+
work = work_store.load(work_id)
|
|
351
|
+
if work.status in {WorkStatus.FAILED, WorkStatus.PENDING}:
|
|
352
|
+
recoverable.append(work)
|
|
353
|
+
state.status = "recoverable" if recoverable else state.status
|
|
354
|
+
self.session_store(root).save(state)
|
|
355
|
+
return state, tuple(recoverable)
|
|
356
|
+
|
|
357
|
+
def state_store(self, project_root: Path):
|
|
358
|
+
return WorkStateStore(project_root / ".multiagentos" / "state")
|
|
359
|
+
|
|
360
|
+
def quota_store(self, project_root: Path):
|
|
361
|
+
"""Return the project-scoped persistent model quota store."""
|
|
362
|
+
return QuotaStore(Path(project_root) / ".multiagentos" / "quota")
|
|
363
|
+
|
|
364
|
+
def health_registry(self, project_root: Path):
|
|
365
|
+
"""Return the project-scoped persistent model health registry."""
|
|
366
|
+
return ModelHealthRegistry(ModelHealthStore(Path(project_root) / ".multiagentos" / "health"))
|
|
367
|
+
|
|
368
|
+
def capability_registry(self, project_root: Path):
|
|
369
|
+
"""Return the project-scoped persistent model capability registry."""
|
|
370
|
+
return CapabilityRegistry(CapabilityStore(Path(project_root) / ".multiagentos" / "capabilities"))
|
|
371
|
+
|
|
372
|
+
def model_control_plane(self, project_root: Path) -> ModelControlPlane:
|
|
373
|
+
"""Return the project-scoped unified model control plane."""
|
|
374
|
+
return ModelControlPlane(Path(project_root).resolve())
|
|
375
|
+
|
|
376
|
+
def agents(self, project_root: Path):
|
|
377
|
+
"""Return the legacy AgentRegistry for compatibility."""
|
|
378
|
+
detections = self.inspect(project_root)
|
|
379
|
+
profile_ids = tuple(result.profile_id for result in detections)
|
|
380
|
+
return build_registry(profile_ids)
|
|
381
|
+
|
|
382
|
+
def profiles(self, project_root: Path):
|
|
383
|
+
"""Resolve one ProjectProfile and its deterministic AgentProfiles."""
|
|
384
|
+
return ProfileResolver().resolve(project_root)
|
|
385
|
+
|
|
386
|
+
def agent_profile(self, project_root: Path, agent_id: str):
|
|
387
|
+
"""Resolve one project-scoped AgentProfile as an AgentContract."""
|
|
388
|
+
_, agent_profiles = self.profiles(project_root)
|
|
389
|
+
for profile in agent_profiles:
|
|
390
|
+
if profile.id == agent_id:
|
|
391
|
+
return profile.to_contract()
|
|
392
|
+
raise LookupError(f"Agent profile not found: {agent_id}")
|
|
393
|
+
|
|
394
|
+
def github_probe(self, repository: str) -> dict:
|
|
395
|
+
return probe(repository)
|
|
396
|
+
|
|
397
|
+
def load_provider_config(self, path: Path) -> None:
|
|
398
|
+
"""Load provider/model definitions from a project JSON configuration."""
|
|
399
|
+
self.provider_config.load(path, self.providers)
|
|
400
|
+
|
|
401
|
+
def load_project_provider_config(self, project_root: Path) -> bool:
|
|
402
|
+
"""Load the optional project provider configuration if present."""
|
|
403
|
+
path = project_root / DEFAULT_CONFIG_PATH
|
|
404
|
+
if not path.exists():
|
|
405
|
+
return False
|
|
406
|
+
self.load_provider_config(path)
|
|
407
|
+
return True
|
|
408
|
+
|
|
409
|
+
def explain_model_routing(
|
|
410
|
+
self,
|
|
411
|
+
project_root: Path,
|
|
412
|
+
*,
|
|
413
|
+
agent_id: str,
|
|
414
|
+
preferred_model_ids: list[str] | None = None,
|
|
415
|
+
routing_strategy: str = "pool",
|
|
416
|
+
):
|
|
417
|
+
"""Return provider-neutral routing evidence for a project agent."""
|
|
418
|
+
root = Path(project_root).resolve()
|
|
419
|
+
self.load_project_provider_config(root)
|
|
420
|
+
models = self.configured_models()
|
|
421
|
+
agent = self.agent_profile(root, agent_id)
|
|
422
|
+
preferred = preferred_model_ids
|
|
423
|
+
if preferred is None and agent.model_ids:
|
|
424
|
+
preferred = list(agent.model_ids)
|
|
425
|
+
quota_store = self.quota_store(root)
|
|
426
|
+
health_registry = self.health_registry(root)
|
|
427
|
+
capability_registry = self.capability_registry(root)
|
|
428
|
+
quota_snapshots = {
|
|
429
|
+
model.id: quota_store.load(model.id)
|
|
430
|
+
for model in models
|
|
431
|
+
if quota_store.exists(model.id)
|
|
432
|
+
}
|
|
433
|
+
health_snapshots = {
|
|
434
|
+
model.id: health
|
|
435
|
+
for model in models
|
|
436
|
+
if (health := health_registry.get(model.id)) is not None
|
|
437
|
+
}
|
|
438
|
+
return AIRouter().explain(
|
|
439
|
+
agent.to_contract() if hasattr(agent, "to_contract") else agent,
|
|
440
|
+
models,
|
|
441
|
+
preferred_model_ids=preferred,
|
|
442
|
+
strategy=RoutingStrategy(routing_strategy),
|
|
443
|
+
quota_snapshots=quota_snapshots,
|
|
444
|
+
health_snapshots=health_snapshots,
|
|
445
|
+
capability_registry=capability_registry,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
def discover_models(self, project_root: Path, *, refresh: bool = True):
|
|
449
|
+
"""Discover and persist provider capabilities and quota metadata."""
|
|
450
|
+
root = Path(project_root).resolve()
|
|
451
|
+
self.load_project_provider_config(root)
|
|
452
|
+
capability_registry = self.capability_registry(root)
|
|
453
|
+
quota_store = self.quota_store(root)
|
|
454
|
+
adapter = ProviderDiscoveryAdapter(policy=self.policy)
|
|
455
|
+
results = []
|
|
456
|
+
for provider in self.providers.providers():
|
|
457
|
+
if not refresh:
|
|
458
|
+
results.append({"provider": provider.id, "status": "skipped"})
|
|
459
|
+
continue
|
|
460
|
+
result, error = adapter.discover_safe(
|
|
461
|
+
provider,
|
|
462
|
+
models=tuple(provider.models),
|
|
463
|
+
)
|
|
464
|
+
if error is not None:
|
|
465
|
+
results.append({
|
|
466
|
+
"provider": provider.id,
|
|
467
|
+
"status": "error",
|
|
468
|
+
"message": error.message,
|
|
469
|
+
"retryable": error.retryable,
|
|
470
|
+
})
|
|
471
|
+
continue
|
|
472
|
+
for profile in result.capabilities:
|
|
473
|
+
capability_registry.merge(profile)
|
|
474
|
+
for snapshot in result.quotas:
|
|
475
|
+
quota_store.merge(snapshot)
|
|
476
|
+
results.append({
|
|
477
|
+
"provider": provider.id,
|
|
478
|
+
"status": "ok",
|
|
479
|
+
"capabilities": len(result.capabilities),
|
|
480
|
+
"quotas": len(result.quotas),
|
|
481
|
+
"source": result.source,
|
|
482
|
+
})
|
|
483
|
+
return tuple(results)
|
|
484
|
+
|
|
485
|
+
def credential_checks(self) -> dict[str, tuple[object, ...]]:
|
|
486
|
+
"""Check configured environment-variable credentials without exposing values."""
|
|
487
|
+
result: dict[str, tuple[object, ...]] = {}
|
|
488
|
+
for model in self.configured_models():
|
|
489
|
+
environment_variables: list[str] = []
|
|
490
|
+
configured = model.metadata.get("credential_env", [])
|
|
491
|
+
if isinstance(configured, list):
|
|
492
|
+
environment_variables.extend(
|
|
493
|
+
item for item in configured if isinstance(item, str) and item
|
|
494
|
+
)
|
|
495
|
+
header_env = model.metadata.get("header_env", {})
|
|
496
|
+
if isinstance(header_env, dict):
|
|
497
|
+
environment_variables.extend(
|
|
498
|
+
item
|
|
499
|
+
for item in header_env.values()
|
|
500
|
+
if isinstance(item, str) and item
|
|
501
|
+
)
|
|
502
|
+
checks = self.credentials.check(list(dict.fromkeys(environment_variables)))
|
|
503
|
+
result[model.id] = checks
|
|
504
|
+
return result
|
|
505
|
+
|
|
506
|
+
def register_provider(self, provider: AIProvider) -> None:
|
|
507
|
+
"""Register provider/model configuration for later model execution."""
|
|
508
|
+
self.providers.register(provider)
|
|
509
|
+
|
|
510
|
+
def register_model_adapter(self, adapter_id: str, adapter) -> None:
|
|
511
|
+
"""Register a runtime adapter referenced by model metadata."""
|
|
512
|
+
self.model_adapters.register(adapter_id, adapter)
|
|
513
|
+
|
|
514
|
+
def configure_model_adapters(self) -> None:
|
|
515
|
+
"""Materialize adapters declared by registered model metadata."""
|
|
516
|
+
for model in self.configured_models():
|
|
517
|
+
adapter_id = str(model.metadata.get("adapter_id", model.provider_id))
|
|
518
|
+
if adapter_id in self.model_adapters.list():
|
|
519
|
+
continue
|
|
520
|
+
metadata = dict(model.metadata)
|
|
521
|
+
adapter_kind = str(metadata.get("adapter_kind", ""))
|
|
522
|
+
if adapter_kind in {"cli", "http"}:
|
|
523
|
+
self.register_model_adapter(
|
|
524
|
+
adapter_id,
|
|
525
|
+
self.adapter_factory.build(adapter_id, metadata, self.policy),
|
|
526
|
+
)
|
|
527
|
+
continue
|
|
528
|
+
provider = self.providers.get_provider(model.provider_id)
|
|
529
|
+
provider_kind = provider.kind.lower()
|
|
530
|
+
if provider_kind in {"openai", "openai_responses", "anthropic", "anthropic_messages", "gemini", "gemini_generate_content"}:
|
|
531
|
+
self.provider_runtime_loader.materializer.materialize(
|
|
532
|
+
(provider,), registry=self.model_adapters
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
def configured_models(self, provider_id: str | None = None) -> list[ModelSpec]:
|
|
536
|
+
"""Return registered models, optionally scoped to one provider."""
|
|
537
|
+
return list(self.providers.models(provider_id))
|
|
538
|
+
|
|
539
|
+
def run_configured_work(
|
|
540
|
+
self,
|
|
541
|
+
project_root: Path,
|
|
542
|
+
*,
|
|
543
|
+
objective: str,
|
|
544
|
+
agent_id: str,
|
|
545
|
+
work_unit_id: str | None = None,
|
|
546
|
+
session_id: str | None = None,
|
|
547
|
+
preferred_model_ids: list[str] | None = None,
|
|
548
|
+
validation_commands: tuple[str, ...] = (),
|
|
549
|
+
apply_changes: bool = False,
|
|
550
|
+
system_prompt: str | None = None,
|
|
551
|
+
adapter_overrides: dict[str, object] | None = None,
|
|
552
|
+
mcp_server_ids: tuple[str, ...] = (),
|
|
553
|
+
fallback_model_ids: tuple[str, ...] = (),
|
|
554
|
+
routing_strategy="pool",
|
|
555
|
+
) -> OrchestrationResult:
|
|
556
|
+
"""Run project-configured work without caller-side provider/model wiring."""
|
|
557
|
+
project_root = Path(project_root).resolve()
|
|
558
|
+
self.load_project_provider_config(project_root)
|
|
559
|
+
models = self.configured_models()
|
|
560
|
+
if not models:
|
|
561
|
+
raise ValueError("No configured models found")
|
|
562
|
+
if adapter_overrides:
|
|
563
|
+
for adapter_id, adapter in adapter_overrides.items():
|
|
564
|
+
if adapter_id in self.model_adapters.list():
|
|
565
|
+
self.model_adapters.replace(adapter_id, adapter)
|
|
566
|
+
else:
|
|
567
|
+
self.register_model_adapter(adapter_id, adapter)
|
|
568
|
+
self.configure_model_adapters()
|
|
569
|
+
|
|
570
|
+
agent = self.agent_profile(project_root, agent_id)
|
|
571
|
+
if preferred_model_ids:
|
|
572
|
+
for model_id in preferred_model_ids:
|
|
573
|
+
self.providers.get_model(model_id)
|
|
574
|
+
elif agent.model_ids:
|
|
575
|
+
preferred_model_ids = list(agent.model_ids)
|
|
576
|
+
else:
|
|
577
|
+
# Project-aware automatic routing also considers the latest persisted quota.
|
|
578
|
+
router = AIRouter()
|
|
579
|
+
quota_store = self.quota_store(project_root)
|
|
580
|
+
health_registry = self.health_registry(project_root)
|
|
581
|
+
capability_registry = self.capability_registry(project_root)
|
|
582
|
+
quota_snapshots = {
|
|
583
|
+
model.id: quota_store.load(model.id)
|
|
584
|
+
for model in models
|
|
585
|
+
if quota_store.exists(model.id)
|
|
586
|
+
}
|
|
587
|
+
health_snapshots = {}
|
|
588
|
+
for model in models:
|
|
589
|
+
health = health_registry.get(model.id)
|
|
590
|
+
if health is not None:
|
|
591
|
+
health_snapshots[model.id] = health
|
|
592
|
+
assignment = router.assign(
|
|
593
|
+
agent,
|
|
594
|
+
models,
|
|
595
|
+
strategy=RoutingStrategy(routing_strategy),
|
|
596
|
+
quota_snapshots=quota_snapshots,
|
|
597
|
+
health_snapshots=health_snapshots,
|
|
598
|
+
capability_registry=capability_registry,
|
|
599
|
+
)
|
|
600
|
+
preferred_model_ids = [assignment.model_id]
|
|
601
|
+
fallback_model_ids = tuple(
|
|
602
|
+
model.id
|
|
603
|
+
for model in models
|
|
604
|
+
if model.id != assignment.model_id
|
|
605
|
+
and router.compatible(agent, model)
|
|
606
|
+
and health_registry.available(model.id)
|
|
607
|
+
and (
|
|
608
|
+
not quota_store.exists(model.id)
|
|
609
|
+
or quota_available(quota_store.load(model.id))
|
|
610
|
+
)
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
from runtime.agent.model import ModelAgentExecutor
|
|
614
|
+
control_plane = self.model_control_plane(project_root)
|
|
615
|
+
quota_intelligence = QuotaIntelligence(control_plane.quota_store)
|
|
616
|
+
capability_registry = self.capability_registry(project_root)
|
|
617
|
+
health_registry = control_plane.health_registry
|
|
618
|
+
|
|
619
|
+
tool_runtime = ToolRuntime(self.policy)
|
|
620
|
+
BuiltinToolBindings(str(project_root), tool_runtime)
|
|
621
|
+
GitToolBindings(str(project_root), tool_runtime)
|
|
622
|
+
for server_id in mcp_server_ids:
|
|
623
|
+
client = self.mcp_clients.get(server_id) or self.connect_mcp(project_root, server_id)
|
|
624
|
+
MCPToolBindings(tool_runtime, client).register_tools()
|
|
625
|
+
if not apply_changes:
|
|
626
|
+
tool_runtime.unregister("patch.apply")
|
|
627
|
+
|
|
628
|
+
executor = ModelAgentExecutor(
|
|
629
|
+
adapters={
|
|
630
|
+
adapter_id: self.model_adapters.get(adapter_id)
|
|
631
|
+
for adapter_id in self.model_adapters.list()
|
|
632
|
+
},
|
|
633
|
+
models=models,
|
|
634
|
+
system_prompt=system_prompt,
|
|
635
|
+
fallback_model_ids=fallback_model_ids,
|
|
636
|
+
quota_intelligence=quota_intelligence,
|
|
637
|
+
health_registry=health_registry,
|
|
638
|
+
model_control=control_plane,
|
|
639
|
+
capability_registry=capability_registry,
|
|
640
|
+
)
|
|
641
|
+
effective_executor = IDECodingExecutor(
|
|
642
|
+
delegate=executor,
|
|
643
|
+
project_root=project_root,
|
|
644
|
+
apply_changes=apply_changes,
|
|
645
|
+
policy=self.policy,
|
|
646
|
+
tool_runtime=tool_runtime,
|
|
647
|
+
)
|
|
648
|
+
verifier = None
|
|
649
|
+
if validation_commands:
|
|
650
|
+
verifier = IDEValidationVerifier(
|
|
651
|
+
project_root=project_root,
|
|
652
|
+
commands=validation_commands,
|
|
653
|
+
policy=self.policy,
|
|
654
|
+
)
|
|
655
|
+
store = self.state_store(project_root)
|
|
656
|
+
if work_unit_id and store.exists(work_unit_id):
|
|
657
|
+
work_unit = store.load(work_unit_id)
|
|
658
|
+
work_unit.objective = objective
|
|
659
|
+
work_unit.metadata.update({
|
|
660
|
+
"source": "configured-runtime",
|
|
661
|
+
"agent_id": agent_id,
|
|
662
|
+
"apply_changes": apply_changes,
|
|
663
|
+
"runtime": "configured-model",
|
|
664
|
+
"session_id": session_id,
|
|
665
|
+
})
|
|
666
|
+
else:
|
|
667
|
+
work_unit = WorkUnit(
|
|
668
|
+
id=work_unit_id or f"work-{uuid4().hex}",
|
|
669
|
+
objective=objective,
|
|
670
|
+
metadata={
|
|
671
|
+
"source": "configured-runtime",
|
|
672
|
+
"agent_id": agent_id,
|
|
673
|
+
"apply_changes": apply_changes,
|
|
674
|
+
"runtime": "configured-model",
|
|
675
|
+
"session_id": session_id,
|
|
676
|
+
},
|
|
677
|
+
)
|
|
678
|
+
if session_id:
|
|
679
|
+
self.attach_work_unit(project_root, session_id, work_unit.id)
|
|
680
|
+
return self.run_persistent(
|
|
681
|
+
project_root=project_root,
|
|
682
|
+
work_unit=work_unit,
|
|
683
|
+
agent=agent,
|
|
684
|
+
models=models,
|
|
685
|
+
executor=effective_executor,
|
|
686
|
+
preferred_model_ids=preferred_model_ids,
|
|
687
|
+
verifier=verifier,
|
|
688
|
+
routing_strategy=routing_strategy,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
def run_registered_model(
|
|
692
|
+
self,
|
|
693
|
+
work_unit: WorkUnit,
|
|
694
|
+
agent: AgentContract,
|
|
695
|
+
preferred_model_ids: list[str] | None = None,
|
|
696
|
+
system_prompt: str | None = None,
|
|
697
|
+
verifier: ResultVerifier | None = None,
|
|
698
|
+
reviewer: ResultReviewer | None = None,
|
|
699
|
+
routing_strategy="pool",
|
|
700
|
+
) -> OrchestrationResult:
|
|
701
|
+
"""Execute using provider/model and adapter configuration registered in VYRELON."""
|
|
702
|
+
from runtime.agent.model import ModelAgentExecutor
|
|
703
|
+
|
|
704
|
+
models = self.configured_models()
|
|
705
|
+
self.configure_model_adapters()
|
|
706
|
+
if preferred_model_ids:
|
|
707
|
+
for model_id in preferred_model_ids:
|
|
708
|
+
self.providers.get_model(model_id)
|
|
709
|
+
|
|
710
|
+
return self.run(
|
|
711
|
+
work_unit=work_unit,
|
|
712
|
+
agent=agent,
|
|
713
|
+
models=models,
|
|
714
|
+
executor=ModelAgentExecutor(
|
|
715
|
+
adapters={
|
|
716
|
+
adapter_id: self.model_adapters.get(adapter_id)
|
|
717
|
+
for adapter_id in self.model_adapters.list()
|
|
718
|
+
},
|
|
719
|
+
models=models,
|
|
720
|
+
system_prompt=system_prompt,
|
|
721
|
+
),
|
|
722
|
+
preferred_model_ids=preferred_model_ids,
|
|
723
|
+
verifier=verifier,
|
|
724
|
+
reviewer=reviewer,
|
|
725
|
+
routing_strategy=routing_strategy,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
def run_persistent_registered_model(
|
|
729
|
+
self,
|
|
730
|
+
project_root: Path,
|
|
731
|
+
work_unit: WorkUnit,
|
|
732
|
+
agent: AgentContract,
|
|
733
|
+
preferred_model_ids: list[str] | None = None,
|
|
734
|
+
system_prompt: str | None = None,
|
|
735
|
+
verifier: ResultVerifier | None = None,
|
|
736
|
+
reviewer: ResultReviewer | None = None,
|
|
737
|
+
routing_strategy="pool",
|
|
738
|
+
) -> OrchestrationResult:
|
|
739
|
+
"""Run a configured model through the persistent WorkUnit lifecycle."""
|
|
740
|
+
self.load_project_provider_config(project_root)
|
|
741
|
+
models = self.configured_models()
|
|
742
|
+
if not models:
|
|
743
|
+
raise ValueError("No configured models found")
|
|
744
|
+
self.configure_model_adapters()
|
|
745
|
+
if preferred_model_ids:
|
|
746
|
+
for model_id in preferred_model_ids:
|
|
747
|
+
self.providers.get_model(model_id)
|
|
748
|
+
from runtime.agent.model import ModelAgentExecutor
|
|
749
|
+
executor = ModelAgentExecutor(
|
|
750
|
+
adapters={
|
|
751
|
+
adapter_id: self.model_adapters.get(adapter_id)
|
|
752
|
+
for adapter_id in self.model_adapters.list()
|
|
753
|
+
},
|
|
754
|
+
models=models,
|
|
755
|
+
system_prompt=system_prompt,
|
|
756
|
+
)
|
|
757
|
+
work_unit.metadata["runtime"] = "configured-model"
|
|
758
|
+
if preferred_model_ids:
|
|
759
|
+
work_unit.metadata["model_id"] = preferred_model_ids[0]
|
|
760
|
+
work_unit.metadata["agent_id"] = agent.id
|
|
761
|
+
return self.run_persistent(
|
|
762
|
+
project_root=project_root,
|
|
763
|
+
work_unit=work_unit,
|
|
764
|
+
agent=agent,
|
|
765
|
+
models=models,
|
|
766
|
+
executor=executor,
|
|
767
|
+
verifier=verifier,
|
|
768
|
+
reviewer=reviewer,
|
|
769
|
+
preferred_model_ids=preferred_model_ids,
|
|
770
|
+
routing_strategy=routing_strategy,
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
def resume_work(
|
|
774
|
+
self,
|
|
775
|
+
project_root: Path,
|
|
776
|
+
work_unit_id: str,
|
|
777
|
+
*,
|
|
778
|
+
agent_id: str,
|
|
779
|
+
preferred_model_ids: list[str] | None = None,
|
|
780
|
+
validation_commands: tuple[str, ...] = (),
|
|
781
|
+
apply_changes: bool = False,
|
|
782
|
+
fallback_model_ids: tuple[str, ...] = (),
|
|
783
|
+
adapter_overrides: dict[str, object] | None = None,
|
|
784
|
+
) -> OrchestrationResult:
|
|
785
|
+
"""Resume a persisted failed WorkUnit through the same configured runtime."""
|
|
786
|
+
project_root = Path(project_root).resolve()
|
|
787
|
+
store = self.state_store(project_root)
|
|
788
|
+
if not store.exists(work_unit_id):
|
|
789
|
+
raise LookupError(f"Persisted WorkUnit not found: {work_unit_id}")
|
|
790
|
+
work_unit = store.load(work_unit_id)
|
|
791
|
+
if work_unit.status not in {WorkStatus.FAILED, WorkStatus.PENDING}:
|
|
792
|
+
raise ValueError(
|
|
793
|
+
f"WorkUnit {work_unit_id} is not resumable from {work_unit.status.value}"
|
|
794
|
+
)
|
|
795
|
+
work_unit.metadata["resume_count"] = int(work_unit.metadata.get("resume_count", 0)) + 1
|
|
796
|
+
work_unit.metadata["resumed"] = True
|
|
797
|
+
store.save(work_unit)
|
|
798
|
+
return self.run_configured_work(
|
|
799
|
+
project_root,
|
|
800
|
+
objective=work_unit.objective,
|
|
801
|
+
agent_id=agent_id,
|
|
802
|
+
work_unit_id=work_unit.id,
|
|
803
|
+
preferred_model_ids=preferred_model_ids,
|
|
804
|
+
validation_commands=validation_commands,
|
|
805
|
+
apply_changes=apply_changes,
|
|
806
|
+
fallback_model_ids=fallback_model_ids,
|
|
807
|
+
adapter_overrides=adapter_overrides,
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
def recoverable_work(self, project_root: Path) -> tuple[WorkUnit, ...]:
|
|
811
|
+
"""Return persisted WorkUnits that can be resumed."""
|
|
812
|
+
store = self.state_store(Path(project_root).resolve())
|
|
813
|
+
result = []
|
|
814
|
+
for work_unit_id in store.list_ids():
|
|
815
|
+
work_unit = store.load(work_unit_id)
|
|
816
|
+
if work_unit.status in {WorkStatus.FAILED, WorkStatus.PENDING}:
|
|
817
|
+
result.append(work_unit)
|
|
818
|
+
return tuple(result)
|
|
819
|
+
|
|
820
|
+
def run_persistent(
|
|
821
|
+
self,
|
|
822
|
+
project_root: Path,
|
|
823
|
+
work_unit: WorkUnit,
|
|
824
|
+
agent: AgentContract,
|
|
825
|
+
models: list[ModelSpec],
|
|
826
|
+
executor: AgentExecutor,
|
|
827
|
+
verifier: ResultVerifier | None = None,
|
|
828
|
+
reviewer: ResultReviewer | None = None,
|
|
829
|
+
preferred_model_ids: list[str] | None = None,
|
|
830
|
+
routing_strategy="pool",
|
|
831
|
+
) -> OrchestrationResult:
|
|
832
|
+
"""Run through the VYRELON lifecycle while persisting every terminal state."""
|
|
833
|
+
work_unit.metadata["cwd"] = str(project_root)
|
|
834
|
+
store = self.state_store(project_root)
|
|
835
|
+
if work_unit.status == WorkStatus.FAILED:
|
|
836
|
+
work_unit.transition(WorkStatus.EXECUTING)
|
|
837
|
+
elif work_unit.status == WorkStatus.PENDING:
|
|
838
|
+
work_unit.transition(WorkStatus.EXECUTING)
|
|
839
|
+
store.save(work_unit)
|
|
840
|
+
try:
|
|
841
|
+
result = self.run(
|
|
842
|
+
work_unit=work_unit,
|
|
843
|
+
agent=agent,
|
|
844
|
+
models=models,
|
|
845
|
+
executor=executor,
|
|
846
|
+
verifier=verifier,
|
|
847
|
+
reviewer=reviewer,
|
|
848
|
+
preferred_model_ids=preferred_model_ids,
|
|
849
|
+
routing_strategy=routing_strategy,
|
|
850
|
+
project_root=project_root,
|
|
851
|
+
)
|
|
852
|
+
output = result.output
|
|
853
|
+
work_unit.metadata["output"] = str(output)
|
|
854
|
+
for field in ("returncode", "stdout", "stderr"):
|
|
855
|
+
if hasattr(output, field):
|
|
856
|
+
work_unit.metadata[field] = getattr(output, field)
|
|
857
|
+
if hasattr(output, "text"):
|
|
858
|
+
work_unit.metadata["model_response"] = output.text
|
|
859
|
+
if hasattr(output, "model_id"):
|
|
860
|
+
work_unit.metadata["model_id"] = output.model_id
|
|
861
|
+
if hasattr(output, "metadata"):
|
|
862
|
+
output_metadata = getattr(output, "metadata")
|
|
863
|
+
if isinstance(output_metadata, dict):
|
|
864
|
+
work_unit.metadata["model_response_metadata"] = dict(output_metadata)
|
|
865
|
+
store.save(work_unit)
|
|
866
|
+
return result
|
|
867
|
+
except Exception as exc:
|
|
868
|
+
work_unit.metadata["error"] = str(exc)
|
|
869
|
+
store.save(work_unit)
|
|
870
|
+
raise
|
|
871
|
+
|
|
872
|
+
def run_model(
|
|
873
|
+
self,
|
|
874
|
+
work_unit: WorkUnit,
|
|
875
|
+
agent: AgentContract,
|
|
876
|
+
models: list[ModelSpec],
|
|
877
|
+
adapters: dict[str, object],
|
|
878
|
+
preferred_model_ids: list[str] | None = None,
|
|
879
|
+
system_prompt: str | None = None,
|
|
880
|
+
verifier: ResultVerifier | None = None,
|
|
881
|
+
reviewer: ResultReviewer | None = None,
|
|
882
|
+
) -> OrchestrationResult:
|
|
883
|
+
"""Execute an agent through a provider-neutral model adapter."""
|
|
884
|
+
from runtime.agent.model import ModelAgentExecutor
|
|
885
|
+
|
|
886
|
+
return self.run(
|
|
887
|
+
work_unit=work_unit,
|
|
888
|
+
agent=agent,
|
|
889
|
+
models=models,
|
|
890
|
+
executor=ModelAgentExecutor(
|
|
891
|
+
adapters=adapters,
|
|
892
|
+
models=models,
|
|
893
|
+
system_prompt=system_prompt,
|
|
894
|
+
),
|
|
895
|
+
preferred_model_ids=preferred_model_ids,
|
|
896
|
+
verifier=verifier,
|
|
897
|
+
reviewer=reviewer,
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
def run(
|
|
901
|
+
self,
|
|
902
|
+
work_unit: WorkUnit,
|
|
903
|
+
agent: AgentContract,
|
|
904
|
+
models: list[ModelSpec],
|
|
905
|
+
executor: AgentExecutor,
|
|
906
|
+
verifier: ResultVerifier | None = None,
|
|
907
|
+
reviewer: ResultReviewer | None = None,
|
|
908
|
+
preferred_model_ids: list[str] | None = None,
|
|
909
|
+
routing_strategy="pool",
|
|
910
|
+
project_root: Path | None = None,
|
|
911
|
+
) -> OrchestrationResult:
|
|
912
|
+
root = project_root or Path.cwd()
|
|
913
|
+
store = self.state_store(root)
|
|
914
|
+
checkpoint_sequence = 0
|
|
915
|
+
|
|
916
|
+
def checkpoint(unit: WorkUnit) -> None:
|
|
917
|
+
nonlocal checkpoint_sequence
|
|
918
|
+
checkpoint_sequence += 1
|
|
919
|
+
store.checkpoint(
|
|
920
|
+
unit,
|
|
921
|
+
workflow="orchestration",
|
|
922
|
+
stage=unit.status.value,
|
|
923
|
+
sequence=checkpoint_sequence,
|
|
924
|
+
next_action=(
|
|
925
|
+
"resume_execution"
|
|
926
|
+
if unit.status.value not in {"completed", "failed"}
|
|
927
|
+
else None
|
|
928
|
+
),
|
|
929
|
+
agent_ids=(agent.id,),
|
|
930
|
+
model_ids=tuple(model.id for model in models),
|
|
931
|
+
resumable=unit.status.value not in {"completed", "failed"},
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
return self.orchestrator.run(
|
|
935
|
+
work_unit=work_unit,
|
|
936
|
+
agent=agent,
|
|
937
|
+
models=models,
|
|
938
|
+
executor=executor,
|
|
939
|
+
preferred_model_ids=preferred_model_ids,
|
|
940
|
+
verifier=verifier,
|
|
941
|
+
reviewer=reviewer,
|
|
942
|
+
routing_strategy=routing_strategy,
|
|
943
|
+
checkpoint=checkpoint,
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
def load_checkpoint(self, work_unit_id: str, project_root: Path | None = None):
|
|
947
|
+
"""Load the durable checkpoint associated with a WorkUnit."""
|
|
948
|
+
root = project_root or Path.cwd()
|
|
949
|
+
return self.state_store(root).load_checkpoint(work_unit_id)
|
|
950
|
+
|
|
951
|
+
def artifact_store(self, project_root: Path):
|
|
952
|
+
"""Return persistent artifact metadata storage for a project."""
|
|
953
|
+
return ArtifactStore(project_root / ".multiagentos" / "artifacts")
|
|
954
|
+
|
|
955
|
+
def openai_chat_agent(self, model: str | None = None):
|
|
956
|
+
"""Create the optional OpenAI Chat Agent adapter."""
|
|
957
|
+
from integrations.openai.chat_agent import OpenAIChatAgentAdapter
|
|
958
|
+
|
|
959
|
+
return OpenAIChatAgentAdapter(model=model)
|
|
960
|
+
|
|
961
|
+
def chat_agent_registry(self):
|
|
962
|
+
"""Return the default provider-neutral Chat Agent registry."""
|
|
963
|
+
return default_chat_agents()
|
|
964
|
+
|
|
965
|
+
def project_chat_agent(self, project_root: Path):
|
|
966
|
+
"""Resolve the configured project Chat Agent from the provider-neutral registry."""
|
|
967
|
+
config = load_chat_config(project_root)
|
|
968
|
+
registry = self.chat_agent_registry()
|
|
969
|
+
agent = registry.get(config.agent_id)
|
|
970
|
+
return agent, config.model
|
|
971
|
+
|
|
972
|
+
def project_chat_adapter(self, project_root: Path):
|
|
973
|
+
"""Resolve the configured Chat Agent to its provider adapter."""
|
|
974
|
+
agent, model = self.project_chat_agent(project_root)
|
|
975
|
+
return agent, resolve_project_chat_adapter(agent, model=model)
|
|
976
|
+
|
|
977
|
+
def chat_agent_router(self) -> ChatAgentRouter:
|
|
978
|
+
"""Return the policy-neutral Chat Agent router."""
|
|
979
|
+
return ChatAgentRouter(self.chat_agent_registry())
|
|
980
|
+
|
|
981
|
+
def route_chat_agent(
|
|
982
|
+
self,
|
|
983
|
+
*,
|
|
984
|
+
preferred_agent_id: str | None = None,
|
|
985
|
+
fallback_agent_ids: tuple[str, ...] = (),
|
|
986
|
+
required_capabilities: frozenset[str] = frozenset(),
|
|
987
|
+
strategy: ChatAgentRoutingStrategy | str = ChatAgentRoutingStrategy.AUTO,
|
|
988
|
+
) -> ChatAgentAssignment:
|
|
989
|
+
return self.chat_agent_router().route(
|
|
990
|
+
preferred_agent_id=preferred_agent_id,
|
|
991
|
+
fallback_agent_ids=fallback_agent_ids,
|
|
992
|
+
required_capabilities=required_capabilities,
|
|
993
|
+
strategy=strategy,
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
def chat_agent_bridge(self) -> ChatAgentBridge:
|
|
997
|
+
"""Return the provider-neutral bridge for conversational AI agents."""
|
|
998
|
+
return ChatAgentBridge(self.chat_agent_registry())
|
|
999
|
+
|
|
1000
|
+
def chat_session_store(self, project_root: Path) -> ChatSessionStore:
|
|
1001
|
+
"""Return persistent Chat Agent session storage for a project."""
|
|
1002
|
+
return ChatSessionStore(project_root / ".multiagentos" / "sessions")
|
|
1003
|
+
|
|
1004
|
+
def create_chat_session(
|
|
1005
|
+
self,
|
|
1006
|
+
session_id: str,
|
|
1007
|
+
chat_agent_id: str = "chatgpt",
|
|
1008
|
+
work_unit_id: str | None = None,
|
|
1009
|
+
) -> ChatSession:
|
|
1010
|
+
return ChatSession(
|
|
1011
|
+
id=session_id,
|
|
1012
|
+
chat_agent_id=chat_agent_id,
|
|
1013
|
+
work_unit_id=work_unit_id,
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
def chat_request(
|
|
1017
|
+
self,
|
|
1018
|
+
request: ChatAgentRequest,
|
|
1019
|
+
adapter,
|
|
1020
|
+
agent_id: str | None = None,
|
|
1021
|
+
) -> tuple[object, object, ChatAgentResponse]:
|
|
1022
|
+
"""Translate a Chat Agent turn into a VYRELON WorkUnit and Plan."""
|
|
1023
|
+
return self.chat_agent_bridge().request(request, adapter, agent_id=agent_id)
|
|
1024
|
+
|
|
1025
|
+
def execute_chat_request(
|
|
1026
|
+
self,
|
|
1027
|
+
request: ChatAgentRequest,
|
|
1028
|
+
adapter,
|
|
1029
|
+
agent: AgentContract,
|
|
1030
|
+
models: list[ModelSpec],
|
|
1031
|
+
executor: AgentExecutor,
|
|
1032
|
+
*,
|
|
1033
|
+
chat_agent_id: str | None = None,
|
|
1034
|
+
preferred_model_ids: list[str] | None = None,
|
|
1035
|
+
verifier: ResultVerifier | None = None,
|
|
1036
|
+
reviewer: ResultReviewer | None = None,
|
|
1037
|
+
routing_strategy="pool",
|
|
1038
|
+
session: ChatSession | None = None,
|
|
1039
|
+
project_root: Path | None = None,
|
|
1040
|
+
) -> ChatAgentExecutionResult:
|
|
1041
|
+
"""Execute a Chat Agent turn through the VYRELON lifecycle."""
|
|
1042
|
+
root = project_root or Path.cwd()
|
|
1043
|
+
return self.chat_agent_bridge().execute(
|
|
1044
|
+
request=request,
|
|
1045
|
+
adapter=adapter,
|
|
1046
|
+
agent=agent,
|
|
1047
|
+
models=models,
|
|
1048
|
+
executor=executor,
|
|
1049
|
+
chat_agent_id=chat_agent_id,
|
|
1050
|
+
preferred_model_ids=preferred_model_ids,
|
|
1051
|
+
verifier=verifier,
|
|
1052
|
+
reviewer=reviewer,
|
|
1053
|
+
routing_strategy=routing_strategy,
|
|
1054
|
+
state_store=self.state_store(root),
|
|
1055
|
+
session_store=self.chat_session_store(root),
|
|
1056
|
+
session=session,
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
def resume_chat_request(
|
|
1060
|
+
self,
|
|
1061
|
+
work_unit_id: str,
|
|
1062
|
+
agent: AgentContract,
|
|
1063
|
+
models: list[ModelSpec],
|
|
1064
|
+
executor: AgentExecutor,
|
|
1065
|
+
*,
|
|
1066
|
+
verifier: ResultVerifier | None = None,
|
|
1067
|
+
reviewer: ResultReviewer | None = None,
|
|
1068
|
+
preferred_model_ids: list[str] | None = None,
|
|
1069
|
+
routing_strategy="pool",
|
|
1070
|
+
project_root: Path | None = None,
|
|
1071
|
+
) -> OrchestrationResult:
|
|
1072
|
+
"""Resume an interrupted Chat Agent WorkUnit from persisted state."""
|
|
1073
|
+
root = project_root or Path.cwd()
|
|
1074
|
+
return self.chat_agent_bridge().resume(
|
|
1075
|
+
work_unit_id,
|
|
1076
|
+
state_store=self.state_store(root),
|
|
1077
|
+
agent=agent,
|
|
1078
|
+
models=models,
|
|
1079
|
+
executor=executor,
|
|
1080
|
+
verifier=verifier,
|
|
1081
|
+
reviewer=reviewer,
|
|
1082
|
+
preferred_model_ids=preferred_model_ids,
|
|
1083
|
+
routing_strategy=routing_strategy,
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
def resume_workflow(
|
|
1087
|
+
self,
|
|
1088
|
+
work_unit_id: str,
|
|
1089
|
+
agent: AgentContract,
|
|
1090
|
+
models: list[ModelSpec],
|
|
1091
|
+
executor: AgentExecutor,
|
|
1092
|
+
*,
|
|
1093
|
+
verifier: ResultVerifier | None = None,
|
|
1094
|
+
reviewer: ResultReviewer | None = None,
|
|
1095
|
+
preferred_model_ids: list[str] | None = None,
|
|
1096
|
+
routing_strategy="pool",
|
|
1097
|
+
project_root: Path | None = None,
|
|
1098
|
+
) -> OrchestrationResult:
|
|
1099
|
+
"""Reload a durable checkpoint and resume a generic VYRELON workflow."""
|
|
1100
|
+
root = project_root or Path.cwd()
|
|
1101
|
+
store = self.state_store(root)
|
|
1102
|
+
checkpoint = store.load_checkpoint(work_unit_id)
|
|
1103
|
+
if not checkpoint.resumable:
|
|
1104
|
+
raise ValueError(f"work unit checkpoint is not resumable: {work_unit_id}")
|
|
1105
|
+
if checkpoint.workflow != "orchestration":
|
|
1106
|
+
raise ValueError(
|
|
1107
|
+
f"checkpoint belongs to workflow {checkpoint.workflow!r}, not orchestration"
|
|
1108
|
+
)
|
|
1109
|
+
if checkpoint.agent_ids and agent.id not in checkpoint.agent_ids:
|
|
1110
|
+
raise ValueError("resume agent does not match checkpoint context")
|
|
1111
|
+
available_models = {model.id for model in models}
|
|
1112
|
+
if checkpoint.model_ids and not set(checkpoint.model_ids).issubset(available_models):
|
|
1113
|
+
raise ValueError("resume models do not match checkpoint context")
|
|
1114
|
+
work_unit = store.load(work_unit_id)
|
|
1115
|
+
if work_unit.status in {WorkStatus.COMPLETED, WorkStatus.FAILED}:
|
|
1116
|
+
raise ValueError(f"work unit is terminal and cannot be resumed: {work_unit_id}")
|
|
1117
|
+
return self.run(
|
|
1118
|
+
work_unit,
|
|
1119
|
+
agent,
|
|
1120
|
+
models,
|
|
1121
|
+
executor,
|
|
1122
|
+
verifier=verifier,
|
|
1123
|
+
reviewer=reviewer,
|
|
1124
|
+
preferred_model_ids=preferred_model_ids,
|
|
1125
|
+
routing_strategy=routing_strategy,
|
|
1126
|
+
project_root=root,
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
def multi_agent_workflow(self) -> MultiAgentWorkflow:
|
|
1130
|
+
"""Return the VYRELON-controlled multi-agent handoff workflow."""
|
|
1131
|
+
return MultiAgentWorkflow()
|
|
1132
|
+
|
|
1133
|
+
def run_multi_agent_workflow(
|
|
1134
|
+
self,
|
|
1135
|
+
*,
|
|
1136
|
+
work_unit: WorkUnit,
|
|
1137
|
+
stages: list[AgentContract],
|
|
1138
|
+
models: list[ModelSpec],
|
|
1139
|
+
executor: AgentExecutor,
|
|
1140
|
+
verifier: ResultVerifier | None = None,
|
|
1141
|
+
reviewers=None,
|
|
1142
|
+
reviewer_runner=None,
|
|
1143
|
+
preferred_model_ids: list[str] | None = None,
|
|
1144
|
+
routing_strategy="pool",
|
|
1145
|
+
project_root: Path | None = None,
|
|
1146
|
+
start_stage_index: int = 0,
|
|
1147
|
+
resume_action: str | None = None,
|
|
1148
|
+
resume_output: object = None,
|
|
1149
|
+
) -> MultiAgentWorkflowResult:
|
|
1150
|
+
"""Run a WorkUnit through multiple agents without transferring authority."""
|
|
1151
|
+
root = project_root or Path.cwd()
|
|
1152
|
+
return self.multi_agent_workflow().run(
|
|
1153
|
+
work_unit=work_unit,
|
|
1154
|
+
stages=stages,
|
|
1155
|
+
models=models,
|
|
1156
|
+
executor=executor,
|
|
1157
|
+
verifier=verifier,
|
|
1158
|
+
reviewers=reviewers,
|
|
1159
|
+
reviewer_runner=reviewer_runner,
|
|
1160
|
+
preferred_model_ids=preferred_model_ids,
|
|
1161
|
+
routing_strategy=routing_strategy,
|
|
1162
|
+
artifact_store=self.artifact_store(root),
|
|
1163
|
+
checkpoint=lambda unit, **kwargs: self.state_store(root).checkpoint(
|
|
1164
|
+
unit,
|
|
1165
|
+
workflow="multi_agent",
|
|
1166
|
+
metadata={
|
|
1167
|
+
"next_stage_index": kwargs.get("sequence", 0),
|
|
1168
|
+
"resume_output": unit.metadata.get("checkpoint_output"),
|
|
1169
|
+
},
|
|
1170
|
+
**kwargs,
|
|
1171
|
+
),
|
|
1172
|
+
start_stage_index=start_stage_index,
|
|
1173
|
+
resume_action=resume_action,
|
|
1174
|
+
resume_output=resume_output,
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
def resume_multi_agent_workflow(
|
|
1178
|
+
self,
|
|
1179
|
+
work_unit_id: str,
|
|
1180
|
+
*,
|
|
1181
|
+
stages: list[AgentContract],
|
|
1182
|
+
models: list[ModelSpec],
|
|
1183
|
+
executor: AgentExecutor,
|
|
1184
|
+
verifier: ResultVerifier | None = None,
|
|
1185
|
+
reviewers=None,
|
|
1186
|
+
reviewer_runner=None,
|
|
1187
|
+
preferred_model_ids: list[str] | None = None,
|
|
1188
|
+
routing_strategy="pool",
|
|
1189
|
+
project_root: Path | None = None,
|
|
1190
|
+
) -> MultiAgentWorkflowResult:
|
|
1191
|
+
"""Reload a multi-agent checkpoint and continue from its next stage."""
|
|
1192
|
+
root = project_root or Path.cwd()
|
|
1193
|
+
store = self.state_store(root)
|
|
1194
|
+
checkpoint = store.load_checkpoint(work_unit_id)
|
|
1195
|
+
if not checkpoint.resumable:
|
|
1196
|
+
raise ValueError("multi-agent checkpoint is not resumable")
|
|
1197
|
+
if checkpoint.workflow != "multi_agent":
|
|
1198
|
+
raise ValueError("checkpoint does not belong to multi-agent workflow")
|
|
1199
|
+
available_models = {model.id for model in models}
|
|
1200
|
+
if checkpoint.model_ids and not set(checkpoint.model_ids).issubset(available_models):
|
|
1201
|
+
raise ValueError("resume models do not match checkpoint context")
|
|
1202
|
+
if checkpoint.agent_ids and tuple(agent.id for agent in stages) != checkpoint.agent_ids:
|
|
1203
|
+
raise ValueError("resume agents do not match checkpoint context")
|
|
1204
|
+
work_unit = store.load(work_unit_id)
|
|
1205
|
+
if work_unit.status in {WorkStatus.COMPLETED, WorkStatus.FAILED}:
|
|
1206
|
+
raise ValueError("work unit is terminal and cannot be resumed")
|
|
1207
|
+
next_stage = int(checkpoint.metadata.get("next_stage_index", 0))
|
|
1208
|
+
if checkpoint.next_action == "execute_stage":
|
|
1209
|
+
return self.run_multi_agent_workflow(
|
|
1210
|
+
work_unit=work_unit,
|
|
1211
|
+
stages=stages,
|
|
1212
|
+
models=models,
|
|
1213
|
+
executor=executor,
|
|
1214
|
+
verifier=verifier,
|
|
1215
|
+
reviewers=reviewers,
|
|
1216
|
+
reviewer_runner=reviewer_runner,
|
|
1217
|
+
preferred_model_ids=preferred_model_ids,
|
|
1218
|
+
routing_strategy=routing_strategy,
|
|
1219
|
+
project_root=root,
|
|
1220
|
+
start_stage_index=next_stage,
|
|
1221
|
+
)
|
|
1222
|
+
if checkpoint.next_action in {"verify", "review"}:
|
|
1223
|
+
if next_stage != len(stages):
|
|
1224
|
+
raise ValueError("verify/review checkpoint does not follow completed stages")
|
|
1225
|
+
return self.run_multi_agent_workflow(
|
|
1226
|
+
work_unit=work_unit,
|
|
1227
|
+
stages=stages,
|
|
1228
|
+
models=models,
|
|
1229
|
+
executor=executor,
|
|
1230
|
+
verifier=verifier,
|
|
1231
|
+
reviewers=reviewers,
|
|
1232
|
+
reviewer_runner=reviewer_runner,
|
|
1233
|
+
preferred_model_ids=preferred_model_ids,
|
|
1234
|
+
routing_strategy=routing_strategy,
|
|
1235
|
+
project_root=root,
|
|
1236
|
+
start_stage_index=len(stages),
|
|
1237
|
+
resume_action=checkpoint.next_action,
|
|
1238
|
+
resume_output=checkpoint.metadata.get("resume_output"),
|
|
1239
|
+
)
|
|
1240
|
+
raise ValueError(f"unsupported multi-agent checkpoint action: {checkpoint.next_action!r}")
|
|
1241
|
+
|
|
1242
|
+
def run_debug_retry_workflow(
|
|
1243
|
+
self,
|
|
1244
|
+
*,
|
|
1245
|
+
work_unit: WorkUnit,
|
|
1246
|
+
developer: AgentContract,
|
|
1247
|
+
tester: AgentContract,
|
|
1248
|
+
debugger: AgentContract,
|
|
1249
|
+
models: list[ModelSpec],
|
|
1250
|
+
executor: AgentExecutor,
|
|
1251
|
+
verifier: ResultVerifier,
|
|
1252
|
+
max_retries: int = 2,
|
|
1253
|
+
project_root: Path | None = None,
|
|
1254
|
+
preferred_model_ids: list[str] | None = None,
|
|
1255
|
+
routing_strategy="pool",
|
|
1256
|
+
) -> MultiAgentWorkflowResult:
|
|
1257
|
+
root = project_root or Path.cwd()
|
|
1258
|
+
return self.multi_agent_workflow().run_with_debug_retry(
|
|
1259
|
+
work_unit=work_unit,
|
|
1260
|
+
developer=developer,
|
|
1261
|
+
tester=tester,
|
|
1262
|
+
debugger=debugger,
|
|
1263
|
+
models=models,
|
|
1264
|
+
executor=executor,
|
|
1265
|
+
verifier=verifier,
|
|
1266
|
+
max_retries=max_retries,
|
|
1267
|
+
preferred_model_ids=preferred_model_ids,
|
|
1268
|
+
routing_strategy=routing_strategy,
|
|
1269
|
+
artifact_store=self.artifact_store(root),
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
def run_review_rework_workflow(
|
|
1273
|
+
self,
|
|
1274
|
+
*,
|
|
1275
|
+
work_unit: WorkUnit,
|
|
1276
|
+
developer: AgentContract,
|
|
1277
|
+
tester: AgentContract,
|
|
1278
|
+
reviewers,
|
|
1279
|
+
models: list[ModelSpec],
|
|
1280
|
+
executor: AgentExecutor,
|
|
1281
|
+
reviewer_runner,
|
|
1282
|
+
verifier: ResultVerifier | None = None,
|
|
1283
|
+
max_review_cycles: int = 2,
|
|
1284
|
+
project_root: Path | None = None,
|
|
1285
|
+
preferred_model_ids: list[str] | None = None,
|
|
1286
|
+
routing_strategy="pool",
|
|
1287
|
+
start_cycle: int = 0,
|
|
1288
|
+
resume_action: str | None = None,
|
|
1289
|
+
resume_output: object = None,
|
|
1290
|
+
) -> MultiAgentWorkflowResult:
|
|
1291
|
+
"""Run bounded Review -> Rework -> Review under VYRELON authority."""
|
|
1292
|
+
root = project_root or Path.cwd()
|
|
1293
|
+
result = self.multi_agent_workflow().run_with_review_rework(
|
|
1294
|
+
work_unit=work_unit,
|
|
1295
|
+
developer=developer,
|
|
1296
|
+
tester=tester,
|
|
1297
|
+
reviewers=reviewers,
|
|
1298
|
+
models=models,
|
|
1299
|
+
executor=executor,
|
|
1300
|
+
reviewer_runner=reviewer_runner,
|
|
1301
|
+
verifier=verifier,
|
|
1302
|
+
max_review_cycles=max_review_cycles,
|
|
1303
|
+
preferred_model_ids=preferred_model_ids,
|
|
1304
|
+
routing_strategy=routing_strategy,
|
|
1305
|
+
artifact_store=self.artifact_store(root),
|
|
1306
|
+
start_cycle=start_cycle,
|
|
1307
|
+
resume_action=resume_action,
|
|
1308
|
+
resume_output=resume_output,
|
|
1309
|
+
checkpoint=lambda unit, **kwargs: self.state_store(root).checkpoint(
|
|
1310
|
+
unit,
|
|
1311
|
+
workflow="review_rework",
|
|
1312
|
+
metadata={
|
|
1313
|
+
"review_cycle": unit.metadata.get("review_cycle", kwargs.get("sequence", 0)),
|
|
1314
|
+
"resume_output": unit.metadata.get("checkpoint_output"),
|
|
1315
|
+
},
|
|
1316
|
+
**kwargs,
|
|
1317
|
+
),
|
|
1318
|
+
)
|
|
1319
|
+
self.state_store(root).save(result.work_unit)
|
|
1320
|
+
return result
|
|
1321
|
+
|
|
1322
|
+
def resume_review_rework_workflow(
|
|
1323
|
+
self,
|
|
1324
|
+
work_unit_id: str,
|
|
1325
|
+
*,
|
|
1326
|
+
developer: AgentContract,
|
|
1327
|
+
tester: AgentContract,
|
|
1328
|
+
reviewers,
|
|
1329
|
+
models: list[ModelSpec],
|
|
1330
|
+
executor: AgentExecutor,
|
|
1331
|
+
reviewer_runner,
|
|
1332
|
+
verifier: ResultVerifier | None = None,
|
|
1333
|
+
max_review_cycles: int = 2,
|
|
1334
|
+
project_root: Path | None = None,
|
|
1335
|
+
preferred_model_ids: list[str] | None = None,
|
|
1336
|
+
routing_strategy="pool",
|
|
1337
|
+
) -> MultiAgentWorkflowResult:
|
|
1338
|
+
"""Resume a review/rework workflow from verify, review, or rework boundary."""
|
|
1339
|
+
root = project_root or Path.cwd()
|
|
1340
|
+
store = self.state_store(root)
|
|
1341
|
+
checkpoint = store.load_checkpoint(work_unit_id)
|
|
1342
|
+
if not checkpoint.resumable:
|
|
1343
|
+
raise ValueError("review-rework checkpoint is not resumable")
|
|
1344
|
+
if checkpoint.workflow != "review_rework":
|
|
1345
|
+
raise ValueError("checkpoint does not belong to review-rework workflow")
|
|
1346
|
+
work_unit = store.load(work_unit_id)
|
|
1347
|
+
if work_unit.status in {WorkStatus.COMPLETED, WorkStatus.FAILED}:
|
|
1348
|
+
raise ValueError("work unit is terminal and cannot be resumed")
|
|
1349
|
+
raw_context = work_unit.metadata.get("resume_context")
|
|
1350
|
+
if not isinstance(raw_context, dict):
|
|
1351
|
+
raise ValueError("work unit has no resumable workflow context")
|
|
1352
|
+
context = WorkflowResumeContext.from_metadata(raw_context)
|
|
1353
|
+
if context.workflow != "review_rework" or context.work_unit_id != work_unit.id:
|
|
1354
|
+
raise ValueError("work unit has incompatible resume context")
|
|
1355
|
+
if context.max_review_cycles != max_review_cycles:
|
|
1356
|
+
raise ValueError("resume max_review_cycles does not match persisted workflow context")
|
|
1357
|
+
if context.developer_id != developer.id or context.tester_id != tester.id:
|
|
1358
|
+
raise ValueError("resume agents do not match persisted workflow context")
|
|
1359
|
+
reviewer_ids = tuple(agent.id for agent, _ in reviewers)
|
|
1360
|
+
if reviewer_ids != context.reviewer_ids:
|
|
1361
|
+
raise ValueError("resume reviewers do not match persisted workflow context")
|
|
1362
|
+
available_models = {model.id for model in models}
|
|
1363
|
+
if checkpoint.agent_ids and checkpoint.agent_ids != (developer.id, tester.id, *reviewer_ids):
|
|
1364
|
+
raise ValueError("resume agents do not match checkpoint context")
|
|
1365
|
+
if checkpoint.model_ids and not set(checkpoint.model_ids).issubset(available_models):
|
|
1366
|
+
raise ValueError("resume models do not match checkpoint context")
|
|
1367
|
+
if context.model_ids and not set(context.model_ids).issubset(available_models):
|
|
1368
|
+
raise ValueError("resume models do not match persisted workflow context")
|
|
1369
|
+
action = checkpoint.next_action
|
|
1370
|
+
if action not in {"verify", "review", "rework"}:
|
|
1371
|
+
raise ValueError(f"unsupported review-rework checkpoint action: {action!r}")
|
|
1372
|
+
cycle = max(0, int(checkpoint.metadata.get("review_cycle", context.review_cycle)) - 1)
|
|
1373
|
+
return self.run_review_rework_workflow(
|
|
1374
|
+
work_unit=work_unit,
|
|
1375
|
+
developer=developer,
|
|
1376
|
+
tester=tester,
|
|
1377
|
+
reviewers=reviewers,
|
|
1378
|
+
models=models,
|
|
1379
|
+
executor=executor,
|
|
1380
|
+
reviewer_runner=reviewer_runner,
|
|
1381
|
+
verifier=verifier,
|
|
1382
|
+
max_review_cycles=max_review_cycles,
|
|
1383
|
+
project_root=root,
|
|
1384
|
+
preferred_model_ids=preferred_model_ids,
|
|
1385
|
+
routing_strategy=routing_strategy,
|
|
1386
|
+
start_cycle=cycle,
|
|
1387
|
+
resume_action=action if action in {"verify", "review"} else None,
|
|
1388
|
+
resume_output=checkpoint.metadata.get("resume_output"),
|
|
1389
|
+
)
|
|
1390
|
+
|
|
1391
|
+
def resolve_human_review(
|
|
1392
|
+
self,
|
|
1393
|
+
*,
|
|
1394
|
+
work_unit: WorkUnit,
|
|
1395
|
+
decision: HumanReviewDecision | str | None = None,
|
|
1396
|
+
approved: bool | None = None,
|
|
1397
|
+
notes: str = "",
|
|
1398
|
+
project_root: Path | None = None,
|
|
1399
|
+
) -> MultiAgentWorkflowResult:
|
|
1400
|
+
"""Resolve a persisted human gate and save the resulting WorkUnit."""
|
|
1401
|
+
root = project_root or Path.cwd()
|
|
1402
|
+
if decision is None:
|
|
1403
|
+
if approved is None:
|
|
1404
|
+
raise ValueError("decision or approved must be provided")
|
|
1405
|
+
decision = (
|
|
1406
|
+
HumanReviewDecision.APPROVE_COMPLETION
|
|
1407
|
+
if approved
|
|
1408
|
+
else HumanReviewDecision.REJECT
|
|
1409
|
+
)
|
|
1410
|
+
decision = HumanReviewDecision.coerce(decision)
|
|
1411
|
+
if decision is HumanReviewDecision.APPROVE_REWORK:
|
|
1412
|
+
raise ValueError("APPROVE_REWORK requires resume_human_review_rework()")
|
|
1413
|
+
result = self.multi_agent_workflow().resolve_human_review(
|
|
1414
|
+
work_unit=work_unit,
|
|
1415
|
+
approved=decision is HumanReviewDecision.APPROVE_COMPLETION,
|
|
1416
|
+
notes=notes,
|
|
1417
|
+
)
|
|
1418
|
+
work_unit.metadata["human_review_decision"] = decision.value
|
|
1419
|
+
self.state_store(root).checkpoint(
|
|
1420
|
+
result.work_unit,
|
|
1421
|
+
workflow="review_rework",
|
|
1422
|
+
stage=result.work_unit.status.value,
|
|
1423
|
+
sequence=int(result.work_unit.metadata.get("review_cycle_count", 0)),
|
|
1424
|
+
next_action=None,
|
|
1425
|
+
agent_ids=tuple(result.work_unit.metadata.get("execution_agent_ids", ())),
|
|
1426
|
+
resumable=False,
|
|
1427
|
+
metadata={"human_decision": decision.value},
|
|
1428
|
+
)
|
|
1429
|
+
return result
|
|
1430
|
+
|
|
1431
|
+
def resolve_persisted_human_review(
|
|
1432
|
+
self,
|
|
1433
|
+
work_unit_id: str,
|
|
1434
|
+
*,
|
|
1435
|
+
decision: HumanReviewDecision | str,
|
|
1436
|
+
notes: str = "",
|
|
1437
|
+
project_root: Path | None = None,
|
|
1438
|
+
) -> MultiAgentWorkflowResult:
|
|
1439
|
+
"""Load a waiting human gate, resolve it, and persist the decision."""
|
|
1440
|
+
root = project_root or Path.cwd()
|
|
1441
|
+
work_unit = self.state_store(root).load(work_unit_id)
|
|
1442
|
+
if work_unit.status is not WorkStatus.WAITING_HUMAN_APPROVAL:
|
|
1443
|
+
raise ValueError("work unit is not waiting for human approval")
|
|
1444
|
+
raw_context = work_unit.metadata.get("resume_context")
|
|
1445
|
+
if not isinstance(raw_context, dict):
|
|
1446
|
+
raise ValueError("work unit has no resumable workflow context")
|
|
1447
|
+
context = WorkflowResumeContext.from_metadata(raw_context)
|
|
1448
|
+
if context.workflow != "review_rework" or context.work_unit_id != work_unit.id:
|
|
1449
|
+
raise ValueError("work unit has incompatible resume context")
|
|
1450
|
+
return self.resolve_human_review(
|
|
1451
|
+
work_unit=work_unit,
|
|
1452
|
+
decision=decision,
|
|
1453
|
+
notes=notes,
|
|
1454
|
+
project_root=root,
|
|
1455
|
+
)
|
|
1456
|
+
|
|
1457
|
+
def resume_human_review_rework(
|
|
1458
|
+
self,
|
|
1459
|
+
work_unit_id: str,
|
|
1460
|
+
*,
|
|
1461
|
+
developer: AgentContract,
|
|
1462
|
+
tester: AgentContract,
|
|
1463
|
+
reviewers,
|
|
1464
|
+
models: list[ModelSpec],
|
|
1465
|
+
executor: AgentExecutor,
|
|
1466
|
+
reviewer_runner,
|
|
1467
|
+
verifier: ResultVerifier | None = None,
|
|
1468
|
+
max_review_cycles: int = 1,
|
|
1469
|
+
project_root: Path | None = None,
|
|
1470
|
+
preferred_model_ids: list[str] | None = None,
|
|
1471
|
+
routing_strategy="pool",
|
|
1472
|
+
notes: str = "",
|
|
1473
|
+
) -> MultiAgentWorkflowResult:
|
|
1474
|
+
"""Load a human-approved gate and authorize a fresh bounded rework cycle."""
|
|
1475
|
+
root = project_root or Path.cwd()
|
|
1476
|
+
work_unit = self.state_store(root).load(work_unit_id)
|
|
1477
|
+
if work_unit.status is not WorkStatus.WAITING_HUMAN_APPROVAL:
|
|
1478
|
+
raise ValueError("work unit is not waiting for human approval")
|
|
1479
|
+
raw_context = work_unit.metadata.get("resume_context")
|
|
1480
|
+
if not isinstance(raw_context, dict):
|
|
1481
|
+
raise ValueError("work unit has no resumable workflow context")
|
|
1482
|
+
context = WorkflowResumeContext.from_metadata(raw_context)
|
|
1483
|
+
if context.workflow != "review_rework" or context.work_unit_id != work_unit.id:
|
|
1484
|
+
raise ValueError("work unit has incompatible resume context")
|
|
1485
|
+
if context.developer_id != developer.id or context.tester_id != tester.id:
|
|
1486
|
+
raise ValueError("resume agents do not match persisted workflow context")
|
|
1487
|
+
reviewer_ids = tuple(agent.id for agent, _ in reviewers)
|
|
1488
|
+
if reviewer_ids != context.reviewer_ids:
|
|
1489
|
+
raise ValueError("resume reviewers do not match persisted workflow context")
|
|
1490
|
+
available_models = {model.id for model in models}
|
|
1491
|
+
if context.model_ids and not set(context.model_ids).issubset(available_models):
|
|
1492
|
+
raise ValueError("resume models do not match persisted workflow context")
|
|
1493
|
+
work_unit.metadata["human_review_required"] = False
|
|
1494
|
+
work_unit.metadata["human_review_decision"] = HumanReviewDecision.APPROVE_REWORK.value
|
|
1495
|
+
if notes.strip():
|
|
1496
|
+
work_unit.metadata["human_review_notes"] = notes
|
|
1497
|
+
work_unit.metadata["rework_required"] = True
|
|
1498
|
+
work_unit.transition(WorkStatus.EXECUTING)
|
|
1499
|
+
self.state_store(root).checkpoint(
|
|
1500
|
+
work_unit,
|
|
1501
|
+
workflow="review_rework",
|
|
1502
|
+
stage=WorkStatus.EXECUTING.value,
|
|
1503
|
+
sequence=int(context.review_cycle),
|
|
1504
|
+
next_action="review_rework",
|
|
1505
|
+
agent_ids=(developer.id, tester.id, *reviewer_ids),
|
|
1506
|
+
model_ids=tuple(model.id for model in models),
|
|
1507
|
+
resumable=True,
|
|
1508
|
+
metadata={"human_decision": HumanReviewDecision.APPROVE_REWORK.value},
|
|
1509
|
+
)
|
|
1510
|
+
result = self.multi_agent_workflow().run_with_review_rework(
|
|
1511
|
+
work_unit=work_unit,
|
|
1512
|
+
developer=developer,
|
|
1513
|
+
tester=tester,
|
|
1514
|
+
reviewers=reviewers,
|
|
1515
|
+
models=models,
|
|
1516
|
+
executor=executor,
|
|
1517
|
+
reviewer_runner=reviewer_runner,
|
|
1518
|
+
verifier=verifier,
|
|
1519
|
+
max_review_cycles=max_review_cycles,
|
|
1520
|
+
preferred_model_ids=preferred_model_ids,
|
|
1521
|
+
routing_strategy=routing_strategy,
|
|
1522
|
+
artifact_store=self.artifact_store(root),
|
|
1523
|
+
checkpoint=lambda unit, **kwargs: self.state_store(root).checkpoint(
|
|
1524
|
+
unit,
|
|
1525
|
+
workflow="review_rework",
|
|
1526
|
+
metadata={
|
|
1527
|
+
"review_cycle": unit.metadata.get("review_cycle", kwargs.get("sequence", 0)),
|
|
1528
|
+
"resume_output": unit.metadata.get("checkpoint_output"),
|
|
1529
|
+
},
|
|
1530
|
+
**kwargs,
|
|
1531
|
+
),
|
|
1532
|
+
)
|
|
1533
|
+
self.state_store(root).save(result.work_unit)
|
|
1534
|
+
return result
|
|
1535
|
+
|
|
1536
|
+
def review_panel(
|
|
1537
|
+
self,
|
|
1538
|
+
work_unit_id: str,
|
|
1539
|
+
reviewers: list[tuple[AgentContract, object]],
|
|
1540
|
+
reviewer_runner,
|
|
1541
|
+
) -> ReviewPanelResult:
|
|
1542
|
+
return ReviewPanel().review(work_unit_id, reviewers, reviewer_runner)
|
|
1543
|
+
|
|
1544
|
+
def mcp_authorizer(self, project_root: Path):
|
|
1545
|
+
"""Build the project-scoped MCP tool authorizer."""
|
|
1546
|
+
from runtime.mcp.policy import MCPToolAuthorizer, MCPToolProfileLoader
|
|
1547
|
+
return MCPToolAuthorizer(MCPToolProfileLoader().load(project_root))
|
|
1548
|
+
|
|
1549
|
+
def mcp_tools(self, project_root: Path, agent: AgentContract | None = None, profile_id: str | None = None):
|
|
1550
|
+
"""List connected MCP tools, optionally filtered by agent/profile policy."""
|
|
1551
|
+
from runtime.mcp.proxy import MCPToolProxy
|
|
1552
|
+
return MCPToolProxy(self.mcp_clients, self.mcp_authorizer(project_root)).list_tools(agent, profile_id)
|
|
1553
|
+
|
|
1554
|
+
def call_mcp_tool(self, project_root: Path, request, agent: AgentContract | None = None, profile_id: str | None = None):
|
|
1555
|
+
"""Invoke an MCP tool after applying the VYRELON agent/profile policy."""
|
|
1556
|
+
from runtime.mcp.proxy import MCPToolProxy
|
|
1557
|
+
return MCPToolProxy(self.mcp_clients, self.mcp_authorizer(project_root)).call(request, agent, profile_id)
|