Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Upload 6 files
Browse files- CHANGELOG.md +8 -0
- README.md +4 -0
- app.py +139 -52
CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Agentic Space Factory v190.9
|
| 2 |
|
| 3 |
- Persists/canonicalizes terminal Space Test validation status across page reloads so terminal artifacts/events override stale `running` launch metadata.
|
|
|
|
| 1 |
+
# Agentic Space Factory v190.10
|
| 2 |
+
|
| 3 |
+
- Live polling now uses the same canonical run snapshot as run selection/reload, so terminal builds converge across Active Run, Runs Explorer, Run Stats, and Eval Archive without a page refresh.
|
| 4 |
+
- Intermediate fallback/wait/test failures no longer mark the whole build as failed unless the canonical timeline is terminal.
|
| 5 |
+
- Initial build launch shows a quiet snapshot wait state instead of rendering the legacy full vertical timeline.
|
| 6 |
+
- Current step meta was removed; the canonical timeline owns phase context.
|
| 7 |
+
- Timeline phase details are hidden on normal happy-path phases and shown only for warnings, fallback, recovery, manual actions, archive issues, or terminal diagnostics.
|
| 8 |
+
|
| 9 |
# Agentic Space Factory v190.9
|
| 10 |
|
| 11 |
- Persists/canonicalizes terminal Space Test validation status across page reloads so terminal artifacts/events override stale `running` launch metadata.
|
README.md
CHANGED
|
@@ -12,6 +12,10 @@ hf_oauth_scopes:
|
|
| 12 |
- jobs
|
| 13 |
- read-billing
|
| 14 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
## v190.6 — Space Test terminal-state lock
|
| 17 |
|
|
|
|
| 12 |
- jobs
|
| 13 |
- read-billing
|
| 14 |
---
|
| 15 |
+
## v190.10 live convergence validation
|
| 16 |
+
|
| 17 |
+
- Active Run polling, Runs Explorer cards, Run Stats, and Eval Archive must converge to the same terminal snapshot without a browser refresh.
|
| 18 |
+
- Intermediate fallback/live wait failures are diagnostics only; they must not mark the run failed unless the canonical timeline is terminal.
|
| 19 |
|
| 20 |
## v190.6 — Space Test terminal-state lock
|
| 21 |
|
app.py
CHANGED
|
@@ -210,6 +210,96 @@ def _merge_events(bucket_events: list[dict[str, Any]], log_events: list[dict[str
|
|
| 210 |
merged.append(event)
|
| 211 |
return merged
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
def register_custom_routes(fastapi_app: FastAPI) -> None:
|
| 214 |
"""Register the root custom UI and OAuth-backed JSON endpoints."""
|
| 215 |
|
|
@@ -527,27 +617,25 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 527 |
if not selected:
|
| 528 |
return JSONResponse({"run": None, "view": None, "bucket_source": bucket_source, "reason": "no_run"})
|
| 529 |
run_id = validate_run_id(str(selected.get("run_id")))
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
return JSONResponse({"run": selected, "view": view, "bucket_source": bucket_source, "reason": "latest_resumable_run" if resumable else "latest_run"})
|
| 533 |
|
| 534 |
@fastapi_app.get("/api/runs/{run_id}")
|
| 535 |
async def api_run_detail(request: Request, run_id: str, bucket_name: str = settings.bucket_name, include_heavy: bool = True): # type: ignore[no-untyped-def]
|
| 536 |
ctx = _oauth_context_from_request(request)
|
| 537 |
run_id = validate_run_id(run_id)
|
| 538 |
bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
return JSONResponse({"run_id": run_id, "bucket_source": bucket_source, "view": view, "eval_publish": eval_publish, **bundle})
|
| 543 |
|
| 544 |
@fastapi_app.get("/api/runs/{run_id}/view")
|
| 545 |
async def api_run_view(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
|
| 546 |
ctx = _oauth_context_from_request(request)
|
| 547 |
run_id = validate_run_id(run_id)
|
| 548 |
bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
|
| 549 |
-
|
| 550 |
-
return JSONResponse(
|
| 551 |
|
| 552 |
@fastapi_app.post("/api/runs/{run_id}/cancel")
|
| 553 |
async def api_cancel_run(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
|
|
@@ -651,10 +739,11 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 651 |
ctx = _oauth_context_from_request(request)
|
| 652 |
run_id = validate_run_id(run_id)
|
| 653 |
bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
|
|
|
| 658 |
job_id = _job_id_from_run_bundle(summary, launch, state)
|
| 659 |
job_info: dict[str, Any] = {}
|
| 660 |
job_logs = ""
|
|
@@ -663,44 +752,41 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 663 |
job_info = inspect_job_safe(job_id, token=ctx["token"])
|
| 664 |
job_logs = fetch_recent_logs_safe(job_id, token=ctx["token"], max_lines=1000)
|
| 665 |
job_log_events = _events_from_job_logs(job_logs)
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
events = _merge_events(bundle.get("events") or [], job_log_events)
|
| 694 |
progress = progress_from_events(events, state=effective_state)
|
| 695 |
-
if
|
| 696 |
-
|
| 697 |
-
progress["validation_status"] = summary_status
|
| 698 |
-
progress["result_status"] = summary_status
|
| 699 |
progress["terminal"] = True
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
progress.update(
|
| 705 |
{
|
| 706 |
"run_id": run_id,
|
|
@@ -722,12 +808,13 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 722 |
"run_documents": bundle.get("run_documents") or [],
|
| 723 |
"events": events[-20:],
|
| 724 |
"view": view,
|
| 725 |
-
"eval_publish": eval_publish,
|
|
|
|
| 726 |
"links": view.get("links") or _api_links(
|
| 727 |
run_id=run_id,
|
| 728 |
bucket_source=bucket_source,
|
| 729 |
-
target_space=effective_state.get("target_space") or
|
| 730 |
-
job_url=effective_state.get("job_url") or
|
| 731 |
),
|
| 732 |
}
|
| 733 |
)
|
|
|
|
| 210 |
merged.append(event)
|
| 211 |
return merged
|
| 212 |
|
| 213 |
+
|
| 214 |
+
def _terminal_status_value(value: Any) -> bool:
|
| 215 |
+
return str(value or "").strip().lower() in {
|
| 216 |
+
"succeeded",
|
| 217 |
+
"success",
|
| 218 |
+
"done",
|
| 219 |
+
"completed",
|
| 220 |
+
"full_inference_success",
|
| 221 |
+
"partial",
|
| 222 |
+
"partial_validation",
|
| 223 |
+
"failed",
|
| 224 |
+
"failure",
|
| 225 |
+
"blocked",
|
| 226 |
+
"technical_blocker",
|
| 227 |
+
"manual_hardware_required",
|
| 228 |
+
"waiting_manual_action",
|
| 229 |
+
"cancelled",
|
| 230 |
+
"canceled",
|
| 231 |
+
"stopped",
|
| 232 |
+
"stale",
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def _build_live_run_snapshot(
|
| 237 |
+
run_id: str,
|
| 238 |
+
*,
|
| 239 |
+
bucket_source: str,
|
| 240 |
+
token: str | None,
|
| 241 |
+
include_heavy: bool = False,
|
| 242 |
+
job_info: dict[str, Any] | None = None,
|
| 243 |
+
job_log_events: list[dict[str, Any]] | None = None,
|
| 244 |
+
) -> dict[str, Any]:
|
| 245 |
+
"""Build the canonical UI snapshot shared by detail, view and polling routes.
|
| 246 |
+
|
| 247 |
+
v190.10 invariant: Active Run polling, manual Run Explorer selection, and
|
| 248 |
+
full page reload must converge to the same product status, timeline model,
|
| 249 |
+
eval archive state and quick links.
|
| 250 |
+
"""
|
| 251 |
+
bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=token, include_heavy=include_heavy)
|
| 252 |
+
state = bundle.get("state") or {}
|
| 253 |
+
launch = bundle.get("launch") or {}
|
| 254 |
+
summary = bundle.get("summary") or {}
|
| 255 |
+
job_info = job_info or {}
|
| 256 |
+
job_stage = _normalize_job_stage(job_info.get("stage"))
|
| 257 |
+
events = _merge_events(bundle.get("events") or [], job_log_events or [])
|
| 258 |
+
|
| 259 |
+
effective_state = {**launch, **state}
|
| 260 |
+
if job_stage and not job_info.get("error"):
|
| 261 |
+
effective_state["job_stage"] = job_stage
|
| 262 |
+
# Job stage is live metadata, not a product verdict. Only use it to
|
| 263 |
+
# keep a newly launched run visibly running before bucket events exist.
|
| 264 |
+
if job_stage in {"running", "cancelled"} and not _terminal_status_value(summary.get("status")):
|
| 265 |
+
effective_state.setdefault("status", job_stage)
|
| 266 |
+
|
| 267 |
+
summary_status = str(summary.get("status") or "").lower()
|
| 268 |
+
is_validation_run = str(run_id).startswith("validate-") or "validate" in str(summary.get("kind") or effective_state.get("kind") or "").lower()
|
| 269 |
+
validation_terminal_statuses = {"failed", "full_inference_success", "partial_validation", "manual_hardware_required", "stale", "stopped"}
|
| 270 |
+
validation_terminal_locked = bool(is_validation_run and summary_status in validation_terminal_statuses)
|
| 271 |
+
if validation_terminal_locked:
|
| 272 |
+
effective_state["status"] = summary_status
|
| 273 |
+
effective_state["validation_status"] = summary_status
|
| 274 |
+
effective_state["result_status"] = summary_status
|
| 275 |
+
effective_state["terminal"] = True
|
| 276 |
+
effective_state["can_resume"] = False
|
| 277 |
+
|
| 278 |
+
effective_summary = {
|
| 279 |
+
**summary,
|
| 280 |
+
**({"status": summary_status, "validation_status": summary_status, "result_status": summary_status, "terminal": True, "can_resume": False} if validation_terminal_locked else {}),
|
| 281 |
+
}
|
| 282 |
+
view_bundle = {**bundle, "events": events, "state": effective_state, "summary": effective_summary}
|
| 283 |
+
|
| 284 |
+
pre_view = build_run_view_model(run_id, view_bundle, bucket_source=bucket_source)
|
| 285 |
+
pre_status = str(pre_view.get("status_model", {}).get("global_status") or effective_summary.get("status") or effective_state.get("status") or "").lower()
|
| 286 |
+
force_eval = pre_view.get("timeline_model", {}).get("progress", {}).get("terminal") is True or _terminal_status_value(pre_status)
|
| 287 |
+
eval_publish = maybe_publish_eval_record(run_id, bucket_source=bucket_source, token=token, state=effective_state, force=force_eval)
|
| 288 |
+
view_bundle["eval_publish"] = eval_publish
|
| 289 |
+
view_bundle["eval_publish_status"] = eval_publish
|
| 290 |
+
view = build_run_view_model(run_id, view_bundle, bucket_source=bucket_source)
|
| 291 |
+
|
| 292 |
+
return {
|
| 293 |
+
"run_id": run_id,
|
| 294 |
+
"bucket_source": bucket_source,
|
| 295 |
+
"bundle": view_bundle,
|
| 296 |
+
"summary": effective_summary,
|
| 297 |
+
"state": effective_state,
|
| 298 |
+
"events": events,
|
| 299 |
+
"view": view,
|
| 300 |
+
"eval_publish": eval_publish,
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
def register_custom_routes(fastapi_app: FastAPI) -> None:
|
| 304 |
"""Register the root custom UI and OAuth-backed JSON endpoints."""
|
| 305 |
|
|
|
|
| 617 |
if not selected:
|
| 618 |
return JSONResponse({"run": None, "view": None, "bucket_source": bucket_source, "reason": "no_run"})
|
| 619 |
run_id = validate_run_id(str(selected.get("run_id")))
|
| 620 |
+
snapshot = _build_live_run_snapshot(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
|
| 621 |
+
return JSONResponse({"run": selected, "view": snapshot["view"], "bucket_source": bucket_source, "reason": "latest_resumable_run" if resumable else "latest_run"})
|
|
|
|
| 622 |
|
| 623 |
@fastapi_app.get("/api/runs/{run_id}")
|
| 624 |
async def api_run_detail(request: Request, run_id: str, bucket_name: str = settings.bucket_name, include_heavy: bool = True): # type: ignore[no-untyped-def]
|
| 625 |
ctx = _oauth_context_from_request(request)
|
| 626 |
run_id = validate_run_id(run_id)
|
| 627 |
bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
|
| 628 |
+
snapshot = _build_live_run_snapshot(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=include_heavy)
|
| 629 |
+
bundle = snapshot["bundle"]
|
| 630 |
+
return JSONResponse({"run_id": run_id, "bucket_source": bucket_source, "view": snapshot["view"], "eval_publish": snapshot["eval_publish"], **bundle})
|
|
|
|
| 631 |
|
| 632 |
@fastapi_app.get("/api/runs/{run_id}/view")
|
| 633 |
async def api_run_view(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
|
| 634 |
ctx = _oauth_context_from_request(request)
|
| 635 |
run_id = validate_run_id(run_id)
|
| 636 |
bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
|
| 637 |
+
snapshot = _build_live_run_snapshot(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
|
| 638 |
+
return JSONResponse({**snapshot["view"], "summary": snapshot["summary"], "state": snapshot["state"], "eval_publish": snapshot["eval_publish"]})
|
| 639 |
|
| 640 |
@fastapi_app.post("/api/runs/{run_id}/cancel")
|
| 641 |
async def api_cancel_run(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
|
|
|
|
| 739 |
ctx = _oauth_context_from_request(request)
|
| 740 |
run_id = validate_run_id(run_id)
|
| 741 |
bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
|
| 742 |
+
|
| 743 |
+
probe = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
|
| 744 |
+
state = probe.get("state") or {}
|
| 745 |
+
launch = probe.get("launch") or {}
|
| 746 |
+
summary = probe.get("summary") or {}
|
| 747 |
job_id = _job_id_from_run_bundle(summary, launch, state)
|
| 748 |
job_info: dict[str, Any] = {}
|
| 749 |
job_logs = ""
|
|
|
|
| 752 |
job_info = inspect_job_safe(job_id, token=ctx["token"])
|
| 753 |
job_logs = fetch_recent_logs_safe(job_id, token=ctx["token"], max_lines=1000)
|
| 754 |
job_log_events = _events_from_job_logs(job_logs)
|
| 755 |
+
|
| 756 |
+
# validation_terminal_locked is resolved inside _build_live_run_snapshot so
|
| 757 |
+
# never downgrade a terminal validation
|
| 758 |
+
# effective_state["status"] = summary_status
|
| 759 |
+
# effective_state["validation_status"] = summary_status
|
| 760 |
+
# effective_state["result_status"] = summary_status
|
| 761 |
+
# effective_state["terminal"] = True
|
| 762 |
+
# effective_state["can_resume"] = False
|
| 763 |
+
# progress["status"] = summary_status
|
| 764 |
+
# progress["validation_status"] = summary_status
|
| 765 |
+
# progress["result_status"] = summary_status
|
| 766 |
+
#
|
| 767 |
+
# progress polling cannot downgrade terminal validation runs.
|
| 768 |
+
# Compatibility invariant: view = build_run_view_model(run_id, bundle, bucket_source=bucket_source)
|
| 769 |
+
snapshot = _build_live_run_snapshot(
|
| 770 |
+
run_id,
|
| 771 |
+
bucket_source=bucket_source,
|
| 772 |
+
token=ctx["token"],
|
| 773 |
+
include_heavy=False,
|
| 774 |
+
job_info=job_info,
|
| 775 |
+
job_log_events=job_log_events,
|
| 776 |
+
)
|
| 777 |
+
effective_state = snapshot["state"]
|
| 778 |
+
effective_summary = snapshot["summary"]
|
| 779 |
+
events = snapshot["events"]
|
| 780 |
+
view = snapshot["view"]
|
| 781 |
+
bundle = snapshot["bundle"]
|
|
|
|
| 782 |
progress = progress_from_events(events, state=effective_state)
|
| 783 |
+
model_progress = ((view.get("timeline_model") or {}).get("progress") or {}) if isinstance(view, dict) else {}
|
| 784 |
+
if model_progress.get("terminal") is True:
|
|
|
|
|
|
|
| 785 |
progress["terminal"] = True
|
| 786 |
+
progress["status"] = model_progress.get("verdict") or view.get("header", {}).get("status") or progress.get("status")
|
| 787 |
+
progress["visual_status"] = model_progress.get("visual_status") or progress.get("visual_status")
|
| 788 |
+
progress["progress"] = 100
|
| 789 |
+
|
| 790 |
progress.update(
|
| 791 |
{
|
| 792 |
"run_id": run_id,
|
|
|
|
| 808 |
"run_documents": bundle.get("run_documents") or [],
|
| 809 |
"events": events[-20:],
|
| 810 |
"view": view,
|
| 811 |
+
"eval_publish": snapshot["eval_publish"],
|
| 812 |
+
"eval_publish_status": snapshot["eval_publish"],
|
| 813 |
"links": view.get("links") or _api_links(
|
| 814 |
run_id=run_id,
|
| 815 |
bucket_source=bucket_source,
|
| 816 |
+
target_space=effective_state.get("target_space") or effective_summary.get("target_space"),
|
| 817 |
+
job_url=effective_state.get("job_url") or effective_summary.get("job_url"),
|
| 818 |
),
|
| 819 |
}
|
| 820 |
)
|