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,400 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Build a static demo-ui site that replays envelopes with no backend."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from mas.lab.controller.ui_stream.bus import EventBus
15
+ from mas.lab.controller.ui_stream.ingest import NativeIngest
16
+ from mas.lab.controller.ui_stream.layouts import load_layout
17
+ from mas.lab.controller.ui_stream.metrics import MetricsStore
18
+ from mas.lab.controller.ui_stream.refs import DataSourceRef, envelope
19
+ from mas.lab.controller.ui_stream.replay import (
20
+ custom_panel_records,
21
+ demo_records,
22
+ merge_replay_records,
23
+ parse_jsonl,
24
+ trace_metric_records,
25
+ )
26
+ from mas.lab.controller.ui_stream.spec_snapshot import snapshot_from_records
27
+
28
+ BUNDLE_VERSION = 1
29
+
30
+
31
+ def find_demo_ui_root(explicit: str | Path | None = None) -> Path:
32
+ if explicit:
33
+ root = Path(explicit)
34
+ if (root / "index.html").is_file():
35
+ return root
36
+ nested = root / "demo-ui"
37
+ if (nested / "index.html").is_file():
38
+ return nested
39
+ raise FileNotFoundError(f"demo-ui not found under {root}")
40
+ env = os.environ.get("MAS_DEMO_UI_ROOT")
41
+ if env:
42
+ return find_demo_ui_root(env)
43
+ here = Path(__file__).resolve()
44
+ for parent in here.parents:
45
+ cand = parent / "demo-ui"
46
+ if (cand / "index.html").is_file():
47
+ return cand
48
+ raise FileNotFoundError("demo-ui/index.html not found; set MAS_DEMO_UI_ROOT")
49
+
50
+
51
+ def default_layouts_root(ui_root: Path | None = None) -> Path:
52
+ override = os.environ.get("MAS_DEMO_UI_LAYOUTS")
53
+ if override:
54
+ return Path(override)
55
+ root = ui_root or find_demo_ui_root()
56
+ return root / "layouts"
57
+
58
+
59
+ def _sibling(anchor: Path, name: str) -> Path | None:
60
+ """Look for ``name`` next to ``anchor`` first, then one directory up (the
61
+ same two-level search every other sibling artefact here already uses,
62
+ since ``events.jsonl`` may sit directly in the run dir or under a nested
63
+ ``traces/`` subdirectory)."""
64
+ candidate = anchor.parent / name
65
+ if candidate.is_file():
66
+ return candidate
67
+ candidate = anchor.parent.parent / name
68
+ return candidate if candidate.is_file() else None
69
+
70
+
71
+ def resolve_trace_paths(
72
+ trace: Path,
73
+ ) -> tuple[Path, Path | None, Path | None, dict[str, Path]]:
74
+ """Return (events.jsonl, metrics.json, progress.jsonl, {owner: custom.{owner}.jsonl})
75
+ from a file or directory. The fourth element covers
76
+ ``custom.{owner}.jsonl`` sibling traces (same convention as
77
+ metrics.json/progress.jsonl -- see file_tail.append_custom_trace) so an
78
+ offline export carries the same panel data a live run or online replay
79
+ would have."""
80
+ path = Path(trace)
81
+ events: Path | None = None
82
+ if path.is_file() and path.name.endswith(".jsonl"):
83
+ events = path
84
+ elif path.is_dir():
85
+ for candidate in (path / "events.jsonl", path / "traces" / "events.jsonl"):
86
+ if candidate.is_file():
87
+ events = candidate
88
+ break
89
+ if events is None:
90
+ raise FileNotFoundError(f"no events.jsonl under {trace}")
91
+ metrics = _sibling(events, "metrics.json")
92
+ progress = _sibling(events, "progress.jsonl")
93
+ # custom.{owner}.jsonl siblings (file_tail.append_custom_trace) --
94
+ # discovered by glob, not a hardcoded owner list.
95
+ custom: dict[str, Path] = {}
96
+ for folder in (events.parent, events.parent.parent):
97
+ if not folder.is_dir():
98
+ continue
99
+ for found in folder.glob("custom.*.jsonl"):
100
+ owner = found.stem.split(".", 1)[1] if found.stem.count(".") >= 1 else ""
101
+ if owner:
102
+ custom.setdefault(owner, found)
103
+ return events, metrics, progress, custom
104
+
105
+
106
+ def build_envelopes(
107
+ records: list[dict[str, Any]],
108
+ ref: DataSourceRef,
109
+ *,
110
+ spec: dict[str, Any] | None = None,
111
+ extra_metrics: list[dict[str, Any]] | None = None,
112
+ ) -> list[dict[str, Any]]:
113
+ bus = EventBus(max_history=None)
114
+ ingest = NativeIngest(bus, MetricsStore())
115
+ topology = spec or snapshot_from_records(records)
116
+ bus.publish(envelope("spec.snapshot", ref, "spec", topology, replay=False))
117
+ ordered = merge_replay_records(demo_records(records), extra_metrics or [])
118
+ for record in ordered:
119
+ ingest.ingest(ref, record, replay=False)
120
+ ingest.flush_snapshot(ref)
121
+ return [item.as_dict() for item in bus.snapshot(("*",), DataSourceRef())]
122
+
123
+
124
+ def build_bundle(
125
+ trace: Path,
126
+ *,
127
+ layout_name: str,
128
+ layouts_root: Path | None = None,
129
+ ref: DataSourceRef | None = None,
130
+ spec: dict[str, Any] | None = None,
131
+ ) -> dict[str, Any]:
132
+ events_path, metrics_path, progress_path, custom_paths = resolve_trace_paths(Path(trace))
133
+ records = parse_jsonl(events_path.read_text(encoding="utf-8"))
134
+ extra = trace_metric_records(metrics=metrics_path, progress=progress_path, events=records)
135
+ for owner_path in custom_paths.values():
136
+ extra.extend(custom_panel_records(owner_path))
137
+ scoped = ref or DataSourceRef(mode="replay")
138
+ ui_root = None
139
+ try:
140
+ ui_root = find_demo_ui_root()
141
+ except FileNotFoundError:
142
+ ui_root = None
143
+ root = layouts_root or (default_layouts_root(ui_root) if ui_root else None)
144
+ layout: dict[str, Any] = {}
145
+ if root is not None:
146
+ layout = load_layout(root, layout_name)
147
+ if layout.get("dataSource"):
148
+ scoped = scoped.merge(layout.get("dataSource"))
149
+ envelopes = build_envelopes(records, scoped, spec=spec, extra_metrics=extra)
150
+ return {
151
+ "version": BUNDLE_VERSION,
152
+ "layoutName": layout_name,
153
+ "layout": layout,
154
+ "ref": scoped.as_dict(),
155
+ "envelopes": envelopes,
156
+ }
157
+
158
+
159
+ def build_merged_bundle(
160
+ scenario_traces: dict[str, Path],
161
+ *,
162
+ layout_name: str,
163
+ layouts_root: Path | None = None,
164
+ ref: DataSourceRef | None = None,
165
+ spec: dict[str, Any] | None = None,
166
+ ) -> dict[str, Any]:
167
+ """Build one bundle covering several scenarios at once -- for a
168
+ multi-panel compare layout where each panel scopes itself to one
169
+ ``dataSource.scenario``.
170
+
171
+ ``build_bundle`` builds one scenario's envelopes under one
172
+ ``DataSourceRef`` (one ``scenario``). The live and online-replay paths
173
+ already support several panels sharing one WS/bundle stream: the
174
+ frontend's own per-panel filter (``demo-ui/src/bus.js``'s
175
+ ``ref.scenario !== envelope.ref.scenario`` check) decides which
176
+ envelopes a given panel accepts, keyed off each envelope's own
177
+ ``ref.scenario`` -- not off which bundle/stream it came from. So a
178
+ merged *offline* bundle only has to put every scenario's own envelopes
179
+ (each correctly tagged with its own ``ref.scenario``) into one combined
180
+ ``envelopes`` list; no frontend change is needed, because the same
181
+ per-panel filter that already runs for live/online-replay runs for an
182
+ offline bundle too.
183
+
184
+ ``scenario_traces`` maps each panel's ``scenario`` slug (exactly what
185
+ that panel's ``dataSource.scenario`` says in the layout) to that
186
+ scenario's own trace (an ``events.jsonl`` file or a trace directory,
187
+ same as ``build_bundle``'s single-scenario ``trace`` argument).
188
+ """
189
+ scoped_base = ref or DataSourceRef(mode="replay")
190
+ ui_root = None
191
+ try:
192
+ ui_root = find_demo_ui_root()
193
+ except FileNotFoundError:
194
+ ui_root = None
195
+ root = layouts_root or (default_layouts_root(ui_root) if ui_root else None)
196
+ layout: dict[str, Any] = {}
197
+ if root is not None:
198
+ layout = load_layout(root, layout_name)
199
+ if layout.get("dataSource"):
200
+ scoped_base = scoped_base.merge(layout.get("dataSource"))
201
+
202
+ merged_envelopes: list[dict[str, Any]] = []
203
+ for scenario, trace in scenario_traces.items():
204
+ events_path, metrics_path, progress_path, custom_paths = resolve_trace_paths(Path(trace))
205
+ records = parse_jsonl(events_path.read_text(encoding="utf-8"))
206
+ extra = trace_metric_records(metrics=metrics_path, progress=progress_path, events=records)
207
+ for owner_path in custom_paths.values():
208
+ extra.extend(custom_panel_records(owner_path))
209
+ scoped = scoped_base.merge(DataSourceRef(scenario=scenario))
210
+ merged_envelopes.extend(build_envelopes(records, scoped, spec=spec, extra_metrics=extra))
211
+
212
+ return {
213
+ "version": BUNDLE_VERSION,
214
+ "layoutName": layout_name,
215
+ "layout": layout,
216
+ "ref": scoped_base.as_dict(),
217
+ "envelopes": merged_envelopes,
218
+ }
219
+
220
+
221
+ def export_merged_trace(
222
+ scenario_traces: dict[str, Path],
223
+ out_dir: Path,
224
+ *,
225
+ layout_name: str,
226
+ layouts_root: Path | None = None,
227
+ ref: DataSourceRef | None = None,
228
+ ui_root: Path | None = None,
229
+ dist_dir: Path | None = None,
230
+ build_ui: bool = True,
231
+ ) -> Path:
232
+ bundle = build_merged_bundle(
233
+ scenario_traces,
234
+ layout_name=layout_name,
235
+ layouts_root=layouts_root,
236
+ ref=ref,
237
+ )
238
+ return write_site(
239
+ Path(out_dir),
240
+ bundle,
241
+ ui_root=ui_root,
242
+ dist_dir=dist_dir,
243
+ build_ui=build_ui,
244
+ )
245
+
246
+
247
+ def _write_bundle_js(path: Path, bundle: dict[str, Any]) -> None:
248
+ payload = json.dumps(bundle, ensure_ascii=False)
249
+ path.write_text(f"window.__DEMO_UI_BUNDLE__ = {payload};\n", encoding="utf-8")
250
+
251
+
252
+ def _patch_index(html: str) -> str:
253
+ inject = (
254
+ '<script>window.__DEMO_UI_OFFLINE__=true;</script>\n'
255
+ '<script src="./bundle.js"></script>\n'
256
+ )
257
+ if "window.__DEMO_UI_OFFLINE__" in html:
258
+ return html
259
+ if "<head>" in html:
260
+ return html.replace("<head>", "<head>\n" + inject, 1)
261
+ if "</head>" in html:
262
+ return html.replace("</head>", inject + "</head>", 1)
263
+ return inject + html
264
+
265
+
266
+ def build_static_ui(ui_root: Path, *, base: str = "./") -> Path:
267
+ dist = ui_root / "dist"
268
+ env = os.environ.copy()
269
+ env["DEMO_UI_BASE"] = base
270
+ subprocess.run(
271
+ ["npm", "run", "build"],
272
+ cwd=ui_root,
273
+ env=env,
274
+ check=True,
275
+ )
276
+ if not dist.is_dir():
277
+ raise FileNotFoundError(f"vite dist missing at {dist}")
278
+ return dist
279
+
280
+
281
+ def write_site(
282
+ out_dir: Path,
283
+ bundle: dict[str, Any],
284
+ *,
285
+ ui_root: Path | None = None,
286
+ dist_dir: Path | None = None,
287
+ build_ui: bool = True,
288
+ ) -> Path:
289
+ dest = Path(out_dir)
290
+ dest.mkdir(parents=True, exist_ok=True)
291
+ root = ui_root
292
+ if dist_dir is None:
293
+ root = root or find_demo_ui_root()
294
+ existing = root / "dist"
295
+ if build_ui or not existing.is_dir():
296
+ dist_dir = build_static_ui(root)
297
+ else:
298
+ dist_dir = existing
299
+ if dist_dir is not None:
300
+ for item in Path(dist_dir).iterdir():
301
+ target = dest / item.name
302
+ if item.is_dir():
303
+ if target.exists():
304
+ shutil.rmtree(target)
305
+ shutil.copytree(item, target)
306
+ else:
307
+ shutil.copy2(item, target)
308
+ (dest / "bundle.json").write_text(
309
+ json.dumps(bundle, ensure_ascii=False, indent=2), encoding="utf-8"
310
+ )
311
+ _write_bundle_js(dest / "bundle.js", bundle)
312
+ index = dest / "index.html"
313
+ if index.is_file():
314
+ index.write_text(_patch_index(index.read_text(encoding="utf-8")), encoding="utf-8")
315
+ else:
316
+ index.write_text(
317
+ _patch_index(
318
+ "<!DOCTYPE html><html><head><meta charset='utf-8'>"
319
+ "<title>MAS Lab demo UI</title></head><body><div id='app'></div></body></html>"
320
+ ),
321
+ encoding="utf-8",
322
+ )
323
+ (dest / "README.txt").write_text(
324
+ "Offline demo UI. Serve this directory (ES modules need http):\n"
325
+ " python3 -m http.server 8766 --bind 127.0.0.1\n"
326
+ " open http://127.0.0.1:8766/\n",
327
+ encoding="utf-8",
328
+ )
329
+ return dest
330
+
331
+
332
+ def export_trace(
333
+ trace: Path,
334
+ out_dir: Path,
335
+ *,
336
+ layout_name: str = "run-view.yaml",
337
+ layouts_root: Path | None = None,
338
+ ref: DataSourceRef | None = None,
339
+ ui_root: Path | None = None,
340
+ dist_dir: Path | None = None,
341
+ build_ui: bool = True,
342
+ ) -> Path:
343
+ bundle = build_bundle(
344
+ Path(trace),
345
+ layout_name=layout_name,
346
+ layouts_root=layouts_root,
347
+ ref=ref,
348
+ )
349
+ return write_site(
350
+ Path(out_dir),
351
+ bundle,
352
+ ui_root=ui_root,
353
+ dist_dir=dist_dir,
354
+ build_ui=build_ui,
355
+ )
356
+
357
+
358
+ def main(argv: list[str] | None = None) -> int:
359
+ import argparse
360
+ import webbrowser
361
+ from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
362
+
363
+ parser = argparse.ArgumentParser(description="Export a static demo-ui replay from a native trace.")
364
+ parser.add_argument("trace", type=Path, help="events.jsonl or a trace/run directory")
365
+ parser.add_argument("-o", "--output", type=Path, required=True, help="Output directory")
366
+ parser.add_argument("--layout", default="run-view.yaml")
367
+ parser.add_argument("--layouts", type=Path, default=None)
368
+ parser.add_argument("--no-build", action="store_true", help="Reuse demo-ui/dist if present")
369
+ parser.add_argument("--serve", action="store_true", help="Serve the export and open a browser")
370
+ parser.add_argument("--port", type=int, default=8766)
371
+ args = parser.parse_args(argv)
372
+ dest = export_trace(
373
+ args.trace,
374
+ args.output,
375
+ layout_name=args.layout,
376
+ layouts_root=args.layouts,
377
+ build_ui=not args.no_build,
378
+ )
379
+ print(f"Wrote {dest}")
380
+ if not args.serve:
381
+ print(f"python3 -m http.server {args.port} --bind 127.0.0.1 --directory {dest}")
382
+ return 0
383
+
384
+ class Handler(SimpleHTTPRequestHandler):
385
+ def __init__(self, *a: Any, **kw: Any) -> None:
386
+ super().__init__(*a, directory=str(dest), **kw)
387
+
388
+ server = ThreadingHTTPServer(("127.0.0.1", args.port), Handler)
389
+ url = f"http://127.0.0.1:{args.port}/"
390
+ print(url)
391
+ webbrowser.open(url)
392
+ try:
393
+ server.serve_forever()
394
+ except KeyboardInterrupt:
395
+ return 0
396
+ return 0
397
+
398
+
399
+ if __name__ == "__main__":
400
+ raise SystemExit(main())
@@ -0,0 +1,141 @@
1
+ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Fold context size / token counts for the governance UI stream."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+ from mas.lab.controller.ui_stream.metrics import extract_cached_tokens, extract_usage
10
+ from mas.lab.controller.ui_stream.refs import DataSourceRef
11
+
12
+ _DROP_NATIVE = frozenset({"messages", "tools", "content", "output"})
13
+
14
+
15
+ def compact_context_record(record: dict[str, Any]) -> dict[str, Any]:
16
+ """Drop prompt text from context_assembled / context_usage records."""
17
+ out = {key: value for key, value in record.items() if key not in _DROP_NATIVE}
18
+ usage = out.get("context_usage")
19
+ if isinstance(usage, dict):
20
+ out["context_usage"] = {k: v for k, v in usage.items() if k not in _DROP_NATIVE}
21
+ return out
22
+
23
+
24
+ def _as_int(value: Any) -> int | None:
25
+ if value in (None, ""):
26
+ return None
27
+ try:
28
+ return int(value)
29
+ except (TypeError, ValueError):
30
+ return None
31
+
32
+
33
+ def _as_float(value: Any) -> float | None:
34
+ if value in (None, ""):
35
+ return None
36
+ try:
37
+ return float(value)
38
+ except (TypeError, ValueError):
39
+ return None
40
+
41
+
42
+ def _public(bucket: dict[str, Any]) -> dict[str, Any]:
43
+ return {key: value for key, value in bucket.items() if not str(key).startswith("_")}
44
+
45
+
46
+ class GovernanceStore:
47
+ def __init__(self) -> None:
48
+ self._by_key: dict[str, dict[str, Any]] = {}
49
+
50
+ def _key(self, ref: DataSourceRef) -> str:
51
+ return "|".join(
52
+ [
53
+ ref.library or "",
54
+ ref.experiment or "",
55
+ ref.scenario or "",
56
+ ref.overlay or "",
57
+ ref.session or ref.run or "",
58
+ ]
59
+ )
60
+
61
+ def _bucket(self, ref: DataSourceRef) -> dict[str, Any]:
62
+ return self._by_key.setdefault(self._key(ref), {})
63
+
64
+ def apply(self, ref: DataSourceRef, payload: dict[str, Any]) -> dict[str, Any]:
65
+ current = self._bucket(ref)
66
+ kind = str(payload.get("kind") or "")
67
+ updated = False
68
+ if payload.get("agent_id"):
69
+ current["agent_id"] = payload.get("agent_id")
70
+ updated = True
71
+ if payload.get("model"):
72
+ current["model"] = payload.get("model")
73
+ updated = True
74
+ usage = payload.get("context_usage") if isinstance(payload.get("context_usage"), dict) else {}
75
+ if kind in {"context_assembled", "context_usage"} or usage:
76
+ tokens = _as_int(
77
+ payload.get("context_tokens")
78
+ or payload.get("estimated_prompt_tokens")
79
+ or usage.get("estimated_prompt_tokens")
80
+ or payload.get("total_tokens")
81
+ )
82
+ if tokens is not None:
83
+ current["context_tokens"] = tokens
84
+ updated = True
85
+ window = _as_int(payload.get("context_window") or usage.get("context_window"))
86
+ if window is not None:
87
+ current["context_window"] = window
88
+ updated = True
89
+ fill = _as_float(payload.get("fill_ratio") or usage.get("fill_ratio"))
90
+ if fill is not None:
91
+ current["fill_ratio"] = fill
92
+ updated = True
93
+ segments = _as_int(payload.get("segments") or payload.get("message_count"))
94
+ if segments is not None:
95
+ current["segments"] = segments
96
+ updated = True
97
+ for key in ("cache_hits", "cache_misses"):
98
+ value = _as_int(payload.get(key) or usage.get(key))
99
+ if value is not None:
100
+ current[key] = value
101
+ updated = True
102
+ rate = _as_float(payload.get("cache_hit_rate") or usage.get("cache_hit_rate"))
103
+ if rate is not None:
104
+ current["cache_hit_rate"] = rate
105
+ updated = True
106
+ breakdown = payload.get("token_breakdown") or usage.get("token_breakdown")
107
+ if isinstance(breakdown, dict) and breakdown:
108
+ current["token_breakdown"] = {
109
+ str(k): int(v) for k, v in breakdown.items() if isinstance(v, (int, float))
110
+ }
111
+ updated = True
112
+ if kind == "llm_call_end":
113
+ folded = extract_usage(payload)
114
+ if folded.get("prompt_tokens") or folded.get("total_tokens") or folded.get("cached_tokens"):
115
+ current["prompt_tokens"] = int(folded["prompt_tokens"])
116
+ current["completion_tokens"] = int(folded["completion_tokens"])
117
+ current["cached_tokens"] = int(folded["cached_tokens"])
118
+ if not current.get("context_tokens") and folded["prompt_tokens"]:
119
+ current["context_tokens"] = int(folded["prompt_tokens"])
120
+ updated = True
121
+ extra = extract_cached_tokens(
122
+ payload.get("tokens_used") if isinstance(payload.get("tokens_used"), dict) else {}
123
+ )
124
+ if extra:
125
+ current["cached_tokens"] = extra
126
+ updated = True
127
+ if current.get("context_window") and current.get("context_tokens") and "fill_ratio" not in current:
128
+ current["fill_ratio"] = min(
129
+ float(current["context_tokens"]) / float(current["context_window"]), 1.0
130
+ )
131
+ updated = True
132
+ out = _public(current)
133
+ if updated:
134
+ out["_updated"] = 1
135
+ return out
136
+
137
+ def snapshot(self, ref: DataSourceRef) -> dict[str, Any]:
138
+ return _public(self._bucket(ref))
139
+
140
+ def clear(self) -> None:
141
+ self._by_key.clear()