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
multiagentos/cli.py ADDED
@@ -0,0 +1,759 @@
1
+ """Command-line entry point for VYRELON project bootstrap and execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import uuid
8
+ from pathlib import Path
9
+
10
+ from core.contracts.agent import AgentContract
11
+ from core.contracts.ai import ModelSpec
12
+ from core.contracts.work_unit import WorkStatus, WorkUnit
13
+ from runtime.execution_registry import resolve_execution_contracts
14
+ from runtime.execution_config import load_execution_config
15
+ from core.chat_agent_bridge import ChatAgentRequest
16
+ from installer.init import ProjectInitializer
17
+ from profiles.detector import ProfileDetector
18
+ from runtime.github_probe import probe
19
+ from runtime.process import ProcessRuntime
20
+ from runtime.status import project_status
21
+ from runtime.vyrelon import VYRELONRuntime
22
+ from runtime.agent.process import ProcessAgentExecutor
23
+
24
+
25
+ def _execution_contracts(root: Path, agent_override: str | None, model_override: str | None):
26
+ config = load_execution_config(root)
27
+ if config.runtime != "process":
28
+ raise ValueError(f"unsupported CLI execution runtime: {config.runtime}")
29
+ agent_id = agent_override or config.agent_id
30
+ model_id = model_override or config.model_id
31
+ agent, model = resolve_execution_contracts(agent_id, model_id)
32
+ return config, agent, [model]
33
+
34
+ def _chat_request(root: Path, objective: str, session_id: str | None) -> ChatAgentRequest:
35
+ inputs: dict[str, object] = {}
36
+ if session_id:
37
+ store = VYRELONRuntime().chat_session_store(root)
38
+ try:
39
+ session = store.load(session_id)
40
+ except FileNotFoundError:
41
+ session = None
42
+ if session is not None:
43
+ inputs["conversation"] = tuple(
44
+ {"role": turn.get("role"), "content": turn.get("content")}
45
+ for turn in session.turns
46
+ )
47
+ return ChatAgentRequest(objective=objective, inputs=inputs)
48
+
49
+ def _run_chat(
50
+ root: Path,
51
+ objective: str,
52
+ agent_override: str | None,
53
+ model_override: str | None,
54
+ session_id: str | None,
55
+ execute: bool = False,
56
+ exec_command: list[str] | None = None,
57
+ ) -> int:
58
+ runtime = VYRELONRuntime()
59
+ configured_agent, configured_model = runtime.project_chat_agent(root)
60
+ agent_id = agent_override or configured_agent.id
61
+ agent = runtime.chat_agent_registry().get(agent_id)
62
+ model = model_override or (configured_model if agent_id == configured_agent.id else None)
63
+
64
+ if agent_id == configured_agent.id and model == configured_model:
65
+ _, adapter = runtime.project_chat_adapter(root)
66
+ else:
67
+ from runtime.chat_adapter_registry import resolve_project_chat_adapter
68
+ adapter = resolve_project_chat_adapter(agent, model=model)
69
+
70
+ session = None
71
+ if session_id:
72
+ store = runtime.chat_session_store(root)
73
+ try:
74
+ session = store.load(session_id)
75
+ except FileNotFoundError:
76
+ session = runtime.create_chat_session(session_id, chat_agent_id=agent.id)
77
+
78
+ request = _chat_request(root, objective, session_id)
79
+
80
+ if execute:
81
+ command = list(exec_command or [])
82
+ while command and command[0] == "--":
83
+ command.pop(0)
84
+ if not command:
85
+ raise SystemExit("chat --execute requires an explicit command after --")
86
+ config, execution_agent, models = _execution_contracts(root, None, model_override)
87
+
88
+ class CommandExecutor:
89
+ def execute(self, *, agent, model_id, work_unit):
90
+ result = ProcessRuntime(runtime.policy).run(
91
+ list(work_unit.inputs["command"]), cwd=str(root)
92
+ )
93
+ work_unit.metadata["process_returncode"] = result.returncode
94
+ work_unit.metadata["process_stdout"] = result.stdout
95
+ work_unit.metadata["process_stderr"] = result.stderr
96
+ if result.returncode != 0:
97
+ raise RuntimeError(f"command failed with exit code {result.returncode}")
98
+ return result
99
+
100
+ request = ChatAgentRequest(
101
+ objective=objective,
102
+ inputs={**(request.inputs or {}), "command": command},
103
+ work_unit_id=request.work_unit_id,
104
+ )
105
+ result = runtime.execute_chat_request(
106
+ request=request,
107
+ adapter=adapter,
108
+ agent=execution_agent,
109
+ models=models,
110
+ executor=CommandExecutor(),
111
+ chat_agent_id=agent.id,
112
+ preferred_model_ids=[models[0].id],
113
+ session=session,
114
+ project_root=root,
115
+ )
116
+ if session is not None:
117
+ session.add_turn("user", objective)
118
+ session.add_turn("assistant", result.chat_response.summary)
119
+ session.metadata["last_plan_steps"] = [step.id for step in result.plan.steps]
120
+ runtime.chat_session_store(root).save(session)
121
+ print(json.dumps({
122
+ "mode": "execute",
123
+ "chat_agent_id": agent.id,
124
+ "provider": agent.provider.value,
125
+ "summary": result.chat_response.summary,
126
+ "work_unit_id": result.work_unit.id,
127
+ "status": result.work_unit.status.value,
128
+ "execution_agent_id": execution_agent.id,
129
+ "execution_model_id": result.orchestration.delegation.assignment.model_id,
130
+ "execution_evidence": result.work_unit.metadata.get("execution_evidence", []),
131
+ "verification_evidence": result.work_unit.metadata.get("verification_evidence", []),
132
+ "review_evidence": result.work_unit.metadata.get("review_evidence", []),
133
+ "session_id": session.id if session is not None else None,
134
+ }, indent=2, ensure_ascii=False))
135
+ return 0
136
+
137
+ _, plan, response = runtime.chat_request(request, adapter, agent_id=agent.id)
138
+ if session is None and session_id:
139
+ session = runtime.create_chat_session(session_id, chat_agent_id=agent.id)
140
+ if session is not None:
141
+ session.add_turn("user", objective)
142
+ session.add_turn("assistant", response.summary)
143
+ session.metadata["last_plan_steps"] = [step.id for step in plan.steps]
144
+ runtime.chat_session_store(root).save(session)
145
+
146
+ print(json.dumps({
147
+ "mode": "chat",
148
+ "chat_agent_id": agent.id,
149
+ "provider": agent.provider.value,
150
+ "summary": response.summary,
151
+ "steps": [
152
+ {"id": step.id, "objective": step.objective, "agent_id": step.agent_id, "depends_on": list(step.depends_on)}
153
+ for step in plan.steps
154
+ ],
155
+ "findings": list(response.findings),
156
+ "artifacts": list(response.artifacts),
157
+ "evidence": list(response.evidence),
158
+ "session_id": session.id if session is not None else None,
159
+ }, indent=2, ensure_ascii=False))
160
+ return 0
161
+
162
+
163
+ def _work_state(root: Path):
164
+ from runtime.vyrelon import VYRELONRuntime
165
+
166
+ return VYRELONRuntime().state_store(root)
167
+
168
+
169
+ def _provider_runtime(root: Path):
170
+ from runtime.vyrelon import VYRELONRuntime
171
+
172
+ runtime = VYRELONRuntime()
173
+ runtime.load_project_provider_config(root)
174
+ return runtime
175
+
176
+
177
+ def _run_process(root: Path, work: WorkUnit, command: list[str]) -> int:
178
+ store = _work_state(root)
179
+ work.metadata["command"] = command
180
+ if work.status == WorkStatus.FAILED:
181
+ work.status = WorkStatus.EXECUTING
182
+ elif work.status != WorkStatus.EXECUTING:
183
+ work.transition(WorkStatus.EXECUTING)
184
+ store.save(work)
185
+
186
+ try:
187
+ result = ProcessRuntime().run(command, cwd=str(root))
188
+ work.metadata["returncode"] = result.returncode
189
+ work.metadata["stdout"] = result.stdout
190
+ work.metadata["stderr"] = result.stderr
191
+ if result.returncode == 0:
192
+ work.transition(WorkStatus.COMPLETED)
193
+ store.save(work)
194
+ print(result.stdout, end="")
195
+ return 0
196
+
197
+ work.transition(WorkStatus.FAILED)
198
+ store.save(work)
199
+ print(result.stdout, end="")
200
+ print(result.stderr, end="")
201
+ return result.returncode
202
+ except Exception as exc:
203
+ work.metadata["error"] = str(exc)
204
+ if work.status not in {WorkStatus.FAILED, WorkStatus.COMPLETED}:
205
+ work.transition(WorkStatus.FAILED)
206
+ store.save(work)
207
+ raise
208
+
209
+
210
+ def build_parser() -> argparse.ArgumentParser:
211
+ parser = argparse.ArgumentParser(prog="multiagentos")
212
+ subparsers = parser.add_subparsers(dest="command", required=True)
213
+
214
+ detect = subparsers.add_parser("detect", help="detect technology profiles")
215
+ detect.add_argument("path", nargs="?", default=".")
216
+
217
+ profile = subparsers.add_parser("profile", help="show resolved VYRELON project/agent profiles")
218
+ profile.add_argument("path", nargs="?", default=".")
219
+
220
+ init = subparsers.add_parser("init", help="initialize VYRELON in a project")
221
+ init.add_argument("path", nargs="?", default=".")
222
+ init.add_argument("--component", choices=("vyrelon", "multi-agent", "all"), default="all")
223
+
224
+ providers = subparsers.add_parser(
225
+ "providers", help="inspect configured AI providers and models"
226
+ )
227
+ provider_sub = providers.add_subparsers(dest="providers_command", required=True)
228
+ providers_list = provider_sub.add_parser("list", help="list configured providers/models")
229
+ providers_list.add_argument("path", nargs="?", default=".")
230
+ providers_validate = provider_sub.add_parser(
231
+ "validate", help="validate provider configuration and environment credentials"
232
+ )
233
+ providers_validate.add_argument("path", nargs="?", default=".")
234
+
235
+ models = subparsers.add_parser("models", help="execute configured AI models")
236
+ model_sub = models.add_subparsers(dest="models_command", required=True)
237
+ models_quota = model_sub.add_parser("quota", help="show observed and estimated model quota")
238
+ models_quota.add_argument("path", nargs="?", default=".")
239
+ models_health = model_sub.add_parser("health", help="show model health and cooldown state")
240
+ models_health.add_argument("path", nargs="?", default=".")
241
+ models_capabilities = model_sub.add_parser("capabilities", help="show normalized model capabilities")
242
+ models_capabilities.add_argument("path", nargs="?", default=".")
243
+ models_discover = model_sub.add_parser("discover", help="refresh provider model capabilities and quota")
244
+ models_discover.add_argument("path", nargs="?", default=".")
245
+ models_discover.add_argument("--no-refresh", action="store_true")
246
+ models_control = model_sub.add_parser("control", help="show unified model control-plane state")
247
+ models_control.add_argument("path", nargs="?", default=".")
248
+ models_control.add_argument("--events", action="store_true", help="include recent control-plane events")
249
+ models_explain = model_sub.add_parser("explain", help="explain model routing eligibility and selection")
250
+ models_explain.add_argument("path", nargs="?", default=".")
251
+ models_explain.add_argument("--agent", default="executor")
252
+ models_explain.add_argument("--model", action="append", dest="preferred_models", default=None)
253
+ models_explain.add_argument("--strategy", choices=("explicit", "pool", "auto"), default="pool")
254
+
255
+ models_run = model_sub.add_parser("run", help="run a configured model through VYRELON")
256
+ models_run.add_argument("path", nargs="?", default=".")
257
+ models_run.add_argument("--model", required=True)
258
+ models_run.add_argument("--objective", required=True)
259
+ models_run.add_argument("--agent", default="executor")
260
+ models_run.add_argument("--apply-changes", action="store_true")
261
+ models_run.add_argument("--validate", action="append", default=[])
262
+ models_run.add_argument("--mcp", action="append", default=[])
263
+
264
+ run = subparsers.add_parser("run", help="create and execute a persistent WorkUnit")
265
+ run.add_argument("path_arg", nargs="?", default=None)
266
+ run.add_argument("--path", dest="path", default=None)
267
+ run.add_argument("--objective", required=True)
268
+ run.add_argument("--agent", default="executor")
269
+ run.add_argument("--id", dest="work_unit_id")
270
+ run.add_argument("--model", help="pin a model; omit to route automatically")
271
+ run.add_argument("--apply-changes", action="store_true")
272
+ run.add_argument("--validate", action="append", default=[])
273
+ run.add_argument("--mcp", action="append", default=[])
274
+ run.add_argument("--command", dest="process_command", nargs=argparse.REMAINDER)
275
+
276
+ resume = subparsers.add_parser("resume", help="resume a persisted WorkUnit")
277
+ resume.add_argument("work_unit_id")
278
+ resume.add_argument("path_arg", nargs="?", default=None)
279
+ resume.add_argument("--path", dest="path", default=None)
280
+
281
+ status = subparsers.add_parser("status", help="show project state or a persisted WorkUnit")
282
+ status.add_argument("target", nargs="?", default=".")
283
+ status.add_argument("path", nargs="?", default=None)
284
+
285
+
286
+
287
+ chat = subparsers.add_parser("chat", help="send a request to the project's configured Chat Agent")
288
+ chat.add_argument("--path", default=".")
289
+ chat.add_argument("--objective", required=True, help="request for the Chat Agent")
290
+ chat.add_argument("--agent", help="override the configured Chat Agent id")
291
+ chat.add_argument("--model", help="override the configured Chat Agent model")
292
+ chat.add_argument("--session", help="persist conversation turns in this session id")
293
+ chat.add_argument("--execute", action="store_true", help="execute the explicit command after -- through VYRELON")
294
+ chat.add_argument("exec_command", nargs=argparse.REMAINDER, help="explicit command after -- when using --execute")
295
+
296
+ mcp = subparsers.add_parser("mcp", help="serve or inspect VYRELON MCP")
297
+ mcp_sub = mcp.add_subparsers(dest="mcp_command", required=True)
298
+ mcp_serve = mcp_sub.add_parser("serve", help="run VYRELON as a stdio MCP server")
299
+ mcp_serve.add_argument("--path", default=".")
300
+ mcp_serve.add_argument("--allow-write", action="store_true", help="expose filesystem write and patch tools")
301
+ mcp_serve.add_argument("--allow-process", action="store_true", help="expose shell execution")
302
+
303
+ github = subparsers.add_parser("github", help="use VYRELON GitHub runtime")
304
+ github_sub = github.add_subparsers(dest="github_command", required=True)
305
+ probe_parser = github_sub.add_parser(
306
+ "probe", help="verify GitHub access for a repository"
307
+ )
308
+ probe_parser.add_argument("repository")
309
+
310
+ return parser
311
+
312
+
313
+ def _provider_list(root: Path) -> int:
314
+ runtime = _provider_runtime(root)
315
+ payload = [
316
+ {
317
+ "id": provider.id,
318
+ "kind": provider.kind,
319
+ "models": [
320
+ {
321
+ "id": model.id,
322
+ "capabilities": sorted(model.capabilities),
323
+ "adapter_id": model.metadata.get("adapter_id"),
324
+ }
325
+ for model in provider.models
326
+ ],
327
+ }
328
+ for provider in runtime.providers.providers()
329
+ ]
330
+ print(json.dumps(payload, indent=2))
331
+ return 0
332
+
333
+
334
+
335
+ def _model_quota(root: Path) -> int:
336
+ runtime = _provider_runtime(root)
337
+ store = runtime.quota_store(root)
338
+ payload = []
339
+ for model in runtime.configured_models():
340
+ if not store.exists(model.id):
341
+ payload.append({
342
+ "model": model.id,
343
+ "provider": model.provider_id,
344
+ "status": "unknown",
345
+ "confidence": "unknown",
346
+ "dimensions": {},
347
+ })
348
+ continue
349
+ snapshot = store.load(model.id)
350
+ payload.append({
351
+ "model": snapshot.model_id,
352
+ "provider": snapshot.provider_id,
353
+ "scope": snapshot.scope,
354
+ "observed_at": snapshot.observed_at.isoformat(),
355
+ "confidence": snapshot.confidence.value,
356
+ "dimensions": {
357
+ item.name: {
358
+ "limit": item.limit,
359
+ "used": item.used,
360
+ "remaining": item.remaining,
361
+ "reset_at": item.reset_at.isoformat() if item.reset_at else None,
362
+ "confidence": item.confidence.value,
363
+ "source": item.source,
364
+ }
365
+ for item in snapshot.dimensions
366
+ },
367
+ })
368
+ print(json.dumps(payload, indent=2))
369
+ return 0
370
+
371
+ def _model_health(root: Path) -> int:
372
+ runtime = _provider_runtime(root)
373
+ registry = runtime.health_registry(root)
374
+ payload = []
375
+ for model in runtime.configured_models():
376
+ health = registry.get(model.id)
377
+ if health is None:
378
+ payload.append({
379
+ "model": model.id,
380
+ "provider": model.provider_id,
381
+ "status": "healthy",
382
+ "available": True,
383
+ "consecutive_failures": 0,
384
+ "successes": 0,
385
+ })
386
+ continue
387
+ payload.append({
388
+ "model": health.model_id,
389
+ "provider": health.provider_id,
390
+ "status": health.status.value,
391
+ "available": health.available,
392
+ "consecutive_failures": health.consecutive_failures,
393
+ "successes": health.successes,
394
+ "last_error": health.last_error,
395
+ "last_failure_at": health.last_failure_at.isoformat() if health.last_failure_at else None,
396
+ "last_success_at": health.last_success_at.isoformat() if health.last_success_at else None,
397
+ "cooldown_until": health.cooldown_until.isoformat() if health.cooldown_until else None,
398
+ })
399
+ print(json.dumps(payload, indent=2))
400
+ return 0
401
+
402
+
403
+ def _model_capabilities(root: Path) -> int:
404
+ runtime = _provider_runtime(root)
405
+ registry = runtime.capability_registry(root)
406
+ payload = []
407
+ for model in runtime.configured_models():
408
+ profile = registry.profile(model)
409
+ payload.append({
410
+ "model": profile.model_id,
411
+ "provider": profile.provider_id,
412
+ "capabilities": sorted(profile.capabilities),
413
+ "confidence": profile.confidence.value,
414
+ "source": profile.source,
415
+ })
416
+ print(json.dumps(payload, indent=2))
417
+ return 0
418
+
419
+ def _model_control(root: Path, include_events: bool = False) -> int:
420
+ runtime = _provider_runtime(root)
421
+ control = runtime.model_control_plane(root)
422
+ models = runtime.configured_models()
423
+ payload = {
424
+ "models": list(control.dashboard(models)),
425
+ }
426
+ if include_events:
427
+ payload["events"] = list(control.events.recent())
428
+ print(json.dumps(payload, indent=2, default=str))
429
+ return 0
430
+
431
+
432
+ def _model_explain(root: Path, agent_id: str, preferred_models=None, strategy: str = "pool") -> int:
433
+ runtime = _provider_runtime(root)
434
+ explanation = runtime.explain_model_routing(
435
+ root,
436
+ agent_id=agent_id,
437
+ preferred_model_ids=preferred_models,
438
+ routing_strategy=strategy,
439
+ )
440
+ print(json.dumps({
441
+ "agent": explanation.agent_id,
442
+ "strategy": explanation.strategy.value,
443
+ "selected_model": explanation.selected_model_id,
444
+ "candidates": [
445
+ {
446
+ "model": candidate.model_id,
447
+ "provider": candidate.provider_id,
448
+ "selected": candidate.selected,
449
+ "compatible": candidate.compatible,
450
+ "health_available": candidate.health_available,
451
+ "quota_available": candidate.quota_available,
452
+ "capability_score": candidate.capability_score,
453
+ "quota_score": candidate.quota_score,
454
+ "missing_capabilities": sorted(candidate.missing_capabilities),
455
+ "rejection_reasons": list(candidate.rejection_reasons),
456
+ }
457
+ for candidate in explanation.candidates
458
+ ],
459
+ }, indent=2, ensure_ascii=False))
460
+ return 0
461
+
462
+
463
+ def _model_run(root: Path, model_id: str, objective: str, agent_id: str, *, apply_changes: bool = False, validation_commands: list[str] | None = None, mcp_server_ids: list[str] | None = None) -> int:
464
+ from runtime.vyrelon import VYRELONRuntime
465
+
466
+ runtime = _provider_runtime(root)
467
+ if not runtime.providers.list_model_ids():
468
+ raise ValueError("No configured models found")
469
+ result = runtime.run_configured_work(
470
+ root,
471
+ objective=objective,
472
+ agent_id=agent_id,
473
+ preferred_model_ids=[model_id],
474
+ apply_changes=apply_changes,
475
+ validation_commands=tuple(validation_commands or ()),
476
+ mcp_server_ids=tuple(mcp_server_ids or ()),
477
+ )
478
+ print(result.output.text, end="")
479
+ return 0
480
+
481
+
482
+ def _provider_validate(root: Path) -> int:
483
+ runtime = _provider_runtime(root)
484
+ payload = []
485
+ valid = True
486
+ for model in runtime.configured_models():
487
+ checks = runtime.credential_checks().get(model.id, ())
488
+ missing = [check.environment_variable for check in checks if not check.present]
489
+ if missing:
490
+ valid = False
491
+ payload.append(
492
+ {
493
+ "model": model.id,
494
+ "provider": model.provider_id,
495
+ "adapter_id": model.metadata.get("adapter_id"),
496
+ "credential_environment_variables": [
497
+ {
498
+ "name": check.environment_variable,
499
+ "present": check.present,
500
+ }
501
+ for check in checks
502
+ ],
503
+ "valid": not missing,
504
+ }
505
+ )
506
+
507
+ print(json.dumps({"valid": valid, "models": payload}, indent=2))
508
+ return 0 if valid else 1
509
+
510
+
511
+ def main(argv: list[str] | None = None) -> int:
512
+ if argv is not None:
513
+ argv = list(argv)
514
+ if argv and argv[0] == "run" and "--" in argv:
515
+ argv[argv.index("--")] = "--command"
516
+ args = build_parser().parse_args(argv)
517
+
518
+ if args.command == "status":
519
+ root = Path(args.path or args.target).expanduser().resolve()
520
+ print(json.dumps(project_status(root), indent=2, ensure_ascii=False))
521
+ return 0
522
+
523
+ if args.command == "chat":
524
+ root = Path(args.path).expanduser().resolve()
525
+ return _run_chat(root, args.objective, args.agent, args.model, args.session, args.execute, args.exec_command)
526
+
527
+ if args.command == "mcp" and args.mcp_command == "serve":
528
+ from runtime.mcp.server import VYRELONMCPServer
529
+ root = Path(args.path).expanduser().resolve()
530
+ VYRELONMCPServer(
531
+ root,
532
+ allow_write=args.allow_write,
533
+ allow_process=args.allow_process,
534
+ ).serve_forever()
535
+ return 0
536
+
537
+ if args.command == "github" and args.github_command == "probe":
538
+ print(json.dumps(probe(args.repository), indent=2))
539
+ return 0
540
+
541
+ if args.command == "profile":
542
+ root = Path(args.path).expanduser().resolve()
543
+ from runtime.vyrelon import VYRELONRuntime
544
+ project, agents = VYRELONRuntime().profiles(root)
545
+ print(
546
+ json.dumps(
547
+ {
548
+ "project": {
549
+ "id": project.id,
550
+ "root": project.root,
551
+ "technology_profiles": list(project.technology_profile_ids),
552
+ "agent_profiles": list(project.agent_profile_ids),
553
+ "metadata": project.metadata,
554
+ },
555
+ "agents": [
556
+ {
557
+ "id": agent.id,
558
+ "role": agent.role,
559
+ "profiles": list(agent.profile_ids),
560
+ "capabilities": sorted(agent.capabilities),
561
+ "tools": sorted(agent.tools),
562
+ "permissions": sorted(agent.permissions),
563
+ "model_ids": list(agent.model_ids),
564
+ }
565
+ for agent in agents
566
+ ],
567
+ },
568
+ indent=2,
569
+ )
570
+ )
571
+ return 0
572
+
573
+ if args.command == "providers":
574
+ root = Path(args.path).expanduser().resolve()
575
+ if args.providers_command == "list":
576
+ return _provider_list(root)
577
+ return _provider_validate(root)
578
+
579
+ if args.command == "models":
580
+ root = Path(args.path).expanduser().resolve()
581
+ if args.models_command == "quota":
582
+ return _model_quota(root)
583
+ if args.models_command == "health":
584
+ return _model_health(root)
585
+ if args.models_command == "capabilities":
586
+ return _model_capabilities(root)
587
+ if args.models_command == "discover":
588
+ runtime = _provider_runtime(root)
589
+ print(json.dumps(list(runtime.discover_models(root, refresh=not args.no_refresh)), indent=2, default=str))
590
+ return 0
591
+ if args.models_command == "control":
592
+ return _model_control(root, args.events)
593
+ if args.models_command == "explain":
594
+ return _model_explain(root, args.agent, args.preferred_models, args.strategy)
595
+ if args.models_command == "run":
596
+ return _model_run(
597
+ root,
598
+ args.model,
599
+ args.objective,
600
+ args.agent,
601
+ apply_changes=args.apply_changes,
602
+ validation_commands=args.validate,
603
+ mcp_server_ids=args.mcp,
604
+ )
605
+
606
+ root = Path(getattr(args, "path", None) or getattr(args, "path_arg", None) or ".").expanduser().resolve()
607
+
608
+ if args.command == "run":
609
+ work = WorkUnit(
610
+ id=args.work_unit_id or uuid.uuid4().hex,
611
+ objective=args.objective,
612
+ )
613
+ agent = AgentContract(
614
+ id=args.agent,
615
+ role=args.agent,
616
+ capabilities=frozenset({"execution"}),
617
+ tools=frozenset({"process"}) if args.process_command else frozenset(),
618
+ )
619
+ print(f"WorkUnit: {work.id}")
620
+ try:
621
+ from runtime.vyrelon import VYRELONRuntime
622
+ runtime = VYRELONRuntime()
623
+ if not args.process_command:
624
+ work.metadata["runtime"] = "configured-model"
625
+ work.metadata["agent_id"] = args.agent
626
+ if args.model:
627
+ work.metadata["model_id"] = args.model
628
+ result = runtime.run_configured_work(
629
+ root,
630
+ objective=args.objective,
631
+ agent_id=args.agent,
632
+ work_unit_id=work.id,
633
+ preferred_model_ids=[args.model] if args.model else None,
634
+ apply_changes=args.apply_changes,
635
+ validation_commands=tuple(args.validate),
636
+ mcp_server_ids=tuple(args.mcp),
637
+ )
638
+ print(result.output.text, end="")
639
+ return 0
640
+
641
+ model = ModelSpec(
642
+ id=args.model or "local-process",
643
+ provider_id="vyrelon-local",
644
+ capabilities=frozenset({"execution"}),
645
+ )
646
+ work.metadata["command"] = args.process_command
647
+ work.metadata["runtime"] = "local-process"
648
+ work.metadata["execution_agent_id"] = agent.id
649
+ work.metadata["execution_model_id"] = model.id
650
+ result = runtime.run_persistent(
651
+ root,
652
+ work,
653
+ agent,
654
+ [model],
655
+ ProcessAgentExecutor(args.process_command),
656
+ preferred_model_ids=[model.id],
657
+ )
658
+ if result.output.returncode == 0:
659
+ print(result.output.stdout, end="")
660
+ return 0
661
+ print(result.output.stdout, end="")
662
+ print(result.output.stderr, end="")
663
+ return result.output.returncode
664
+ except Exception as exc:
665
+ print(str(exc))
666
+ return 1
667
+
668
+ if args.command == "resume":
669
+ work = _work_state(root).load(args.work_unit_id)
670
+ if work.status not in {WorkStatus.EXECUTING, WorkStatus.FAILED}:
671
+ raise ValueError(
672
+ f"WorkUnit {work.id} is not resumable from status {work.status.value}"
673
+ )
674
+ agent_id = str(work.metadata.get("agent_id", work.assigned_agents[-1] if work.assigned_agents else "executor"))
675
+ from runtime.vyrelon import VYRELONRuntime
676
+ runtime = VYRELONRuntime()
677
+ if work.metadata.get("runtime") == "configured-model":
678
+ model_id = str(work.metadata.get("model_id", ""))
679
+ if not model_id:
680
+ raise ValueError(f"WorkUnit {work.id} has no persisted model id")
681
+ agent = AgentContract(
682
+ id=agent_id,
683
+ role=agent_id,
684
+ capabilities=frozenset({"execution"}),
685
+ )
686
+ result = runtime.run_persistent_registered_model(
687
+ root,
688
+ work,
689
+ agent,
690
+ preferred_model_ids=[model_id],
691
+ )
692
+ print(result.output.text, end="")
693
+ return 0
694
+
695
+ command = list(work.metadata.get("command", []))
696
+ if not command:
697
+ raise ValueError(f"WorkUnit {work.id} has no persisted command")
698
+ agent = AgentContract(
699
+ id=agent_id,
700
+ role=agent_id,
701
+ capabilities=frozenset({"execution"}),
702
+ tools=frozenset({"process"}),
703
+ )
704
+ model = ModelSpec(
705
+ id="local-process",
706
+ provider_id="vyrelon-local",
707
+ capabilities=frozenset({"execution"}),
708
+ )
709
+ result = runtime.run_persistent(
710
+ root,
711
+ work,
712
+ agent,
713
+ [model],
714
+ ProcessAgentExecutor(command),
715
+ preferred_model_ids=["local-process"],
716
+ )
717
+ if result.output.returncode == 0:
718
+ print(result.output.stdout, end="")
719
+ return 0
720
+ print(result.output.stdout, end="")
721
+ print(result.output.stderr, end="")
722
+ return result.output.returncode
723
+
724
+ if args.command == "status":
725
+ target = Path(args.target).expanduser().resolve() if args.path is None else Path(args.path).expanduser().resolve()
726
+ if args.path is None and target.is_dir():
727
+ print(json.dumps(project_status(target), indent=2, ensure_ascii=False))
728
+ return 0
729
+ work_id = args.target
730
+ work = _work_state(target).load(work_id)
731
+ print(json.dumps({"id": work.id, "objective": work.objective, "status": work.status.value, "assigned_agents": work.assigned_agents, "metadata": work.metadata}, indent=2))
732
+ return 0
733
+
734
+ detector = ProfileDetector()
735
+ detections = detector.detect(root)
736
+
737
+ if args.command == "detect":
738
+ print(
739
+ json.dumps(
740
+ [
741
+ {
742
+ "profile": result.profile_id,
743
+ "confidence": result.confidence,
744
+ "evidence": list(result.evidence),
745
+ }
746
+ for result in detections
747
+ ],
748
+ indent=2,
749
+ )
750
+ )
751
+ return 0
752
+
753
+ config = ProjectInitializer().apply(root, detections, component=args.component)
754
+ print(f"Initialized VYRELON: {config}")
755
+ return 0
756
+
757
+
758
+ if __name__ == "__main__":
759
+ raise SystemExit(main())