33import json
44import os
55import sys
6+ import time
67from datetime import datetime , timezone
78
89from .agent import agent
2930 "Try your best and have fun with this one! If you "
3031 "think of several options, pick one and run with it - I will not be available "
3132 "to make decisions for you, I give you my full permission to explore and make "
32- "your own best judgement towards our goal! Remember to update SCIENCE.md. "
33+ "your own best judgement towards our goal! If you are in a git repository, "
34+ "create and use a local branch for this run. Make local commits for improvements "
35+ "worth keeping, but never commit or reset LOGBOOK.md or SCIENCE.md. "
36+ "Remember to update SCIENCE.md. "
3337 "Good hunting!"
3438)
3539_LOGBOOK_NAME = "LOGBOOK.md"
@@ -97,7 +101,10 @@ def __init__(
97101 max_iterations = 0 ,
98102 completion_promise = None ,
99103 fresh = True ,
104+ max_duration_seconds = 0 ,
100105 ):
106+ if max_duration_seconds < 0 :
107+ raise ValueError ("max_duration_seconds must be >= 0" )
101108 self ._task = task .strip () if isinstance (task , str ) else task
102109 prompt_a , prompt_b = _science_parts (task )
103110 prompt = f"{ prompt_a } { prompt_b } "
@@ -117,11 +124,20 @@ def __init__(
117124 self ._best_metrics = None
118125 self ._run_title = None
119126 self ._pushover = Pushover ()
127+ self ._pushover_enabled = False
128+ self ._max_duration_seconds = float (max_duration_seconds )
129+ self ._loop_started_monotonic = None
130+ self ._duration_limit_hit = False
131+ self ._last_iteration = 0
120132
121133 def hook_before_loop (self ):
122134 super ().hook_before_loop ()
123- self ._pushover .ensure_ready ()
124- self ._run_title = self ._build_run_title ()
135+ self ._loop_started_monotonic = time .monotonic ()
136+ self ._pushover_enabled = self ._pushover .ensure_ready ()
137+ if self ._pushover_enabled :
138+ self ._run_title = self ._build_run_title ()
139+ else :
140+ self ._run_title = _fallback_title (self ._task )
125141
126142 def build_prompt (self , iteration ):
127143 if iteration <= 1 :
@@ -131,8 +147,33 @@ def build_prompt(self, iteration):
131147
132148 def hook_after_iteration (self , iteration , message ):
133149 super ().hook_after_iteration (iteration , message )
150+ self ._last_iteration = iteration
134151 self ._append_logbook (iteration , message )
135152 self ._extract_and_notify (message )
153+ self ._mark_duration_stop (iteration )
154+
155+ def hook_after_loop (self , last_message , stop_reason ):
156+ super ().hook_after_loop (last_message , stop_reason )
157+ if not self ._pushover_enabled :
158+ return
159+ status = _format_final_status (
160+ stop_reason ,
161+ self .max_iterations ,
162+ self .completion_promise ,
163+ self ._duration_limit_hit ,
164+ )
165+ lines = [
166+ f"Science run ended: { status } " ,
167+ f"Iterations completed: { self ._last_iteration } " ,
168+ ]
169+ if self ._best_metrics :
170+ summary = _single_line (self ._best_metrics .get ("summary" , "" )).strip ()
171+ metrics_text = _format_metrics (self ._best_metrics .get ("metrics" ) or [])
172+ if summary :
173+ lines .append (f"Best summary: { summary } " )
174+ if metrics_text :
175+ lines .append (f"Best metrics: { metrics_text } " )
176+ self ._pushover .send (self ._run_title , "\n " .join (lines ))
136177
137178 def hook_new_best (self , result ):
138179 super ().hook_new_best (result )
@@ -186,6 +227,25 @@ def _build_run_title(self):
186227 title = _fallback_title (self ._task )
187228 return title
188229
230+ def _mark_duration_stop (self , iteration ):
231+ if self ._duration_limit_hit :
232+ return
233+ if self ._max_duration_seconds <= 0 :
234+ return
235+ if self ._loop_started_monotonic is None :
236+ return
237+ elapsed = time .monotonic () - self ._loop_started_monotonic
238+ if elapsed < self ._max_duration_seconds :
239+ return
240+ self ._duration_limit_hit = True
241+ self .max_iterations = (
242+ iteration if self .max_iterations == 0 else min (self .max_iterations , iteration )
243+ )
244+ print (
245+ "Science loop: Max duration reached; "
246+ "stopping after the current iteration."
247+ )
248+
189249
190250
191251def _build_metrics_prompt (task , message , previous_best ):
@@ -300,3 +360,30 @@ def _fallback_title(task):
300360
301361def _warn (message ):
302362 print (message , file = sys .stderr )
363+
364+
365+ def _format_final_status (
366+ stop_reason ,
367+ max_iterations ,
368+ completion_promise ,
369+ duration_limit_hit ,
370+ ):
371+ if stop_reason == "max_iterations" :
372+ if duration_limit_hit :
373+ return "max duration reached"
374+ return f"max iterations reached ({ max_iterations } )"
375+ if stop_reason == "promise" :
376+ if completion_promise :
377+ return f"completion promise met ({ completion_promise } )"
378+ return "completion promise met"
379+ if stop_reason == "welfare_stop" :
380+ return "agent requested welfare stop"
381+ if stop_reason == "canceled" :
382+ return "loop canceled"
383+ if stop_reason == "interrupted" :
384+ return "interrupted"
385+ if stop_reason == "error" :
386+ return "stopped due to error"
387+ if stop_reason :
388+ return _single_line (stop_reason )
389+ return "finished"
0 commit comments