Skip to content

Commit fefc859

Browse files
committed
Add history window for models with small context window
1 parent 551af74 commit fefc859

1 file changed

Lines changed: 82 additions & 2 deletions

File tree

agents/attackers/llm_react/llm_agent_react_tools.py

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
906986
if __name__ == "__main__":
907987
main()

0 commit comments

Comments
 (0)