From 65a81701923aa53dd98bdb900d38c2367b10f830 Mon Sep 17 00:00:00 2001 From: gilesb Date: Mon, 12 Jan 2026 00:26:05 +0000 Subject: [PATCH] Use native S-expression for recipe/plan display - Display recipe's original S-expression when available (code is data) - Fall back to generating S-expression from plan for legacy JSON - Run service now prefers .sexp plan files over .json - Add get_run_plan_sexp() for direct S-expression access Co-Authored-By: Claude Opus 4.5 --- app/routers/runs.py | 12 +++++++++--- app/services/run_service.py | 26 ++++++++++++++++++++++---- app/templates/runs/detail.html | 6 ++++-- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/app/routers/runs.py b/app/routers/runs.py index 643c691..e437cf1 100644 --- a/app/routers/runs.py +++ b/app/routers/runs.py @@ -178,6 +178,7 @@ async def get_run( # Try to load the recipe to show the plan plan = None + plan_sexp = None # Native S-expression if available recipe_id = run.get("recipe") if recipe_id and len(recipe_id) == 64: # Looks like a hash try: @@ -185,7 +186,11 @@ async def get_run( recipe_service = RecipeService(get_redis_client(), get_cache_manager()) recipe = await recipe_service.get_recipe(recipe_id) if recipe: - # Use the new build_dag method if available + # Use native S-expression if available (code is data!) + if recipe.get("sexp"): + plan_sexp = recipe["sexp"] + + # Build steps for DAG visualization dag = recipe.get("dag", {}) nodes = dag.get("nodes", []) @@ -311,8 +316,9 @@ async def get_run( } }) - # Generate S-expression representation of the plan - plan_sexp = plan_to_sexp(plan, run.get("recipe_name")) + # Use native S-expression if available, otherwise generate from plan + if not plan_sexp and plan: + plan_sexp = plan_to_sexp(plan, run.get("recipe_name")) templates = get_templates(request) return render(templates, "runs/detail.html", request, diff --git a/app/services/run_service.py b/app/services/run_service.py index 207e5e9..4eb84b7 100644 --- a/app/services/run_service.py +++ b/app/services/run_service.py @@ -482,10 +482,28 @@ class RunService: async def get_run_plan(self, run_id: str) -> Optional[Dict[str, Any]]: """Get execution plan for a run.""" - plan_path = self.cache_dir / "plans" / f"{run_id}.json" - if plan_path.exists(): - with open(plan_path) as f: - return json.load(f) + # Prefer S-expression plan + sexp_path = self.cache_dir / "plans" / f"{run_id}.sexp" + if sexp_path.exists(): + with open(sexp_path) as f: + return {"sexp": f.read(), "format": "sexp"} + + # Fall back to JSON for legacy plans + json_path = self.cache_dir / "plans" / f"{run_id}.json" + if json_path.exists(): + with open(json_path) as f: + plan = json.load(f) + plan["format"] = "json" + return plan + + return None + + async def get_run_plan_sexp(self, run_id: str) -> Optional[str]: + """Get execution plan as S-expression string.""" + sexp_path = self.cache_dir / "plans" / f"{run_id}.sexp" + if sexp_path.exists(): + with open(sexp_path) as f: + return f.read() return None async def get_run_artifacts(self, run_id: str) -> List[Dict[str, Any]]: diff --git a/app/templates/runs/detail.html b/app/templates/runs/detail.html index b2eb06a..f0a87ed 100644 --- a/app/templates/runs/detail.html +++ b/app/templates/runs/detail.html @@ -156,15 +156,17 @@ {% endfor %} - + + {% if plan_sexp %}
- Plan (S-expression) + Recipe (S-expression)
{{ plan_sexp }}
+ {% endif %}