|
| 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