@@ -272,6 +272,7 @@ def __init__(
272272 temperature : float = 0.2 ,
273273 action_memory_size : int = 0 ,
274274 max_consecutive_failures : int = 3 ,
275+ history_window : Optional [int ] = None ,
275276 ):
276277 super ().__init__ (host , port , role )
277278 if OpenAI is None :
@@ -288,6 +289,10 @@ def __init__(
288289 self .temperature = temperature
289290 self .action_memory_size = max (0 , int (action_memory_size ))
290291 self .max_consecutive_failures = max (1 , int (max_consecutive_failures ))
292+ # None or <=0 means unbounded; positive int caps the dialogue history.
293+ self .history_window = (
294+ int (history_window ) if history_window and history_window > 0 else None
295+ )
291296
292297 # Per-episode mutable state
293298 self ._messages : list = []
@@ -611,6 +616,20 @@ def _append_tool_result(self, last_tool_call_id: str, result: str):
611616 }
612617 )
613618
619+ def _enforce_history_window (self ):
620+ """Trim _messages to keep system + last (history_window * 3) messages.
621+
622+ Each step always contributes exactly 3 messages — (user, assistant,
623+ tool-result-or-corrective-user) — so dropping in chunks of 3 from the
624+ front always lands on a step boundary and preserves tool_call_id
625+ linkage. No-op when history_window is None.
626+ """
627+ if self .history_window is None :
628+ return
629+ max_history = self .history_window * 3
630+ while len (self ._messages ) - 1 > max_history :
631+ del self ._messages [1 :4 ]
632+
614633 @_weave_op
615634 def run_episode (
616635 self , observation : Observation , verbose : bool = False
@@ -635,6 +654,7 @@ def run_episode(
635654 end_reason = "token_budget"
636655 break
637656
657+ self ._enforce_history_window ()
638658 user_msg = self ._build_user_message (observation .state , step , last_result )
639659 self ._messages .append ({"role" : "user" , "content" : user_msg })
640660
@@ -810,6 +830,18 @@ def main():
810830 "Default 3."
811831 ),
812832 )
833+ parser .add_argument (
834+ "--history_window" ,
835+ type = int ,
836+ default = 0 ,
837+ help = (
838+ "If > 0, truncate the assistant message history to the last N turns "
839+ "(turn = user + assistant + tool/corrective triple). The system prompt "
840+ "and the current turn are always kept. Use this for models with smaller "
841+ "context windows; pair with --action_memory to preserve action history "
842+ "outside the window. Default 0 (unbounded)."
843+ ),
844+ )
813845 # ── W&B Weave (optional LLM tracing) ─────────────────────────────────────
814846 parser .add_argument (
815847 "--weave" ,
@@ -828,6 +860,19 @@ def main():
828860 type = str ,
829861 help = "Weave entity/team (optional). Combined as 'entity/project'." ,
830862 )
863+ parser .add_argument (
864+ "--log_file" ,
865+ default = None ,
866+ help = "Path to a JSONL file. One line per episode is appended with "
867+ "outcome fields plus run-level metadata (model, scenario, seed). "
868+ "Use this to drive Phase 1 analysis." ,
869+ )
870+ parser .add_argument (
871+ "--scenario" ,
872+ default = "unknown" ,
873+ help = "Free-text label for the topology / configuration of the docker "
874+ "server (e.g. '2-net-deterministic'). Recorded in the JSONL log." ,
875+ )
831876 parser .add_argument ("--verbose" , action = "store_true" )
832877 args = parser .parse_args ()
833878
@@ -857,6 +902,7 @@ def main():
857902 temperature = args .temperature ,
858903 action_memory_size = args .action_memory ,
859904 max_consecutive_failures = args .max_consecutive_failures ,
905+ history_window = args .history_window if args .history_window > 0 else None ,
860906 )
861907
862908 try :
@@ -865,7 +911,7 @@ def main():
865911 # randomized topology (rather than the server's default starting state).
866912 observation = agent .request_game_reset (
867913 randomize_topology = args .randomize_topology ,
868- seed = 421 , # Fixed seed for initial reset to ensure consistent starting state.
914+ seed = 0 , # Fixed seed for initial reset to ensure consistent starting state.
869915 )
870916 observation = filter_log_files_from_state (observation )
871917 except Exception as e :
@@ -874,9 +920,11 @@ def main():
874920 return
875921
876922 stats = RunStats ()
923+ log_fh = open (args .log_file , "a" ) if args .log_file else None
877924 try :
878925 for ep in range (1 , args .episodes + 1 ):
879926 print (f"\n === Episode { ep } ===" )
927+ episode_seed = ep - 1 # matches initial reset (seed=0) and per-ep reset (seed=ep) below
880928 outcome = agent .run_episode (observation , verbose = args .verbose )
881929 total_tok = outcome .input_tokens + outcome .output_tokens
882930 print (
@@ -890,18 +938,50 @@ def main():
890938 f"(in={ outcome .input_tokens :,} out={ outcome .output_tokens :,} )"
891939 )
892940 stats .record_outcome (outcome )
941+
942+ if log_fh is not None :
943+ row = _build_log_row (args , ep , episode_seed , outcome )
944+ log_fh .write (json .dumps (row ) + "\n " )
945+ log_fh .flush ()
946+
893947 if ep < args .episodes :
894948 observation = agent .request_game_reset (
895949 randomize_topology = args .randomize_topology ,
896- seed = ep + 2 ,
950+ seed = ep ,
897951 )
898952 observation = filter_log_files_from_state (observation )
899953 finally :
954+ if log_fh is not None :
955+ log_fh .close ()
900956 agent .terminate_connection ()
901957
902958 print ()
903959 stats .print_summary ()
904960
905961
962+ def _build_log_row (args , episode : int , seed : int , outcome : EpisodeOutcome ) -> dict :
963+ """Flatten an EpisodeOutcome + run-level metadata into a JSON-serializable row."""
964+ return {
965+ "scenario" : args .scenario ,
966+ "model" : args .model ,
967+ "agent" : "react_raw" ,
968+ "mission" : args .mission ,
969+ "max_steps" : args .max_steps ,
970+ "max_consecutive_failures" : args .max_consecutive_failures ,
971+ "action_memory" : args .action_memory ,
972+ "history_window" : args .history_window ,
973+ "temperature" : args .temperature ,
974+ "episode" : episode ,
975+ "seed" : seed ,
976+ "won" : bool (outcome .won ),
977+ "detected" : bool (outcome .detected ),
978+ "end_reason" : str (outcome .end_reason ),
979+ "steps" : int (outcome .steps ),
980+ "input_tokens" : int (outcome .input_tokens ),
981+ "output_tokens" : int (outcome .output_tokens ),
982+ "total_reward" : float (outcome .total_reward ),
983+ }
984+
985+
906986if __name__ == "__main__" :
907987 main ()
0 commit comments