@@ -2117,6 +2117,219 @@ def callback(
21172117 assert [cast (dict [str , Any ], item ).get ("content" ) for item in session_items ] == ["new" ]
21182118
21192119
2120+ @pytest .mark .asyncio
2121+ async def test_prepare_input_with_openai_conversation_strips_assistant_history_ids () -> None :
2122+ class DummyOpenAIConversationsSession (OpenAIConversationsSession ):
2123+ def __init__ (self , history : list [TResponseInputItem ]) -> None :
2124+ self .history = history
2125+
2126+ async def get_items (self , limit : int | None = None ) -> list [TResponseInputItem ]:
2127+ if limit is None :
2128+ return list (self .history )
2129+ return self .history [- limit :]
2130+
2131+ async def add_items (self , items : list [TResponseInputItem ]) -> None :
2132+ self .history .extend (items )
2133+
2134+ async def pop_item (self ) -> TResponseInputItem | None :
2135+ return self .history .pop () if self .history else None
2136+
2137+ async def clear_session (self ) -> None :
2138+ self .history .clear ()
2139+
2140+ history_item = cast (
2141+ TResponseInputItem ,
2142+ {
2143+ "id" : "conv_item_assistant" ,
2144+ "type" : "message" ,
2145+ "role" : "assistant" ,
2146+ "content" : "history" ,
2147+ "provider_data" : {"server" : "metadata" },
2148+ },
2149+ )
2150+ user_history_item = cast (
2151+ TResponseInputItem ,
2152+ {
2153+ "id" : "conv_item_user" ,
2154+ "type" : "message" ,
2155+ "role" : "user" ,
2156+ "content" : "user history" ,
2157+ "provider_data" : {"server" : "metadata" },
2158+ },
2159+ )
2160+ function_call_item = cast (
2161+ TResponseInputItem ,
2162+ {
2163+ "id" : "conv_item_call" ,
2164+ "type" : "function_call" ,
2165+ "call_id" : "call_history" ,
2166+ "name" : "lookup" ,
2167+ "arguments" : "{}" ,
2168+ },
2169+ )
2170+ function_call_output_item = cast (
2171+ TResponseInputItem ,
2172+ {
2173+ "id" : "conv_item_output" ,
2174+ "type" : "function_call_output" ,
2175+ "call_id" : "call_history" ,
2176+ "output" : "ok" ,
2177+ },
2178+ )
2179+ session = DummyOpenAIConversationsSession (
2180+ history = [user_history_item , history_item , function_call_item , function_call_output_item ]
2181+ )
2182+
2183+ prepared , session_items = await prepare_input_with_session ("new" , session , None )
2184+
2185+ assert isinstance (prepared , list )
2186+ user_payload = cast (dict [str , Any ], prepared [0 ])
2187+ history_payload = cast (dict [str , Any ], prepared [1 ])
2188+ call_payload = cast (dict [str , Any ], prepared [2 ])
2189+ output_payload = cast (dict [str , Any ], prepared [3 ])
2190+ new_payload = cast (dict [str , Any ], prepared [4 ])
2191+ assert user_payload ["role" ] == "user"
2192+ assert user_payload ["id" ] == "conv_item_user"
2193+ assert "provider_data" in user_payload
2194+ assert history_payload ["role" ] == "assistant"
2195+ assert "id" not in history_payload
2196+ assert "provider_data" not in history_payload
2197+ assert call_payload ["id" ] == "conv_item_call"
2198+ assert output_payload ["id" ] == "conv_item_output"
2199+ assert new_payload ["role" ] == "user"
2200+ assert new_payload ["content" ] == "new"
2201+ assert [cast (dict [str , Any ], item ).get ("content" ) for item in session_items ] == ["new" ]
2202+
2203+
2204+ @pytest .mark .asyncio
2205+ async def test_prepare_input_with_regular_session_preserves_history_ids () -> None :
2206+ history_item = cast (
2207+ TResponseInputItem ,
2208+ {
2209+ "id" : "message_id" ,
2210+ "type" : "message" ,
2211+ "role" : "assistant" ,
2212+ "content" : "history" ,
2213+ },
2214+ )
2215+ session = SimpleListSession (history = [history_item ])
2216+
2217+ prepared , _ = await prepare_input_with_session ("new" , session , None )
2218+
2219+ assert isinstance (prepared , list )
2220+ history_payload = cast (dict [str , Any ], prepared [0 ])
2221+ assert history_payload ["id" ] == "message_id"
2222+
2223+
2224+ @pytest .mark .asyncio
2225+ async def test_prepare_input_with_openai_conversation_callback_matches_assistant_no_ids () -> None :
2226+ class DummyOpenAIConversationsSession (OpenAIConversationsSession ):
2227+ def __init__ (self , history : list [TResponseInputItem ]) -> None :
2228+ self .history = history
2229+
2230+ async def get_items (self , limit : int | None = None ) -> list [TResponseInputItem ]:
2231+ if limit is None :
2232+ return list (self .history )
2233+ return self .history [- limit :]
2234+
2235+ async def add_items (self , items : list [TResponseInputItem ]) -> None :
2236+ self .history .extend (items )
2237+
2238+ async def pop_item (self ) -> TResponseInputItem | None :
2239+ return self .history .pop () if self .history else None
2240+
2241+ async def clear_session (self ) -> None :
2242+ self .history .clear ()
2243+
2244+ history_item = cast (
2245+ TResponseInputItem ,
2246+ {
2247+ "id" : "conv_item_assistant" ,
2248+ "type" : "message" ,
2249+ "role" : "assistant" ,
2250+ "content" : "history" ,
2251+ "provider_data" : {"server" : "metadata" },
2252+ },
2253+ )
2254+ session = DummyOpenAIConversationsSession (history = [history_item ])
2255+
2256+ def callback (
2257+ history : list [TResponseInputItem ], new_input : list [TResponseInputItem ]
2258+ ) -> list [TResponseInputItem ]:
2259+ history_copy = dict (cast (dict [str , Any ], history [0 ]))
2260+ history_copy .pop ("id" , None )
2261+ history_copy .pop ("provider_data" , None )
2262+ return [
2263+ cast (TResponseInputItem , history_copy ),
2264+ cast (TResponseInputItem , dict (cast (dict [str , Any ], new_input [0 ]))),
2265+ ]
2266+
2267+ prepared , session_items = await prepare_input_with_session ("new" , session , callback )
2268+
2269+ assert isinstance (prepared , list )
2270+ assert [cast (dict [str , Any ], item ).get ("content" ) for item in prepared ] == [
2271+ "history" ,
2272+ "new" ,
2273+ ]
2274+ assert [cast (dict [str , Any ], item ).get ("content" ) for item in session_items ] == ["new" ]
2275+
2276+
2277+ @pytest .mark .asyncio
2278+ async def test_prepare_input_with_openai_conversation_callback_keeps_user_ids_distinct () -> None :
2279+ class DummyOpenAIConversationsSession (OpenAIConversationsSession ):
2280+ def __init__ (self , history : list [TResponseInputItem ]) -> None :
2281+ self .history = history
2282+
2283+ async def get_items (self , limit : int | None = None ) -> list [TResponseInputItem ]:
2284+ if limit is None :
2285+ return list (self .history )
2286+ return self .history [- limit :]
2287+
2288+ async def add_items (self , items : list [TResponseInputItem ]) -> None :
2289+ self .history .extend (items )
2290+
2291+ async def pop_item (self ) -> TResponseInputItem | None :
2292+ return self .history .pop () if self .history else None
2293+
2294+ async def clear_session (self ) -> None :
2295+ self .history .clear ()
2296+
2297+ history_item = cast (
2298+ TResponseInputItem ,
2299+ {
2300+ "id" : "conv_item_user" ,
2301+ "type" : "message" ,
2302+ "role" : "user" ,
2303+ "content" : "history" ,
2304+ "provider_data" : {"server" : "metadata" },
2305+ },
2306+ )
2307+ session = DummyOpenAIConversationsSession (history = [history_item ])
2308+
2309+ def callback (
2310+ history : list [TResponseInputItem ], new_input : list [TResponseInputItem ]
2311+ ) -> list [TResponseInputItem ]:
2312+ history_copy = dict (cast (dict [str , Any ], history [0 ]))
2313+ history_copy .pop ("id" , None )
2314+ history_copy .pop ("provider_data" , None )
2315+ return [
2316+ cast (TResponseInputItem , history_copy ),
2317+ cast (TResponseInputItem , dict (cast (dict [str , Any ], new_input [0 ]))),
2318+ ]
2319+
2320+ prepared , session_items = await prepare_input_with_session ("new" , session , callback )
2321+
2322+ assert isinstance (prepared , list )
2323+ assert [cast (dict [str , Any ], item ).get ("content" ) for item in prepared ] == [
2324+ "history" ,
2325+ "new" ,
2326+ ]
2327+ assert [cast (dict [str , Any ], item ).get ("content" ) for item in session_items ] == [
2328+ "history" ,
2329+ "new" ,
2330+ ]
2331+
2332+
21202333@pytest .mark .asyncio
21212334async def test_persist_session_items_for_guardrail_trip_uses_original_input_when_missing () -> None :
21222335 session = SimpleListSession ()
0 commit comments