diff --git a/openhands/core/config/condenser_config.py b/openhands/core/config/condenser_config.py
index 4dfc036050..609ba3725d 100644
--- a/openhands/core/config/condenser_config.py
+++ b/openhands/core/config/condenser_config.py
@@ -75,6 +75,10 @@ class LLMSummarizingCondenserConfig(BaseModel):
description='Maximum size of the condensed history before triggering forgetting.',
ge=2,
)
+ max_event_length: int = Field(
+ default=10_000,
+ description='Maximum length of the event representations to be passed to the LLM.',
+ )
model_config = {'extra': 'forbid'}
diff --git a/openhands/memory/condenser/impl/llm_summarizing_condenser.py b/openhands/memory/condenser/impl/llm_summarizing_condenser.py
index f3ad5791ee..8d5a46fd47 100644
--- a/openhands/memory/condenser/impl/llm_summarizing_condenser.py
+++ b/openhands/memory/condenser/impl/llm_summarizing_condenser.py
@@ -4,6 +4,7 @@ from openhands.core.config.condenser_config import LLMSummarizingCondenserConfig
from openhands.core.message import Message, TextContent
from openhands.events.action.agent import CondensationAction
from openhands.events.observation.agent import AgentCondensationObservation
+from openhands.events.serialization.event import truncate_content
from openhands.llm import LLM
from openhands.memory.condenser.condenser import (
Condensation,
@@ -20,7 +21,13 @@ class LLMSummarizingCondenser(RollingCondenser):
and newly forgotten events.
"""
- def __init__(self, llm: LLM, max_size: int = 100, keep_first: int = 1):
+ def __init__(
+ self,
+ llm: LLM,
+ max_size: int = 100,
+ keep_first: int = 1,
+ max_event_length: int = 10_000,
+ ):
if keep_first >= max_size // 2:
raise ValueError(
f'keep_first ({keep_first}) must be less than half of max_size ({max_size})'
@@ -32,10 +39,15 @@ class LLMSummarizingCondenser(RollingCondenser):
self.max_size = max_size
self.keep_first = keep_first
+ self.max_event_length = max_event_length
self.llm = llm
super().__init__()
+ def _truncate(self, content: str) -> str:
+ """Truncate the content to fit within the specified maximum event length."""
+ return truncate_content(content, max_chars=self.max_event_length)
+
def get_condensation(self, view: View) -> Condensation:
head = view[: self.keep_first]
target_size = self.max_size // 2
@@ -56,40 +68,66 @@ class LLMSummarizingCondenser(RollingCondenser):
forgotten_events.append(event)
# Construct prompt for summarization
- prompt = """You are maintaining state history for an LLM-based code agent. Track:
+ prompt = """You are maintaining a context-aware state summary for an interactive agent. You will be given a list of events corresponding to actions taken by the agent, and the most recent previous summary if one exists. Track:
-USER_CONTEXT: (Preserve essential user requirements, problem descriptions, and clarifications in concise form)
+USER_CONTEXT: (Preserve essential user requirements, goals, and clarifications in concise form)
-STATE: {File paths, function signatures, data structures}
+COMPLETED: (Tasks completed so far, with brief results)
+PENDING: (Tasks that still need to be done)
+CURRENT_STATE: (Current variables, data structures, or relevant state)
+
+For code-specific tasks, also include:
+CODE_STATE: {File paths, function signatures, data structures}
TESTS: {Failing cases, error messages, outputs}
CHANGES: {Code edits, variable updates}
DEPS: {Dependencies, imports, external calls}
-INTENT: {Why changes were made, acceptance criteria}
+VERSION_CONTROL_STATUS: {Repository state, current branch, PR status, commit history}
PRIORITIZE:
-1. Capture key user requirements and constraints
-2. Maintain critical problem context
-3. Keep all sections concise
+1. Adapt tracking format to match the actual task type
+2. Capture key user requirements and goals
+3. Distinguish between completed and pending tasks
+4. Keep all sections concise and relevant
-SKIP: {Git clones, build logs, file listings}
+SKIP: Tracking irrelevant details for the current task type
-Example history format:
-USER_CONTEXT: Fix FITS card float representation - "0.009125" becomes "0.009124999999999999" causing comment truncation. Use Python's str() when possible while maintaining FITS compliance.
+Example formats:
-STATE: mod_float() in card.py updated
+For code tasks:
+USER_CONTEXT: Fix FITS card float representation issue
+COMPLETED: Modified mod_float() in card.py, all tests passing
+PENDING: Create PR, update documentation
+CODE_STATE: mod_float() in card.py updated
TESTS: test_format() passed
CHANGES: str(val) replaces f"{val:.16G}"
DEPS: None modified
-INTENT: Fix precision while maintaining FITS compliance"""
+VERSION_CONTROL_STATUS: Branch: fix-float-precision, Latest commit: a1b2c3d
+
+For other tasks:
+USER_CONTEXT: Write 20 haikus based on coin flip results
+COMPLETED: 15 haikus written for results [T,H,T,H,T,H,T,T,H,T,H,T,H,T,H]
+PENDING: 5 more haikus needed
+CURRENT_STATE: Last flip: Heads, Haiku count: 15/20"""
prompt += '\n\n'
- prompt += ('\n' + summary_event.message + '\n') if summary_event.message else ''
+ # Add the previous summary if it exists. We'll always have a summary
+ # event, but the types aren't precise enought to guarantee that it has a
+ # message attribute.
+ summary_event_content = self._truncate(
+ summary_event.message if summary_event.message else ''
+ )
+ prompt += f'\n{summary_event_content}\n\n'
prompt += '\n\n'
+ # Add all events that are being forgotten. We use the string
+ # representation defined by the event, and truncate it if necessary.
for forgotten_event in forgotten_events:
- prompt += str(forgotten_event) + '\n\n'
+ event_content = self._truncate(str(forgotten_event))
+ prompt += f'\n{event_content}\n\n'
+
+ prompt += 'Now summarize the events using the rules above.'
messages = [Message(role='user', content=[TextContent(text=prompt)])]
@@ -121,6 +159,7 @@ INTENT: Fix precision while maintaining FITS compliance"""
llm=LLM(config=config.llm_config),
max_size=config.max_size,
keep_first=config.keep_first,
+ max_event_length=config.max_event_length,
)