Skip to content

Commit abac25c

Browse files
authored
Keep the first user message by default in condensers (#6888)
1 parent 70b21d1 commit abac25c

File tree

4 files changed

+57
-16
lines changed

4 files changed

+57
-16
lines changed

openhands/core/config/condenser_config.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ class RecentEventsCondenserConfig(BaseModel):
2626
"""Configuration for RecentEventsCondenser."""
2727

2828
type: Literal['recent'] = Field('recent')
29+
30+
# at least one event by default, because the best guess is that it is the user task
2931
keep_first: int = Field(
30-
default=0,
32+
default=1,
3133
description='The number of initial events to condense.',
3234
ge=0,
3335
)
@@ -43,6 +45,8 @@ class LLMSummarizingCondenserConfig(BaseModel):
4345
llm_config: LLMConfig = Field(
4446
..., description='Configuration for the LLM to use for condensing.'
4547
)
48+
49+
# at least one event by default, because the best guess is that it's the user task
4650
keep_first: int = Field(
4751
default=1,
4852
description='The number of initial events to condense.',
@@ -62,8 +66,10 @@ class AmortizedForgettingCondenserConfig(BaseModel):
6266
description='Maximum size of the condensed history before triggering forgetting.',
6367
ge=2,
6468
)
69+
70+
# at least one event by default, because the best guess is that it's the user task
6571
keep_first: int = Field(
66-
default=0,
72+
default=1,
6773
description='Number of initial events to always keep in history.',
6874
ge=0,
6975
)
@@ -81,8 +87,10 @@ class LLMAttentionCondenserConfig(BaseModel):
8187
description='Maximum size of the condensed history before triggering forgetting.',
8288
ge=2,
8389
)
90+
91+
# at least one event by default, because the best guess is that it's the user task
8492
keep_first: int = Field(
85-
default=0,
93+
default=1,
8694
description='Number of initial events to always keep in history.',
8795
ge=0,
8896
)

openhands/memory/condenser/impl/llm_attention_condenser.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class ImportantEventSelection(BaseModel):
1818
class LLMAttentionCondenser(RollingCondenser):
1919
"""Rolling condenser strategy that uses an LLM to select the most important events when condensing the history."""
2020

21-
def __init__(self, llm: LLM, max_size: int = 100, keep_first: int = 0):
21+
def __init__(self, llm: LLM, max_size: int = 100, keep_first: int = 1):
2222
if keep_first >= max_size // 2:
2323
raise ValueError(
2424
f'keep_first ({keep_first}) must be less than half of max_size ({max_size})'

openhands/memory/condenser/impl/recent_events_condenser.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class RecentEventsCondenser(Condenser):
99
"""A condenser that only keeps a certain number of the most recent events."""
1010

11-
def __init__(self, keep_first: int = 0, max_events: int = 10):
11+
def __init__(self, keep_first: int = 1, max_events: int = 10):
1212
self.keep_first = keep_first
1313
self.max_events = max_events
1414

tests/unit/test_condenser.py

+44-11
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def create_test_event(
3838
event = Event()
3939
event._message = message
4040
event.timestamp = timestamp if timestamp else datetime.now()
41-
if id:
41+
if id is not None:
4242
event._id = id
4343
event._source = EventSource.USER
4444
return event
@@ -186,13 +186,14 @@ def test_recent_events_condenser():
186186
assert result == events
187187

188188
# If the max_events are smaller than the number of events, only keep the last few.
189-
max_events = 2
189+
max_events = 3
190190
condenser = RecentEventsCondenser(max_events=max_events)
191191
result = condenser.condensed_history(mock_state)
192192

193193
assert len(result) == max_events
194-
assert result[0]._message == 'Event 4'
195-
assert result[1]._message == 'Event 5'
194+
assert result[0]._message == 'Event 1' # kept from keep_first
195+
assert result[1]._message == 'Event 4' # kept from max_events
196+
assert result[2]._message == 'Event 5' # kept from max_events
196197

197198
# If the keep_first flag is set, the first event will always be present.
198199
keep_first = 1
@@ -211,9 +212,9 @@ def test_recent_events_condenser():
211212
result = condenser.condensed_history(mock_state)
212213

213214
assert len(result) == max_events
214-
assert result[0]._message == 'Event 1'
215-
assert result[1]._message == 'Event 2'
216-
assert result[2]._message == 'Event 5'
215+
assert result[0]._message == 'Event 1' # kept from keep_first
216+
assert result[1]._message == 'Event 2' # kept from keep_first
217+
assert result[2]._message == 'Event 5' # kept from max_events
217218

218219

219220
def test_llm_summarization_condenser_from_config():
@@ -539,7 +540,7 @@ def test_llm_attention_condenser_forgets_when_larger_than_max_size(
539540
):
540541
"""Test that the LLMAttentionCondenser forgets events when the context grows too large."""
541542
max_size = 2
542-
condenser = LLMAttentionCondenser(max_size=max_size, llm=mock_llm)
543+
condenser = LLMAttentionCondenser(max_size=max_size, keep_first=0, llm=mock_llm)
543544

544545
for i in range(max_size * 10):
545546
event = create_test_event(f'Event {i}', id=i)
@@ -560,7 +561,7 @@ def test_llm_attention_condenser_forgets_when_larger_than_max_size(
560561
def test_llm_attention_condenser_handles_events_outside_history(mock_llm, mock_state):
561562
"""Test that the LLMAttentionCondenser handles event IDs that aren't from the event history."""
562563
max_size = 2
563-
condenser = LLMAttentionCondenser(max_size=max_size, llm=mock_llm)
564+
condenser = LLMAttentionCondenser(max_size=max_size, keep_first=0, llm=mock_llm)
564565

565566
for i in range(max_size * 10):
566567
event = create_test_event(f'Event {i}', id=i)
@@ -580,7 +581,7 @@ def test_llm_attention_condenser_handles_events_outside_history(mock_llm, mock_s
580581
def test_llm_attention_condenser_handles_too_many_events(mock_llm, mock_state):
581582
"""Test that the LLMAttentionCondenser handles when the response contains too many event IDs."""
582583
max_size = 2
583-
condenser = LLMAttentionCondenser(max_size=max_size, llm=mock_llm)
584+
condenser = LLMAttentionCondenser(max_size=max_size, keep_first=0, llm=mock_llm)
584585

585586
for i in range(max_size * 10):
586587
event = create_test_event(f'Event {i}', id=i)
@@ -600,7 +601,9 @@ def test_llm_attention_condenser_handles_too_many_events(mock_llm, mock_state):
600601
def test_llm_attention_condenser_handles_too_few_events(mock_llm, mock_state):
601602
"""Test that the LLMAttentionCondenser handles when the response contains too few event IDs."""
602603
max_size = 2
603-
condenser = LLMAttentionCondenser(max_size=max_size, llm=mock_llm)
604+
# Developer note: We must specify keep_first=0 because
605+
# keep_first (1) >= max_size//2 (1) is invalid.
606+
condenser = LLMAttentionCondenser(max_size=max_size, keep_first=0, llm=mock_llm)
604607

605608
for i in range(max_size * 10):
606609
event = create_test_event(f'Event {i}', id=i)
@@ -614,3 +617,33 @@ def test_llm_attention_condenser_handles_too_few_events(mock_llm, mock_state):
614617

615618
# The number of results should bounce back and forth between 1, 2, 1, 2, ...
616619
assert len(results) == (i % 2) + 1
620+
621+
# Add a new test verifying that keep_first=1 works with max_size > 2
622+
623+
624+
def test_llm_attention_condenser_handles_keep_first_for_larger_max_size(
625+
mock_llm, mock_state
626+
):
627+
"""Test that LLMAttentionCondenser works when keep_first=1 is allowed (must be less than half of max_size)."""
628+
max_size = 4 # so keep_first=1 < (max_size // 2) = 2
629+
condenser = LLMAttentionCondenser(max_size=max_size, keep_first=1, llm=mock_llm)
630+
631+
for i in range(max_size * 2):
632+
# We append new events, then ensure some are pruned.
633+
event = create_test_event(f'Event {i}', id=i)
634+
mock_state.history.append(event)
635+
636+
mock_llm.set_mock_response_content(
637+
ImportantEventSelection(ids=[]).model_dump_json()
638+
)
639+
640+
results = condenser.condensed_history(mock_state)
641+
642+
# We expect that the first event is always kept, and the tail grows until max_size
643+
if len(mock_state.history) <= max_size:
644+
# No condensation needed yet
645+
assert len(results) == len(mock_state.history)
646+
else:
647+
# The first event is kept, plus some from the tail
648+
assert results[0].id == 0
649+
assert len(results) <= max_size

0 commit comments

Comments
 (0)