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,137 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Worker model types."""
4
+ from __future__ import annotations
5
+
6
+ import enum
7
+ import time
8
+ import uuid
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Dict, List, Optional
11
+
12
+
13
+ class WorkerKind(str, enum.Enum):
14
+ APPLICATION = "application"
15
+ BENCHMARK = "benchmark"
16
+ EXPERIMENT = "experiment"
17
+ PIPELINE = "pipeline"
18
+
19
+
20
+ class WorkerStatus(str, enum.Enum):
21
+ PENDING = "pending"
22
+ RUNNING = "running"
23
+ COMPLETED = "completed"
24
+ FAILED = "failed"
25
+ CANCELLED = "cancelled"
26
+
27
+
28
+ @dataclass
29
+ class WorkerRecord:
30
+ """In-memory worker state owned by the controller daemon."""
31
+
32
+ id: str
33
+ kind: WorkerKind
34
+ status: WorkerStatus = WorkerStatus.PENDING
35
+ created_at: float = field(default_factory=time.time)
36
+ started_at: Optional[float] = None
37
+ finished_at: Optional[float] = None
38
+ exit_code: Optional[int] = None
39
+ error: Optional[str] = None
40
+ command: str = ""
41
+ endpoint: str = ""
42
+ spec: Dict[str, Any] = field(default_factory=dict)
43
+ stdout: List[str] = field(default_factory=list)
44
+ stderr: List[str] = field(default_factory=list)
45
+ stdout_text: str = ""
46
+ stderr_text: str = ""
47
+ result: Any = None
48
+ parent_id: Optional[str] = None
49
+ pid: Optional[int] = None
50
+
51
+ def append_stdout(self, line: str) -> None:
52
+ self.stdout.append(line)
53
+
54
+ def append_stderr(self, line: str) -> None:
55
+ self.stderr.append(line)
56
+
57
+ def append_stdout_chunk(self, text: str) -> None:
58
+ self.stdout_text += text
59
+
60
+ def append_stderr_chunk(self, text: str) -> None:
61
+ self.stderr_text += text
62
+
63
+ def combined_stdout(self) -> str:
64
+ parts = []
65
+ if self.stdout:
66
+ parts.append("\n".join(self.stdout))
67
+ if self.stdout_text:
68
+ parts.append(self.stdout_text)
69
+ return "\n".join(parts)
70
+
71
+ def combined_stderr(self) -> str:
72
+ parts = []
73
+ if self.stderr:
74
+ parts.append("\n".join(self.stderr))
75
+ if self.stderr_text:
76
+ parts.append(self.stderr_text)
77
+ return "\n".join(parts)
78
+
79
+ def to_job_summary(self) -> Dict[str, Any]:
80
+ origin = _ui_origin(self.spec)
81
+ summary = {
82
+ "id": self.id,
83
+ "kind": self.kind.value,
84
+ "endpoint": self.endpoint,
85
+ "command": self.command,
86
+ "status": self._job_status(),
87
+ "created_at": _iso(self.created_at),
88
+ "started_at": _iso(self.started_at),
89
+ "finished_at": _iso(self.finished_at),
90
+ "pid": self.pid,
91
+ "exit_code": self.exit_code,
92
+ "origin": origin,
93
+ }
94
+ if self.error:
95
+ summary["error"] = self.error
96
+ return summary
97
+
98
+ def to_job_detail(self) -> Dict[str, Any]:
99
+ detail = self.to_job_summary()
100
+ detail.update(
101
+ {
102
+ "stdout": self.combined_stdout(),
103
+ "stderr": self.combined_stderr(),
104
+ "error": self.error,
105
+ "request_body": self.spec,
106
+ }
107
+ )
108
+ return detail
109
+
110
+ def _job_status(self) -> str:
111
+ mapping = {
112
+ WorkerStatus.PENDING: "pending",
113
+ WorkerStatus.RUNNING: "running",
114
+ WorkerStatus.COMPLETED: "completed",
115
+ WorkerStatus.FAILED: "failed",
116
+ WorkerStatus.CANCELLED: "cancelled",
117
+ }
118
+ return mapping.get(self.status, "running")
119
+
120
+
121
+ def _ui_origin(spec: Dict[str, Any] | None) -> str | None:
122
+ ref = (spec or {}).get("demo_ui_ref") if isinstance(spec, dict) else None
123
+ if not isinstance(ref, dict) or not ref:
124
+ return None
125
+ return str(ref.get("origin") or "demo-ui")
126
+
127
+
128
+ def new_worker_id(prefix: str = "w") -> str:
129
+ return f"{prefix}-{uuid.uuid4().hex[:12]}"
130
+
131
+
132
+ def _iso(ts: Optional[float]) -> Optional[str]:
133
+ if ts is None:
134
+ return None
135
+ from datetime import datetime, timezone
136
+
137
+ return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
@@ -0,0 +1,296 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Worker implementations."""
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import logging
8
+ import os
9
+ import tempfile
10
+ import threading
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Any, Callable
14
+
15
+ from mas.lab.controller.io_capture import capture_worker_io
16
+ from mas.lab.controller.registry import WorkerRegistry
17
+ from mas.lab.controller.worker_model import WorkerKind, WorkerRecord, WorkerStatus
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class WorkerRunner:
23
+ """Runs worker callables in background threads."""
24
+
25
+ def __init__(self, registry: WorkerRegistry) -> None:
26
+ self.registry = registry
27
+ self._cancel_flags: dict[str, threading.Event] = {}
28
+
29
+ def submit(self, record: WorkerRecord, fn: Callable[[], Any]) -> None:
30
+ cancel = threading.Event()
31
+ self._cancel_flags[record.id] = cancel
32
+
33
+ def _run() -> None:
34
+ record.status = WorkerStatus.RUNNING
35
+ record.started_at = time.time()
36
+ record.pid = threading.get_ident()
37
+ try:
38
+ if cancel.is_set():
39
+ record.status = WorkerStatus.CANCELLED
40
+ return
41
+ result = fn()
42
+ if cancel.is_set():
43
+ record.status = WorkerStatus.CANCELLED
44
+ elif result is False:
45
+ record.status = WorkerStatus.FAILED
46
+ record.exit_code = 1
47
+ record.error = "worker returned failure"
48
+ else:
49
+ record.status = WorkerStatus.COMPLETED
50
+ record.exit_code = 0
51
+ record.result = result
52
+ except Exception as exc:
53
+ logger.exception("Worker %s failed", record.id)
54
+ record.status = WorkerStatus.FAILED
55
+ record.exit_code = 1
56
+ record.error = str(exc)
57
+ record.append_stderr(str(exc))
58
+ finally:
59
+ record.finished_at = time.time()
60
+ self._cancel_flags.pop(record.id, None)
61
+
62
+ threading.Thread(target=_run, daemon=True, name=f"worker-{record.id}").start()
63
+
64
+ def cancel(self, worker_id: str) -> bool:
65
+ event = self._cancel_flags.get(worker_id)
66
+ if event is not None:
67
+ event.set()
68
+ return self.registry.cancel(worker_id)
69
+
70
+
71
+ def run_benchmark_worker(registry: WorkerRegistry, runner: "WorkerRunner", spec: dict) -> WorkerRecord:
72
+ experiment_yaml = Path(spec["experiment_yaml"])
73
+ cmd_parts = ["mas-lab", "benchmark", "run", str(experiment_yaml)]
74
+ for flag in ("force", "dry_run", "single_run", "force_lock"):
75
+ if spec.get(flag):
76
+ cmd_parts.append(f"--{flag.replace('_', '-')}")
77
+ record = registry.create(
78
+ WorkerKind.BENCHMARK,
79
+ spec=spec,
80
+ command=" ".join(cmd_parts),
81
+ endpoint="/api/benchmark/run",
82
+ )
83
+
84
+ def _execute() -> bool:
85
+ import traceback
86
+
87
+ from mas.lab.benchmark.engine import BenchmarkRunOptions, run_benchmark
88
+
89
+ with capture_worker_io(record):
90
+ try:
91
+ ok = asyncio.run(
92
+ run_benchmark(
93
+ experiment_yaml=experiment_yaml,
94
+ options=BenchmarkRunOptions.from_spec(spec),
95
+ )
96
+ )
97
+ except Exception:
98
+ # Print the full traceback into the captured stream, then re-raise
99
+ # so the CLI shows the real cause, not just "benchmark run failed".
100
+ traceback.print_exc()
101
+ raise
102
+ if not ok:
103
+ raise RuntimeError(
104
+ "benchmark run failed — see the logged error above for the cause"
105
+ )
106
+ return ok
107
+
108
+ runner.submit(record, _execute)
109
+ return record
110
+
111
+
112
+ def _bind_demo_ui_env(spec: dict) -> dict[str, str | None]:
113
+ import json
114
+ import os
115
+
116
+ keys = (
117
+ "MAS_DEMO_UI_INGEST_URL",
118
+ "MAS_DEMO_UI_INGEST_KINDS",
119
+ "MAS_DEMO_UI_REF",
120
+ "MAS_DEMO_UI_PROMPT",
121
+ "MAS_INFRA_REFS",
122
+ "MAS_DEMO_UI_SCENARIO_INFRA",
123
+ "MAS_DEMO_UI_SCENARIO_OVERLAYS",
124
+ "MAS_LLM_CACHE",
125
+ )
126
+ previous = {key: os.environ.get(key) for key in keys}
127
+ ingest = spec.get("demo_ui_ingest_url")
128
+ if ingest:
129
+ os.environ["MAS_DEMO_UI_INGEST_URL"] = str(ingest)
130
+ os.environ.setdefault("MAS_DEMO_UI_INGEST_KINDS", "all")
131
+ ref = spec.get("demo_ui_ref")
132
+ if ref:
133
+ os.environ["MAS_DEMO_UI_REF"] = json.dumps(ref)
134
+ query = spec.get("query")
135
+ if query:
136
+ os.environ["MAS_DEMO_UI_PROMPT"] = str(query)
137
+ refs = spec.get("infra_refs") or []
138
+ if refs:
139
+ os.environ["MAS_INFRA_REFS"] = ",".join(str(item) for item in refs if item)
140
+ elif "MAS_INFRA_REFS" in os.environ and not spec.get("keep_process_infra"):
141
+ os.environ.pop("MAS_INFRA_REFS", None)
142
+ mapping = spec.get("scenario_infra_refs") or {}
143
+ if mapping:
144
+ os.environ["MAS_DEMO_UI_SCENARIO_INFRA"] = json.dumps(mapping)
145
+ elif "MAS_DEMO_UI_SCENARIO_INFRA" in os.environ and not spec.get("keep_process_infra"):
146
+ os.environ.pop("MAS_DEMO_UI_SCENARIO_INFRA", None)
147
+ overlays = spec.get("scenario_overlays") or {}
148
+ if overlays:
149
+ os.environ["MAS_DEMO_UI_SCENARIO_OVERLAYS"] = json.dumps(overlays)
150
+ elif "MAS_DEMO_UI_SCENARIO_OVERLAYS" in os.environ and not spec.get("keep_process_infra"):
151
+ os.environ.pop("MAS_DEMO_UI_SCENARIO_OVERLAYS", None)
152
+ cwd = spec.get("cwd")
153
+ if cwd and not os.environ.get("MAS_LLM_CACHE"):
154
+ eval_cache = Path(cwd) / "infra" / ".cache" / "eval.llm-cache.json"
155
+ os.environ["MAS_LLM_CACHE"] = str(eval_cache)
156
+ return previous
157
+
158
+
159
+ def _restore_env(previous: dict[str, str | None]) -> None:
160
+ import os
161
+
162
+ for key, value in previous.items():
163
+ if value is None:
164
+ os.environ.pop(key, None)
165
+ else:
166
+ os.environ[key] = value
167
+
168
+
169
+ def run_experiment_worker(registry: WorkerRegistry, runner: "WorkerRunner", spec: dict) -> WorkerRecord:
170
+ """In-process experiment worker: same engine as a benchmark, live ingest, no stdout dump."""
171
+ experiment_yaml = Path(spec["experiment_yaml"])
172
+ record = registry.create(
173
+ WorkerKind.EXPERIMENT,
174
+ spec=spec,
175
+ command=spec.get("command") or f"mas-lab experiment run {experiment_yaml}",
176
+ endpoint=spec.get("endpoint", "/api/experiment/run"),
177
+ )
178
+
179
+ def _execute() -> bool:
180
+ import traceback
181
+
182
+ from mas.lab.benchmark.engine import BenchmarkRunOptions, run_benchmark
183
+
184
+ previous = _bind_demo_ui_env(spec)
185
+ live_spec = {**spec, "progress": False}
186
+ cwd = spec.get("cwd")
187
+ prev_cwd = os.getcwd()
188
+ with capture_worker_io(record):
189
+ try:
190
+ if cwd:
191
+ os.chdir(str(cwd))
192
+ ok = asyncio.run(
193
+ run_benchmark(
194
+ experiment_yaml=experiment_yaml,
195
+ options=BenchmarkRunOptions.from_spec(live_spec),
196
+ )
197
+ )
198
+ except Exception:
199
+ traceback.print_exc()
200
+ raise
201
+ finally:
202
+ os.chdir(prev_cwd)
203
+ _restore_env(previous)
204
+ if not ok:
205
+ raise RuntimeError(
206
+ "experiment run failed — see the logged error above for the cause"
207
+ )
208
+ return ok
209
+
210
+ runner.submit(record, _execute)
211
+ return record
212
+
213
+
214
+ def run_application_worker(registry: WorkerRegistry, runner: "WorkerRunner", spec: dict) -> WorkerRecord:
215
+ record = registry.create(
216
+ WorkerKind.APPLICATION,
217
+ spec=spec,
218
+ command=spec.get("command", "application run"),
219
+ endpoint=spec.get("endpoint", "/api/run"),
220
+ )
221
+
222
+ def _execute() -> dict:
223
+ from mas.lab.runners.constants import DEFAULT_LAB_RUNNER_ID, normalize_runner_id
224
+ from mas.lab.runners.factory import RunnerFactory
225
+
226
+ runner_id = normalize_runner_id(spec.get("runner_id", DEFAULT_LAB_RUNNER_ID))
227
+ app_runner = RunnerFactory.get(runner_id)
228
+ output_dir = Path(spec.get("output_dir") or tempfile.mkdtemp(prefix="mas-lab-run-"))
229
+ output_dir.mkdir(parents=True, exist_ok=True)
230
+ result = app_runner.run(
231
+ spec["prompt"],
232
+ config=spec.get("config") or {},
233
+ spec_path=Path(spec["spec_path"]),
234
+ flavour=spec.get("flavour"),
235
+ output_dir=output_dir,
236
+ turns=spec.get("turns"),
237
+ session_id=spec.get("session_id"),
238
+ memory_seeds=spec.get("memory_seeds"),
239
+ run_seed=spec.get("run_seed", 0),
240
+ emulation_plugins=spec.get("emulation_plugins"),
241
+ )
242
+ return {
243
+ "content": result.content,
244
+ "status": result.status,
245
+ "error": result.error,
246
+ "output_dir": str(output_dir),
247
+ "metadata": result.metadata,
248
+ }
249
+
250
+ runner.submit(record, _execute)
251
+ return record
252
+
253
+
254
+ def run_pipeline_worker(registry: WorkerRegistry, runner: "WorkerRunner", spec: dict) -> WorkerRecord:
255
+ record = registry.create(
256
+ WorkerKind.PIPELINE,
257
+ spec=spec,
258
+ command=spec.get("command", "pipeline run"),
259
+ endpoint="/api/pipeline/run",
260
+ )
261
+
262
+ def _execute() -> Any:
263
+ import asyncio
264
+ from pathlib import Path
265
+
266
+ from mas.lab.benchmark.pipeline import Pipeline, PipelineExecutor
267
+ from mas.lab.benchmark.pipeline.cli import _load_lab_custom_steps
268
+ from mas.lab.benchmark.pipeline.config import PipelineEngineSpec
269
+ from mas.lab.benchmark.pipeline.channel import install_channel
270
+
271
+ pipeline_yaml = Path(spec["pipeline_yaml"])
272
+ _load_lab_custom_steps(pipeline_yaml.resolve())
273
+ try:
274
+ install_channel(PipelineEngineSpec())
275
+ except Exception:
276
+ pass
277
+ pipeline = Pipeline.from_yaml(pipeline_yaml)
278
+ executor = PipelineExecutor(
279
+ pipeline,
280
+ output_dir=Path(spec["output_dir"]) if spec.get("output_dir") else None,
281
+ )
282
+ result = asyncio.run(
283
+ executor.run(
284
+ steps=spec.get("only"),
285
+ force_rerun=spec.get("force"),
286
+ dry_run=spec.get("dry_run", False),
287
+ parallel=spec.get("parallel", True),
288
+ template_vars=spec.get("variables") or {},
289
+ )
290
+ )
291
+ if not result.success:
292
+ raise RuntimeError(f"pipeline failed — {result.summary()}")
293
+ return {"success": True, "summary": result.summary()}
294
+
295
+ runner.submit(record, _execute)
296
+ return record
@@ -0,0 +1,70 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Writable controller workspace: the place new apps and datasets land."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+ from mas.lab.controller.kinds import LIBRARY_BASE_KINDS
10
+ from mas.lab.controller.scope_catalog import Scope
11
+ from mas.runtime.constants import LIBRARY_MANIFEST_FILENAME
12
+
13
+ WORKSPACE_SLUG = "workspace"
14
+ WORKSPACE_SCOPE_ID = f"library:{WORKSPACE_SLUG}"
15
+
16
+ _MANIFEST = """\
17
+ apiVersion: mas/v1
18
+ kind: Library
19
+ metadata:
20
+ name: workspace
21
+ description: Controller workspace for new applications, overlays, and datasets
22
+ """
23
+
24
+ _root: Path | None = None
25
+
26
+
27
+ def init_workspace_library(parent: Path) -> Path:
28
+ """Create ``parent/library`` if needed and remember it for the catalog.
29
+
30
+ *parent* is the project workspace (``MAS_WORKSPACE_ROOT`` /
31
+ ``mas-lab serve --workspace``) when one is pinned, otherwise the
32
+ controller ``--state-dir``.
33
+ """
34
+ global _root
35
+ root = Path(parent) / "library"
36
+ root.mkdir(parents=True, exist_ok=True)
37
+ (root / "apps").mkdir(exist_ok=True)
38
+ (root / "overlays").mkdir(exist_ok=True)
39
+ (root / "datasets").mkdir(exist_ok=True)
40
+ manifest = root / LIBRARY_MANIFEST_FILENAME
41
+ if not manifest.is_file():
42
+ manifest.write_text(_MANIFEST, encoding="utf-8")
43
+ _root = root
44
+ return root
45
+
46
+
47
+ def reset_workspace_library() -> None:
48
+ """Clear the process-wide workspace pin (tests / apps without ``state_dir``)."""
49
+ global _root
50
+ _root = None
51
+
52
+
53
+ def workspace_root() -> Path | None:
54
+ return _root
55
+
56
+
57
+ def workspace_scope() -> Scope | None:
58
+ if _root is None:
59
+ return None
60
+ return Scope(
61
+ id=WORKSPACE_SCOPE_ID,
62
+ kind="library",
63
+ name=WORKSPACE_SLUG,
64
+ title="Workspace",
65
+ description="Controller workspace for new applications, overlays, and datasets",
66
+ origin="workspace",
67
+ writable=True,
68
+ root=_root.resolve(),
69
+ kinds=list(LIBRARY_BASE_KINDS),
70
+ )
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.5
2
+ Name: mas-lab-component-controller
3
+ Version: 0.2.0
4
+ Summary: MAS Lab controller daemon — workers, IPC, HTTP API for CLI and UI.
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: click>=8.0
8
+ Requires-Dist: fastapi>=0.110
9
+ Requires-Dist: mas-lab-component-bench
10
+ Requires-Dist: mas-lab-component-core
11
+ Requires-Dist: mas-lab-ctl
12
+ Requires-Dist: mas-lab-runtime
13
+ Requires-Dist: pydantic>=2.0
14
+ Requires-Dist: python-multipart>=0.0.9
15
+ Requires-Dist: pyyaml>=6.0
16
+ Requires-Dist: uvicorn[standard]>=0.27
17
+ Provides-Extra: dev
18
+ Requires-Dist: httpx>=0.27; extra == 'dev'
19
+ Requires-Dist: pytest-asyncio; extra == 'dev'
20
+ Requires-Dist: pytest-cov>=6.0; extra == 'dev'
21
+ Requires-Dist: pytest>=9.0; extra == 'dev'
@@ -0,0 +1,89 @@
1
+ mas/lab/controller/__init__.py,sha256=XLifiSasGCaun4Id8QUcaLEmwZJdDskRtOvfIcg2zzQ,347
2
+ mas/lab/controller/__main__.py,sha256=8KchWhPetfnGEWp_WyUAQnuvXcfO35AF-vSo2f71Tlw,290
3
+ mas/lab/controller/agent_chat.py,sha256=smM3NrvQoKc17cBfRCuOxum0MemG0TgfTFs2any9QcY,6743
4
+ mas/lab/controller/api.py,sha256=aM3Kopab5nYkImfQqK1XzzIHXyFykAh44eaY5bSbvfY,15008
5
+ mas/lab/controller/artifact_discovery.py,sha256=epfftnYyTauQVcQ12Oin8cpG8jnkbCX8xk3la2z0ZLU,4046
6
+ mas/lab/controller/artifacts.py,sha256=Q1Tj8z0yqYoJjDyZt1b1_G5zTxP3i-y2I1_HffgtI5k,3395
7
+ mas/lab/controller/cli.py,sha256=aR0q9YDVG_Q5PZVq3nCCoZ6DQ5jYf53X49bppANKn6A,17126
8
+ mas/lab/controller/client.py,sha256=ok9nl0ivoyeS1fv79WEayOhBthZdY0xUKS9basMmLb0,9226
9
+ mas/lab/controller/config.py,sha256=8j7gbMAuj6-CvXIWm41enEXY7FG5RltsBSe8cwsdQgc,2659
10
+ mas/lab/controller/constants.py,sha256=EVrABwkEiFWqPThSqI8us50bNqkIQRWA1bgZ40H_l-g,838
11
+ mas/lab/controller/daemon.py,sha256=UE5miB4W-jywl6FGGFcFt5BLr3PUYXc_2Qjpi_pmrTA,6813
12
+ mas/lab/controller/deps.py,sha256=ZDLENKw51wKcUBFgMR2dNmsPywctfnbOr7Mozx1JFfM,5612
13
+ mas/lab/controller/fastapi_app.py,sha256=cSggjH-88OkPIDc8rEPf_RI2HNG0oaR1_hYC6gVEY1U,5518
14
+ mas/lab/controller/http_server.py,sha256=d8cXAFtn8E_LmTn_qRLu2NDUoMrZ1NzT5rpaFESdQUk,11020
15
+ mas/lab/controller/io_capture.py,sha256=qoftW6x1jmhfgpxaWlnEWhdNOqVLxBIAAXDDUzLy3cA,1726
16
+ mas/lab/controller/item_service.py,sha256=SGV4DUTAPgQilLA8J2c4gwVIonGduu1pAGDG7hHmLQQ,21109
17
+ mas/lab/controller/job_store.py,sha256=Hi-fIEAWHvl5y2O2W8ntenpVaJ52KKql5R3SjhpjVAY,6042
18
+ mas/lab/controller/jobs.py,sha256=uS2i-GuordSy0tF1IdYgyzILYxiJ6rJhsn5uIi9vKO4,10889
19
+ mas/lab/controller/kinds.py,sha256=zWtiDh0NKELXY9Ycz-VEnojV_5rSe4vmQL9B2nybHxA,4034
20
+ mas/lab/controller/lab_registry.py,sha256=9ykKAGq1VJAeG6cEEYNfNLq90kjqzmM-YR5vLR6zkg0,43761
21
+ mas/lab/controller/manifest_layout.py,sha256=pF2b3_YKgLXT7aVWHjnneqZUn7uhbpTM9rTbFOZwMog,28422
22
+ mas/lab/controller/manifest_store.py,sha256=LltRk-jr9HTKhfBC_MROnFK0RNmxsMnE55DrLsI5lCk,8464
23
+ mas/lab/controller/manifest_validation.py,sha256=0es70b2SwkkVDMpqqr2nU3TVs_hZQ2y1_xk_T_hf_VU,2705
24
+ mas/lab/controller/models.py,sha256=9VoM1RNiNmO4STu_jl01PTVWsrWE4EFcErM0_kZt7AA,9052
25
+ mas/lab/controller/path_safety.py,sha256=16iP0_RTSoJen40yGHfHdRxITtfulDX4zAzYI0Sjz4M,2390
26
+ mas/lab/controller/pipeline_validation.py,sha256=tbXLukZ95MSUTzsPHhUHhbqmJvoioPqC06q6VSKOOec,2714
27
+ mas/lab/controller/registry.py,sha256=yzD4AufuesCYFujz62IXm-d28t7eznpM0-Oqi8J5oUg,2773
28
+ mas/lab/controller/run_bridge.py,sha256=IpHk5w_EH3bG3ueExq3k3Zx44_u3h9wYxS5JUzf1--4,8667
29
+ mas/lab/controller/run_store.py,sha256=jT2m948lrW58sBo8u4ugIYT-iblGLBbbv0X28iqn4Zs,5521
30
+ mas/lab/controller/schema_registry.py,sha256=wWlFARQGPwl1U7fZgZoEqLUPybWk5kpfgUF4HZppTdQ,4726
31
+ mas/lab/controller/scope_catalog.py,sha256=RIVSb9JXydMzMIYcqhVlnNhsbjcP7LbDekNTQq6kMrU,9690
32
+ mas/lab/controller/scope_service.py,sha256=W6thTeKnmiSN9eJC6y5dZPFyCYdMk0N-dGGEGhvo60E,4559
33
+ mas/lab/controller/sessions.py,sha256=q5Ls-K32CngLEPjn9L2PVCQJK5IxJvmbvbxw1HesiOA,2129
34
+ mas/lab/controller/settings.py,sha256=heAbkQ7MiPI2HtxIR4Y156g8t6lQqBKCnBtsYuhnMAo,1860
35
+ mas/lab/controller/uploads.py,sha256=q2ZG3IF6CA8A6A1hnv4XZRxLIkP5bZJsOtul3hU0n3I,1696
36
+ mas/lab/controller/worker_model.py,sha256=6baEqc59May2bPM7EtDreV3dDB2DM4XqXw4UGxWxtFc,4019
37
+ mas/lab/controller/workers.py,sha256=gkWdQlti6h01oMToPI423_X5CZga-f80mS_8Qp8emwA,10715
38
+ mas/lab/controller/workspace_library.py,sha256=GlbrlzF4FmKRn2G2YAf2ahoA4Ij0UBo6bu9_hvTIrXU,2018
39
+ mas/lab/controller/routes/__init__.py,sha256=9cFo_KpfmW9ualwAmhXMLtcwMtWpMUm3ddn6VpgsHc4,2377
40
+ mas/lab/controller/routes/_api.py,sha256=KzyTu3OV3d9p9VITIeznXk71xaAp-J8eO4wUVSMrMfo,483
41
+ mas/lab/controller/routes/apps.py,sha256=ztdynzbxhbMfhxZ5sH3kD7kQt2M_I2j33AeXN0unFxU,7201
42
+ mas/lab/controller/routes/artifacts.py,sha256=k6eAfpVDWmDO7CH4jL09Ch6xVzd7bzD84JTM7PXbNu8,1251
43
+ mas/lab/controller/routes/benchmark.py,sha256=GTFQNizOrQt-I7iXt-EdinN-5k4YoqzrhxYevTGK4V8,11907
44
+ mas/lab/controller/routes/choices.py,sha256=UDnmaoL6VL4Pb8okys7KCiJgrcup71nDzJr01Jh2mIY,4913
45
+ mas/lab/controller/routes/datasets.py,sha256=SCPMV_6COC50h6TPvIIywmW-RlEUQoOjQMpfAaBf6b0,3627
46
+ mas/lab/controller/routes/design_patterns.py,sha256=4yT0lnDZnRGmjTCRG6CE0Mr7G-ksbtXLnxvOMS6cH5Y,4294
47
+ mas/lab/controller/routes/eval_output.py,sha256=y_URJXvkpguw06jJ_vFpsQliLuto7I6QKZtMXsDnv6U,1123
48
+ mas/lab/controller/routes/experiments.py,sha256=1YnHRUOeiZPwhYyEY-mgWLI7xq18hp_9dy4gVCR7awg,7691
49
+ mas/lab/controller/routes/health.py,sha256=bKqwVRRTLb1faF935y5B3b0FiEc3r1ThCprcDSf1ZyM,5988
50
+ mas/lab/controller/routes/items.py,sha256=LbXs6OtvZ9VtS1wWhanCMLuqJLhEpc4yRzqMDTczm0A,5997
51
+ mas/lab/controller/routes/jobs.py,sha256=AXD8fzRYHdDmRs5-Kzn2iLTirbAg8R2DGYkuDjFoh1Y,3895
52
+ mas/lab/controller/routes/libraries.py,sha256=drV_YR_OWvJQUoD0Lim97VAHSqjjQoqYJdRCxNph8Wg,5611
53
+ mas/lab/controller/routes/overlays.py,sha256=7zK7AahxJ0tDCc54HdvJKn5LelHIIBGlW8_WY5mDerE,6647
54
+ mas/lab/controller/routes/pipelines.py,sha256=-7eFcr6UqqRLLlgtH-b_0qBdgrNo4YIyI7N-c565i54,5699
55
+ mas/lab/controller/routes/registry.py,sha256=OW2Mm86G5utmWaSadugPcZcm_30m5-gaB-UDlhEfC9s,1307
56
+ mas/lab/controller/routes/run.py,sha256=noofHo7CHyMOS9XDvqfmpIb3xTYGxXJhODP5zqAUPS8,7507
57
+ mas/lab/controller/routes/runs.py,sha256=FspLVdpu-oaPi1fH1OpIz3cyEQS6kmiNUwcrzOZ8aIA,13913
58
+ mas/lab/controller/routes/scopes.py,sha256=eBVMFlo_f1lVJO3-GRLfBtPYWLYwXsuoJY5maanUzVU,5021
59
+ mas/lab/controller/routes/uploads.py,sha256=hdAXuwY8qyVKbMVGGLA69M3c2KFNr4SQTaOiz6FabpA,808
60
+ mas/lab/controller/routes/validate.py,sha256=TfFy513V0UVyZLKJgkGcA2B8oYnq-kdQQfIRanHFvMo,1452
61
+ mas/lab/controller/schemas/pipeline-step-types-post.json,sha256=vtvYp-sYSVynbqDsd9bys_K9TqZcpzDAx2E93A0Ez0w,24320
62
+ mas/lab/controller/schemas/pipeline-step-types-pre.json,sha256=yv5G2i37nMNrF34W00Ld-qlJGROnlgnEwOiyQAiyB-8,1446
63
+ mas/lab/controller/ui_stream/__init__.py,sha256=YESQ5Qt3S9G0gsCW7YwSgl_Guw0DDAC4gK7Y9xbtbYI,392
64
+ mas/lab/controller/ui_stream/app.py,sha256=WBpjGHFD3FCbWkogl8DqxhukyucA-QPVqXEcCjTg9LM,77850
65
+ mas/lab/controller/ui_stream/artefact_bind.py,sha256=9-6ciWsSZvTuGdWHgN54g6wZdnM_kpJ1CymdTrLcSoI,10548
66
+ mas/lab/controller/ui_stream/auth.py,sha256=29haISdJ521_KQX3gRx1TdNW59fVCtGCgC-B_djApvU,4234
67
+ mas/lab/controller/ui_stream/bus.py,sha256=U2GG3gIdWBGZMDh_9HBDQZu2sHieBVGSNSo0Mjmtotw,5470
68
+ mas/lab/controller/ui_stream/export.py,sha256=_2MsMzCkz-4yDAkS_yg1geiODS-hGJdvuZuqqQVW3pQ,14128
69
+ mas/lab/controller/ui_stream/governance.py,sha256=OEM8Qn7PdcVpFgA8aOoi2qk_8RHM4Zwg0QLyiTLjNB4,5638
70
+ mas/lab/controller/ui_stream/ingest.py,sha256=0PEmmcjWpN3594gdKW4qLP9gjvDPZVDFOusaK2PFu3c,11762
71
+ mas/lab/controller/ui_stream/inspect.py,sha256=z_tyJKW4DEbIMjao9fFxAHP7Ur2-xdkxSxtLW315ahA,1794
72
+ mas/lab/controller/ui_stream/layouts.py,sha256=2iTV6b4vzXd0_NvSdIWaAxb0qvE5KwSv2kfLU4jcnrQ,6171
73
+ mas/lab/controller/ui_stream/metrics.py,sha256=iPN5wF_tl5cAgeHdE4SwjNnq_P4lglU_usp-UOCMf18,9697
74
+ mas/lab/controller/ui_stream/plot_folds.py,sha256=Mk8ROe8VWFwGGZpFyhMulp8QRmqL2mQIYETp-dkJ0bw,21363
75
+ mas/lab/controller/ui_stream/plugin_registry.py,sha256=QFont6XVqB-IiEOUctO-uGx69cVUzdY-G_7EegBNVm0,4082
76
+ mas/lab/controller/ui_stream/refs.py,sha256=8y8mCOokJqNFMCOzCLwu_6FWOab7L8B_eJ5vTfqPwOs,7722
77
+ mas/lab/controller/ui_stream/replay.py,sha256=pSIH5TanEFbY4g_88Rzat2gwKFIXKCjSjNvdl93A4Fc,8116
78
+ mas/lab/controller/ui_stream/replay_cli.py,sha256=yHxy9UXXfEKawNxRa7Fq7sP8bZBuGoIDRYHDcZ83bSg,25753
79
+ mas/lab/controller/ui_stream/run_index.py,sha256=Qfklq4CYFrGBiRpg1yzwlkF-omL1vo-OpzImChGlZRE,11736
80
+ mas/lab/controller/ui_stream/spec_load.py,sha256=lBLGEKU9aMzgh8_s4VSkHWin3riya-P6SuziCpLWjsU,1814
81
+ mas/lab/controller/ui_stream/spec_snapshot.py,sha256=ZBfPkzJWigoF6PtYjFCI1i1BORnoYCgTQ8bJblagCAU,11037
82
+ mas/lab/controller/ui_stream/stream.py,sha256=1DYCA7qVNxacXSeDjQkj5VAya_ymDUSuxo07nrIwa1M,3152
83
+ mas/lab/controller/ui_stream/streams.py,sha256=riyKrKWVxvYFxSa5mjdgYumkIwFW-ahWPAa4iZ5Ny1Q,1573
84
+ mas/lab/controller/ui_stream/timeline.py,sha256=RyUIOh_EdbB3CBWQn5B6Kr9c1dMC6FVdvsWP_Z6GNdw,5776
85
+ mas/lab/controller/ui_stream/ui_trace.py,sha256=8T8WN6-W1eyvSQuWk3vRsQ0v3fhOLPEhFCXzR5Tl_X8,9439
86
+ mas_lab_component_controller-0.2.0.dist-info/METADATA,sha256=jOTMfrEiVfOjtyYKPgRqRlmE8gihMfoUxhbM1nycaeU,718
87
+ mas_lab_component_controller-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
88
+ mas_lab_component_controller-0.2.0.dist-info/entry_points.txt,sha256=ROEyM3R_CDwZy8STHFNl4on8qShxpY8rkkW2ZfbmGsw,84
89
+ mas_lab_component_controller-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [mas.lab.cli.components]
2
+ controller = mas.lab.controller.cli:ControllerCliComponent