@@ -314,6 +314,7 @@ def main() -> None:
314314
315315 session_id = None # Initialize session_id
316316 optimization_completed_normally = False # Flag for finally block
317+ user_stop_requested_flag = False # New flag for user-initiated stop
317318 # --- Check Authentication ---
318319 weco_api_key = load_weco_api_key ()
319320 llm_api_keys = read_api_keys_from_env () # Read keys from client environment
@@ -504,6 +505,30 @@ def main() -> None:
504505 additional_instructions = args .additional_instructions
505506 )
506507
508+ # Get current session status BEFORE proceeding with suggestion/evaluation
509+ # This is where the CLI checks if it should stop
510+ if session_id : # Ensure session_id is available
511+ try :
512+ current_status_response = get_optimization_session_status (
513+ session_id = session_id ,
514+ include_history = False , # Don't need full history here
515+ timeout = 30 , # Shorter timeout for status check
516+ auth_headers = auth_headers ,
517+ )
518+ current_run_status = current_status_response .get ("status" )
519+ if current_run_status == "stopping" :
520+ console .print ("\n [bold yellow]Stop request received. Terminating run gracefully...[/]" )
521+ user_stop_requested_flag = True
522+ break # Exit the optimization loop
523+ except requests .exceptions .RequestException as e :
524+ console .print (
525+ f"\n [bold red]Warning: Could not check session status: { e } . Continuing optimization...[/]"
526+ )
527+ except Exception as e : # Catch any other error during status check
528+ console .print (
529+ f"\n [bold red]Warning: Error checking session status: { e } . Continuing optimization...[/]"
530+ )
531+
507532 # Send feedback and get next suggestion
508533 eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution (
509534 session_id = session_id ,
@@ -599,94 +624,96 @@ def main() -> None:
599624 transition_delay = 0.1 , # Slightly longer delay for evaluation results
600625 )
601626
602- # Re-read instructions from the original source (file path or string) BEFORE each suggest call
603- current_additional_instructions = read_additional_instructions (
604- additional_instructions = args .additional_instructions
605- )
606-
607- # Final evaluation report
608- eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution (
609- session_id = session_id ,
610- execution_output = term_out ,
611- additional_instructions = current_additional_instructions ,
612- api_keys = llm_api_keys ,
613- timeout = timeout ,
614- auth_headers = auth_headers ,
615- )
627+ # If loop finished without user_stop_requested_flag
628+ if not user_stop_requested_flag :
629+ # Re-read instructions from the original source (file path or string) BEFORE each suggest call
630+ current_additional_instructions = read_additional_instructions (
631+ additional_instructions = args .additional_instructions
632+ )
616633
617- # Update the progress bar
618- summary_panel .set_step (step = steps )
619- # Update the token counts
620- summary_panel .update_token_counts (usage = eval_and_next_solution_response ["usage" ])
621- # No need to update the plan panel since we have finished the optimization
622- # Get the optimization session status for
623- # the best solution, its score, and the history to plot the tree
624- status_response = get_optimization_session_status (
625- session_id = session_id , include_history = True , timeout = timeout , auth_headers = auth_headers
626- )
627- # Build the metric tree
628- tree_panel .build_metric_tree (nodes = status_response ["history" ])
629- # No need to set any solution to unevaluated since we have finished the optimization
630- # and all solutions have been evaluated
631- # No neeed to update the current solution panel since we have finished the optimization
632- # We only need to update the best solution panel
633- # Figure out if we have a best solution so far
634- if status_response ["best_result" ] is not None :
635- best_solution_node = Node (
636- id = status_response ["best_result" ]["solution_id" ],
637- parent_id = status_response ["best_result" ]["parent_id" ],
638- code = status_response ["best_result" ]["code" ],
639- metric = status_response ["best_result" ]["metric_value" ],
640- is_buggy = status_response ["best_result" ]["is_buggy" ],
634+ # Final evaluation report
635+ eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution (
636+ session_id = session_id ,
637+ execution_output = term_out ,
638+ additional_instructions = current_additional_instructions ,
639+ api_keys = llm_api_keys ,
640+ timeout = timeout ,
641+ auth_headers = auth_headers ,
641642 )
642- else :
643- best_solution_node = None
644- solution_panels .update (current_node = None , best_node = best_solution_node )
645- _ , best_solution_panel = solution_panels .get_display (current_step = steps )
646-
647- # Update the end optimization layout
648- final_message = (
649- f"{ summary_panel .metric_name .capitalize ()} { 'maximized' if summary_panel .maximize else 'minimized' } ! Best solution { summary_panel .metric_name .lower ()} = [green]{ status_response ['best_result' ]['metric_value' ]} [/] 🏆"
650- if best_solution_node is not None and best_solution_node .metric is not None
651- else "[red] No valid solution found.[/]"
652- )
653- end_optimization_layout ["summary" ].update (summary_panel .get_display (final_message = final_message ))
654- end_optimization_layout ["tree" ].update (tree_panel .get_display (is_done = True ))
655- end_optimization_layout ["best_solution" ].update (best_solution_panel )
656-
657- # Save optimization results
658- # If the best solution does not exist or is has not been measured at the end of the optimization
659- # save the original solution as the best solution
660- if best_solution_node is not None :
661- best_solution_code = best_solution_node .code
662- best_solution_score = best_solution_node .metric
663- else :
664- best_solution_code = None
665- best_solution_score = None
666643
667- if best_solution_code is None or best_solution_score is None :
668- best_solution_content = f"# Weco could not find a better solution\n \n { read_from_path (fp = runs_dir / f'step_0{ source_fp .suffix } ' , is_json = False )} "
669- else :
670- # Format score for the comment
671- best_score_str = (
672- format_number (best_solution_score )
673- if best_solution_score is not None and isinstance (best_solution_score , (int , float ))
674- else "N/A"
644+ # Update the progress bar
645+ summary_panel .set_step (step = steps )
646+ # Update the token counts
647+ summary_panel .update_token_counts (usage = eval_and_next_solution_response ["usage" ])
648+ # No need to update the plan panel since we have finished the optimization
649+ # Get the optimization session status for
650+ # the best solution, its score, and the history to plot the tree
651+ status_response = get_optimization_session_status (
652+ session_id = session_id , include_history = True , timeout = timeout , auth_headers = auth_headers
675653 )
676- best_solution_content = (
677- f"# Best solution from Weco with a score of { best_score_str } \n \n { best_solution_code } "
654+ # Build the metric tree
655+ tree_panel .build_metric_tree (nodes = status_response ["history" ])
656+ # No need to set any solution to unevaluated since we have finished the optimization
657+ # and all solutions have been evaluated
658+ # No neeed to update the current solution panel since we have finished the optimization
659+ # We only need to update the best solution panel
660+ # Figure out if we have a best solution so far
661+ if status_response ["best_result" ] is not None :
662+ best_solution_node = Node (
663+ id = status_response ["best_result" ]["solution_id" ],
664+ parent_id = status_response ["best_result" ]["parent_id" ],
665+ code = status_response ["best_result" ]["code" ],
666+ metric = status_response ["best_result" ]["metric_value" ],
667+ is_buggy = status_response ["best_result" ]["is_buggy" ],
668+ )
669+ else :
670+ best_solution_node = None
671+ solution_panels .update (current_node = None , best_node = best_solution_node )
672+ _ , best_solution_panel = solution_panels .get_display (current_step = steps )
673+
674+ # Update the end optimization layout
675+ final_message = (
676+ f"{ summary_panel .metric_name .capitalize ()} { 'maximized' if summary_panel .maximize else 'minimized' } ! Best solution { summary_panel .metric_name .lower ()} = [green]{ status_response ['best_result' ]['metric_value' ]} [/] 🏆"
677+ if best_solution_node is not None and best_solution_node .metric is not None
678+ else "[red] No valid solution found.[/]"
678679 )
680+ end_optimization_layout ["summary" ].update (summary_panel .get_display (final_message = final_message ))
681+ end_optimization_layout ["tree" ].update (tree_panel .get_display (is_done = True ))
682+ end_optimization_layout ["best_solution" ].update (best_solution_panel )
683+
684+ # Save optimization results
685+ # If the best solution does not exist or is has not been measured at the end of the optimization
686+ # save the original solution as the best solution
687+ if best_solution_node is not None :
688+ best_solution_code = best_solution_node .code
689+ best_solution_score = best_solution_node .metric
690+ else :
691+ best_solution_code = None
692+ best_solution_score = None
679693
680- # Save best solution to .runs/<session-id>/best.<extension>
681- write_to_path (fp = runs_dir / f"best{ source_fp .suffix } " , content = best_solution_content )
694+ if best_solution_code is None or best_solution_score is None :
695+ best_solution_content = f"# Weco could not find a better solution\n \n { read_from_path (fp = runs_dir / f'step_0{ source_fp .suffix } ' , is_json = False )} "
696+ else :
697+ # Format score for the comment
698+ best_score_str = (
699+ format_number (best_solution_score )
700+ if best_solution_score is not None and isinstance (best_solution_score , (int , float ))
701+ else "N/A"
702+ )
703+ best_solution_content = (
704+ f"# Best solution from Weco with a score of { best_score_str } \n \n { best_solution_code } "
705+ )
706+
707+ # Save best solution to .runs/<session-id>/best.<extension>
708+ write_to_path (fp = runs_dir / f"best{ source_fp .suffix } " , content = best_solution_content )
682709
683- # write the best solution to the source file
684- write_to_path (fp = source_fp , content = best_solution_content )
710+ # write the best solution to the source file
711+ write_to_path (fp = source_fp , content = best_solution_content )
685712
686- # Mark as completed normally for the finally block
687- optimization_completed_normally = True
713+ # Mark as completed normally for the finally block
714+ optimization_completed_normally = True # Only set if loop completes all steps
688715
689- console .print (end_optimization_layout )
716+ console .print (end_optimization_layout ) # Moved inside the if
690717
691718 except Exception as e :
692719 # Catch errors during the main optimization loop or setup
@@ -718,39 +745,51 @@ def main() -> None:
718745
719746 # Report final status if a session was started
720747 if session_id :
721- final_status = "unknown"
722- final_reason = "unknown_termination"
748+ final_status_update = "unknown"
749+ final_reason_code = "unknown_termination"
723750 final_details = None
724751
725- if optimization_completed_normally :
726- final_status = "completed"
727- final_reason = "completed_successfully"
728- else :
729- # If an exception was caught and we have details
752+ if optimization_completed_normally : # All steps completed
753+ final_status_update = "completed"
754+ final_reason_code = "completed_successfully"
755+ elif user_stop_requested_flag : # Stopped by user request
756+ final_status_update = "terminated"
757+ final_reason_code = "user_requested_stop"
758+ final_details = "Run stopped by user request via dashboard."
759+ else : # Any other non-normal termination (e.g., CLI error)
760+ final_status_update = "error"
761+ final_reason_code = "error_cli_internal"
762+ # Use error_details from the existing except block if available
730763 if "error_details" in locals ():
731- final_status = "error"
732- final_reason = "error_cli_internal"
733- final_details = error_details
734- # else: # Should have been handled by signal handler if terminated by user
735- # Keep default 'unknown' if we somehow end up here without error/completion/signal
736-
737- # Avoid reporting if terminated by signal handler (already reported)
738- # Check a flag or rely on status not being 'unknown'
739- if final_status != "unknown" :
764+ final_details = locals ()["error_details" ]
765+ elif "e" in locals () and isinstance (locals ()["e" ], Exception ): # Fallback if e is somehow there
766+ final_details = traceback .format_exc ()
767+ else :
768+ final_details = "CLI terminated unexpectedly without a specific exception captured."
769+
770+ # The signal_handler will call report_termination with its own reasons.
771+ # This `finally` block's report_termination is for loop completion,
772+ # user_requested_stop, or internal CLI errors not caught by signals.
773+ # We assume signal_handler calls sys.exit, so this block might not fully
774+ # execute if a signal terminates the process abruptly *before* this finally.
775+ # However, for user_requested_stop detected by the loop, this will run.
776+
777+ if final_status_update != "unknown" :
740778 report_termination (
741779 session_id = session_id ,
742- status_update = final_status ,
743- reason = final_reason ,
780+ status_update = final_status_update ,
781+ reason = final_reason_code ,
744782 details = final_details ,
745- auth_headers = auth_headers ,
783+ auth_headers = current_auth_headers_for_heartbeat ,
746784 )
747785
748- # Ensure proper exit code if an error occurred
749- if not optimization_completed_normally and "exit_code" in locals () and exit_code != 0 :
750- sys .exit (exit_code )
751- elif not optimization_completed_normally :
752- # Generic error exit if no specific code was set but try block failed
753- sys .exit (1 )
754- else :
755- # Normal exit
786+ # Exit code logic
787+ if optimization_completed_normally :
756788 sys .exit (0 )
789+ elif user_stop_requested_flag :
790+ console .print ("[yellow]Run terminated by user request.[/]" )
791+ sys .exit (0 ) # Graceful exit for user stop
792+ else :
793+ # If an error occurred and was caught by the `except Exception as e` block
794+ # exit_code might have been set by the existing except block.
795+ sys .exit (locals ().get ("exit_code" , 1 ))
0 commit comments