1616from datetime import datetime
1717from enum import Enum
1818from pathlib import Path
19- from typing import NamedTuple
19+ from typing import Any , NamedTuple
2020from ralphify ._events import Event , EventEmitter , EventType , NullEmitter
2121from ralphify ._output import collect_output , format_duration
2222from ralphify .checks import (
@@ -172,34 +172,47 @@ def _discover_enabled_primitives(root: Path) -> EnabledPrimitives:
172172 )
173173
174174
175- def _wait_for_resume (state : RunState , emitter : EventEmitter ) -> bool :
175+ class _BoundEmitter :
176+ """Wraps an EventEmitter with a fixed run_id for concise emission.
177+
178+ Engine-internal helper so every call site doesn't have to repeat
179+ ``Event(type=..., run_id=state.run_id, data={...})``.
180+ """
181+
182+ def __init__ (self , emitter : EventEmitter , run_id : str ) -> None :
183+ self ._emitter = emitter
184+ self ._run_id = run_id
185+
186+ def __call__ (
187+ self , event_type : EventType , data : dict [str , Any ] | None = None ,
188+ ) -> None :
189+ self ._emitter .emit (Event (
190+ type = event_type , run_id = self ._run_id , data = data or {},
191+ ))
192+
193+
194+ def _wait_for_resume (state : RunState , emit : _BoundEmitter ) -> bool :
176195 """Block until the run is resumed or a stop is requested.
177196
178197 Returns ``True`` if the run should continue, ``False`` if a stop was
179198 requested while paused.
180199 """
181- emitter .emit (Event (
182- type = EventType .RUN_PAUSED ,
183- run_id = state .run_id ,
184- ))
200+ emit (EventType .RUN_PAUSED )
185201 while not state .wait_for_unpause (timeout = 0.25 ):
186202 if state .stop_requested :
187203 break
188204 if state .stop_requested :
189205 state .status = RunStatus .STOPPED
190206 return False
191- emitter .emit (Event (
192- type = EventType .RUN_RESUMED ,
193- run_id = state .run_id ,
194- ))
207+ emit (EventType .RUN_RESUMED )
195208 return True
196209
197210
198211def _handle_loop_transitions (
199212 state : RunState ,
200213 config : RunConfig ,
201214 primitives : EnabledPrimitives ,
202- emitter : EventEmitter ,
215+ emit : _BoundEmitter ,
203216) -> tuple [bool , EnabledPrimitives ]:
204217 """Handle stop, pause, and reload transitions at the top of each iteration.
205218
@@ -212,20 +225,16 @@ def _handle_loop_transitions(
212225 return False , primitives
213226
214227 if state .paused :
215- if not _wait_for_resume (state , emitter ):
228+ if not _wait_for_resume (state , emit ):
216229 return False , primitives
217230
218231 if state .consume_reload_request ():
219232 primitives = _discover_enabled_primitives (config .project_root )
220- emitter .emit (Event (
221- type = EventType .PRIMITIVES_RELOADED ,
222- run_id = state .run_id ,
223- data = {
224- "checks" : len (primitives .checks ),
225- "contexts" : len (primitives .contexts ),
226- "instructions" : len (primitives .instructions ),
227- },
228- ))
233+ emit (EventType .PRIMITIVES_RELOADED , {
234+ "checks" : len (primitives .checks ),
235+ "contexts" : len (primitives .contexts ),
236+ "instructions" : len (primitives .instructions ),
237+ })
229238
230239 return True , primitives
231240
@@ -262,7 +271,7 @@ def _execute_agent(
262271 config : RunConfig ,
263272 state : RunState ,
264273 log_path_dir : Path | None ,
265- emitter : EventEmitter ,
274+ emit : _BoundEmitter ,
266275) -> int | None :
267276 """Run the agent subprocess and emit the result event.
268277
@@ -318,26 +327,22 @@ def _execute_agent(
318327 event_type = EventType .ITERATION_FAILED
319328 state_detail = f"failed with exit code { returncode } ({ duration } )"
320329
321- emitter .emit (Event (
322- type = event_type ,
323- run_id = state .run_id ,
324- data = {
325- "iteration" : iteration ,
326- "returncode" : returncode ,
327- "duration" : elapsed ,
328- "duration_formatted" : duration ,
329- "detail" : state_detail ,
330- "log_file" : str (log_file ) if log_file else None ,
331- },
332- ))
330+ emit (event_type , {
331+ "iteration" : iteration ,
332+ "returncode" : returncode ,
333+ "duration" : elapsed ,
334+ "duration_formatted" : duration ,
335+ "detail" : state_detail ,
336+ "log_file" : str (log_file ) if log_file else None ,
337+ })
333338 return returncode
334339
335340
336341def _run_checks_phase (
337342 enabled_checks : list [Check ],
338343 project_root : Path ,
339344 state : RunState ,
340- emitter : EventEmitter ,
345+ emit : _BoundEmitter ,
341346) -> str :
342347 """Execute all checks, emit per-check and summary events.
343348
@@ -346,11 +351,7 @@ def _run_checks_phase(
346351 """
347352 iteration = state .iteration
348353
349- emitter .emit (Event (
350- type = EventType .CHECKS_STARTED ,
351- run_id = state .run_id ,
352- data = {"iteration" : iteration , "count" : len (enabled_checks )},
353- ))
354+ emit (EventType .CHECKS_STARTED , {"iteration" : iteration , "count" : len (enabled_checks )})
354355
355356 check_results = run_all_checks (enabled_checks , project_root )
356357
@@ -364,23 +365,18 @@ def _run_checks_phase(
364365 "timed_out" : cr .timed_out ,
365366 }
366367 results_data .append (result )
367- emitter .emit (Event (
368- type = EventType .CHECK_PASSED if cr .passed else EventType .CHECK_FAILED ,
369- run_id = state .run_id ,
370- data = {"iteration" : iteration , ** result },
371- ))
368+ emit (
369+ EventType .CHECK_PASSED if cr .passed else EventType .CHECK_FAILED ,
370+ {"iteration" : iteration , ** result },
371+ )
372372
373373 passed = sum (1 for r in results_data if r ["passed" ])
374- emitter .emit (Event (
375- type = EventType .CHECKS_COMPLETED ,
376- run_id = state .run_id ,
377- data = {
378- "iteration" : iteration ,
379- "passed" : passed ,
380- "failed" : len (results_data ) - passed ,
381- "results" : results_data ,
382- },
383- ))
374+ emit (EventType .CHECKS_COMPLETED , {
375+ "iteration" : iteration ,
376+ "passed" : passed ,
377+ "failed" : len (results_data ) - passed ,
378+ "results" : results_data ,
379+ })
384380
385381 return format_check_failures (check_results )
386382
@@ -391,7 +387,7 @@ def _run_iteration(
391387 primitives : EnabledPrimitives ,
392388 log_path_dir : Path | None ,
393389 check_failures_text : str ,
394- emitter : EventEmitter ,
390+ emit : _BoundEmitter ,
395391) -> tuple [str , bool ]:
396392 """Execute one iteration of the agent loop.
397393
@@ -402,49 +398,33 @@ def _run_iteration(
402398 """
403399 iteration = state .iteration
404400
405- emitter .emit (Event (
406- type = EventType .ITERATION_STARTED ,
407- run_id = state .run_id ,
408- data = {"iteration" : iteration },
409- ))
401+ emit (EventType .ITERATION_STARTED , {"iteration" : iteration })
410402
411403 # Run contexts (subprocess I/O)
412404 context_results : list [ContextResult ] = []
413405 if primitives .contexts :
414406 context_results = run_all_contexts (
415407 primitives .contexts , config .project_root ,
416408 )
417- emitter .emit (Event (
418- type = EventType .CONTEXTS_RESOLVED ,
419- run_id = state .run_id ,
420- data = {"iteration" : iteration , "count" : len (primitives .contexts )},
421- ))
409+ emit (EventType .CONTEXTS_RESOLVED , {"iteration" : iteration , "count" : len (primitives .contexts )})
422410
423411 # Assemble prompt (pure text resolution)
424412 prompt = _assemble_prompt (
425413 config , primitives , context_results , check_failures_text ,
426414 )
427- emitter .emit (Event (
428- type = EventType .PROMPT_ASSEMBLED ,
429- run_id = state .run_id ,
430- data = {"iteration" : iteration , "prompt_length" : len (prompt )},
431- ))
415+ emit (EventType .PROMPT_ASSEMBLED , {"iteration" : iteration , "prompt_length" : len (prompt )})
432416
433417 returncode = _execute_agent (
434- prompt , config , state , log_path_dir , emitter ,
418+ prompt , config , state , log_path_dir , emit ,
435419 )
436420
437421 if returncode != 0 and config .stop_on_error :
438- emitter .emit (Event (
439- type = EventType .LOG_MESSAGE ,
440- run_id = state .run_id ,
441- data = {"message" : "Stopping due to --stop-on-error." , "level" : "error" },
442- ))
422+ emit (EventType .LOG_MESSAGE , {"message" : "Stopping due to --stop-on-error." , "level" : "error" })
443423 return check_failures_text , False
444424
445425 if primitives .checks :
446426 check_failures_text = _run_checks_phase (
447- primitives .checks , config .project_root , state , emitter ,
427+ primitives .checks , config .project_root , state , emit ,
448428 )
449429
450430 return check_failures_text , True
@@ -467,6 +447,7 @@ def run_loop(
467447 if emitter is None :
468448 emitter = NullEmitter ()
469449
450+ emit = _BoundEmitter (emitter , state .run_id )
470451 state .status = RunStatus .RUNNING
471452
472453 log_path_dir : Path | None = None
@@ -477,24 +458,20 @@ def run_loop(
477458 check_failures_text = ""
478459 primitives = _discover_enabled_primitives (config .project_root )
479460
480- emitter .emit (Event (
481- type = EventType .RUN_STARTED ,
482- run_id = state .run_id ,
483- data = {
484- "checks" : len (primitives .checks ),
485- "contexts" : len (primitives .contexts ),
486- "instructions" : len (primitives .instructions ),
487- "max_iterations" : config .max_iterations ,
488- "timeout" : config .timeout ,
489- "delay" : config .delay ,
490- "prompt_name" : config .prompt_name ,
491- },
492- ))
461+ emit (EventType .RUN_STARTED , {
462+ "checks" : len (primitives .checks ),
463+ "contexts" : len (primitives .contexts ),
464+ "instructions" : len (primitives .instructions ),
465+ "max_iterations" : config .max_iterations ,
466+ "timeout" : config .timeout ,
467+ "delay" : config .delay ,
468+ "prompt_name" : config .prompt_name ,
469+ })
493470
494471 try :
495472 while True :
496473 should_continue , primitives = _handle_loop_transitions (
497- state , config , primitives , emitter ,
474+ state , config , primitives , emit ,
498475 )
499476 if not should_continue :
500477 break
@@ -504,7 +481,7 @@ def run_loop(
504481 break
505482
506483 check_failures_text , should_continue = _run_iteration (
507- config , state , primitives , log_path_dir , check_failures_text , emitter ,
484+ config , state , primitives , log_path_dir , check_failures_text , emit ,
508485 )
509486 if not should_continue :
510487 break
@@ -513,27 +490,19 @@ def run_loop(
513490 if config .delay > 0 and (
514491 config .max_iterations is None or state .iteration < config .max_iterations
515492 ):
516- emitter .emit (Event (
517- type = EventType .LOG_MESSAGE ,
518- run_id = state .run_id ,
519- data = {"message" : f"Waiting { config .delay } s..." , "level" : "info" },
520- ))
493+ emit (EventType .LOG_MESSAGE , {"message" : f"Waiting { config .delay } s..." , "level" : "info" })
521494 time .sleep (config .delay )
522495
523496 except KeyboardInterrupt :
524497 pass
525498 except Exception as exc :
526499 state .status = RunStatus .FAILED
527500 tb = traceback .format_exc ()
528- emitter .emit (Event (
529- type = EventType .LOG_MESSAGE ,
530- run_id = state .run_id ,
531- data = {
532- "message" : f"Run crashed: { exc } " ,
533- "level" : "error" ,
534- "traceback" : tb ,
535- },
536- ))
501+ emit (EventType .LOG_MESSAGE , {
502+ "message" : f"Run crashed: { exc } " ,
503+ "level" : "error" ,
504+ "traceback" : tb ,
505+ })
537506
538507 if state .status == RunStatus .RUNNING :
539508 state .status = RunStatus .COMPLETED
@@ -544,14 +513,10 @@ def run_loop(
544513 else "completed"
545514 )
546515 total = state .completed + state .failed
547- emitter .emit (Event (
548- type = EventType .RUN_STOPPED ,
549- run_id = state .run_id ,
550- data = {
551- "reason" : reason ,
552- "total" : total ,
553- "completed" : state .completed ,
554- "failed" : state .failed ,
555- "timed_out" : state .timed_out ,
556- },
557- ))
516+ emit (EventType .RUN_STOPPED , {
517+ "reason" : reason ,
518+ "total" : total ,
519+ "completed" : state .completed ,
520+ "failed" : state .failed ,
521+ "timed_out" : state .timed_out ,
522+ })
0 commit comments