mas-lab-component-controller 0.2.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 (89) hide show
  1. mas/lab/controller/__init__.py +7 -0
  2. mas/lab/controller/__main__.py +5 -0
  3. mas/lab/controller/agent_chat.py +226 -0
  4. mas/lab/controller/api.py +364 -0
  5. mas/lab/controller/artifact_discovery.py +107 -0
  6. mas/lab/controller/artifacts.py +112 -0
  7. mas/lab/controller/cli.py +493 -0
  8. mas/lab/controller/client.py +271 -0
  9. mas/lab/controller/config.py +75 -0
  10. mas/lab/controller/constants.py +30 -0
  11. mas/lab/controller/daemon.py +190 -0
  12. mas/lab/controller/deps.py +172 -0
  13. mas/lab/controller/fastapi_app.py +166 -0
  14. mas/lab/controller/http_server.py +243 -0
  15. mas/lab/controller/io_capture.py +55 -0
  16. mas/lab/controller/item_service.py +625 -0
  17. mas/lab/controller/job_store.py +209 -0
  18. mas/lab/controller/jobs.py +375 -0
  19. mas/lab/controller/kinds.py +149 -0
  20. mas/lab/controller/lab_registry.py +1076 -0
  21. mas/lab/controller/manifest_layout.py +832 -0
  22. mas/lab/controller/manifest_store.py +214 -0
  23. mas/lab/controller/manifest_validation.py +90 -0
  24. mas/lab/controller/models.py +215 -0
  25. mas/lab/controller/path_safety.py +68 -0
  26. mas/lab/controller/pipeline_validation.py +79 -0
  27. mas/lab/controller/registry.py +83 -0
  28. mas/lab/controller/routes/__init__.py +82 -0
  29. mas/lab/controller/routes/_api.py +15 -0
  30. mas/lab/controller/routes/apps.py +190 -0
  31. mas/lab/controller/routes/artifacts.py +37 -0
  32. mas/lab/controller/routes/benchmark.py +307 -0
  33. mas/lab/controller/routes/choices.py +141 -0
  34. mas/lab/controller/routes/datasets.py +91 -0
  35. mas/lab/controller/routes/design_patterns.py +121 -0
  36. mas/lab/controller/routes/eval_output.py +31 -0
  37. mas/lab/controller/routes/experiments.py +194 -0
  38. mas/lab/controller/routes/health.py +178 -0
  39. mas/lab/controller/routes/items.py +187 -0
  40. mas/lab/controller/routes/jobs.py +107 -0
  41. mas/lab/controller/routes/libraries.py +144 -0
  42. mas/lab/controller/routes/overlays.py +164 -0
  43. mas/lab/controller/routes/pipelines.py +139 -0
  44. mas/lab/controller/routes/registry.py +40 -0
  45. mas/lab/controller/routes/run.py +215 -0
  46. mas/lab/controller/routes/runs.py +392 -0
  47. mas/lab/controller/routes/scopes.py +139 -0
  48. mas/lab/controller/routes/uploads.py +24 -0
  49. mas/lab/controller/routes/validate.py +36 -0
  50. mas/lab/controller/run_bridge.py +276 -0
  51. mas/lab/controller/run_store.py +190 -0
  52. mas/lab/controller/schema_registry.py +109 -0
  53. mas/lab/controller/schemas/pipeline-step-types-post.json +764 -0
  54. mas/lab/controller/schemas/pipeline-step-types-pre.json +35 -0
  55. mas/lab/controller/scope_catalog.py +266 -0
  56. mas/lab/controller/scope_service.py +134 -0
  57. mas/lab/controller/sessions.py +63 -0
  58. mas/lab/controller/settings.py +65 -0
  59. mas/lab/controller/ui_stream/__init__.py +13 -0
  60. mas/lab/controller/ui_stream/app.py +2147 -0
  61. mas/lab/controller/ui_stream/artefact_bind.py +308 -0
  62. mas/lab/controller/ui_stream/auth.py +136 -0
  63. mas/lab/controller/ui_stream/bus.py +156 -0
  64. mas/lab/controller/ui_stream/export.py +400 -0
  65. mas/lab/controller/ui_stream/governance.py +141 -0
  66. mas/lab/controller/ui_stream/ingest.py +300 -0
  67. mas/lab/controller/ui_stream/inspect.py +55 -0
  68. mas/lab/controller/ui_stream/layouts.py +161 -0
  69. mas/lab/controller/ui_stream/metrics.py +274 -0
  70. mas/lab/controller/ui_stream/plot_folds.py +609 -0
  71. mas/lab/controller/ui_stream/plugin_registry.py +139 -0
  72. mas/lab/controller/ui_stream/refs.py +259 -0
  73. mas/lab/controller/ui_stream/replay.py +236 -0
  74. mas/lab/controller/ui_stream/replay_cli.py +707 -0
  75. mas/lab/controller/ui_stream/run_index.py +337 -0
  76. mas/lab/controller/ui_stream/spec_load.py +48 -0
  77. mas/lab/controller/ui_stream/spec_snapshot.py +313 -0
  78. mas/lab/controller/ui_stream/stream.py +95 -0
  79. mas/lab/controller/ui_stream/streams.py +42 -0
  80. mas/lab/controller/ui_stream/timeline.py +183 -0
  81. mas/lab/controller/ui_stream/ui_trace.py +258 -0
  82. mas/lab/controller/uploads.py +61 -0
  83. mas/lab/controller/worker_model.py +137 -0
  84. mas/lab/controller/workers.py +296 -0
  85. mas/lab/controller/workspace_library.py +70 -0
  86. mas_lab_component_controller-0.2.0.dist-info/METADATA +21 -0
  87. mas_lab_component_controller-0.2.0.dist-info/RECORD +89 -0
  88. mas_lab_component_controller-0.2.0.dist-info/WHEEL +4 -0
  89. mas_lab_component_controller-0.2.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,7 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """MAS Lab controller — daemon, workers, and HTTP API."""
4
+ from mas.lab.controller.api import ControllerAPI
5
+ from mas.lab.controller.lab_registry import LabRegistry, get_lab_registry
6
+
7
+ __all__ = ["ControllerAPI", "LabRegistry", "get_lab_registry"]
@@ -0,0 +1,5 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # Reference entry point for running the controller HTTP API directly (PR #5).
4
+ # Prefer: mas-lab control start or mas-lab serve
5
+ from mas.lab.controller.fastapi_app import app # noqa: F401
@@ -0,0 +1,226 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """In-process agent chat for the controller UI.
4
+
5
+ Runs agents via v2 ``SessionController`` (no subprocess / stdout capture) and
6
+ returns structured interaction fields: user query in, agent response or error out.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import re
12
+ import uuid
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Literal, Optional
16
+
17
+ import yaml
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ _STATUS = Literal["ok", "error"]
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class AgentTurnResult:
26
+ status: _STATUS
27
+ response: str = ""
28
+ error_message: str = ""
29
+ error_detail: str = ""
30
+ session_id: str = ""
31
+
32
+
33
+ def format_error(raw: str) -> tuple[str, str]:
34
+ """Return (short_message, full_detail) from a raw agent/LLM error string."""
35
+ full = (raw or "").strip()
36
+ if not full:
37
+ return "Agent execution failed.", ""
38
+
39
+ short = full
40
+
41
+ msg_match = re.search(r"['\"]message['\"]\s*:\s*['\"]([^'\"]+)['\"]", full)
42
+ if msg_match:
43
+ short = msg_match.group(1)
44
+ elif "ExceededBudget" in full:
45
+ budget_match = re.search(r"(ExceededBudget:[^\n'\"]+)", full)
46
+ if budget_match:
47
+ short = budget_match.group(1).strip()
48
+
49
+ if len(short) > 240:
50
+ short = short[:237] + "..."
51
+
52
+ return short, full
53
+
54
+
55
+ class _SilentDisplay:
56
+ """No-op conversation display for embedded UI turns."""
57
+
58
+ def on_user(self, text: str, *, turn_id: str = "") -> None:
59
+ return
60
+
61
+ def on_agent(self, text: str) -> None:
62
+ return
63
+
64
+ def on_turn_error(self, message: str, *, detail: str = "") -> None:
65
+ return
66
+
67
+ def on_hitl_request(self, request: object) -> None:
68
+ return
69
+
70
+ def on_working(self, message: str = "Agent working…") -> None:
71
+ return
72
+
73
+ def end_working(self) -> None:
74
+ return
75
+
76
+ def on_system(self, message: str) -> None:
77
+ return
78
+
79
+ def on_error(self, message: str) -> None:
80
+ return
81
+
82
+
83
+ def _load_workspace_dotenv(base_dir: Path) -> None:
84
+ """Load optional .env files; container env from docker/.env still wins (override=False)."""
85
+ import os
86
+
87
+ try:
88
+ from dotenv import load_dotenv
89
+ except ImportError:
90
+ return
91
+
92
+ candidates = [
93
+ Path("/workspace/.env"),
94
+ base_dir / ".env",
95
+ Path(os.environ.get("MAS_WORKSPACE_ROOT", "")) / ".env",
96
+ ]
97
+ for path in candidates:
98
+ if path.is_file():
99
+ load_dotenv(path, override=False)
100
+
101
+
102
+ def _quiet_logging_for_chat() -> None:
103
+ """Suppress exchange logs; UI chat must not capture CLI-style stdout."""
104
+ import os
105
+
106
+ from mas.ctl.logging_setup import setup_logging
107
+
108
+ os.environ.pop("LOG_LEVEL", None)
109
+ setup_logging(verbosity=0)
110
+ logging.getLogger("mas.runtime").setLevel(logging.WARNING)
111
+ logging.getLogger("mas.ctl").setLevel(logging.WARNING)
112
+
113
+
114
+ def run_agent_turn(
115
+ manifest_yaml: str,
116
+ query: str,
117
+ *,
118
+ base_dir: Path,
119
+ flavour: Optional[str] = None,
120
+ session_id: Optional[str] = None,
121
+ ) -> AgentTurnResult:
122
+ """Execute one user → agent turn in-process via v2 SessionController."""
123
+ from mas.ctl.infra.resolve import resolve_infra_refs
124
+ from mas.ctl.runtime_cli import pattern_from_manifest
125
+ from mas.ctl.session.bootstrap import InstantiationOptions, instantiate_runtime
126
+ from mas.ctl.session.controller import ConversationConfig, SessionController
127
+ from mas.ctl.session.hitl_config import resolve_hitl_from_manifest
128
+ from mas.ctl.ui.turn_result import turn_to_agent_result
129
+ from mas.ctl.workspace.config import (
130
+ UserConfig,
131
+ WorkspaceConfig,
132
+ merge_infra_interceptors,
133
+ merge_infra_refs,
134
+ )
135
+
136
+ _quiet_logging_for_chat()
137
+ _load_workspace_dotenv(base_dir)
138
+
139
+ base_dir = Path(base_dir)
140
+ sid = session_id or f"ui:{uuid.uuid4()}"
141
+ prompt = (query or "").strip()
142
+ if not prompt:
143
+ return AgentTurnResult(
144
+ status="error",
145
+ error_message="No query provided.",
146
+ session_id=sid,
147
+ )
148
+
149
+ try:
150
+ current_manifest = yaml.safe_load(manifest_yaml)
151
+ except yaml.YAMLError as exc:
152
+ return AgentTurnResult(
153
+ status="error",
154
+ error_message=f"Invalid manifest YAML: {exc}",
155
+ error_detail=str(exc),
156
+ session_id=sid,
157
+ )
158
+
159
+ if not isinstance(current_manifest, dict):
160
+ return AgentTurnResult(
161
+ status="error",
162
+ error_message="Manifest must be a YAML mapping.",
163
+ session_id=sid,
164
+ )
165
+
166
+ try:
167
+ workspace = WorkspaceConfig.load(base_dir)
168
+ user = UserConfig.load()
169
+ pattern = pattern_from_manifest(current_manifest)
170
+ merged_infra = merge_infra_refs(
171
+ workspace_refs=workspace.effective_infra_refs,
172
+ user_refs=[user.default_infra] if user.default_infra else [],
173
+ cli_refs=[],
174
+ workspace_found=workspace.found,
175
+ )
176
+ resolved_infra = resolve_infra_refs(
177
+ merged_infra,
178
+ anchor=base_dir,
179
+ workspace=workspace,
180
+ user=user,
181
+ interceptors=merge_infra_interceptors(
182
+ workspace_interceptors=workspace.infra_interceptors,
183
+ cli_interceptors=[],
184
+ ),
185
+ )
186
+ hitl_responder, hitl_terminal = resolve_hitl_from_manifest(
187
+ current_manifest, session_interactive=False
188
+ )
189
+ instance, _store = instantiate_runtime(
190
+ InstantiationOptions(
191
+ pattern_plugin_id=pattern,
192
+ agent_manifest=current_manifest,
193
+ manifest_dir=base_dir,
194
+ resolved_infra=resolved_infra,
195
+ workspace=workspace,
196
+ validate_manifests=False,
197
+ ),
198
+ hitl=hitl_responder,
199
+ )
200
+ controller = SessionController(
201
+ instance=instance,
202
+ display=_SilentDisplay(),
203
+ hitl_terminal=hitl_terminal,
204
+ config=ConversationConfig(single_turn=True),
205
+ verbose=0,
206
+ session_id=sid,
207
+ )
208
+ turn = controller.run_turn(prompt, turn_id=sid, auto_hitl=True)
209
+ mapped = turn_to_agent_result(turn)
210
+ except Exception as exc:
211
+ logger.debug("Agent session failed", exc_info=True)
212
+ short, full = format_error(str(exc))
213
+ return AgentTurnResult(
214
+ status="error",
215
+ error_message=short,
216
+ error_detail=full,
217
+ session_id=sid,
218
+ )
219
+
220
+ return AgentTurnResult(
221
+ status=mapped.status,
222
+ response=mapped.response,
223
+ error_message=mapped.error_message,
224
+ error_detail=mapped.error_detail,
225
+ session_id=sid,
226
+ )
@@ -0,0 +1,364 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """ControllerAPI — pure Python backend for CLI and HTTP."""
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from mas.lab.controller.manifest_store import ManifestStore
12
+ from mas.lab.controller.registry import WorkerRegistry
13
+ from mas.lab.controller.worker_model import WorkerKind, WorkerStatus
14
+ from mas.lab.controller.workers import (
15
+ WorkerRunner,
16
+ run_application_worker,
17
+ run_benchmark_worker,
18
+ run_experiment_worker,
19
+ run_pipeline_worker,
20
+ )
21
+ from mas.runtime.spec.source import load_yaml_file
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ _INSTANCE: "ControllerAPI | None" = None
26
+
27
+
28
+ def set_controller_api(api: Optional["ControllerAPI"]) -> None:
29
+ global _INSTANCE
30
+ _INSTANCE = api
31
+
32
+
33
+ def peek_controller_api() -> "ControllerAPI | None":
34
+ return _INSTANCE
35
+
36
+
37
+ def get_controller_api(workspace: Any = None) -> "ControllerAPI":
38
+ global _INSTANCE
39
+ if _INSTANCE is None:
40
+ _INSTANCE = ControllerAPI(workspace)
41
+ return _INSTANCE
42
+
43
+
44
+ class ControllerAPI:
45
+ """Shared API surface for Unix-socket CLI and HTTP UI."""
46
+
47
+ def __init__(self, workspace: Any = None) -> None:
48
+ self.workspace = workspace
49
+ self.workers = WorkerRegistry()
50
+ self.runner = WorkerRunner(self.workers)
51
+ self.manifests = ManifestStore(workspace)
52
+
53
+ # -- Service ----------------------------------------------------------------
54
+
55
+ def status(self) -> Dict[str, Any]:
56
+ from mas.lab.controller import daemon as daemon_mod
57
+
58
+ active = self.workers.list_workers(
59
+ status=WorkerStatus.RUNNING,
60
+ )
61
+ return {
62
+ "status": "ok",
63
+ "workers": len(self.workers.list_workers()),
64
+ "running": len(active),
65
+ "libraries": len(self.manifests.libraries()),
66
+ "sessions": daemon_mod._sessions.status(),
67
+ }
68
+
69
+ # -- Workers ----------------------------------------------------------------
70
+
71
+ def list_workers(
72
+ self,
73
+ *,
74
+ kind: Optional[str] = None,
75
+ status: Optional[str] = None,
76
+ ) -> List[Dict[str, Any]]:
77
+ kind_enum = WorkerKind(kind) if kind else None
78
+ status_enum = WorkerStatus(status) if status else None
79
+ return [w.to_job_summary() for w in self.workers.list_workers(kind=kind_enum, status=status_enum)]
80
+
81
+ def get_worker(self, worker_id: str) -> Optional[Dict[str, Any]]:
82
+ record = self.workers.get(worker_id)
83
+ return record.to_job_detail() if record else None
84
+
85
+ def cancel_worker(self, worker_id: str) -> bool:
86
+ return self.runner.cancel(worker_id)
87
+
88
+ def drop_worker(self, worker_id: str) -> bool:
89
+ self.runner.cancel(worker_id)
90
+ return self.workers.drop(worker_id)
91
+
92
+ def drop_workers(self, *, origin: str | None = None, terminal_only: bool = True) -> int:
93
+ from mas.lab.controller.worker_model import _ui_origin
94
+
95
+ removed = 0
96
+ for record in list(self.workers.list_workers()):
97
+ if terminal_only and record.status not in {
98
+ WorkerStatus.COMPLETED,
99
+ WorkerStatus.FAILED,
100
+ WorkerStatus.CANCELLED,
101
+ }:
102
+ continue
103
+ job_origin = _ui_origin(record.spec)
104
+ if origin is not None and job_origin != origin:
105
+ continue
106
+ if self.drop_worker(record.id):
107
+ removed += 1
108
+ return removed
109
+
110
+ def submit_benchmark(self, spec: dict) -> Dict[str, Any]:
111
+ record = run_benchmark_worker(self.workers, self.runner, spec)
112
+ return {"worker_id": record.id, "job_id": record.id, "status": record.status.value}
113
+
114
+ def submit_experiment(self, spec: dict) -> Dict[str, Any]:
115
+ live_spec = {**spec, "live": True, "progress": False}
116
+ record = run_experiment_worker(self.workers, self.runner, live_spec)
117
+ return {"worker_id": record.id, "job_id": record.id, "status": record.status.value}
118
+
119
+ def submit_application(self, spec: dict) -> Dict[str, Any]:
120
+ record = run_application_worker(self.workers, self.runner, spec)
121
+ return {"worker_id": record.id, "job_id": record.id, "status": record.status.value}
122
+
123
+ def submit_pipeline(self, spec: dict) -> Dict[str, Any]:
124
+ record = run_pipeline_worker(self.workers, self.runner, spec)
125
+ return {"worker_id": record.id, "job_id": record.id, "status": record.status.value}
126
+
127
+ # -- Jobs (UI alias) --------------------------------------------------------
128
+
129
+ def list_jobs(self, status: Optional[str] = None) -> List[Dict[str, Any]]:
130
+ return self.list_workers(status=status)
131
+
132
+ def get_job(self, job_id: str) -> Optional[Dict[str, Any]]:
133
+ return self.get_worker(job_id)
134
+
135
+ # -- Libraries / manifests ----------------------------------------------------
136
+
137
+ def list_libraries(self) -> List[Dict[str, str]]:
138
+ self.manifests.refresh()
139
+ return self.manifests.libraries()
140
+
141
+ def list_runtime_runners(self) -> List[Dict[str, str]]:
142
+ """Return registered ``mas.lab.runners`` plugins for UI / CLI."""
143
+ from mas.lab.runners.factory import RunnerFactory
144
+
145
+ return [{"id": rid, "label": rid} for rid in RunnerFactory.available()]
146
+
147
+ def validate_manifest_yaml(self, manifest_yaml: str, *, base_dir: Path | None = None) -> Dict[str, Any]:
148
+ from mas.lab.controller.manifest_validation import validate_manifest_yaml_content
149
+
150
+ root = base_dir or Path.cwd()
151
+ return validate_manifest_yaml_content(manifest_yaml, base_dir=root, resolve_refs=True)
152
+
153
+ def list_experiments(self, library: str) -> List[Dict[str, Any]]:
154
+ summaries: List[Dict[str, Any]] = []
155
+ for item in self.manifests.list_yaml_resources(library, "experiments"):
156
+ try:
157
+ data = load_yaml_file(Path(item["file"]))
158
+ meta = data.get("metadata") or data.get("experiment") or {}
159
+ summaries.append(
160
+ {
161
+ "name": item["name"],
162
+ "description": meta.get("description", ""),
163
+ "version": str(meta.get("version", "")),
164
+ "scenarios": list((data.get("scenarios") or {}).keys()),
165
+ "dataset": str(data.get("dataset") or ""),
166
+ }
167
+ )
168
+ except Exception:
169
+ summaries.append(
170
+ {
171
+ "name": item["name"],
172
+ "description": "",
173
+ "version": "",
174
+ "scenarios": [],
175
+ "dataset": "",
176
+ }
177
+ )
178
+ return summaries
179
+
180
+ def get_experiment_content(self, library: str, name: str) -> Dict[str, str]:
181
+ return {"name": name, "content": self.manifests.read_text(library, "experiments", name)}
182
+
183
+ def save_experiment(self, library: str, name: str, content: str) -> None:
184
+ self.manifests.write_text(library, "experiments", name, content)
185
+
186
+ def delete_experiment(self, library: str, name: str) -> None:
187
+ self.manifests.delete_resource(library, "experiments", name)
188
+
189
+ def list_pipelines(self, library: str) -> List[Dict[str, Any]]:
190
+ out: List[Dict[str, Any]] = []
191
+ for item in self.manifests.list_yaml_resources(library, "pipelines"):
192
+ try:
193
+ data = load_yaml_file(Path(item["file"]))
194
+ md = data.get("metadata") or {}
195
+ steps = []
196
+ for step in data.get("steps") or []:
197
+ steps.append(
198
+ {
199
+ "name": step.get("name", step.get("id", "")),
200
+ "type": step.get("type", ""),
201
+ "depends_on": list(step.get("depends_on") or []),
202
+ }
203
+ )
204
+ out.append(
205
+ {
206
+ "filename": item["name"],
207
+ "name": md.get("name") or item["name"],
208
+ "description": md.get("description", ""),
209
+ "steps": steps,
210
+ "experiment": str(data.get("experiment") or ""),
211
+ }
212
+ )
213
+ except Exception:
214
+ out.append(
215
+ {
216
+ "filename": item["name"],
217
+ "name": item["name"],
218
+ "description": "",
219
+ "steps": [],
220
+ "experiment": "",
221
+ }
222
+ )
223
+ return out
224
+
225
+ def get_pipeline_content(self, library: str, name: str) -> Dict[str, str]:
226
+ return {"name": name, "content": self.manifests.read_text(library, "pipelines", name)}
227
+
228
+ def save_pipeline(self, library: str, name: str, content: str) -> None:
229
+ self.manifests.write_text(library, "pipelines", name, content)
230
+
231
+ def delete_pipeline(self, library: str, name: str) -> None:
232
+ self.manifests.delete_resource(library, "pipelines", name)
233
+
234
+ def list_overlays(self, library: str) -> List[Dict[str, Any]]:
235
+ out: List[Dict[str, Any]] = []
236
+ for item in self.manifests.list_yaml_resources(library, "overlays"):
237
+ try:
238
+ data = load_yaml_file(Path(item["file"]))
239
+ out.append({"name": item["name"], "description": data.get("description", "")})
240
+ except Exception:
241
+ out.append({"name": item["name"]})
242
+ return out
243
+
244
+ def get_overlay_content(self, library: str, name: str) -> Dict[str, str]:
245
+ return {"name": name, "content": self.manifests.read_text(library, "overlays", name)}
246
+
247
+ def save_overlay(self, library: str, name: str, content: str) -> None:
248
+ self.manifests.write_text(library, "overlays", name, content)
249
+
250
+ def delete_overlay(self, library: str, name: str) -> None:
251
+ self.manifests.delete_resource(library, "overlays", name)
252
+
253
+ def list_datasets(self, library: str) -> List[Dict[str, Any]]:
254
+ out: List[Dict[str, Any]] = []
255
+ for item in self.manifests.list_yaml_resources(library, "datasets"):
256
+ try:
257
+ data = load_yaml_file(Path(item["file"]))
258
+ md = data.get("metadata") or {}
259
+ out.append({"name": item["name"], "description": md.get("description", "")})
260
+ except Exception:
261
+ out.append({"name": item["name"], "description": ""})
262
+ return out
263
+
264
+ def get_dataset_content(self, library: str, name: str) -> Dict[str, str]:
265
+ return {"name": name, "content": self.manifests.read_text(library, "datasets", name)}
266
+
267
+ def save_dataset(self, library: str, name: str, content: str) -> None:
268
+ self.manifests.write_text(library, "datasets", name, content)
269
+
270
+ def delete_dataset(self, library: str, name: str) -> None:
271
+ self.manifests.delete_resource(library, "datasets", name)
272
+
273
+ def list_scenarios(self, library: str) -> List[Dict[str, str]]:
274
+ return [
275
+ {"name": item["name"], "path": item["path"]}
276
+ for item in self.manifests.list_yaml_resources(library, "scenarios")
277
+ ]
278
+
279
+ def config_files(self, library: str) -> Dict[str, Dict[str, str]]:
280
+ return self.manifests.config_files(library)
281
+
282
+ def list_tools(self, library: str) -> List[Dict[str, str]]:
283
+ from mas.lab.controller.artifact_discovery import discover_tools
284
+
285
+ return discover_tools(self.manifests.library_root(library))
286
+
287
+ def list_skills(self, library: str) -> List[Dict[str, str]]:
288
+ from mas.lab.controller.artifact_discovery import discover_skills
289
+
290
+ return discover_skills(self.manifests.library_root(library))
291
+
292
+ def pipeline_step_types(self) -> Dict[str, Any]:
293
+ from mas.lab.controller.lab_registry import get_lab_registry
294
+
295
+ return get_lab_registry(self.workspace).pipeline_step_types()
296
+
297
+ def registry_catalog(self) -> Dict[str, Any]:
298
+ from mas.lab.controller.lab_registry import get_lab_registry
299
+
300
+ return get_lab_registry(self.workspace).catalog()
301
+
302
+ def run_agent_job(self, library: str, body: dict) -> Dict[str, Any]:
303
+ root = self.manifests.library_root(library)
304
+ agents_dir = root / "agents"
305
+ agents_dir.mkdir(parents=True, exist_ok=True)
306
+ with tempfile.NamedTemporaryFile("w", suffix=".yaml", dir=agents_dir, delete=False) as fh:
307
+ fh.write(body.get("manifest_yaml") or "")
308
+ spec_path = fh.name
309
+ spec = {
310
+ "prompt": body.get("query") or "",
311
+ "spec_path": spec_path,
312
+ "config": {},
313
+ "endpoint": f"/api/libraries/{library}/run",
314
+ "command": f"run agent ({library})",
315
+ }
316
+ return self.submit_application(spec)
317
+
318
+ def run_mas_job(self, library: str, body: dict) -> Dict[str, Any]:
319
+ root = self.manifests.library_root(library)
320
+ mas_dir = root / "apps"
321
+ mas_dir.mkdir(parents=True, exist_ok=True)
322
+ with tempfile.NamedTemporaryFile("w", suffix=".yaml", dir=mas_dir, delete=False) as fh:
323
+ fh.write(body.get("manifest_yaml") or "")
324
+ spec_path = fh.name
325
+ spec = {
326
+ "prompt": body.get("query") or "",
327
+ "spec_path": spec_path,
328
+ "config": {"overlays": body.get("overlays") or []},
329
+ "endpoint": f"/api/libraries/{library}/run-mas",
330
+ "command": f"run mas ({library})",
331
+ }
332
+ return self.submit_application(spec)
333
+
334
+ def run_benchmark_job(self, library: str, body: dict) -> Dict[str, Any]:
335
+ content = body.get("experiment_yaml") or ""
336
+ root = self.manifests.library_root(library)
337
+ exp_dir = root / "experiments"
338
+ exp_dir.mkdir(parents=True, exist_ok=True)
339
+ with tempfile.NamedTemporaryFile("w", suffix=".yaml", dir=exp_dir, delete=False) as fh:
340
+ fh.write(content)
341
+ experiment_yaml = fh.name
342
+ spec = {
343
+ "experiment_yaml": experiment_yaml,
344
+ "progress": body.get("progress", True),
345
+ "max_runs": body.get("n_runs"),
346
+ "endpoint": f"/api/libraries/{library}/benchmark/run",
347
+ }
348
+ result = self.submit_benchmark(spec)
349
+ result["command"] = f"mas-lab benchmark run {experiment_yaml}"
350
+ return result
351
+
352
+ def run_pipeline_job(self, library: str, body: dict) -> Dict[str, Any]:
353
+ root = self.manifests.library_root(library)
354
+ pipe_dir = root / "pipelines"
355
+ pipe_dir.mkdir(parents=True, exist_ok=True)
356
+ with tempfile.NamedTemporaryFile("w", suffix=".yaml", dir=pipe_dir, delete=False) as fh:
357
+ fh.write(body.get("pipeline_yaml") or "")
358
+ pipeline_yaml = fh.name
359
+ spec = {
360
+ "pipeline_yaml": pipeline_yaml,
361
+ "only": body.get("only"),
362
+ "command": f"pipeline run {pipeline_yaml}",
363
+ }
364
+ return self.submit_pipeline(spec)