From c68c0cedbafef55ca4033aa1f89cb8fb0b5deffd Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 11 Jan 2026 10:23:03 +0000 Subject: [PATCH] Fix plan node detail to generate plan from recipe if not cached MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add load_plan_for_run_with_fallback() that generates plan from recipe when not found in cache - Share this helper between plan page and node detail endpoint - Removes code duplication 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- server.py | 80 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/server.py b/server.py index 13bfc62..961d158 100644 --- a/server.py +++ b/server.py @@ -1373,6 +1373,46 @@ def load_plan_for_run(run: RunStatus) -> Optional[dict]: return None +async def load_plan_for_run_with_fallback(run: RunStatus) -> Optional[dict]: + """Load plan data for a run, with fallback to generate from recipe.""" + # First try cached plans + plan_data = load_plan_for_run(run) + if plan_data: + return plan_data + + # Fallback: generate from recipe + recipe_name = run.recipe.replace("recipe:", "") if run.recipe.startswith("recipe:") else run.recipe + recipe_status = None + for recipe in list_all_recipes(): + if recipe.name == recipe_name: + recipe_status = recipe + break + + if recipe_status: + recipe_path = cache_manager.get_by_content_hash(recipe_status.recipe_id) + if recipe_path and recipe_path.exists(): + try: + recipe_yaml = recipe_path.read_text() + # Build input_hashes mapping from run inputs + input_hashes = {} + for i, var_input in enumerate(recipe_status.variable_inputs): + if i < len(run.inputs): + input_hashes[var_input.node_id] = run.inputs[i] + + # Try to generate plan + try: + from tasks.orchestrate import generate_plan as gen_plan_task + plan_result = gen_plan_task(recipe_yaml, input_hashes) + if plan_result and plan_result.get("status") == "planned": + return plan_result + except ImportError: + pass + except Exception as e: + logger.warning(f"Failed to generate plan for run {run.run_id}: {e}") + + return None + + @app.get("/run/{run_id}/plan/node/{step_id}", response_class=HTMLResponse) async def run_plan_node_detail(run_id: str, step_id: str, request: Request): """HTMX partial: Get node detail HTML fragment.""" @@ -1384,8 +1424,8 @@ async def run_plan_node_detail(run_id: str, step_id: str, request: Request): if not run: return HTMLResponse(f'

Run not found

', status_code=404) - # Load plan data - plan_data = await asyncio.to_thread(load_plan_for_run, run) + # Load plan data (with fallback to generate from recipe) + plan_data = await asyncio.to_thread(load_plan_for_run_with_fallback, run) if not plan_data: return HTMLResponse('

Plan not found

') @@ -1531,40 +1571,8 @@ async def run_plan_visualization(run_id: str, request: Request, node: Optional[s content = '

Access denied.

' return HTMLResponse(render_page("Access Denied", content, ctx.actor_id, active_tab="runs"), status_code=403) - # Try to load existing plan from cache - plan_data = await asyncio.to_thread(load_plan_for_run, run) - - # If no cached plan, try to generate one from the recipe - if not plan_data: - recipe_name = run.recipe.replace("recipe:", "") if run.recipe.startswith("recipe:") else run.recipe - recipe_status = None - for recipe in list_all_recipes(): - if recipe.name == recipe_name: - recipe_status = recipe - break - - if recipe_status: - recipe_path = cache_manager.get_by_content_hash(recipe_status.recipe_id) - if recipe_path and recipe_path.exists(): - try: - recipe_yaml = recipe_path.read_text() - # Build input_hashes mapping from run inputs - input_hashes = {} - for i, var_input in enumerate(recipe_status.variable_inputs): - if i < len(run.inputs): - input_hashes[var_input.node_id] = run.inputs[i] - - # Try to generate plan using the orchestrate module - try: - from tasks.orchestrate import generate_plan as gen_plan_task - # Call synchronously (it's fast for just planning) - plan_result = gen_plan_task(recipe_yaml, input_hashes) - if plan_result and plan_result.get("status") == "planned": - plan_data = plan_result - except ImportError: - pass - except Exception as e: - logger.warning(f"Failed to generate plan for run {run_id}: {e}") + # Load plan data (with fallback to generate from recipe) + plan_data = await asyncio.to_thread(load_plan_for_run_with_fallback, run) # Build sub-navigation tabs tabs_html = render_run_sub_tabs(run_id, active="plan")