Skip to content

Commit 669c4b3

Browse files
committed
done with condenser
1 parent 24d6b40 commit 669c4b3

1 file changed

Lines changed: 397 additions & 0 deletions

File tree

‎sdk/arch/condenser.mdx‎

Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
---
2+
title: Condenser
3+
description: High-level architecture of the conversation history compression system
4+
---
5+
6+
The **Condenser** system manages conversation history compression to keep agent context within LLM token limits. It reduces long event histories into condensed summaries while preserving critical information for reasoning. For more details, read the [blog here](https://openhands.dev/blog/openhands-context-condensensation-for-more-efficient-ai-agents).
7+
8+
**Source:** [`openhands-sdk/openhands/sdk/context/condenser/`](https://github.com/All-Hands-AI/agent-sdk/tree/main/openhands-sdk/openhands/sdk/context/condenser)
9+
10+
## Core Responsibilities
11+
12+
The Condenser system has four primary responsibilities:
13+
14+
1. **History Compression** - Reduce event lists to fit within context windows
15+
2. **Threshold Detection** - Determine when condensation should trigger
16+
3. **Summary Generation** - Create meaningful summaries via LLM or heuristics
17+
4. **View Management** - Transform event history into LLM-ready views
18+
19+
## Architecture
20+
21+
```mermaid
22+
%%{init: {"theme": "default", "flowchart": {"nodeSpacing": 25, "rankSpacing": 50}} }%%
23+
flowchart TB
24+
subgraph Interface["Abstract Interface"]
25+
Base["CondenserBase<br><i>Abstract base</i>"]
26+
end
27+
28+
subgraph Implementations["Concrete Implementations"]
29+
NoOp["NoOpCondenser<br><i>No compression</i>"]
30+
LLM["LLMSummarizingCondenser<br><i>LLM-based</i>"]
31+
Pipeline["PipelineCondenser<br><i>Multi-stage</i>"]
32+
end
33+
34+
subgraph Process["Condensation Process"]
35+
View["View<br><i>Event history</i>"]
36+
Check["should_condense()?"]
37+
Condense["get_condensation()"]
38+
Result["View | Condensation"]
39+
end
40+
41+
subgraph Output["Condensation Output"]
42+
CondEvent["Condensation Event<br><i>Summary metadata</i>"]
43+
NewView["Condensed View<br><i>Reduced tokens</i>"]
44+
end
45+
46+
Base --> NoOp
47+
Base --> LLM
48+
Base --> Pipeline
49+
50+
View --> Check
51+
Check -->|Yes| Condense
52+
Check -->|No| Result
53+
Condense --> CondEvent
54+
CondEvent --> NewView
55+
NewView --> Result
56+
57+
classDef primary fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px
58+
classDef secondary fill:#e8f3ff,stroke:#2b6cb0,stroke-width:2px
59+
classDef tertiary fill:#fff4df,stroke:#b7791f,stroke-width:2px
60+
61+
class Base primary
62+
class LLM,Pipeline secondary
63+
class Check,Condense tertiary
64+
```
65+
66+
### Key Components
67+
68+
| Component | Purpose | Design |
69+
|-----------|---------|--------|
70+
| **[`CondenserBase`](https://github.com/All-Hands-AI/agent-sdk/blob/main/openhands-sdk/openhands/sdk/context/condenser/base.py)** | Abstract interface | Defines `condense()` contract |
71+
| **[`RollingCondenser`](https://github.com/All-Hands-AI/agent-sdk/blob/main/openhands-sdk/openhands/sdk/context/condenser/base.py)** | Rolling window base | Implements threshold-based triggering |
72+
| **[`LLMSummarizingCondenser`](https://github.com/All-Hands-AI/agent-sdk/blob/main/openhands-sdk/openhands/sdk/context/condenser/llm_summarizing_condenser.py)** | LLM summarization | Uses LLM to generate summaries |
73+
| **[`NoOpCondenser`](https://github.com/All-Hands-AI/agent-sdk/blob/main/openhands-sdk/openhands/sdk/context/condenser/no_op_condenser.py)** | No-op implementation | Returns view unchanged |
74+
| **[`PipelineCondenser`](https://github.com/All-Hands-AI/agent-sdk/blob/main/openhands-sdk/openhands/sdk/context/condenser/pipeline_condenser.py)** | Multi-stage pipeline | Chains multiple condensers |
75+
| **[`View`](https://github.com/All-Hands-AI/agent-sdk/blob/main/openhands-sdk/openhands/sdk/context/view.py)** | Event view | Represents history for LLM |
76+
| **[`Condensation`](https://github.com/All-Hands-AI/agent-sdk/blob/main/openhands-sdk/openhands/sdk/event/condenser.py)** | Condensation event | Metadata about compression |
77+
78+
## Condenser Types
79+
80+
### NoOpCondenser
81+
82+
Pass-through condenser that performs no compression:
83+
84+
```mermaid
85+
%%{init: {"theme": "default", "flowchart": {"nodeSpacing": 30}} }%%
86+
flowchart LR
87+
View["View"]
88+
NoOp["NoOpCondenser"]
89+
Same["Same View"]
90+
91+
View --> NoOp --> Same
92+
93+
style NoOp fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px
94+
```
95+
96+
### LLMSummarizingCondenser
97+
98+
Uses an LLM to generate summaries of conversation history:
99+
100+
```mermaid
101+
%%{init: {"theme": "default", "flowchart": {"nodeSpacing": 30, "rankSpacing": 40}} }%%
102+
flowchart TB
103+
View["Long View<br><i>120+ events</i>"]
104+
Check["Threshold<br>exceeded?"]
105+
Summarize["LLM Summarization"]
106+
Summary["Summary Text"]
107+
Metadata["Condensation Event"]
108+
AddToHistory["Add to History"]
109+
NextStep["Next Step: View.from_events()"]
110+
NewView["Condensed View"]
111+
112+
View --> Check
113+
Check -->|Yes| Summarize
114+
Summarize --> Summary
115+
Summary --> Metadata
116+
Metadata --> AddToHistory
117+
AddToHistory --> NextStep
118+
NextStep --> NewView
119+
120+
style Check fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px
121+
style Summarize fill:#e8f3ff,stroke:#2b6cb0,stroke-width:2px
122+
style NewView fill:#fff4df,stroke:#b7791f,stroke-width:2px
123+
```
124+
125+
**Process:**
126+
1. **Check Threshold:** Compare view size to configured limit (e.g., event count > `max_size`)
127+
2. **Select Events:** Identify events to keep (first N + last M) and events to summarize (middle)
128+
3. **LLM Call:** Generate summary of middle events using dedicated LLM
129+
4. **Create Event:** Wrap summary in `Condensation` event with `forgotten_event_ids`
130+
5. **Add to History:** Agent adds `Condensation` to event log and returns early
131+
6. **Next Step:** `View.from_events()` filters forgotten events and inserts summary
132+
133+
**Configuration:**
134+
- **`max_size`:** Event count threshold before condensation triggers (default: 120)
135+
- **`keep_first`:** Number of initial events to preserve verbatim (default: 4)
136+
- **`llm`:** LLM instance for summarization (often cheaper model than reasoning LLM)
137+
138+
### PipelineCondenser
139+
140+
Chains multiple condensers in sequence:
141+
142+
```mermaid
143+
%%{init: {"theme": "default", "flowchart": {"nodeSpacing": 30}} }%%
144+
flowchart LR
145+
View["Original View"]
146+
C1["Condenser 1"]
147+
C2["Condenser 2"]
148+
C3["Condenser 3"]
149+
Final["Final View"]
150+
151+
View --> C1 --> C2 --> C3 --> Final
152+
153+
style C1 fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px
154+
style C2 fill:#e8f3ff,stroke:#2b6cb0,stroke-width:2px
155+
style C3 fill:#fff4df,stroke:#b7791f,stroke-width:2px
156+
```
157+
158+
**Use Case:** Multi-stage compression (e.g., remove old events, then summarize, then truncate)
159+
160+
## Condensation Flow
161+
162+
### Trigger Mechanisms
163+
164+
Condensers can be triggered in two ways:
165+
166+
```mermaid
167+
%%{init: {"theme": "default", "flowchart": {"nodeSpacing": 30, "rankSpacing": 40}} }%%
168+
flowchart TB
169+
subgraph Automatic["Automatic Trigger"]
170+
Agent1["Agent Step"]
171+
Build1["View.from_events()"]
172+
Check1["condenser.condense(view)"]
173+
Trigger1["should_condense()?"]
174+
end
175+
176+
Agent1 --> Build1 --> Check1 --> Trigger1
177+
178+
style Check1 fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px
179+
```
180+
181+
**Automatic Trigger:**
182+
- **When:** Threshold exceeded (e.g., event count > `max_size`)
183+
- **Who:** Agent calls `condenser.condense()` each step
184+
- **Purpose:** Proactively keep context within limits
185+
186+
187+
```mermaid
188+
%%{init: {"theme": "default", "flowchart": {"nodeSpacing": 30, "rankSpacing": 40}} }%%
189+
flowchart TB
190+
subgraph Manual["Manual Trigger"]
191+
Error["LLM Context Error"]
192+
Request["CondensationRequest Event"]
193+
NextStep["Next Agent Step"]
194+
Trigger2["condense() detects request"]
195+
end
196+
197+
Error --> Request --> NextStep --> Trigger2
198+
199+
style Request fill:#e8f3ff,stroke:#2b6cb0,stroke-width:2px
200+
```
201+
**Manual Trigger:**
202+
- **When:** `CondensationRequest` event added to history (via `view.unhandled_condensation_request`)
203+
- **Who:** Agent (on LLM context window error) or application code
204+
- **Purpose:** Force compression when context limit exceeded
205+
206+
### Condensation Workflow
207+
208+
```mermaid
209+
%%{init: {"theme": "default", "flowchart": {"nodeSpacing": 30, "rankSpacing": 40}} }%%
210+
flowchart TB
211+
Start["Agent calls condense(view)"]
212+
213+
Decision{"should_condense?"}
214+
215+
ReturnView["Return View<br><i>Agent proceeds</i>"]
216+
217+
Extract["Select Events to Keep/Forget"]
218+
Generate["LLM Generates Summary"]
219+
Create["Create Condensation Event"]
220+
ReturnCond["Return Condensation"]
221+
AddHistory["Agent adds to history"]
222+
NextStep["Next Step: View.from_events()"]
223+
FilterEvents["Filter forgotten events"]
224+
InsertSummary["Insert summary at offset"]
225+
NewView["New condensed view"]
226+
227+
Start --> Decision
228+
Decision -->|No| ReturnView
229+
Decision -->|Yes| Extract
230+
Extract --> Generate
231+
Generate --> Create
232+
Create --> ReturnCond
233+
ReturnCond --> AddHistory
234+
AddHistory --> NextStep
235+
NextStep --> FilterEvents
236+
FilterEvents --> InsertSummary
237+
InsertSummary --> NewView
238+
239+
style Decision fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px
240+
style Generate fill:#e8f3ff,stroke:#2b6cb0,stroke-width:2px
241+
style Create fill:#fff4df,stroke:#b7791f,stroke-width:2px
242+
```
243+
244+
**Key Steps:**
245+
246+
1. **Threshold Check:** `should_condense()` determines if condensation needed
247+
2. **Event Selection:** Identify events to keep (head + tail) vs forget (middle)
248+
3. **Summary Generation:** LLM creates compressed representation of forgotten events
249+
4. **Condensation Creation:** Create `Condensation` event with `forgotten_event_ids` and summary
250+
5. **Return to Agent:** Condenser returns `Condensation` (not `View`)
251+
6. **History Update:** Agent adds `Condensation` to event log and exits step
252+
7. **Next Step:** `View.from_events()` ([source](https://github.com/All-Hands-AI/agent-sdk/blob/main/openhands-sdk/openhands/sdk/context/view.py)) processes Condensation to filter events and insert summary
253+
254+
## View and Condensation
255+
256+
### View Structure
257+
258+
A `View` represents the conversation history as it will be sent to the LLM:
259+
260+
```mermaid
261+
%%{init: {"theme": "default", "flowchart": {"nodeSpacing": 30}} }%%
262+
flowchart LR
263+
Events["Full Event List<br><i>+ Condensation events</i>"]
264+
FromEvents["View.from_events()"]
265+
Filter["Filter forgotten events"]
266+
Insert["Insert summary"]
267+
View["View<br><i>LLMConvertibleEvents</i>"]
268+
Convert["events_to_messages()"]
269+
LLM["LLM Input"]
270+
271+
Events --> FromEvents
272+
FromEvents --> Filter
273+
Filter --> Insert
274+
Insert --> View
275+
View --> Convert
276+
Convert --> LLM
277+
278+
style View fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px
279+
style FromEvents fill:#e8f3ff,stroke:#2b6cb0,stroke-width:2px
280+
```
281+
282+
**View Components:**
283+
- **`events`:** List of `LLMConvertibleEvent` objects (filtered by Condensation)
284+
- **`unhandled_condensation_request`:** Flag for pending manual condensation
285+
- **`condensations`:** List of all Condensation events processed
286+
- **Methods:** `from_events()` creates view from raw events, handling Condensation semantics
287+
288+
### Condensation Event
289+
290+
When condensation occurs, a `Condensation` event is created:
291+
292+
```mermaid
293+
%%{init: {"theme": "default", "flowchart": {"nodeSpacing": 30}} }%%
294+
flowchart LR
295+
Old["Middle Events<br><i>~60 events</i>"]
296+
Summary["Summary Text<br><i>LLM-generated</i>"]
297+
Event["Condensation Event<br><i>forgotten_event_ids</i>"]
298+
Applied["View.from_events()"]
299+
New["New View<br><i>~60 events + summary</i>"]
300+
301+
Old -.->|Summarized| Summary
302+
Summary --> Event
303+
Event --> Applied
304+
Applied --> New
305+
306+
style Event fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px
307+
style Summary fill:#e8f3ff,stroke:#2b6cb0,stroke-width:2px
308+
```
309+
310+
**Condensation Fields:**
311+
- **`forgotten_event_ids`:** List of event IDs to filter out
312+
- **`summary`:** Compressed text representation of forgotten events
313+
- **`summary_offset`:** Index where summary event should be inserted
314+
- Inherits from `Event`: `id`, `timestamp`, `source`
315+
316+
## Rolling Window Pattern
317+
318+
`RollingCondenser` implements a common pattern for threshold-based condensation:
319+
320+
```mermaid
321+
%%{init: {"theme": "default", "flowchart": {"nodeSpacing": 30, "rankSpacing": 40}} }%%
322+
flowchart TB
323+
View["Current View<br><i>120+ events</i>"]
324+
Check["Count Events"]
325+
326+
Compare{"Count ><br>max_size?"}
327+
328+
Keep["Keep All Events"]
329+
330+
Split["Split Events"]
331+
Head["Head<br><i>First 4 events</i>"]
332+
Middle["Middle<br><i>~56 events</i>"]
333+
Tail["Tail<br><i>~56 events</i>"]
334+
Summarize["LLM Summarizes Middle"]
335+
Result["Head + Summary + Tail<br><i>~60 events total</i>"]
336+
337+
View --> Check
338+
Check --> Compare
339+
340+
Compare -->|Under| Keep
341+
Compare -->|Over| Split
342+
343+
Split --> Head
344+
Split --> Middle
345+
Split --> Tail
346+
347+
Middle --> Summarize
348+
Head --> Result
349+
Summarize --> Result
350+
Tail --> Result
351+
352+
style Compare fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px
353+
style Split fill:#e8f3ff,stroke:#2b6cb0,stroke-width:2px
354+
style Summarize fill:#fff4df,stroke:#b7791f,stroke-width:2px
355+
```
356+
357+
**Rolling Window Strategy:**
358+
1. **Keep Head:** Preserve first `keep_first` events (default: 4) - usually system prompts
359+
2. **Keep Tail:** Preserve last `target_size - keep_first - 1` events - recent context
360+
3. **Summarize Middle:** Compress events between head and tail into summary
361+
4. **Target Size:** After condensation, view has `max_size // 2` events (default: 60)
362+
363+
## Component Relationships
364+
365+
### How Condenser Integrates
366+
367+
```mermaid
368+
%%{init: {"theme": "default", "flowchart": {"nodeSpacing": 30}} }%%
369+
flowchart LR
370+
Agent["Agent"]
371+
Condenser["Condenser"]
372+
State["Conversation State"]
373+
Events["Event Log"]
374+
375+
Agent -->|"View.from_events()"| State
376+
State -->|View| Agent
377+
Agent -->|"condense(view)"| Condenser
378+
Condenser -->|"View | Condensation"| Agent
379+
Agent -->|Adds Condensation| Events
380+
381+
style Condenser fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px
382+
style Agent fill:#e8f3ff,stroke:#2b6cb0,stroke-width:2px
383+
style Events fill:#fff4df,stroke:#b7791f,stroke-width:2px
384+
```
385+
386+
**Relationship Characteristics:**
387+
- **Agent → State**: Calls `View.from_events()` to get current view
388+
- **Agent → Condenser**: Calls `condense(view)` each step if condenser registered
389+
- **Condenser → Agent**: Returns `View` (proceed) or `Condensation` (defer)
390+
- **Agent → Events**: Adds `Condensation` event to log when returned
391+
392+
## See Also
393+
394+
- **[Agent Architecture](/sdk/arch/agent)** - How agents use condensers during reasoning
395+
- **[Conversation Architecture](/sdk/arch/conversation)** - View generation and event management
396+
- **[Events](/sdk/arch/events)** - Condensation event type and append-only log
397+
- **[Context Condenser Guide](/sdk/guides/context-condenser)** - Configuring and using condensers

0 commit comments

Comments
 (0)