99import time
1010import uuid
1111from collections .abc import Callable
12+ from concurrent .futures import ThreadPoolExecutor , as_completed
1213from contextlib import contextmanager
1314from typing import Any
1415
@@ -135,9 +136,15 @@ def __init__(
135136 custom_tools : dict [str , Any ] | None = None ,
136137 custom_sub_tools : dict [str , Any ] | None = None ,
137138 compaction : bool = False ,
139+ max_concurrent_subcalls : int = 4 ,
138140 ** kwargs ,
139141 ):
140- super ().__init__ (persistent = persistent , depth = depth , ** kwargs )
142+ super ().__init__ (
143+ persistent = persistent ,
144+ depth = depth ,
145+ max_concurrent_subcalls = max_concurrent_subcalls ,
146+ ** kwargs ,
147+ )
141148
142149 self .lm_handler_address = lm_handler_address
143150 self .subcall_fn = subcall_fn # Callback for recursive RLM calls (depth > 1 support)
@@ -316,9 +323,13 @@ def _rlm_query(self, prompt: str, model: str | None = None) -> str:
316323 return self ._llm_query (prompt , model )
317324
318325 def _rlm_query_batched (self , prompts : list [str ], model : str | None = None ) -> list [str ]:
319- """Spawn recursive RLM sub-calls for multiple prompts.
326+ """Spawn recursive RLM sub-calls for multiple prompts in parallel.
327+
328+ Each prompt gets its own child RLM for deeper thinking. When multiple
329+ prompts are provided, subcalls run concurrently using a thread pool
330+ (bounded by max_concurrent_subcalls) since they are independent and
331+ I/O-bound. Results are returned in the same order as input prompts.
320332
321- Each prompt gets its own child RLM for deeper thinking.
322333 Falls back to llm_query_batched if no recursive capability is configured.
323334
324335 Args:
@@ -329,14 +340,47 @@ def _rlm_query_batched(self, prompts: list[str], model: str | None = None) -> li
329340 List of responses in the same order as input prompts.
330341 """
331342 if self .subcall_fn is not None :
332- results = []
333- for prompt in prompts :
343+ # For 0 or 1 prompts, no need for thread pool overhead
344+ if len (prompts ) <= 1 :
345+ results = []
346+ for prompt in prompts :
347+ try :
348+ completion = self .subcall_fn (prompt , model )
349+ self ._pending_llm_calls .append (completion )
350+ results .append (completion .response )
351+ except Exception as e :
352+ results .append (f"Error: RLM query failed - { e } " )
353+ return results
354+
355+ # Parallel execution for multiple prompts
356+ max_workers = min (self .max_concurrent_subcalls , len (prompts ))
357+ # Pre-allocate result slots to preserve ordering
358+ results : list [str ] = ["" ] * len (prompts )
359+ completions : list [tuple [int , RLMChatCompletion ]] = []
360+ lock = threading .Lock ()
361+
362+ def _run_subcall (index : int , prompt : str ) -> None :
334363 try :
335364 completion = self .subcall_fn (prompt , model )
336- self ._pending_llm_calls .append (completion )
337- results .append (completion .response )
365+ with lock :
366+ completions .append ((index , completion ))
367+ results [index ] = completion .response
338368 except Exception as e :
339- results .append (f"Error: RLM query failed - { e } " )
369+ results [index ] = f"Error: RLM query failed - { e } "
370+
371+ with ThreadPoolExecutor (max_workers = max_workers ) as executor :
372+ futures = [
373+ executor .submit (_run_subcall , i , prompt ) for i , prompt in enumerate (prompts )
374+ ]
375+ # Wait for all futures to complete; exceptions are captured inside _run_subcall
376+ for future in as_completed (futures ):
377+ future .result () # Re-raises unexpected executor errors
378+
379+ # Append completions in original prompt order for deterministic metadata
380+ completions .sort (key = lambda x : x [0 ])
381+ for _ , completion in completions :
382+ self ._pending_llm_calls .append (completion )
383+
340384 return results
341385
342386 # Fall back to plain batched LM call if no recursive capability
0 commit comments