|
9 | 9 | import tomllib |
10 | 10 | import uuid |
11 | 11 | from pathlib import Path |
12 | | -from typing import Optional |
| 12 | +from collections.abc import Callable |
13 | 13 |
|
14 | 14 | import typer |
15 | 15 | from rich.console import Console |
@@ -290,90 +290,96 @@ def __init__(self, console: Console) -> None: |
290 | 290 | self._rprint = console.print |
291 | 291 |
|
292 | 292 | def emit(self, event: Event) -> None: |
293 | | - d = event.data |
294 | | - t = event.type |
295 | | - |
296 | | - if t == EventType.RUN_STARTED: |
297 | | - if d.get("timeout"): |
298 | | - self._rprint(f"[dim]Timeout: {_format_duration(d['timeout'])} per iteration[/dim]") |
299 | | - if d.get("checks"): |
300 | | - self._rprint(f"[dim]Checks: {d['checks']} enabled[/dim]") |
301 | | - if d.get("contexts"): |
302 | | - self._rprint(f"[dim]Contexts: {d['contexts']} enabled[/dim]") |
303 | | - if d.get("instructions"): |
304 | | - self._rprint(f"[dim]Instructions: {d['instructions']} enabled[/dim]") |
305 | | - |
306 | | - elif t == EventType.ITERATION_STARTED: |
307 | | - self._rprint(f"\n[bold blue]── Iteration {d['iteration']} ──[/bold blue]") |
308 | | - |
309 | | - elif t in (EventType.ITERATION_COMPLETED, EventType.ITERATION_FAILED, EventType.ITERATION_TIMED_OUT): |
310 | | - iteration = d["iteration"] |
311 | | - returncode = d.get("returncode") |
312 | | - detail = d["detail"] |
313 | | - log_file = d.get("log_file") |
314 | | - |
315 | | - if returncode is None: |
316 | | - color, icon = "yellow", "\u23f1" |
317 | | - elif returncode == 0: |
318 | | - color, icon = "green", "\u2713" |
| 293 | + handler = self._handlers.get(event.type) |
| 294 | + if handler: |
| 295 | + handler(self, event.data) |
| 296 | + |
| 297 | + def _on_run_started(self, d: dict) -> None: |
| 298 | + if d.get("timeout"): |
| 299 | + self._rprint(f"[dim]Timeout: {_format_duration(d['timeout'])} per iteration[/dim]") |
| 300 | + if d.get("checks"): |
| 301 | + self._rprint(f"[dim]Checks: {d['checks']} enabled[/dim]") |
| 302 | + if d.get("contexts"): |
| 303 | + self._rprint(f"[dim]Contexts: {d['contexts']} enabled[/dim]") |
| 304 | + if d.get("instructions"): |
| 305 | + self._rprint(f"[dim]Instructions: {d['instructions']} enabled[/dim]") |
| 306 | + |
| 307 | + def _on_iteration_started(self, d: dict) -> None: |
| 308 | + self._rprint(f"\n[bold blue]── Iteration {d['iteration']} ──[/bold blue]") |
| 309 | + |
| 310 | + def _on_iteration_ended(self, d: dict) -> None: |
| 311 | + returncode = d.get("returncode") |
| 312 | + if returncode is None: |
| 313 | + color, icon = "yellow", "\u23f1" |
| 314 | + elif returncode == 0: |
| 315 | + color, icon = "green", "\u2713" |
| 316 | + else: |
| 317 | + color, icon = "red", "\u2717" |
| 318 | + |
| 319 | + status_msg = f"[{color}]{icon} Iteration {d['iteration']} {d['detail']}" |
| 320 | + if d.get("log_file"): |
| 321 | + status_msg += f" \u2192 {d['log_file']}" |
| 322 | + status_msg += f"[/{color}]" |
| 323 | + self._rprint(status_msg) |
| 324 | + |
| 325 | + def _on_checks_completed(self, d: dict) -> None: |
| 326 | + parts = [] |
| 327 | + if d["passed"]: |
| 328 | + parts.append(f"{d['passed']} passed") |
| 329 | + if d["failed"]: |
| 330 | + parts.append(f"{d['failed']} failed") |
| 331 | + self._rprint(f" [bold]Checks:[/bold] {', '.join(parts)}") |
| 332 | + for r in d["results"]: |
| 333 | + if r["passed"]: |
| 334 | + self._rprint(f" [green]\u2713[/green] {r['name']}") |
| 335 | + elif r["timed_out"]: |
| 336 | + self._rprint(f" [yellow]\u23f1[/yellow] {r['name']} (timed out)") |
319 | 337 | else: |
320 | | - color, icon = "red", "\u2717" |
321 | | - |
322 | | - status_msg = f"[{color}]{icon} Iteration {iteration} {detail}" |
323 | | - if log_file: |
324 | | - status_msg += f" \u2192 {log_file}" |
325 | | - status_msg += f"[/{color}]" |
326 | | - self._rprint(status_msg) |
327 | | - |
328 | | - elif t == EventType.CHECKS_COMPLETED: |
329 | | - passed = d["passed"] |
330 | | - failed = d["failed"] |
331 | | - parts = [] |
332 | | - if passed: |
333 | | - parts.append(f"{passed} passed") |
| 338 | + self._rprint(f" [red]\u2717[/red] {r['name']} (exit {r['exit_code']})") |
| 339 | + |
| 340 | + def _on_log_message(self, d: dict) -> None: |
| 341 | + msg = d.get("message", "") |
| 342 | + if "Stopping" in msg: |
| 343 | + self._rprint(f"[red]{msg}[/red]") |
| 344 | + elif "Waiting" in msg: |
| 345 | + self._rprint(f"[dim]{msg}[/dim]") |
| 346 | + |
| 347 | + def _on_run_stopped(self, d: dict) -> None: |
| 348 | + if d.get("reason") == "completed": |
| 349 | + total = d.get("total", 0) |
| 350 | + completed = d.get("completed", 0) |
| 351 | + failed = d.get("failed", 0) |
| 352 | + timed_out_count = d.get("timed_out", 0) |
| 353 | + summary = f"\n[green]Done: {total} iteration(s) \u2014 {completed} succeeded" |
334 | 354 | if failed: |
335 | | - parts.append(f"{failed} failed") |
336 | | - self._rprint(f" [bold]Checks:[/bold] {', '.join(parts)}") |
337 | | - for r in d["results"]: |
338 | | - if r["passed"]: |
339 | | - self._rprint(f" [green]\u2713[/green] {r['name']}") |
340 | | - elif r["timed_out"]: |
341 | | - self._rprint(f" [yellow]\u23f1[/yellow] {r['name']} (timed out)") |
342 | | - else: |
343 | | - self._rprint(f" [red]\u2717[/red] {r['name']} (exit {r['exit_code']})") |
344 | | - |
345 | | - elif t == EventType.LOG_MESSAGE: |
346 | | - msg = d.get("message", "") |
347 | | - if "Stopping" in msg: |
348 | | - self._rprint(f"[red]{msg}[/red]") |
349 | | - elif "Waiting" in msg: |
350 | | - self._rprint(f"[dim]{msg}[/dim]") |
351 | | - |
352 | | - elif t == EventType.RUN_STOPPED: |
353 | | - if d.get("reason") == "completed": |
354 | | - total = d.get("total", 0) |
355 | | - completed = d.get("completed", 0) |
356 | | - failed = d.get("failed", 0) |
357 | | - timed_out_count = d.get("timed_out", 0) |
358 | | - summary = f"\n[green]Done: {total} iteration(s) \u2014 {completed} succeeded" |
359 | | - if failed: |
360 | | - summary += f", {failed} failed" |
361 | | - if timed_out_count: |
362 | | - summary += f" ({timed_out_count} timed out)" |
363 | | - summary += "[/green]" |
364 | | - self._rprint(summary) |
| 355 | + summary += f", {failed} failed" |
| 356 | + if timed_out_count: |
| 357 | + summary += f" ({timed_out_count} timed out)" |
| 358 | + summary += "[/green]" |
| 359 | + self._rprint(summary) |
| 360 | + |
| 361 | + _handlers: dict[EventType, "Callable[[ConsoleEmitter, dict], None]"] = { |
| 362 | + EventType.RUN_STARTED: _on_run_started, |
| 363 | + EventType.ITERATION_STARTED: _on_iteration_started, |
| 364 | + EventType.ITERATION_COMPLETED: _on_iteration_ended, |
| 365 | + EventType.ITERATION_FAILED: _on_iteration_ended, |
| 366 | + EventType.ITERATION_TIMED_OUT: _on_iteration_ended, |
| 367 | + EventType.CHECKS_COMPLETED: _on_checks_completed, |
| 368 | + EventType.LOG_MESSAGE: _on_log_message, |
| 369 | + EventType.RUN_STOPPED: _on_run_stopped, |
| 370 | + } |
365 | 371 |
|
366 | 372 |
|
367 | 373 | @app.command() |
368 | 374 | def run( |
369 | | - prompt_name: Optional[str] = typer.Argument(None, help="Name of a prompt in .ralph/prompts/."), |
370 | | - n: Optional[int] = typer.Option(None, "-n", help="Max number of iterations. Infinite if not set."), |
371 | | - prompt_text: Optional[str] = typer.Option(None, "-p", "--prompt", help="Ad-hoc prompt text. Overrides the prompt file."), |
372 | | - prompt_file: Optional[str] = typer.Option(None, "--prompt-file", "-f", help="Path to prompt file. Overrides ralph.toml."), |
| 375 | + prompt_name: str | None = typer.Argument(None, help="Name of a prompt in .ralph/prompts/."), |
| 376 | + n: int | None = typer.Option(None, "-n", help="Max number of iterations. Infinite if not set."), |
| 377 | + prompt_text: str | None = typer.Option(None, "-p", "--prompt", help="Ad-hoc prompt text. Overrides the prompt file."), |
| 378 | + prompt_file: str | None = typer.Option(None, "--prompt-file", "-f", help="Path to prompt file. Overrides ralph.toml."), |
373 | 379 | stop_on_error: bool = typer.Option(False, "--stop-on-error", "-s", help="Stop if the agent exits with non-zero."), |
374 | 380 | delay: float = typer.Option(0, "--delay", "-d", help="Seconds to wait between iterations."), |
375 | | - log_dir: Optional[str] = typer.Option(None, "--log-dir", "-l", help="Save iteration output to log files in this directory."), |
376 | | - timeout: Optional[float] = typer.Option(None, "--timeout", "-t", help="Max seconds per iteration. Kill agent if exceeded."), |
| 381 | + log_dir: str | None = typer.Option(None, "--log-dir", "-l", help="Save iteration output to log files in this directory."), |
| 382 | + timeout: float | None = typer.Option(None, "--timeout", "-t", help="Max seconds per iteration. Kill agent if exceeded."), |
377 | 383 | ) -> None: |
378 | 384 | """Run the autonomous coding loop. |
379 | 385 |
|
|
0 commit comments