11from __future__ import annotations
22
33import logging
4- from typing import TYPE_CHECKING , Any , Callable
4+ from typing import TYPE_CHECKING , Any , Callable , Literal
55
66from openai import AsyncOpenAI
77
2121
2222DEFAULT_COMPACTION_THRESHOLD = 10
2323
24+ OpenAIResponsesCompactionMode = Literal ["previous_response_id" , "input" , "auto" ]
25+
2426
2527def select_compaction_candidate_items (
2628 items : list [TResponseInputItem ],
@@ -85,6 +87,7 @@ def __init__(
8587 * ,
8688 client : AsyncOpenAI | None = None ,
8789 model : str = "gpt-4.1" ,
90+ compaction_mode : OpenAIResponsesCompactionMode = "auto" ,
8891 should_trigger_compaction : Callable [[dict [str , Any ]], bool ] | None = None ,
8992 ):
9093 """Initialize the compaction session.
@@ -97,6 +100,9 @@ def __init__(
97100 get_default_openai_client() or new AsyncOpenAI().
98101 model: Model to use for responses.compact. Defaults to "gpt-4.1". Must be an
99102 OpenAI model name (gpt-*, o*, or ft:gpt-*).
103+ compaction_mode: Controls how the compaction request provides conversation
104+ history. "auto" (default) uses input when the last response was not
105+ stored or no response_id is available.
100106 should_trigger_compaction: Custom decision hook. Defaults to triggering when
101107 10+ compaction candidates exist.
102108 """
@@ -113,6 +119,7 @@ def __init__(
113119 self .underlying_session = underlying_session
114120 self ._client = client
115121 self .model = model
122+ self .compaction_mode = compaction_mode
116123 self .should_trigger_compaction = (
117124 should_trigger_compaction or default_should_trigger_compaction
118125 )
@@ -122,21 +129,54 @@ def __init__(
122129 self ._session_items : list [TResponseInputItem ] | None = None
123130 self ._response_id : str | None = None
124131 self ._deferred_response_id : str | None = None
132+ self ._last_unstored_response_id : str | None = None
125133
126134 @property
127135 def client (self ) -> AsyncOpenAI :
128136 if self ._client is None :
129137 self ._client = get_default_openai_client () or AsyncOpenAI ()
130138 return self ._client
131139
140+ def _resolve_compaction_mode_for_response (
141+ self ,
142+ * ,
143+ response_id : str | None ,
144+ store : bool | None ,
145+ requested_mode : OpenAIResponsesCompactionMode | None ,
146+ ) -> _ResolvedCompactionMode :
147+ mode = requested_mode or self .compaction_mode
148+ if (
149+ mode == "auto"
150+ and store is None
151+ and response_id is not None
152+ and response_id == self ._last_unstored_response_id
153+ ):
154+ return "input"
155+ return _resolve_compaction_mode (mode , response_id = response_id , store = store )
156+
132157 async def run_compaction (self , args : OpenAIResponsesCompactionArgs | None = None ) -> None :
133158 """Run compaction using responses.compact API."""
134159 if args and args .get ("response_id" ):
135160 self ._response_id = args ["response_id" ]
161+ requested_mode = args .get ("compaction_mode" ) if args else None
162+ if args and "store" in args :
163+ store = args ["store" ]
164+ if store is False and self ._response_id :
165+ self ._last_unstored_response_id = self ._response_id
166+ elif store is True and self ._response_id == self ._last_unstored_response_id :
167+ self ._last_unstored_response_id = None
168+ else :
169+ store = None
170+ resolved_mode = self ._resolve_compaction_mode_for_response (
171+ response_id = self ._response_id ,
172+ store = store ,
173+ requested_mode = requested_mode ,
174+ )
136175
137- if not self ._response_id :
176+ if resolved_mode == "previous_response_id" and not self ._response_id :
138177 raise ValueError (
139- "OpenAIResponsesCompactionSession.run_compaction requires a response_id"
178+ "OpenAIResponsesCompactionSession.run_compaction requires a response_id "
179+ "when using previous_response_id compaction."
140180 )
141181
142182 compaction_candidate_items , session_items = await self ._ensure_compaction_candidates ()
@@ -145,23 +185,32 @@ async def run_compaction(self, args: OpenAIResponsesCompactionArgs | None = None
145185 should_compact = force or self .should_trigger_compaction (
146186 {
147187 "response_id" : self ._response_id ,
188+ "compaction_mode" : resolved_mode ,
148189 "compaction_candidate_items" : compaction_candidate_items ,
149190 "session_items" : session_items ,
150191 }
151192 )
152193
153194 if not should_compact :
154- logger .debug (f"skip: decision hook declined compaction for { self ._response_id } " )
195+ logger .debug (
196+ f"skip: decision hook declined compaction for { self ._response_id } "
197+ f"(mode={ resolved_mode } )"
198+ )
155199 return
156200
157201 self ._deferred_response_id = None
158- logger .debug (f"compact: start for { self ._response_id } using { self .model } " )
159-
160- compacted = await self .client .responses .compact (
161- previous_response_id = self ._response_id ,
162- model = self .model ,
202+ logger .debug (
203+ f"compact: start for { self ._response_id } using { self .model } (mode={ resolved_mode } )"
163204 )
164205
206+ compact_kwargs : dict [str , Any ] = {"model" : self .model }
207+ if resolved_mode == "previous_response_id" :
208+ compact_kwargs ["previous_response_id" ] = self ._response_id
209+ else :
210+ compact_kwargs ["input" ] = session_items
211+
212+ compacted = await self .client .responses .compact (** compact_kwargs )
213+
165214 await self .underlying_session .clear_session ()
166215 output_items : list [TResponseInputItem ] = []
167216 if compacted .output :
@@ -183,19 +232,26 @@ async def run_compaction(self, args: OpenAIResponsesCompactionArgs | None = None
183232
184233 logger .debug (
185234 f"compact: done for { self ._response_id } "
186- f"(output={ len (output_items )} , candidates={ len (self ._compaction_candidate_items )} )"
235+ f"(mode={ resolved_mode } , output={ len (output_items )} , "
236+ f"candidates={ len (self ._compaction_candidate_items )} )"
187237 )
188238
189239 async def get_items (self , limit : int | None = None ) -> list [TResponseInputItem ]:
190240 return await self .underlying_session .get_items (limit )
191241
192- async def _defer_compaction (self , response_id : str ) -> None :
242+ async def _defer_compaction (self , response_id : str , store : bool | None = None ) -> None :
193243 if self ._deferred_response_id is not None :
194244 return
195245 compaction_candidate_items , session_items = await self ._ensure_compaction_candidates ()
246+ resolved_mode = self ._resolve_compaction_mode_for_response (
247+ response_id = response_id ,
248+ store = store ,
249+ requested_mode = None ,
250+ )
196251 should_compact = self .should_trigger_compaction (
197252 {
198253 "response_id" : response_id ,
254+ "compaction_mode" : resolved_mode ,
199255 "compaction_candidate_items" : compaction_candidate_items ,
200256 "session_items" : session_items ,
201257 }
@@ -247,3 +303,21 @@ async def _ensure_compaction_candidates(
247303 f"candidates: initialized (history={ len (history )} , candidates={ len (candidates )} )"
248304 )
249305 return (candidates [:], history [:])
306+
307+
308+ _ResolvedCompactionMode = Literal ["previous_response_id" , "input" ]
309+
310+
311+ def _resolve_compaction_mode (
312+ requested_mode : OpenAIResponsesCompactionMode ,
313+ * ,
314+ response_id : str | None ,
315+ store : bool | None ,
316+ ) -> _ResolvedCompactionMode :
317+ if requested_mode != "auto" :
318+ return requested_mode
319+ if store is False :
320+ return "input"
321+ if not response_id :
322+ return "input"
323+ return "previous_response_id"
0 commit comments