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.
Files changed (122) hide show
  1. agents/__init__.py +1 -0
  2. agents/catalog.py +52 -0
  3. agents/registry.py +11 -0
  4. core/__init__.py +38 -0
  5. core/artifacts.py +47 -0
  6. core/chat_agent_bridge.py +280 -0
  7. core/chat_agent_registry.py +61 -0
  8. core/chat_agent_router.py +85 -0
  9. core/chat_session.py +62 -0
  10. core/contracts/__init__.py +22 -0
  11. core/contracts/agent.py +21 -0
  12. core/contracts/ai.py +25 -0
  13. core/contracts/capability.py +44 -0
  14. core/contracts/chat_agent.py +34 -0
  15. core/contracts/checkpoint.py +78 -0
  16. core/contracts/discovery.py +30 -0
  17. core/contracts/execution.py +28 -0
  18. core/contracts/github.py +74 -0
  19. core/contracts/handoff.py +56 -0
  20. core/contracts/health.py +206 -0
  21. core/contracts/human_review.py +20 -0
  22. core/contracts/ide.py +113 -0
  23. core/contracts/mcp.py +76 -0
  24. core/contracts/model_runtime.py +25 -0
  25. core/contracts/planning.py +25 -0
  26. core/contracts/profile.py +66 -0
  27. core/contracts/quota.py +62 -0
  28. core/contracts/repository.py +34 -0
  29. core/contracts/resume.py +55 -0
  30. core/contracts/runtime.py +19 -0
  31. core/contracts/vyrelon_runtime.py +118 -0
  32. core/contracts/work_unit.py +48 -0
  33. core/delegation.py +33 -0
  34. core/handoff.py +68 -0
  35. core/lifecycle.py +85 -0
  36. core/multi_agent_workflow.py +574 -0
  37. core/orchestrator.py +51 -0
  38. core/planning.py +22 -0
  39. core/registry.py +44 -0
  40. core/routing.py +195 -0
  41. core/state.py +155 -0
  42. installer/__init__.py +1 -0
  43. installer/init.py +71 -0
  44. integrations/__init__.py +0 -0
  45. integrations/github/__init__.py +3 -0
  46. integrations/github/gateway.py +164 -0
  47. integrations/openai/__init__.py +1 -0
  48. integrations/openai/chat_agent.py +147 -0
  49. multiagentos/__init__.py +1 -0
  50. multiagentos/cli.py +759 -0
  51. multiagentos-0.4.0.dist-info/METADATA +7 -0
  52. multiagentos-0.4.0.dist-info/RECORD +122 -0
  53. multiagentos-0.4.0.dist-info/WHEEL +5 -0
  54. multiagentos-0.4.0.dist-info/entry_points.txt +2 -0
  55. multiagentos-0.4.0.dist-info/top_level.txt +7 -0
  56. profiles/__init__.py +1 -0
  57. profiles/common.py +11 -0
  58. profiles/detector.py +67 -0
  59. profiles/registry.py +46 -0
  60. profiles/resolver.py +63 -0
  61. runtime/__init__.py +5 -0
  62. runtime/agent/__init__.py +1 -0
  63. runtime/agent/ide.py +74 -0
  64. runtime/agent/model.py +197 -0
  65. runtime/agent/model_backed.py +49 -0
  66. runtime/agent/process.py +21 -0
  67. runtime/builtin_tools.py +57 -0
  68. runtime/capability.py +143 -0
  69. runtime/chat_adapter_registry.py +43 -0
  70. runtime/chat_config.py +57 -0
  71. runtime/chat_execution.py +52 -0
  72. runtime/config/__init__.py +1 -0
  73. runtime/config/loader.py +67 -0
  74. runtime/discovery.py +334 -0
  75. runtime/execution_config.py +71 -0
  76. runtime/execution_registry.py +50 -0
  77. runtime/git.py +84 -0
  78. runtime/github.py +56 -0
  79. runtime/github_probe.py +23 -0
  80. runtime/health.py +14 -0
  81. runtime/ide/__init__.py +6 -0
  82. runtime/ide/bridge.py +198 -0
  83. runtime/ide/registry.py +24 -0
  84. runtime/ide/runtime.py +35 -0
  85. runtime/local/__init__.py +18 -0
  86. runtime/local/filesystem.py +58 -0
  87. runtime/local/patch.py +79 -0
  88. runtime/local/path_security.py +31 -0
  89. runtime/local/permissions.py +20 -0
  90. runtime/local/process.py +56 -0
  91. runtime/local/shell.py +97 -0
  92. runtime/mcp/__init__.py +7 -0
  93. runtime/mcp/client.py +120 -0
  94. runtime/mcp/config.py +36 -0
  95. runtime/mcp/policy.py +57 -0
  96. runtime/mcp/proxy.py +30 -0
  97. runtime/mcp/server.py +137 -0
  98. runtime/mcp/session.py +19 -0
  99. runtime/model/__init__.py +5 -0
  100. runtime/model/ai_runtime.py +165 -0
  101. runtime/model/cli.py +49 -0
  102. runtime/model/config.py +98 -0
  103. runtime/model/credentials.py +42 -0
  104. runtime/model/factory.py +82 -0
  105. runtime/model/http.py +78 -0
  106. runtime/model/invoker.py +16 -0
  107. runtime/model/materializer.py +101 -0
  108. runtime/model/native.py +244 -0
  109. runtime/model/providers.py +55 -0
  110. runtime/model/registry.py +26 -0
  111. runtime/model/runtime_loader.py +51 -0
  112. runtime/model_control.py +136 -0
  113. runtime/multi_agent.py +142 -0
  114. runtime/policy.py +28 -0
  115. runtime/process.py +41 -0
  116. runtime/quota.py +259 -0
  117. runtime/repository.py +103 -0
  118. runtime/repository_tools.py +62 -0
  119. runtime/status.py +87 -0
  120. runtime/tool_calling.py +134 -0
  121. runtime/validation.py +121 -0
  122. 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)