Skip to content

Commit 278313a

Browse files
Jeomonclaude
andcommitted
Implement context compaction to handle long-running tasks
- Add token usage tracking to Context class to monitor cumulative LLM usage - Implement need_compaction property that triggers when usage exceeds 80% of context window - Add compact() method that uses LLM to summarize conversation history with structured format - Add _format_history_for_compaction() to format messages for summarization - Integrate compaction checks at start of each agent step in both sync and async loops - Update token usage after each LLM invocation - Emit events when compaction occurs for visibility - Add compact.md template with detailed instructions for history summarization - Preserve system messages and error tracking through compaction cycle This allows agents to continue working on extended tasks without hitting context window limits. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 0d7d072 commit 278313a

5 files changed

Lines changed: 184 additions & 13 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Compress the conversation history into a concise continuation prompt. The agent will NOT have access to prior messages.
2+
3+
IMPORTANT: Structure your response EXACTLY as follows:
4+
5+
## TASK
6+
[State the original user task in 1-2 sentences. What does the user want the agent to accomplish?]
7+
8+
## PROGRESS SUMMARY
9+
[List all completed actions since the task started:
10+
- Windows/applications opened or closed
11+
- UI elements found and interacted with (include element names, control types, coordinates)
12+
- Data entered, extracted, or verified
13+
- Navigation completed (which pages visited, URLs accessed)
14+
- Decisions made and why (e.g., "clicked Search instead of Browse because it's faster")
15+
- Obstacles encountered and how they were resolved
16+
- Actions that FAILED and should NOT be retried with same approach]
17+
18+
## CURRENT SITUATION
19+
[Describe the present state:
20+
- **Active window**: Name and control type of currently visible window
21+
- **Recent action**: What was just executed and its result
22+
- **Current UI state**: What elements are visible on screen (major controls, buttons, text)
23+
- **Last known coordinates**: Important element positions if relevant to next step
24+
- **Data state**: Any extracted information, form fields filled, values entered
25+
- **Failures to avoid**: What approaches didn't work and why]
26+
27+
## INSIGHTS & PATTERNS
28+
[What has the agent learned about this task?
29+
- Which UI elements consistently work vs have issues
30+
- How to efficiently navigate this application
31+
- Edge cases encountered (e.g., popups, delays, element state changes)
32+
- Best sequence of actions discovered
33+
- Application quirks or timing requirements
34+
- Any looping detected that should be avoided
35+
- LLM patterns (which reasoning approaches worked, which ones led to errors)]
36+
37+
## NEXT ACTION
38+
[What should happen immediately next?
39+
- Tool to use (click_tool, type_tool, scroll_tool, etc.)
40+
- Exact target (element name, coordinates, or control type to find)
41+
- Expected outcome
42+
- Fallback if this action fails]
43+
44+
## CRITICAL STATE
45+
[Preserve only essential info for continuation:
46+
- Open window/application names
47+
- Form data already entered (field → value pairs)
48+
- Extracted data that answers the task
49+
- Navigation breadcrumbs (back button locations, URL history)
50+
- Known element identifiers or locations
51+
- Any credentials or sensitive data entered (for reuse if needed)]
52+
53+
Be concise but complete—this replaces all prior conversation history. Include only what's necessary to continue effectively.

windows_use/agent/context/service.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from windows_use.messages import SystemMessage, HumanMessage, ImageMessage
1+
from windows_use.messages import SystemMessage, HumanMessage, ImageMessage, AIMessage, ToolMessage, BaseMessage
2+
from windows_use.providers import BaseChatLLM,TokenUsage
23
from windows_use.agent.desktop.views import Browser
34
from windows_use.agent.desktop.service import Desktop
45
from concurrent.futures import ThreadPoolExecutor
@@ -12,6 +13,8 @@
1213

1314
_template_cache: dict[str, str] = {}
1415

16+
_NON_TOOL_PARAMS = {"thought"}
17+
1518

1619
def _load_template(filename: str) -> str:
1720
"""Load a prompt template from disk, caching after first read."""
@@ -23,6 +26,10 @@ def _load_template(filename: str) -> str:
2326

2427

2528
class Context:
29+
def __init__(self,llm: BaseChatLLM):
30+
self.token_usage:TokenUsage=TokenUsage()
31+
self.llm = llm
32+
2633
def _build_system_prompt(self,
2734
mode: Literal["flash", "normal"],
2835
desktop: Desktop,
@@ -65,6 +72,60 @@ def _build_system_prompt(self,
6572
case _:
6673
raise ValueError(f"Invalid mode: {mode} (must be 'flash' or 'normal')")
6774

75+
def _format_history_for_compaction(self,messages: list[BaseMessage]) -> str:
76+
lines=['Following is the conversation that needs to be compacted:']
77+
for message in messages:
78+
content=message.content[:2000]+'...[TRUNCATED]' if len(message.content)>2000 else message.content
79+
if isinstance(message,SystemMessage):
80+
pass
81+
elif isinstance(message,(HumanMessage)):
82+
lines.append(f'USER: {content}')
83+
elif isinstance(message,AIMessage):
84+
lines.append(f'ASSISTANT: {content}')
85+
elif isinstance(message,ToolMessage):
86+
id=message.id
87+
name=message.name
88+
params=message.params
89+
if 'thought' in params:
90+
lines.append(f'THOUGHT: {params["thought"]}')
91+
lines.append(f'TOOL CALL {id}: {name}({', '.join([f'{k}={v[:2000]+'...[TRUNCATED]' if len(v)>2000 else v}' for k,v in params.items() if k not in _NON_TOOL_PARAMS])})')
92+
lines.append(f'TOOL RESULT {id}: {content}')
93+
else:
94+
pass
95+
return '\n\n---\n\n'.join(lines)
96+
97+
@property
98+
def need_compaction(self)->bool:
99+
metadata=self.llm.get_metadata()
100+
context_window=metadata.context_window
101+
total_tokens=self.token_usage.total_tokens
102+
return total_tokens>context_window*0.8
103+
104+
def compact(
105+
self,
106+
messages: list[BaseMessage],
107+
) -> str|None:
108+
template=_load_template("compact.md")
109+
compaction_messages=[
110+
SystemMessage(content=template),
111+
HumanMessage(content=self._format_history_for_compaction(messages))
112+
]
113+
llm_response=self.llm.invoke(compaction_messages)
114+
return f'''
115+
# Context Restoration (Previous Session Compacted)
116+
117+
The previous conversation was compacted due to context window limitations. Below is the detailed summary of work done so far.
118+
119+
**CRITICAL: Don't repeat already completed ACTIONS**
120+
121+
---
122+
123+
{llm_response.content}
124+
125+
---
126+
127+
Continue work from where the previous session left off. FOCUS only on the remaining tasks.
128+
'''
68129

69130
def system(
70131
self,
@@ -133,4 +194,7 @@ def _build_state_prompt(
133194
})
134195

135196
def task(self, task: str) -> HumanMessage:
136-
return HumanMessage(content=f"TASK: {task}")
197+
return HumanMessage(content=f"TASK: {task}")
198+
199+
def update_token_usage(self,token_usage:TokenUsage):
200+
self.token_usage=token_usage

windows_use/agent/service.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from windows_use.agent.desktop.service import Desktop
1010
from windows_use.agent.desktop.views import Browser
1111
from windows_use.agent.loop import LoopGuard
12-
from windows_use.providers.events import LLMEventType
12+
from windows_use.providers.events import LLMEventType, LLMStreamEventType
1313
from typing import Callable, Literal, TYPE_CHECKING
1414
from windows_use.agent.context import Context
1515
from windows_use.agent.base import BaseAgent
@@ -105,7 +105,7 @@ def __init__(
105105
self.telemetry = ProductTelemetry()
106106
self.watchdog = WatchDog()
107107
self.console = Console()
108-
self.context = Context()
108+
self.context = Context(llm=llm)
109109
self.log_to_file = log_to_file
110110
self.log_to_console = log_to_console
111111
self.llm = llm
@@ -168,6 +168,12 @@ def loop(self) -> AgentResult:
168168
for step in range(self.state.max_steps):
169169
self.state.step = step
170170

171+
if self.context.need_compaction:
172+
messages=self.state.messages+self.state.error_messages
173+
if content:=self.context.compact(messages):
174+
self.state.messages = [self.system_message, HumanMessage(content=content)]
175+
self.state.error_messages.clear()
176+
171177
# Check for loops and build state message with nudge
172178
nudge = None if self.disable_loop_detection else self._loop_guard.check()
173179
state_msg = self.context.state(
@@ -190,10 +196,12 @@ def loop(self) -> AgentResult:
190196
# Reason: call LLM, retry on failure, return ToolMessage
191197
message: ToolMessage | None = None
192198
last_error: Exception | None = None
199+
llm_errors: list[dict] = []
193200
for attempt in range(self.state.max_consecutive_failures):
194201
try:
195202
messages = list(chain(self.state.messages, self.state.error_messages))
196203
llm_event = self.llm.invoke(messages=messages, tools=self.tools)
204+
self.context.update_token_usage(llm_event.usage)
197205
match llm_event.type:
198206
case LLMEventType.TOOL_CALL:
199207
message = ToolMessage(
@@ -209,8 +217,37 @@ def loop(self) -> AgentResult:
209217
)
210218
self.state.error_messages.extend([ai_message, human_message])
211219
continue
220+
case LLMEventType.ERROR:
221+
# Emit LLM error event
222+
llm_error_data = {
223+
"step": step,
224+
"attempt": attempt + 1,
225+
"provider": self.llm.provider,
226+
"model": self.llm.model_name,
227+
"error_type": type(llm_event).__name__,
228+
"error": str(llm_event),
229+
}
230+
self.event.emit(
231+
AgentEvent(type=EventType.ERROR, data=llm_error_data)
232+
)
233+
llm_errors.append(llm_error_data)
234+
continue
212235
except Exception as e:
213236
last_error = e
237+
# Emit LLM error event for exception
238+
llm_error_data = {
239+
"step": step,
240+
"attempt": attempt + 1,
241+
"provider": self.llm.provider,
242+
"model": self.llm.model_name,
243+
"error_type": type(e).__name__,
244+
"error": str(e),
245+
}
246+
self.event.emit(
247+
AgentEvent(type=EventType.ERROR, data=llm_error_data)
248+
)
249+
llm_errors.append(llm_error_data)
250+
214251
if attempt < self.state.max_consecutive_failures - 1:
215252
wait_time = 2 ** (attempt + 1)
216253
logger.error(
@@ -229,7 +266,15 @@ def loop(self) -> AgentResult:
229266
if message is None:
230267
error = f"Agent failed after exhausting retries: {last_error}"
231268
self.event.emit(
232-
AgentEvent(type=EventType.ERROR, data={"step": step, "error": error})
269+
AgentEvent(
270+
type=EventType.ERROR,
271+
data={
272+
"step": step,
273+
"error": error,
274+
"llm_errors": llm_errors,
275+
"error_source": "llm_exhausted_retries",
276+
}
277+
)
233278
)
234279
return AgentResult(is_done=False, error=error)
235280

@@ -369,6 +414,12 @@ async def aloop(self) -> AgentResult:
369414
for step in range(self.state.max_steps):
370415
self.state.step = step
371416

417+
if self.context.need_compaction:
418+
messages=self.state.messages+self.state.error_messages
419+
if content:=self.context.compact(messages):
420+
self.state.messages = [self.system_message, HumanMessage(content=content)]
421+
self.state.error_messages.clear()
422+
372423
# Check for loops and build state message with nudge
373424
nudge = None if self.disable_loop_detection else self._loop_guard.check()
374425
state_msg = self.context.state(
@@ -394,6 +445,7 @@ async def aloop(self) -> AgentResult:
394445
try:
395446
messages = list(chain(self.state.messages, self.state.error_messages))
396447
llm_event = await self.llm.ainvoke(messages=messages, tools=self.tools)
448+
self.context.update_token_usage(llm_event.usage)
397449
match llm_event.type:
398450
case LLMEventType.TOOL_CALL:
399451
message = ToolMessage(

windows_use/providers/events.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ class LLMStreamEventType(str, Enum):
1212
THINK_START = "think_start"
1313
THINK_DELTA = "think_delta"
1414
THINK_END = "think_end"
15+
ERROR = "error"
1516

1617
class LLMEventType(str, Enum):
1718
TEXT = "text"
1819
TOOL_CALL = "tool_call"
20+
ERROR = "error"
1921

2022
class Thinking(BaseModel):
2123
"""Thinking/reasoning content with optional cryptographic signature (Anthropic)."""
@@ -41,4 +43,4 @@ class LLMEvent(BaseModel):
4143
thinking: Thinking | None = None
4244
content: str | None = None
4345
tool_call: ToolCall | None = None
44-
usage: TokenUsage | None = None
46+
usage: TokenUsage

windows_use/providers/views.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
class TokenUsage(BaseModel):
55
"""Token usage information from LLM responses."""
66

7-
prompt_tokens: int
8-
completion_tokens: int
9-
total_tokens: int
10-
image_tokens: int | None = None
11-
thinking_tokens: int | None = None
12-
cache_creation_input_tokens: int | None = None
13-
cache_read_input_tokens: int | None = None
7+
prompt_tokens: int=0
8+
completion_tokens: int=0
9+
total_tokens: int=0
10+
image_tokens: int=0
11+
thinking_tokens: int=0
12+
cache_creation_input_tokens: int=0
13+
cache_read_input_tokens: int=0
1414

1515

1616
class Metadata(BaseModel):

0 commit comments

Comments
 (0)