weco 0.3.7__py3-none-any.whl → 0.3.8__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.
weco/optimizer.py CHANGED
@@ -1,36 +1,43 @@
1
- import pathlib
1
+ import json
2
2
  import math
3
- import requests
4
- import threading
5
- import signal
3
+ import pathlib
6
4
  import sys
5
+ import threading
6
+ import time
7
7
  import traceback
8
- import json
8
+ from dataclasses import dataclass
9
9
  from datetime import datetime
10
10
  from typing import Optional
11
+
11
12
  from rich.console import Console
12
- from rich.live import Live
13
- from rich.panel import Panel
14
13
  from rich.prompt import Confirm
14
+
15
+ from . import __dashboard_url__
15
16
  from .api import (
16
- start_optimization_run,
17
- evaluate_feedback_then_suggest_next_solution,
17
+ claim_execution_task,
18
+ get_execution_tasks,
18
19
  get_optimization_run_status,
19
- send_heartbeat,
20
20
  report_termination,
21
21
  resume_optimization_run,
22
+ send_heartbeat,
23
+ start_optimization_run,
24
+ submit_execution_result,
22
25
  )
23
26
  from .auth import handle_authentication
24
- from .panels import (
25
- SummaryPanel,
26
- Node,
27
- MetricTreePanel,
28
- EvaluationOutputPanel,
29
- SolutionPanels,
30
- create_optimization_layout,
31
- create_end_optimization_layout,
32
- )
33
- from .utils import read_additional_instructions, read_from_path, write_to_path, run_evaluation_with_file_swap, smooth_update
27
+ from .browser import open_browser
28
+ from .ui import OptimizationUI, LiveOptimizationUI
29
+ from .utils import read_additional_instructions, read_from_path, write_to_path, run_evaluation_with_file_swap
30
+
31
+
32
+ @dataclass
33
+ class OptimizationResult:
34
+ """Result from a queue-based optimization loop."""
35
+
36
+ success: bool
37
+ final_step: int
38
+ status: str # "completed", "terminated", "error"
39
+ reason: str # e.g. "completed_successfully", "user_terminated_sigint"
40
+ details: Optional[str] = None
34
41
 
35
42
 
36
43
  def save_execution_output(runs_dir: pathlib.Path, step: int, output: str) -> None:
@@ -92,867 +99,553 @@ class HeartbeatSender(threading.Thread):
92
99
  # The loop will break due to the exception, and thread will terminate via finally.
93
100
 
94
101
 
95
- def get_best_node_from_status(status_response: dict) -> Optional[Node]:
96
- """Extract the best node from a status response as a panels.Node instance."""
97
- if status_response.get("best_result") is not None:
98
- return Node(
99
- id=status_response["best_result"]["solution_id"],
100
- parent_id=status_response["best_result"]["parent_id"],
101
- code=status_response["best_result"]["code"],
102
- metric=status_response["best_result"]["metric_value"],
103
- is_buggy=status_response["best_result"]["is_buggy"],
104
- )
105
- return None
106
-
107
-
108
- def get_node_from_status(status_response: dict, solution_id: str) -> Node:
109
- """Find the node with the given solution_id from a status response; raise if not found."""
110
- nodes = status_response.get("nodes") or []
111
- for node_data in nodes:
112
- if node_data.get("solution_id") == solution_id:
113
- return Node(
114
- id=node_data["solution_id"],
115
- parent_id=node_data["parent_id"],
116
- code=node_data["code"],
117
- metric=node_data["metric_value"],
118
- is_buggy=node_data["is_buggy"],
119
- )
120
- raise ValueError(
121
- "Current solution node not found in the optimization status response. This may indicate a synchronization issue with the backend."
122
- )
123
-
124
-
125
- def execute_optimization(
126
- source: str,
102
+ def _run_optimization_loop(
103
+ ui: OptimizationUI,
104
+ run_id: str,
105
+ auth_headers: dict,
106
+ source_fp: pathlib.Path,
107
+ source_code: str,
127
108
  eval_command: str,
128
- metric: str,
129
- goal: str, # "maximize" or "minimize"
130
- model: str,
131
- steps: int = 100,
132
- log_dir: str = ".runs",
133
- additional_instructions: Optional[str] = None,
134
- console: Optional[Console] = None,
135
- eval_timeout: Optional[int] = None,
136
- save_logs: bool = False,
137
- apply_change: bool = False,
138
- api_keys: Optional[dict[str, str]] = None,
139
- ) -> bool:
109
+ eval_timeout: Optional[int],
110
+ runs_dir: pathlib.Path,
111
+ save_logs: bool,
112
+ start_step: int = 0,
113
+ poll_interval: float = 2.0,
114
+ max_poll_attempts: int = 300,
115
+ api_keys: Optional[dict] = None,
116
+ ) -> OptimizationResult:
140
117
  """
141
- Execute the core optimization logic.
118
+ Shared queue-based execution loop for optimize and resume.
119
+
120
+ Polls for execution tasks, executes locally, and submits results.
121
+ This function handles the core optimization loop and returns a result
122
+ object describing the outcome.
123
+
124
+ Args:
125
+ ui: UI handler for displaying progress and events.
126
+ run_id: The optimization run ID.
127
+ auth_headers: Authentication headers.
128
+ source_fp: Path to the source file.
129
+ source_code: Original source code content.
130
+ eval_command: Evaluation command to run.
131
+ eval_timeout: Timeout for evaluation in seconds.
132
+ runs_dir: Directory for logs.
133
+ save_logs: Whether to save execution logs.
134
+ start_step: Initial step number (0 for new runs, current_step for resume).
135
+ poll_interval: Seconds between polling attempts.
136
+ max_poll_attempts: Max polls before timeout (~10 min with 2s interval).
137
+ api_keys: Optional API keys for LLM providers.
142
138
 
143
139
  Returns:
144
- bool: True if optimization completed successfully, False otherwise
140
+ OptimizationResult with success status and termination info.
145
141
  """
146
- if console is None:
147
- console = Console()
148
- # Global variables for this optimization run
149
- heartbeat_thread = None
150
- stop_heartbeat_event = threading.Event()
151
- current_run_id_for_heartbeat = None
152
- current_auth_headers_for_heartbeat = {}
153
- live_ref = None # Reference to the Live object for the optimization run
142
+ step = start_step
154
143
 
155
- best_solution_code = None
156
- original_source_code = None
157
-
158
- # --- Signal Handler for this optimization run ---
159
- def signal_handler(signum, frame):
160
- nonlocal live_ref
144
+ try:
145
+ while True:
146
+ # Check if run has been stopped via dashboard
147
+ try:
148
+ status_response = get_optimization_run_status(
149
+ console=None, run_id=run_id, include_history=False, auth_headers=auth_headers
150
+ )
151
+ if status_response.get("status") == "stopping":
152
+ ui.on_stop_requested()
153
+ return OptimizationResult(
154
+ success=False,
155
+ final_step=step,
156
+ status="terminated",
157
+ reason="user_requested_stop",
158
+ details="Run stopped by user request via dashboard.",
159
+ )
160
+ except Exception as e:
161
+ ui.on_warning(f"Unable to check run status: {e}")
162
+
163
+ # Poll for ready tasks
164
+ ui.on_polling(step)
165
+ tasks_result = None
166
+ poll_attempts = 0
167
+
168
+ while not tasks_result or not tasks_result.tasks:
169
+ tasks_result = get_execution_tasks(run_id, auth_headers)
170
+
171
+ # Check if run was stopped (from run summary in response)
172
+ if tasks_result and tasks_result.run:
173
+ run_status = tasks_result.run.status
174
+ if run_status in ("stopping", "stopped", "terminated", "error", "completed"):
175
+ ui.on_stop_requested()
176
+ return OptimizationResult(
177
+ success=False,
178
+ final_step=step,
179
+ status="terminated",
180
+ reason="user_requested_stop",
181
+ details=f"Run status is '{run_status}'.",
182
+ )
161
183
 
162
- if live_ref is not None:
163
- live_ref.stop() # Stop the live update loop so that messages are printed to the console
184
+ if not tasks_result or not tasks_result.tasks:
185
+ poll_attempts += 1
186
+ if poll_attempts >= max_poll_attempts:
187
+ ui.on_error("Timeout waiting for execution tasks")
188
+ return OptimizationResult(
189
+ success=False,
190
+ final_step=step,
191
+ status="error",
192
+ reason="timeout_waiting_for_tasks",
193
+ details="Timeout waiting for execution tasks",
194
+ )
195
+ time.sleep(poll_interval)
164
196
 
165
- signal_name = signal.Signals(signum).name
166
- console.print(f"\n[bold yellow]Termination signal ({signal_name}) received. Shutting down...[/]\n")
197
+ task = tasks_result.tasks[0]
198
+ task_id = task["id"]
167
199
 
168
- # Stop heartbeat thread
169
- stop_heartbeat_event.set()
170
- if heartbeat_thread and heartbeat_thread.is_alive():
171
- heartbeat_thread.join(timeout=2) # Give it a moment to stop
172
-
173
- # Report termination (best effort)
174
- if current_run_id_for_heartbeat:
175
- report_termination(
176
- run_id=current_run_id_for_heartbeat,
177
- status_update="terminated",
178
- reason=f"user_terminated_{signal_name.lower()}",
179
- details=f"Process terminated by signal {signal_name} ({signum}).",
180
- auth_headers=current_auth_headers_for_heartbeat,
181
- )
182
- console.print(f"[cyan]To resume this run, use:[/] [bold cyan]weco resume {current_run_id_for_heartbeat}[/]\n")
200
+ # Claim the task
201
+ claimed = claim_execution_task(task_id, auth_headers)
202
+ if claimed is None:
203
+ ui.on_warning(f"Task {task_id} already claimed, retrying...")
204
+ continue
183
205
 
184
- # Exit gracefully
185
- sys.exit(0)
206
+ code = claimed["revision"]["code"]
207
+ plan = claimed["revision"]["plan"]
186
208
 
187
- # Set up signal handlers for this run
188
- original_sigint_handler = signal.signal(signal.SIGINT, signal_handler)
189
- original_sigterm_handler = signal.signal(signal.SIGTERM, signal_handler)
209
+ ui.on_executing(step)
210
+ ui.on_task_claimed(task_id, plan)
190
211
 
191
- run_id = None
192
- optimization_completed_normally = False
193
- user_stop_requested_flag = False
212
+ # Save code to log
213
+ write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=code)
194
214
 
195
- try:
196
- # --- Login/Authentication Handling (now mandatory) ---
197
- weco_api_key, auth_headers = handle_authentication(console)
198
- if weco_api_key is None:
199
- # Authentication failed or user declined
200
- return False
201
-
202
- current_auth_headers_for_heartbeat = auth_headers
203
-
204
- # --- Process Parameters ---
205
- maximize = goal.lower() in ["maximize", "max"]
206
-
207
- code_generator_config = {"model": model}
208
- evaluator_config = {"model": model, "include_analysis": True}
209
- search_policy_config = {
210
- "num_drafts": max(1, math.ceil(0.15 * steps)),
211
- "debug_prob": 0.5,
212
- "max_debug_depth": max(1, math.ceil(0.1 * steps)),
213
- }
214
- processed_additional_instructions = read_additional_instructions(additional_instructions=additional_instructions)
215
- source_fp = pathlib.Path(source)
216
- source_code = read_from_path(fp=source_fp, is_json=False)
217
- original_source_code = source_code
218
-
219
- # --- Panel Initialization ---
220
- summary_panel = SummaryPanel(maximize=maximize, metric_name=metric, total_steps=steps, model=model, runs_dir=log_dir)
221
- solution_panels = SolutionPanels(metric_name=metric, source_fp=source_fp)
222
- eval_output_panel = EvaluationOutputPanel()
223
- tree_panel = MetricTreePanel(maximize=maximize)
224
- layout = create_optimization_layout()
225
- end_optimization_layout = create_end_optimization_layout()
226
-
227
- # --- Start Optimization Run ---
228
- run_response = start_optimization_run(
229
- console=console,
230
- source_code=source_code,
231
- source_path=str(source_fp),
232
- evaluation_command=eval_command,
233
- metric_name=metric,
234
- maximize=maximize,
235
- steps=steps,
236
- code_generator_config=code_generator_config,
237
- evaluator_config=evaluator_config,
238
- search_policy_config=search_policy_config,
239
- additional_instructions=processed_additional_instructions,
240
- eval_timeout=eval_timeout,
241
- save_logs=save_logs,
242
- log_dir=log_dir,
243
- auth_headers=auth_headers,
244
- api_keys=api_keys,
245
- )
246
- # Indicate the endpoint failed to return a response and the optimization was unsuccessful
247
- if run_response is None:
248
- return False
249
-
250
- run_id = run_response["run_id"]
251
- run_name = run_response["run_name"]
252
- current_run_id_for_heartbeat = run_id
253
-
254
- # --- Start Heartbeat Thread ---
255
- stop_heartbeat_event.clear()
256
- heartbeat_thread = HeartbeatSender(run_id, auth_headers, stop_heartbeat_event)
257
- heartbeat_thread.start()
258
-
259
- # --- Live Update Loop ---
260
- refresh_rate = 4
261
- with Live(layout, refresh_per_second=refresh_rate) as live:
262
- live_ref = live
263
- # Define the runs directory (.runs/<run-id>) to store logs and results
264
- runs_dir = pathlib.Path(log_dir) / run_id
265
- runs_dir.mkdir(parents=True, exist_ok=True)
266
-
267
- # Initialize logging structure if save_logs is enabled
268
- if save_logs:
269
- # Initialize JSONL index with metadata
270
- jsonl_file = runs_dir / "exec_output.jsonl"
271
- metadata = {
272
- "type": "metadata",
273
- "run_id": run_id,
274
- "run_name": run_name,
275
- "started": datetime.now().isoformat(),
276
- "eval_command": eval_command,
277
- "metric": metric,
278
- "goal": "maximize" if maximize else "minimize",
279
- "total_steps": steps,
280
- }
281
- with open(jsonl_file, "w", encoding="utf-8") as f:
282
- f.write(json.dumps(metadata) + "\n")
283
-
284
- # Update the panels with the initial solution
285
- # Add run id and run name now that we have it
286
- summary_panel.set_run_id(run_id=run_id)
287
- summary_panel.set_run_name(run_name=run_name)
288
- # Set the step of the progress bar
289
- summary_panel.set_step(step=0)
290
- summary_panel.update_thinking(thinking=run_response["plan"])
291
- # Build the metric tree
292
- tree_panel.build_metric_tree(
293
- nodes=[
294
- {
295
- "solution_id": run_response["solution_id"],
296
- "parent_id": None,
297
- "code": run_response["code"],
298
- "step": 0,
299
- "metric_value": None,
300
- "is_buggy": None,
301
- }
302
- ]
303
- )
304
- # Set the current solution as unevaluated since we haven't run the evaluation function and fed it back to the model yet
305
- tree_panel.set_unevaluated_node(node_id=run_response["solution_id"])
306
- # Update the solution panels with the initial solution and get the panel displays
307
- solution_panels.update(
308
- current_node=Node(
309
- id=run_response["solution_id"], parent_id=None, code=run_response["code"], metric=None, is_buggy=None
310
- ),
311
- best_node=None,
312
- )
313
- current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=0)
314
-
315
- # Update the live layout with the initial solution panels
316
- smooth_update(
317
- live=live,
318
- layout=layout,
319
- sections_to_update=[
320
- ("summary", summary_panel.get_display()),
321
- ("tree", tree_panel.get_display(is_done=False)),
322
- ("current_solution", current_solution_panel),
323
- ("best_solution", best_solution_panel),
324
- ("eval_output", eval_output_panel.get_display()),
325
- ],
326
- transition_delay=0.1,
327
- )
328
-
329
- # Write the initial code string to the logs
330
- write_to_path(fp=runs_dir / f"step_0{source_fp.suffix}", content=run_response["code"])
331
- # Run evaluation on the initial solution (file swap ensures original is restored)
215
+ # Execute locally
332
216
  term_out = run_evaluation_with_file_swap(
333
217
  file_path=source_fp,
334
- new_content=run_response["code"],
218
+ new_content=code,
335
219
  original_content=source_code,
336
220
  eval_command=eval_command,
337
221
  timeout=eval_timeout,
338
222
  )
339
223
 
340
- # Save logs if requested
341
224
  if save_logs:
342
- save_execution_output(runs_dir, step=0, output=term_out)
343
- # Update the evaluation output panel
344
- eval_output_panel.update(output=term_out)
345
- smooth_update(
346
- live=live,
347
- layout=layout,
348
- sections_to_update=[("eval_output", eval_output_panel.get_display())],
349
- transition_delay=0.1,
350
- )
225
+ save_execution_output(runs_dir, step=step, output=term_out)
351
226
 
352
- # Starting from step 1 to steps (inclusive) because the baseline solution is step 0, so we want to optimize for steps worth of steps
353
- for step in range(1, steps + 1):
354
- if run_id:
355
- try:
356
- current_status_response = get_optimization_run_status(
357
- console=console, run_id=run_id, include_history=False, auth_headers=auth_headers
358
- )
359
- current_run_status_val = current_status_response.get("status")
360
- if current_run_status_val == "stopping":
361
- console.print("\n[bold yellow]Stop request received. Terminating run gracefully...[/]")
362
- user_stop_requested_flag = True
363
- break
364
- except requests.exceptions.RequestException as e:
365
- console.print(f"\n[bold red]Warning: Unable to check run status: {e}. Continuing optimization...[/]")
366
- except Exception as e:
367
- console.print(f"\n[bold red]Warning: Error checking run status: {e}. Continuing optimization...[/]")
368
-
369
- # Send feedback and get next suggestion
370
- eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
371
- console=console,
372
- step=step,
373
- run_id=run_id,
374
- execution_output=term_out,
375
- auth_headers=auth_headers,
376
- api_keys=api_keys,
377
- )
378
- # Save next solution (.runs/<run-id>/step_<step>.<extension>)
379
- write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"])
227
+ ui.on_output(term_out)
380
228
 
381
- status_response = get_optimization_run_status(
382
- console=console, run_id=run_id, include_history=True, auth_headers=auth_headers
383
- )
384
- # Update the step of the progress bar, plan and metric tree
385
- summary_panel.set_step(step=step)
386
- summary_panel.update_thinking(thinking=eval_and_next_solution_response["plan"])
387
-
388
- nodes_list_from_status = status_response.get("nodes")
389
- tree_panel.build_metric_tree(nodes=nodes_list_from_status if nodes_list_from_status is not None else [])
390
- tree_panel.set_unevaluated_node(node_id=eval_and_next_solution_response["solution_id"])
391
-
392
- # Update the solution panels with the next solution and best solution (and score)
393
- # Figure out if we have a best solution so far
394
- best_solution_node = get_best_node_from_status(status_response=status_response)
395
- current_solution_node = get_node_from_status(
396
- status_response=status_response, solution_id=eval_and_next_solution_response["solution_id"]
397
- )
229
+ # Submit result
230
+ ui.on_submitting()
231
+ result = submit_execution_result(
232
+ run_id=run_id, task_id=task_id, execution_output=term_out, auth_headers=auth_headers, api_keys=api_keys
233
+ )
398
234
 
399
- # Set best solution and save optimization results
400
- try:
401
- best_solution_code = best_solution_node.code
402
- except AttributeError:
403
- # Can happen if the code was buggy
404
- best_solution_code = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
405
-
406
- # Save best solution to .runs/<run-id>/best.<extension>
407
- write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
408
-
409
- # Update the solution panels with the current and best solution
410
- solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
411
- current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
412
- # Clear evaluation output since we are running a evaluation on a new solution
413
- eval_output_panel.clear()
414
- smooth_update(
415
- live=live,
416
- layout=layout,
417
- sections_to_update=[
418
- ("summary", summary_panel.get_display()),
419
- ("tree", tree_panel.get_display(is_done=False)),
420
- ("current_solution", current_solution_panel),
421
- ("best_solution", best_solution_panel),
422
- ("eval_output", eval_output_panel.get_display()),
423
- ],
424
- transition_delay=0.08, # Slightly longer delay for more noticeable transitions
235
+ if result is None:
236
+ ui.on_error("Failed to submit result")
237
+ return OptimizationResult(
238
+ success=False,
239
+ final_step=step,
240
+ status="error",
241
+ reason="submit_failed",
242
+ details="Failed to submit execution result",
425
243
  )
426
244
 
427
- # Run evaluation and restore original code after
428
- term_out = run_evaluation_with_file_swap(
429
- file_path=source_fp,
430
- new_content=eval_and_next_solution_response["code"],
431
- original_content=source_code,
432
- eval_command=eval_command,
433
- timeout=eval_timeout,
434
- )
245
+ is_done = result.get("is_done", False)
246
+ prev_metric = result.get("previous_solution_metric_value")
435
247
 
436
- # Save logs if requested
437
- if save_logs:
438
- save_execution_output(runs_dir, step=step, output=term_out)
439
- eval_output_panel.update(output=term_out)
440
- smooth_update(
441
- live=live,
442
- layout=layout,
443
- sections_to_update=[("eval_output", eval_output_panel.get_display())],
444
- transition_delay=0.1,
445
- )
248
+ if prev_metric is not None:
249
+ ui.on_metric(step, prev_metric)
446
250
 
447
- if not user_stop_requested_flag:
448
- # Evaluate the final solution thats been generated
449
- eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
450
- console=console,
451
- step=steps,
452
- run_id=run_id,
453
- execution_output=term_out,
454
- auth_headers=auth_headers,
455
- api_keys=api_keys,
456
- )
457
- summary_panel.set_step(step=steps)
458
- status_response = get_optimization_run_status(
459
- console=console, run_id=run_id, include_history=True, auth_headers=auth_headers
460
- )
461
- # No need to update the plan panel since we have finished the optimization
462
- # Get the optimization run status for
463
- # the best solution, its score, and the history to plot the tree
464
- nodes_list_from_status_final = status_response.get("nodes")
465
- tree_panel.build_metric_tree(
466
- nodes=nodes_list_from_status_final if nodes_list_from_status_final is not None else []
467
- )
468
- # No need to set any solution to unevaluated since we have finished the optimization
469
- # and all solutions have been evaluated
470
- # No need to update the current solution panel since we have finished the optimization
471
- # We only need to update the best solution panel
472
- # Figure out if we have a best solution so far
473
- best_solution_node = get_best_node_from_status(status_response=status_response)
474
- best_solution_code = best_solution_node.code
475
- # Save best solution to .runs/<run-id>/best.<extension>
476
- write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
477
- solution_panels.update(current_node=None, best_node=best_solution_node)
478
- _, best_solution_panel = solution_panels.get_display(current_step=steps)
479
- # Update the end optimization layout
480
- final_message = (
481
- 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']}[/] 🏆"
482
- if best_solution_node is not None and best_solution_node.metric is not None
483
- else "[red] No valid solution found.[/]"
484
- )
485
- end_optimization_layout["summary"].update(summary_panel.get_display(final_message=final_message))
486
- end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
487
- end_optimization_layout["best_solution"].update(best_solution_panel)
251
+ step += 1
488
252
 
489
- # Mark as completed normally for the finally block
490
- optimization_completed_normally = True
491
- live.update(end_optimization_layout)
253
+ if is_done:
254
+ ui.on_complete(step)
255
+ return OptimizationResult(success=True, final_step=step, status="completed", reason="completed_successfully")
492
256
 
257
+ except KeyboardInterrupt:
258
+ ui.on_interrupted()
259
+ return OptimizationResult(success=False, final_step=step, status="terminated", reason="user_terminated_sigint")
493
260
  except Exception as e:
494
- # Catch errors during the main optimization loop or setup
495
- try:
496
- error_message = e.response.json()["detail"]
497
- except Exception:
498
- error_message = str(e)
499
- console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
500
- console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]\n")
501
- # Ensure optimization_completed_normally is False
502
- optimization_completed_normally = False
503
- finally:
504
- # Restore original signal handlers
505
- signal.signal(signal.SIGINT, original_sigint_handler)
506
- signal.signal(signal.SIGTERM, original_sigterm_handler)
261
+ ui.on_error(f"Error: {e}")
262
+ return OptimizationResult(success=False, final_step=step, status="error", reason="unknown", details=str(e))
507
263
 
508
- # Stop heartbeat thread
509
- stop_heartbeat_event.set()
510
- if heartbeat_thread and heartbeat_thread.is_alive():
511
- heartbeat_thread.join(timeout=2)
512
-
513
- # Report final status if run exists
514
- if run_id:
515
- if optimization_completed_normally:
516
- status, reason, details = "completed", "completed_successfully", None
517
- elif user_stop_requested_flag:
518
- status, reason, details = "terminated", "user_requested_stop", "Run stopped by user request via dashboard."
519
- else:
520
- status, reason = "error", "error_cli_internal"
521
- details = locals().get("error_details") or (
522
- traceback.format_exc()
523
- if "e" in locals() and isinstance(locals()["e"], Exception)
524
- else "CLI terminated unexpectedly without a specific exception captured."
525
- )
526
264
 
527
- if best_solution_code and best_solution_code != original_source_code:
528
- # Determine whether to apply: automatically if --apply-change is set, otherwise ask user
529
- should_apply = apply_change or Confirm.ask(
530
- "Would you like to apply the best solution to the source file?", default=True
531
- )
532
- if should_apply:
533
- write_to_path(fp=source_fp, content=best_solution_code)
534
- console.print("\n[green]Best solution applied to the source file.[/]\n")
535
- else:
536
- console.print("\n[green]A better solution was not found. No changes to apply.[/]\n")
265
+ def _offer_apply_best_solution(
266
+ console: Console,
267
+ run_id: str,
268
+ source_fp: pathlib.Path,
269
+ source_code: str,
270
+ runs_dir: pathlib.Path,
271
+ auth_headers: dict,
272
+ apply_change: bool = False,
273
+ ) -> None:
274
+ """
275
+ Fetch the best solution from the backend and offer to apply it to the source file.
537
276
 
538
- report_termination(
539
- run_id=run_id,
540
- status_update=status,
541
- reason=reason,
542
- details=details,
543
- auth_headers=current_auth_headers_for_heartbeat,
544
- )
277
+ Args:
278
+ console: Rich console for output.
279
+ run_id: The optimization run ID.
280
+ source_fp: Path to the source file.
281
+ source_code: Original source code content.
282
+ runs_dir: Directory for run logs.
283
+ auth_headers: Authentication headers.
284
+ apply_change: If True, apply automatically without prompting.
285
+ """
286
+ try:
287
+ # Fetch final status to get best solution
288
+ status = get_optimization_run_status(console=console, run_id=run_id, include_history=False, auth_headers=auth_headers)
289
+ best_result = status.get("best_result")
290
+
291
+ if best_result is None:
292
+ console.print("\n[yellow]No solution found. No changes to apply.[/]\n")
293
+ return
294
+
295
+ best_code = best_result.get("code")
296
+ best_metric = best_result.get("metric_value")
545
297
 
546
- # Handle exit
547
- if user_stop_requested_flag:
548
- console.print("[yellow]Run terminated by user request.[/]")
549
- console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]\n")
298
+ if not best_code or best_code == source_code:
299
+ console.print("\n[green]Best solution is the same as original. No changes to apply.[/]\n")
300
+ return
550
301
 
551
- return optimization_completed_normally or user_stop_requested_flag
302
+ # Save best solution to logs
303
+ write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_code)
304
+
305
+ # Show summary
306
+ console.print("\n[bold green]Optimization complete![/]")
307
+ if best_metric is not None:
308
+ console.print(f"[green]Best metric value: {best_metric}[/]")
309
+
310
+ # Ask user or auto-apply
311
+ if apply_change:
312
+ should_apply = True
313
+ else:
314
+ should_apply = Confirm.ask("Would you like to apply the best solution to your source file?", default=True)
315
+
316
+ if should_apply:
317
+ write_to_path(fp=source_fp, content=best_code)
318
+ console.print(f"[green]Best solution applied to {source_fp}[/]\n")
319
+ else:
320
+ console.print(f"[dim]Best solution saved to {runs_dir / f'best{source_fp.suffix}'}[/]\n")
321
+
322
+ except Exception as e:
323
+ console.print(f"[yellow]Could not fetch best solution: {e}[/]")
552
324
 
553
325
 
554
326
  def resume_optimization(
555
- run_id: str, console: Optional[Console] = None, apply_change: bool = False, api_keys: Optional[dict[str, str]] = None
327
+ run_id: str, api_keys: Optional[dict] = None, poll_interval: float = 2.0, apply_change: bool = False
556
328
  ) -> bool:
557
- """Resume an interrupted run from the most recent node and continue optimization."""
558
- if console is None:
559
- console = Console()
329
+ """
330
+ Resume an interrupted run using the queue-based optimization loop.
560
331
 
561
- # Globals for this optimization run
562
- heartbeat_thread = None
563
- stop_heartbeat_event = threading.Event()
564
- current_run_id_for_heartbeat = None
565
- current_auth_headers_for_heartbeat = {}
566
- live_ref = None # Reference to the Live object for the optimization run
332
+ Polls for execution tasks, executes locally, and submits results.
333
+ Uses the execution queue flow instead of the legacy direct flow.
567
334
 
568
- best_solution_code = None
569
- original_source_code = None
335
+ Args:
336
+ run_id: The UUID of the run to resume.
337
+ api_keys: Optional API keys for LLM providers.
338
+ poll_interval: Seconds between polling attempts.
339
+ apply_change: If True, automatically apply best solution; if False, prompt user.
570
340
 
571
- # Signal handler for this optimization run
572
- def signal_handler(signum, frame):
573
- nonlocal live_ref
574
- if live_ref is not None:
575
- live_ref.stop() # Stop the live update loop so that messages are printed to the console
341
+ Returns:
342
+ True if optimization completed successfully, False otherwise.
343
+ """
344
+ console = Console()
576
345
 
577
- signal_name = signal.Signals(signum).name
578
- console.print(f"\n[bold yellow]Termination signal ({signal_name}) received. Shutting down...[/]\n")
579
- stop_heartbeat_event.set()
580
- if heartbeat_thread and heartbeat_thread.is_alive():
581
- heartbeat_thread.join(timeout=2)
582
- if current_run_id_for_heartbeat:
583
- report_termination(
584
- run_id=current_run_id_for_heartbeat,
585
- status_update="terminated",
586
- reason=f"user_terminated_{signal_name.lower()}",
587
- details=f"Process terminated by signal {signal_name} ({signum}).",
588
- auth_headers=current_auth_headers_for_heartbeat,
589
- )
590
- console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {current_run_id_for_heartbeat}[/]\n")
591
- sys.exit(0)
346
+ # Authenticate
347
+ weco_api_key, auth_headers = handle_authentication(console)
348
+ if weco_api_key is None:
349
+ return False
592
350
 
593
- # Set up signal handlers for this run
594
- original_sigint_handler = signal.signal(signal.SIGINT, signal_handler)
595
- original_sigterm_handler = signal.signal(signal.SIGTERM, signal_handler)
351
+ # Fetch status first for validation and to display confirmation info
352
+ try:
353
+ status = get_optimization_run_status(console=console, run_id=run_id, include_history=True, auth_headers=auth_headers)
354
+ except Exception as e:
355
+ console.print(f"[bold red]Error fetching run status: {e}[/]")
356
+ return False
357
+
358
+ run_status_val = status.get("status")
359
+ if run_status_val not in ("error", "terminated"):
360
+ console.print(
361
+ f"[yellow]Run {run_id} cannot be resumed (status: {run_status_val}). "
362
+ f"Only 'error' or 'terminated' runs can be resumed.[/]"
363
+ )
364
+ return False
596
365
 
597
- optimization_completed_normally = False
598
- user_stop_requested_flag = False
366
+ objective = status.get("objective", {})
367
+ metric_name = objective.get("metric_name", "metric")
368
+ maximize = bool(objective.get("maximize", True))
369
+ eval_command = objective.get("evaluation_command", "")
599
370
 
600
- try:
601
- # --- Login/Authentication Handling (now mandatory) ---
602
- weco_api_key, auth_headers = handle_authentication(console)
603
- if weco_api_key is None:
604
- # Authentication failed or user declined
605
- return False
371
+ optimizer = status.get("optimizer", {})
372
+ total_steps = optimizer.get("steps", 0)
373
+ current_step = int(status.get("current_step", 0))
374
+ steps_remaining = int(total_steps) - current_step
606
375
 
607
- current_auth_headers_for_heartbeat = auth_headers
376
+ model_name = (
377
+ (optimizer.get("code_generator") or {}).get("model") or (optimizer.get("evaluator") or {}).get("model") or "unknown"
378
+ )
608
379
 
609
- # Fetch status first for validation and to display confirmation info
610
- try:
611
- status = get_optimization_run_status(
612
- console=console, run_id=run_id, include_history=True, auth_headers=auth_headers
613
- )
614
- except Exception as e:
615
- console.print(
616
- Panel(f"[bold red]Error fetching run status: {e}", title="[bold red]Resume Error", border_style="red")
617
- )
618
- return False
619
-
620
- run_status_val = status.get("status")
621
- if run_status_val not in ("error", "terminated"):
622
- console.print(
623
- Panel(
624
- f"Run {run_id} cannot be resumed (status: {run_status_val}). Only 'error' or 'terminated' runs can be resumed.",
625
- title="[bold yellow]Resume Not Allowed",
626
- border_style="yellow",
627
- )
380
+ # Display confirmation info
381
+ console.print("[cyan]Resume Run Confirmation[/]")
382
+ console.print(f" Run ID: {run_id}")
383
+ console.print(f" Run Name: {status.get('run_name', 'N/A')}")
384
+ console.print(f" Status: {run_status_val}")
385
+ console.print(f" Objective: {metric_name} ({'maximize' if maximize else 'minimize'})")
386
+ console.print(f" Model: {model_name}")
387
+ console.print(f" Eval Command: {eval_command}")
388
+ console.print(f" Total Steps: {total_steps} | Current Step: {current_step} | Steps Remaining: {steps_remaining}")
389
+ console.print(f" Last Updated: {status.get('updated_at', 'N/A')}")
390
+
391
+ unchanged = Confirm.ask(
392
+ "Have you kept the source file and evaluation command unchanged since the original run?", default=True
393
+ )
394
+ if not unchanged:
395
+ console.print("[yellow]Resume cancelled. Please start a new run if the environment changed.[/]")
396
+ return False
397
+
398
+ # Call backend to prepare resume (this sets status to 'running')
399
+ resume_resp = resume_optimization_run(console=console, run_id=run_id, auth_headers=auth_headers)
400
+ if resume_resp is None:
401
+ return False
402
+
403
+ source_path = resume_resp.get("source_path")
404
+ log_dir = resume_resp.get("log_dir", ".runs")
405
+ save_logs = bool(resume_resp.get("save_logs", False))
406
+ eval_timeout = resume_resp.get("eval_timeout")
407
+
408
+ # Read the original source code
409
+ source_fp = pathlib.Path(source_path)
410
+ source_fp.parent.mkdir(parents=True, exist_ok=True)
411
+ source_code = read_from_path(fp=source_fp, is_json=False) if source_fp.exists() else ""
412
+
413
+ dashboard_url = f"{__dashboard_url__}/runs/{run_id}"
414
+ run_name = resume_resp.get("run_name", run_id)
415
+
416
+ # Open dashboard in the user's browser
417
+ open_browser(dashboard_url)
418
+
419
+ # Setup logging directory
420
+ runs_dir = pathlib.Path(log_dir) / run_id
421
+ runs_dir.mkdir(parents=True, exist_ok=True)
422
+
423
+ # Start heartbeat thread
424
+ stop_heartbeat_event = threading.Event()
425
+ heartbeat_thread = HeartbeatSender(run_id, auth_headers, stop_heartbeat_event)
426
+ heartbeat_thread.start()
427
+
428
+ # Extract best solution info from resume response (if available)
429
+ best_metric_value = resume_resp.get("best_metric_value")
430
+ best_step = resume_resp.get("best_step")
431
+
432
+ result: Optional[OptimizationResult] = None
433
+ try:
434
+ with LiveOptimizationUI(
435
+ console, run_id, run_name, total_steps, dashboard_url, model=model_name, metric_name=metric_name
436
+ ) as ui:
437
+ # Populate UI with best solution from previous run if available
438
+ if best_metric_value is not None and best_step is not None:
439
+ ui.on_metric(best_step, best_metric_value)
440
+
441
+ result = _run_optimization_loop(
442
+ ui=ui,
443
+ run_id=run_id,
444
+ auth_headers=auth_headers,
445
+ source_fp=source_fp,
446
+ source_code=source_code,
447
+ eval_command=eval_command,
448
+ eval_timeout=eval_timeout,
449
+ runs_dir=runs_dir,
450
+ save_logs=save_logs,
451
+ start_step=current_step,
452
+ poll_interval=poll_interval,
453
+ api_keys=api_keys,
628
454
  )
629
- return False
630
-
631
- objective = status.get("objective", {})
632
- metric_name = objective.get("metric_name", "metric")
633
- maximize = bool(objective.get("maximize", True))
634
-
635
- optimizer = status.get("optimizer", {})
636
-
637
- console.print("[cyan]Resume Run Confirmation[/]")
638
- console.print(f" Run ID: {run_id}")
639
- console.print(f" Run Name: {status.get('run_name', 'N/A')}")
640
- console.print(f" Status: {run_status_val}")
641
- # Objective and model
642
- console.print(f" Objective: {metric_name} ({'maximize' if maximize else 'minimize'})")
643
- model_name = (
644
- (optimizer.get("code_generator") or {}).get("model")
645
- or (optimizer.get("evaluator") or {}).get("model")
646
- or "unknown"
647
- )
648
- console.print(f" Model: {model_name}")
649
- console.print(f" Eval Command: {objective.get('evaluation_command', 'N/A')}")
650
- # Steps summary
651
- total_steps = optimizer.get("steps")
652
- current_step = int(status["current_step"])
653
- steps_remaining = int(total_steps) - int(current_step)
654
- console.print(f" Total Steps: {total_steps} | Resume Step: {current_step} | Steps Remaining: {steps_remaining}")
655
- console.print(f" Last Updated: {status.get('updated_at', 'N/A')}")
656
- unchanged = Confirm.ask(
657
- "Have you kept the source file and evaluation command unchanged since the original run?", default=True
658
- )
659
- if not unchanged:
660
- console.print("[yellow]Resume cancelled. Please start a new run if the environment changed.[/]")
661
- return False
662
-
663
- # Call backend to prepare resume
664
- resume_resp = resume_optimization_run(console=console, run_id=run_id, auth_headers=auth_headers)
665
- if resume_resp is None:
666
- return False
667
-
668
- eval_command = resume_resp["evaluation_command"]
669
- source_path = resume_resp.get("source_path")
670
-
671
- # Use backend-saved values
672
- log_dir = resume_resp.get("log_dir", ".runs")
673
- save_logs = bool(resume_resp.get("save_logs", False))
674
- eval_timeout = resume_resp.get("eval_timeout")
675
-
676
- # Read the original source code from the file before we start modifying it
677
- source_fp = pathlib.Path(source_path)
678
- source_fp.parent.mkdir(parents=True, exist_ok=True)
679
- # Store the original content to restore after each evaluation
680
- original_source_code = read_from_path(fp=source_fp, is_json=False) if source_fp.exists() else ""
681
- # The code to restore is the code from the last step of the previous run
682
- code_to_restore = resume_resp.get("code") or resume_resp.get("source_code") or ""
683
-
684
- # Prepare UI panels
685
- summary_panel = SummaryPanel(
686
- maximize=maximize, metric_name=metric_name, total_steps=total_steps, model=model_name, runs_dir=log_dir
455
+
456
+ # Stop heartbeat immediately after loop completes
457
+ stop_heartbeat_event.set()
458
+ heartbeat_thread.join(timeout=2)
459
+
460
+ # Show resume message if interrupted
461
+ if result.status == "terminated":
462
+ console.print(f"\n[cyan]To resume this run, use:[/] [bold]weco resume {run_id}[/]\n")
463
+
464
+ # Offer to apply best solution
465
+ _offer_apply_best_solution(
466
+ console=console,
467
+ run_id=run_id,
468
+ source_fp=source_fp,
469
+ source_code=source_code,
470
+ runs_dir=runs_dir,
471
+ auth_headers=auth_headers,
472
+ apply_change=apply_change,
687
473
  )
688
- summary_panel.set_run_id(run_id=resume_resp["run_id"])
689
- if resume_resp.get("run_name"):
690
- summary_panel.set_run_name(resume_resp.get("run_name"))
691
- summary_panel.set_step(step=current_step)
692
- summary_panel.update_thinking(resume_resp.get("plan"))
693
-
694
- solution_panels = SolutionPanels(metric_name=metric_name, source_fp=source_fp)
695
- eval_output_panel = EvaluationOutputPanel()
696
- tree_panel = MetricTreePanel(maximize=maximize)
697
- layout = create_optimization_layout()
698
- end_optimization_layout = create_end_optimization_layout()
699
-
700
- # Build tree from nodes returned by status (history)
701
- nodes_list_from_status = status.get("nodes") or []
702
- tree_panel.build_metric_tree(nodes=nodes_list_from_status)
703
-
704
- # Compute best and current nodes
705
- best_solution_node = get_best_node_from_status(status_response=status)
706
- current_solution_node = get_node_from_status(status_response=status, solution_id=resume_resp.get("solution_id"))
707
-
708
- # If there's no best solution yet (baseline evaluation didn't complete),
709
- # mark the current node as unevaluated so the tree renders correctly
710
- if best_solution_node is None:
711
- tree_panel.set_unevaluated_node(node_id=resume_resp.get("solution_id"))
712
-
713
- # Ensure runs dir exists
714
- runs_dir = pathlib.Path(log_dir) / resume_resp["run_id"]
715
- runs_dir.mkdir(parents=True, exist_ok=True)
716
- # Persist last step's code into logs as step_<current_step>
717
- write_to_path(fp=runs_dir / f"step_{current_step}{source_fp.suffix}", content=code_to_restore)
718
-
719
- # Initialize best solution code
720
- try:
721
- best_solution_code = best_solution_node.code
722
- except AttributeError:
723
- # Edge case: best solution node is not available.
724
- # This can happen if the user has cancelled the run before even running the baseline solution
725
- pass # Leave best solution code as None
726
-
727
- # Start Heartbeat Thread
728
- stop_heartbeat_event.clear()
729
- heartbeat_thread = HeartbeatSender(resume_resp["run_id"], auth_headers, stop_heartbeat_event)
730
- heartbeat_thread.start()
731
- current_run_id_for_heartbeat = resume_resp["run_id"]
732
-
733
- # Seed solution panels with current and best nodes
734
- solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
735
-
736
- # --- Live UI ---
737
- refresh_rate = 4
738
- with Live(layout, refresh_per_second=refresh_rate) as live:
739
- live_ref = live
740
- # Initial panels
741
- current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=current_step)
742
- # Use backend-provided execution output only (no fallback)
743
- term_out = resume_resp.get("execution_output") or ""
744
- eval_output_panel.update(output=term_out)
745
-
746
- # Update the initial panels
747
- smooth_update(
748
- live=live,
749
- layout=layout,
750
- sections_to_update=[
751
- ("summary", summary_panel.get_display()),
752
- ("tree", tree_panel.get_display(is_done=False)),
753
- ("current_solution", current_solution_panel),
754
- ("best_solution", best_solution_panel),
755
- ("eval_output", eval_output_panel.get_display()),
756
- ],
757
- transition_delay=0.1,
758
- )
759
474
 
760
- # If missing output, evaluate once before first suggest
761
- if term_out is None or len(term_out.strip()) == 0:
762
- term_out = run_evaluation_with_file_swap(
763
- file_path=source_fp,
764
- new_content=code_to_restore,
765
- original_content=original_source_code,
766
- eval_command=eval_command,
767
- timeout=eval_timeout,
768
- )
769
- eval_output_panel.update(output=term_out)
770
- # Update the evaluation output panel
771
- smooth_update(
772
- live=live,
773
- layout=layout,
774
- sections_to_update=[("eval_output", eval_output_panel.get_display())],
775
- transition_delay=0.1,
776
- )
475
+ return result.success
476
+ finally:
477
+ # Ensure heartbeat is stopped (in case of early exit/exception)
478
+ stop_heartbeat_event.set()
479
+ heartbeat_thread.join(timeout=2)
777
480
 
778
- if save_logs:
779
- save_execution_output(runs_dir, step=current_step, output=term_out)
780
-
781
- # Continue optimization: steps current_step+1..total_steps
782
- for step in range(current_step + 1, total_steps + 1):
783
- # Stop polling
784
- try:
785
- current_status_response = get_optimization_run_status(
786
- console=console, run_id=resume_resp["run_id"], include_history=False, auth_headers=auth_headers
787
- )
788
- if current_status_response.get("status") == "stopping":
789
- console.print("\n[bold yellow]Stop request received. Terminating run gracefully...[/]")
790
- user_stop_requested_flag = True
791
- break
792
- except requests.exceptions.RequestException as e:
793
- console.print(f"\n[bold red]Warning: Unable to check run status: {e}. Continuing optimization...[/]")
794
- except Exception as e:
795
- console.print(f"\n[bold red]Warning: Error checking run status: {e}. Continuing optimization...[/]")
796
-
797
- # Suggest next
798
- eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
799
- console=console,
800
- step=step,
801
- run_id=resume_resp["run_id"],
802
- execution_output=term_out,
481
+ # Report termination to backend
482
+ if result is not None:
483
+ try:
484
+ report_termination(
485
+ run_id=run_id,
486
+ status_update=result.status,
487
+ reason=result.reason,
488
+ details=result.details,
803
489
  auth_headers=auth_headers,
804
- api_keys=api_keys,
805
490
  )
491
+ except Exception:
492
+ pass # Best effort
806
493
 
807
- # Save next solution to logs
808
- write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"])
809
494
 
810
- # Refresh status with history and update panels
811
- status_response = get_optimization_run_status(
812
- console=console, run_id=resume_resp["run_id"], include_history=True, auth_headers=auth_headers
813
- )
814
- summary_panel.set_step(step=step)
815
- summary_panel.update_thinking(thinking=eval_and_next_solution_response.get("plan", ""))
816
- nodes_list = status_response.get("nodes") or []
817
- tree_panel.build_metric_tree(nodes=nodes_list)
818
- tree_panel.set_unevaluated_node(node_id=eval_and_next_solution_response["solution_id"])
819
- best_solution_node = get_best_node_from_status(status_response=status_response)
820
- current_solution_node = get_node_from_status(
821
- status_response=status_response, solution_id=eval_and_next_solution_response["solution_id"]
822
- )
495
+ def optimize(
496
+ source: str,
497
+ eval_command: str,
498
+ metric: str,
499
+ goal: str = "maximize",
500
+ model: str = "o4-mini",
501
+ steps: int = 5,
502
+ additional_instructions: Optional[str] = None,
503
+ eval_timeout: Optional[int] = None,
504
+ save_logs: bool = False,
505
+ log_dir: str = ".runs",
506
+ api_keys: Optional[dict] = None,
507
+ poll_interval: float = 2.0,
508
+ apply_change: bool = False,
509
+ require_review: bool = False,
510
+ ) -> bool:
511
+ """
512
+ Simplified queue-based optimization loop.
823
513
 
824
- # Set best solution and save optimization results
825
- try:
826
- best_solution_code = best_solution_node.code
827
- except AttributeError:
828
- # Can happen if the code was buggy
829
- best_solution_code = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
830
-
831
- # Save best solution to .runs/<run-id>/best.<extension>
832
- write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
833
-
834
- solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
835
- current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
836
- eval_output_panel.clear()
837
- smooth_update(
838
- live=live,
839
- layout=layout,
840
- sections_to_update=[
841
- ("summary", summary_panel.get_display()),
842
- ("tree", tree_panel.get_display(is_done=False)),
843
- ("current_solution", current_solution_panel),
844
- ("best_solution", best_solution_panel),
845
- ("eval_output", eval_output_panel.get_display()),
846
- ],
847
- transition_delay=0.08,
848
- )
514
+ Polls for execution tasks, executes locally, and submits results.
515
+ Uses the new execution queue flow instead of the legacy direct flow.
849
516
 
850
- # Evaluate this new solution and restore original code after
851
- term_out = run_evaluation_with_file_swap(
852
- file_path=source_fp,
853
- new_content=eval_and_next_solution_response["code"],
854
- original_content=original_source_code,
855
- eval_command=eval_command,
856
- timeout=eval_timeout,
857
- )
858
- if save_logs:
859
- save_execution_output(runs_dir, step=step, output=term_out)
860
- eval_output_panel.update(output=term_out)
861
- smooth_update(
862
- live=live,
863
- layout=layout,
864
- sections_to_update=[("eval_output", eval_output_panel.get_display())],
865
- transition_delay=0.1,
866
- )
517
+ Args:
518
+ source: Path to the source file to optimize.
519
+ eval_command: Command to run for evaluation.
520
+ metric: Name of the metric to optimize.
521
+ goal: "maximize" or "minimize".
522
+ model: LLM model to use.
523
+ steps: Number of optimization steps.
524
+ additional_instructions: Optional instructions for the optimizer.
525
+ eval_timeout: Timeout for evaluation command in seconds.
526
+ save_logs: Whether to save execution logs.
527
+ log_dir: Directory for logs.
528
+ api_keys: Optional API keys for LLM providers.
529
+ poll_interval: Seconds between polling attempts.
530
+ apply_change: If True, automatically apply best solution; if False, prompt user.
867
531
 
868
- # Final flush if not stopped
869
- if not user_stop_requested_flag:
870
- eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
871
- console=console,
872
- step=total_steps,
873
- run_id=resume_resp["run_id"],
874
- execution_output=term_out,
875
- auth_headers=auth_headers,
876
- api_keys=api_keys,
877
- )
878
- summary_panel.set_step(step=total_steps)
879
- status_response = get_optimization_run_status(
880
- console=console, run_id=resume_resp["run_id"], include_history=True, auth_headers=auth_headers
881
- )
882
- nodes_final = status_response.get("nodes") or []
883
- tree_panel.build_metric_tree(nodes=nodes_final)
884
- # Best solution panel and final message
885
- best_solution_node = get_best_node_from_status(status_response=status_response)
886
- best_solution_code = best_solution_node.code
887
- # Save best solution to .runs/<run-id>/best.<extension>
888
- write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
889
-
890
- solution_panels.update(current_node=None, best_node=best_solution_node)
891
- _, best_solution_panel = solution_panels.get_display(current_step=total_steps)
892
- final_message = (
893
- 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']}[/] 🏆"
894
- if best_solution_node is not None and best_solution_node.metric is not None
895
- else "[red] No valid solution found.[/]"
896
- )
897
- end_optimization_layout["summary"].update(summary_panel.get_display(final_message=final_message))
898
- end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
899
- end_optimization_layout["best_solution"].update(best_solution_panel)
532
+ Returns:
533
+ True if optimization completed successfully, False otherwise.
534
+ """
535
+ console = Console()
536
+
537
+ # Authenticate
538
+ weco_api_key, auth_headers = handle_authentication(console)
539
+ if weco_api_key is None:
540
+ # Authentication failed or user declined
541
+ return False
542
+
543
+ # Process parameters
544
+ maximize = goal.lower() in ["maximize", "max"]
545
+ source_fp = pathlib.Path(source)
546
+ source_code = read_from_path(fp=source_fp, is_json=False)
547
+
548
+ code_generator_config = {"model": model}
549
+ evaluator_config = {"model": model, "include_analysis": True}
550
+ search_policy_config = {
551
+ "num_drafts": max(1, math.ceil(0.15 * steps)),
552
+ "debug_prob": 0.5,
553
+ "max_debug_depth": max(1, math.ceil(0.1 * steps)),
554
+ }
555
+ processed_instructions = read_additional_instructions(additional_instructions)
556
+
557
+ # Start the run
558
+ run_response = start_optimization_run(
559
+ console=console,
560
+ source_code=source_code,
561
+ source_path=str(source_fp),
562
+ evaluation_command=eval_command,
563
+ metric_name=metric,
564
+ maximize=maximize,
565
+ steps=steps,
566
+ code_generator_config=code_generator_config,
567
+ evaluator_config=evaluator_config,
568
+ search_policy_config=search_policy_config,
569
+ additional_instructions=processed_instructions,
570
+ eval_timeout=eval_timeout,
571
+ save_logs=save_logs,
572
+ log_dir=log_dir,
573
+ auth_headers=auth_headers,
574
+ api_keys=api_keys,
575
+ require_review=require_review,
576
+ )
900
577
 
901
- optimization_completed_normally = True
902
- live.update(end_optimization_layout)
578
+ if run_response is None:
579
+ return False
903
580
 
904
- except Exception as e:
905
- try:
906
- error_message = e.response.json()["detail"]
907
- except Exception:
908
- error_message = str(e)
909
- console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
910
- console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]\n")
911
- optimization_completed_normally = False
912
- finally:
913
- signal.signal(signal.SIGINT, original_sigint_handler)
914
- signal.signal(signal.SIGTERM, original_sigterm_handler)
915
- stop_heartbeat_event.set()
916
- if heartbeat_thread and heartbeat_thread.is_alive():
917
- heartbeat_thread.join(timeout=2)
581
+ run_id = run_response["run_id"]
582
+ run_name = run_response["run_name"]
583
+ dashboard_url = f"{__dashboard_url__}/runs/{run_id}"
918
584
 
919
- try:
920
- run_id = resume_resp.get("run_id")
921
- except Exception:
922
- run_id = None
923
-
924
- # Report final status if run exists
925
- if run_id:
926
- if optimization_completed_normally:
927
- status, reason, details = "completed", "completed_successfully", None
928
- elif user_stop_requested_flag:
929
- status, reason, details = "terminated", "user_requested_stop", "Run stopped by user request via dashboard."
930
- else:
931
- status, reason = "error", "error_cli_internal"
932
- details = locals().get("error_details") or (
933
- traceback.format_exc()
934
- if "e" in locals() and isinstance(locals()["e"], Exception)
935
- else "CLI terminated unexpectedly without a specific exception captured."
936
- )
585
+ # Open dashboard in the user's browser
586
+ open_browser(dashboard_url)
937
587
 
938
- if best_solution_code and best_solution_code != original_source_code:
939
- should_apply = apply_change or Confirm.ask(
940
- "Would you like to apply the best solution to the source file?", default=True
941
- )
942
- if should_apply:
943
- write_to_path(fp=source_fp, content=best_solution_code)
944
- console.print("\n[green]Best solution applied to the source file.[/]\n")
945
- else:
946
- console.print("\n[green]A better solution was not found. No changes to apply.[/]\n")
588
+ # Setup logging directory
589
+ runs_dir = pathlib.Path(log_dir) / run_id
590
+ runs_dir.mkdir(parents=True, exist_ok=True)
591
+
592
+ # Start heartbeat thread
593
+ stop_heartbeat_event = threading.Event()
594
+ heartbeat_thread = HeartbeatSender(run_id, auth_headers, stop_heartbeat_event)
595
+ heartbeat_thread.start()
947
596
 
948
- report_termination(
597
+ result: Optional[OptimizationResult] = None
598
+ try:
599
+ with LiveOptimizationUI(console, run_id, run_name, steps, dashboard_url, model=model, metric_name=metric) as ui:
600
+ result = _run_optimization_loop(
601
+ ui=ui,
949
602
  run_id=run_id,
950
- status_update=status,
951
- reason=reason,
952
- details=details,
953
- auth_headers=current_auth_headers_for_heartbeat,
603
+ auth_headers=auth_headers,
604
+ source_fp=source_fp,
605
+ source_code=source_code,
606
+ eval_command=eval_command,
607
+ eval_timeout=eval_timeout,
608
+ runs_dir=runs_dir,
609
+ save_logs=save_logs,
610
+ start_step=0,
611
+ poll_interval=poll_interval,
612
+ api_keys=api_keys,
954
613
  )
955
- if user_stop_requested_flag:
956
- console.print("[yellow]Run terminated by user request.[/]")
957
- console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]\n")
958
- return optimization_completed_normally or user_stop_requested_flag
614
+
615
+ # Stop heartbeat immediately after loop completes
616
+ stop_heartbeat_event.set()
617
+ heartbeat_thread.join(timeout=2)
618
+
619
+ # Show resume message if interrupted
620
+ if result.status == "terminated":
621
+ console.print(f"\n[cyan]To resume this run, use:[/] [bold]weco resume {run_id}[/]\n")
622
+
623
+ # Offer to apply best solution
624
+ _offer_apply_best_solution(
625
+ console=console,
626
+ run_id=run_id,
627
+ source_fp=source_fp,
628
+ source_code=source_code,
629
+ runs_dir=runs_dir,
630
+ auth_headers=auth_headers,
631
+ apply_change=apply_change,
632
+ )
633
+
634
+ return result.success
635
+ finally:
636
+ # Ensure heartbeat is stopped (in case of early exit/exception)
637
+ stop_heartbeat_event.set()
638
+ heartbeat_thread.join(timeout=2)
639
+
640
+ # Report termination to backend
641
+ if result is not None:
642
+ try:
643
+ report_termination(
644
+ run_id=run_id,
645
+ status_update=result.status,
646
+ reason=result.reason,
647
+ details=result.details,
648
+ auth_headers=auth_headers,
649
+ )
650
+ except Exception:
651
+ pass # Best effort