fix: Prevent old instructions from being re-executed after conversation condensation (#11982)

This commit is contained in:
Nhan Nguyen
2025-12-17 07:05:10 -05:00
committed by GitHub
parent dc14624480
commit 435e537693
6 changed files with 197 additions and 18 deletions

View File

@@ -194,9 +194,12 @@ class CodeActAgent(Agent):
# event we'll just return that instead of an action. The controller will
# immediately ask the agent to step again with the new view.
condensed_history: list[Event] = []
# Track which event IDs have been forgotten/condensed
forgotten_event_ids: set[int] = set()
match self.condenser.condensed_history(state):
case View(events=events):
case View(events=events, forgotten_event_ids=forgotten_ids):
condensed_history = events
forgotten_event_ids = forgotten_ids
case Condensation(action=condensation_action):
return condensation_action
@@ -206,7 +209,9 @@ class CodeActAgent(Agent):
)
initial_user_message = self._get_initial_user_message(state.history)
messages = self._get_messages(condensed_history, initial_user_message)
messages = self._get_messages(
condensed_history, initial_user_message, forgotten_event_ids
)
params: dict = {
'messages': messages,
}
@@ -245,7 +250,10 @@ class CodeActAgent(Agent):
return initial_user_message
def _get_messages(
self, events: list[Event], initial_user_message: MessageAction
self,
events: list[Event],
initial_user_message: MessageAction,
forgotten_event_ids: set[int],
) -> list[Message]:
"""Constructs the message history for the LLM conversation.
@@ -284,6 +292,7 @@ class CodeActAgent(Agent):
messages = self.conversation_memory.process_events(
condensed_history=events,
initial_user_action=initial_user_message,
forgotten_event_ids=forgotten_event_ids,
max_message_chars=self.llm.config.max_message_chars,
vision_is_active=self.llm.vision_is_active(),
)

View File

@@ -76,6 +76,7 @@ class ConversationMemory:
self,
condensed_history: list[Event],
initial_user_action: MessageAction,
forgotten_event_ids: set[int] | None = None,
max_message_chars: int | None = None,
vision_is_active: bool = False,
) -> list[Message]:
@@ -85,16 +86,23 @@ class ConversationMemory:
Args:
condensed_history: The condensed history of events to convert
initial_user_action: The initial user message action, if available. Used to ensure the conversation starts correctly.
forgotten_event_ids: Set of event IDs that have been forgotten/condensed. If the initial user action's ID
is in this set, it will not be re-inserted to prevent re-execution of old instructions.
max_message_chars: The maximum number of characters in the content of an event included
in the prompt to the LLM. Larger observations are truncated.
vision_is_active: Whether vision is active in the LLM. If True, image URLs will be included.
initial_user_action: The initial user message action, if available. Used to ensure the conversation starts correctly.
"""
events = condensed_history
# Default to empty set if not provided
if forgotten_event_ids is None:
forgotten_event_ids = set()
# Ensure the event list starts with SystemMessageAction, then MessageAction(source='user')
self._ensure_system_message(events)
self._ensure_initial_user_message(events, initial_user_action)
self._ensure_initial_user_message(
events, initial_user_action, forgotten_event_ids
)
# log visual browsing status
logger.debug(f'Visual browsing: {self.agent_config.enable_som_visual_browsing}')
@@ -827,9 +835,23 @@ class ConversationMemory:
)
def _ensure_initial_user_message(
self, events: list[Event], initial_user_action: MessageAction
self,
events: list[Event],
initial_user_action: MessageAction,
forgotten_event_ids: set[int],
) -> None:
"""Checks if the second event is a user MessageAction and inserts the provided one if needed."""
"""Checks if the second event is a user MessageAction and inserts the provided one if needed.
IMPORTANT: If the initial user action has been condensed (its ID is in forgotten_event_ids),
we do NOT re-insert it. This prevents old instructions from being re-executed after
conversation condensation. The condensation summary already contains the context of
what was requested and completed.
Args:
events: The list of events to modify in-place
initial_user_action: The initial user message action from the full history
forgotten_event_ids: Set of event IDs that have been forgotten/condensed
"""
if (
not events
): # Should have system message from previous step, but safety check
@@ -837,6 +859,17 @@ class ConversationMemory:
# Or raise? Let's log for now, _ensure_system_message should handle this.
return
# Check if the initial user action has been condensed/forgotten.
# If so, we should NOT re-insert it to prevent re-execution of old instructions.
# The condensation summary already contains the context of what was requested.
initial_user_action_id = initial_user_action.id
if initial_user_action_id in forgotten_event_ids:
logger.info(
f'Initial user action (id={initial_user_action_id}) has been condensed. '
'Not re-inserting to prevent re-execution of old instructions.'
)
return
# We expect events[0] to be SystemMessageAction after _ensure_system_message
if len(events) == 1:
# Only system message exists

View File

@@ -18,6 +18,8 @@ class View(BaseModel):
events: list[Event]
unhandled_condensation_request: bool = False
# Set of event IDs that have been forgotten/condensed
forgotten_event_ids: set[int] = set()
def __len__(self) -> int:
return len(self.events)
@@ -90,4 +92,5 @@ class View(BaseModel):
return View(
events=kept_events,
unhandled_condensation_request=unhandled_condensation_request,
forgotten_event_ids=forgotten_event_ids,
)