@@ -13,7 +13,7 @@ fn text_response(text: impl Into<String>) -> AssistantContent {
1313 AssistantContent :: Text ( Text { text : text. into ( ) } )
1414}
1515
16- async fn configure_test_lm ( responses : Vec < String > ) -> LM {
16+ async fn build_test_lm_with_client ( responses : Vec < String > ) -> ( LM , TestCompletionModel ) {
1717 let client = TestCompletionModel :: new ( responses. into_iter ( ) . map ( text_response) ) ;
1818 let lm = temp_env:: async_with_vars (
1919 [ ( "OPENAI_API_KEY" , Some ( "test" ) ) ] ,
@@ -24,27 +24,20 @@ async fn configure_test_lm(responses: Vec<String>) -> LM {
2424 )
2525 . await
2626 . expect ( "build lm" )
27- . with_client ( LMClient :: Test ( client) )
27+ . with_client ( LMClient :: Test ( client. clone ( ) ) )
2828 . await
2929 . expect ( "install test client" ) ;
30+ ( lm, client)
31+ }
32+
33+ async fn configure_test_lm ( responses : Vec < String > ) -> LM {
34+ let ( lm, _) = build_test_lm_with_client ( responses) . await ;
3035 configure ( lm. clone ( ) , ChatAdapter :: new ( ) ) ;
3136 lm
3237}
3338
3439async fn configure_test_lm_with_client ( responses : Vec < String > ) -> ( LM , TestCompletionModel ) {
35- let client = TestCompletionModel :: new ( responses. into_iter ( ) . map ( text_response) ) ;
36- let lm = temp_env:: async_with_vars (
37- [ ( "OPENAI_API_KEY" , Some ( "test" ) ) ] ,
38- LM :: builder ( )
39- . model ( "openai:gpt-4o-mini" . to_string ( ) )
40- . temperature ( 0.0 )
41- . build ( ) ,
42- )
43- . await
44- . expect ( "build lm" )
45- . with_client ( LMClient :: Test ( client. clone ( ) ) )
46- . await
47- . expect ( "install test client" ) ;
40+ let ( lm, client) = build_test_lm_with_client ( responses) . await ;
4841 configure ( lm. clone ( ) , ChatAdapter :: new ( ) ) ;
4942 ( lm, client)
5043}
@@ -206,3 +199,52 @@ async fn rlm_feedback_carries_truncation_marker_with_configured_budget() {
206199 "[output truncated at 10 chars - full content in variable. pass to llm_query() to analyze]"
207200 ) ) ;
208201}
202+
203+ #[ cfg_attr( miri, ignore = "MIRI has issues with tokio's I/O driver" ) ]
204+ #[ tokio:: test( flavor = "multi_thread" ) ]
205+ async fn rlm_sub_lm_tools_persist_state_and_decrement_budget_across_turns ( ) {
206+ let _lock = SETTINGS_LOCK . lock ( ) . await ;
207+ let ( _action_lm, action_client) = configure_test_lm_with_client ( vec ! [
208+ "single = llm_query('single')\n batch = llm_query_batched(['left', 'right'])\n counter = 40 + len(batch)" . to_string( ) ,
209+ "try:\n llm_query('should_fail')\n budget_state = 'not_exhausted'\n except Exception as err:\n budget_state = 'exhausted' if 'budget exhausted' in str(err) else f'unexpected:{err}'\n SUBMIT(answer=f'{counter}:{budget_state}:{single}')" . to_string( ) ,
210+ ] )
211+ . await ;
212+ let ( sub_lm, _) = build_test_lm_with_client ( vec ! [
213+ "single-ok" . to_string( ) ,
214+ "batch-a" . to_string( ) ,
215+ "batch-b" . to_string( ) ,
216+ ] )
217+ . await ;
218+
219+ let rlm = Rlm :: < RlmLoopSig > :: builder ( )
220+ . runtime ( Arc :: new ( PyO3Runtime ) )
221+ . sub_lm ( Arc :: new ( sub_lm) )
222+ . max_iterations ( 2 )
223+ . max_llm_calls ( 3 )
224+ . enable_extraction_fallback ( false )
225+ . build ( ) ;
226+
227+ let predicted = rlm
228+ . call ( RlmLoopSigInput {
229+ prompt : "Use both sub-LM helpers, then submit on turn two." . to_string ( ) ,
230+ } )
231+ . await
232+ . expect ( "rlm should complete with persisted state and enforced budget" ) ;
233+
234+ assert_eq ! ( predicted. answer, "42:exhausted:single-ok" ) ;
235+ assert ! (
236+ predicted
237+ . metadata( )
238+ . raw_response
239+ . contains( "llm_query_batched" )
240+ ) ;
241+
242+ let last_request = action_client
243+ . last_request ( )
244+ . expect ( "expected second-turn request with feedback" ) ;
245+ let request_debug = format ! ( "{last_request:?}" ) ;
246+ assert ! (
247+ request_debug. contains( "0/3 sub-model calls remaining" ) ,
248+ "second turn should see depleted sub-LM budget"
249+ ) ;
250+ }
0 commit comments