1+ import threading
12import time
23from collections .abc import Callable
34from contextlib import contextmanager
@@ -74,6 +75,9 @@ def __init__(
7475 on_subcall_complete : Callable [[int , str , float , str | None ], None ] | None = None ,
7576 on_iteration_start : Callable [[int , int ], None ] | None = None ,
7677 on_iteration_complete : Callable [[int , int , float ], None ] | None = None ,
78+ # Internal: shared handler/client for child RLMs (reduces per-subcall overhead)
79+ _shared_lm_handler_address : tuple [str , int ] | None = None ,
80+ _shared_client : "BaseLM | None" = None ,
7781 ):
7882 """
7983 Args:
@@ -152,11 +156,18 @@ def __init__(
152156
153157 # Tracking (cumulative across all calls including children)
154158 self ._cumulative_cost : float = 0.0
159+ self ._cumulative_cost_lock = threading .Lock ()
155160 self ._consecutive_errors : int = 0
156161 self ._last_error : str | None = None
157162 self ._best_partial_answer : str | None = None
158163 self ._completion_start_time : float | None = None # Set when completion() starts
159164
165+ # Shared handler/client for child RLMs (Phase D optimization)
166+ self ._shared_lm_handler_address = _shared_lm_handler_address
167+ self ._shared_client = _shared_client
168+ self ._active_handler_address : tuple [str , int ] | None = None
169+ self ._active_client : BaseLM | None = None
170+
160171 # Persistence support
161172 self .persistent = persistent
162173 self ._persistent_env : SupportsPersistence | None = None
@@ -192,24 +203,44 @@ def _spawn_completion_context(self, prompt: str | dict[str, Any]):
192203
193204 When persistent=True, the environment is reused across calls.
194205 When persistent=False (default), creates fresh environment each call.
195- """
196- # Create client and wrap in handler
197- client : BaseLM = get_client (self .backend , self .backend_kwargs )
198206
199- # Create other_backend_client if provided (for depth=1 routing)
200- other_backend_client : BaseLM | None = None
201- if self .other_backends and self .other_backend_kwargs :
202- other_backend_client = get_client (self .other_backends [0 ], self .other_backend_kwargs [0 ])
207+ When _shared_lm_handler_address is set (child RLM reusing parent's handler),
208+ skips creating a new LMHandler/TCP server and creates a proxy instead.
209+ """
210+ owns_handler = False
211+
212+ if self ._shared_lm_handler_address is not None and self ._shared_client is not None :
213+ # Reuse parent's handler — create a lightweight proxy
214+ lm_handler = _SharedLMHandlerProxy (self ._shared_client , self ._shared_lm_handler_address )
215+ # Active handler info for children to reuse
216+ self ._active_handler_address = self ._shared_lm_handler_address
217+ self ._active_client = self ._shared_client
218+ else :
219+ # Create client and wrap in handler
220+ client : BaseLM = get_client (self .backend , self .backend_kwargs )
203221
204- lm_handler = LMHandler (client , other_backend_client = other_backend_client )
222+ # Create other_backend_client if provided (for depth=1 routing)
223+ other_backend_client : BaseLM | None = None
224+ if self .other_backends and self .other_backend_kwargs :
225+ other_backend_client = get_client (
226+ self .other_backends [0 ], self .other_backend_kwargs [0 ]
227+ )
205228
206- # Register other clients to be available as sub-call options (by model name)
207- if self .other_backends and self .other_backend_kwargs :
208- for backend , kwargs in zip (self .other_backends , self .other_backend_kwargs , strict = True ):
209- other_client : BaseLM = get_client (backend , kwargs )
210- lm_handler .register_client (other_client .model_name , other_client )
229+ lm_handler = LMHandler (client , other_backend_client = other_backend_client )
211230
212- lm_handler .start ()
231+ # Register other clients to be available as sub-call options (by model name)
232+ if self .other_backends and self .other_backend_kwargs :
233+ for backend , kwargs in zip (
234+ self .other_backends , self .other_backend_kwargs , strict = True
235+ ):
236+ other_client : BaseLM = get_client (backend , kwargs )
237+ lm_handler .register_client (other_client .model_name , other_client )
238+
239+ lm_handler .start ()
240+ owns_handler = True
241+ # Active handler info for children to reuse
242+ self ._active_handler_address = (lm_handler .host , lm_handler .port )
243+ self ._active_client = client
213244
214245 # Environment: reuse if persistent, otherwise create fresh
215246 if self .persistent and self ._persistent_env is not None :
@@ -221,11 +252,12 @@ def _spawn_completion_context(self, prompt: str | dict[str, Any]):
221252 f"implement required methods (update_handler_address, add_context, get_context_count). "
222253 f"This should have been caught at initialization."
223254 )
224- environment .update_handler_address ((lm_handler .host , lm_handler .port ))
255+ handler_addr = self ._active_handler_address
256+ environment .update_handler_address (handler_addr )
225257 environment .add_context (prompt )
226258 else :
227259 env_kwargs = self .environment_kwargs .copy ()
228- env_kwargs ["lm_handler_address" ] = ( lm_handler . host , lm_handler . port )
260+ env_kwargs ["lm_handler_address" ] = self . _active_handler_address
229261 env_kwargs ["context_payload" ] = prompt
230262 env_kwargs ["depth" ] = self .depth + 1 # Environment depth is RLM depth + 1
231263 # For local environment with max_depth > 1, pass subcall callback for recursive RLM calls
@@ -246,7 +278,10 @@ def _spawn_completion_context(self, prompt: str | dict[str, Any]):
246278 try :
247279 yield lm_handler , environment
248280 finally :
249- lm_handler .stop ()
281+ self ._active_handler_address = None
282+ self ._active_client = None
283+ if owns_handler :
284+ lm_handler .stop ()
250285 if not self .persistent and hasattr (environment , "cleanup" ):
251286 environment .cleanup ()
252287
@@ -491,7 +526,8 @@ def _check_iteration_limits(
491526 if self .max_budget is not None :
492527 current_usage = lm_handler .get_usage_summary ()
493528 current_cost = current_usage .total_cost or 0.0
494- self ._cumulative_cost = current_cost
529+ with self ._cumulative_cost_lock :
530+ self ._cumulative_cost = current_cost
495531 if self ._cumulative_cost > self .max_budget :
496532 self .verbose .print_budget_exceeded (self ._cumulative_cost , self .max_budget )
497533 raise BudgetExceededError (
@@ -703,7 +739,8 @@ def _subcall(self, prompt: str, model: str | None = None) -> RLMChatCompletion:
703739 # Calculate remaining budget for child (if budget tracking enabled)
704740 remaining_budget = None
705741 if self .max_budget is not None :
706- remaining_budget = self .max_budget - self ._cumulative_cost
742+ with self ._cumulative_cost_lock :
743+ remaining_budget = self .max_budget - self ._cumulative_cost
707744 if remaining_budget <= 0 :
708745 return RLMChatCompletion (
709746 root_model = resolved_model ,
@@ -743,7 +780,7 @@ def _subcall(self, prompt: str, model: str | None = None) -> RLMChatCompletion:
743780 subcall_start = time .perf_counter ()
744781 error_msg : str | None = None
745782
746- # Spawn a child RLM with its own LocalREPL
783+ # Spawn a child RLM, sharing parent's handler when possible
747784 child = RLM (
748785 backend = self .backend ,
749786 backend_kwargs = child_backend_kwargs ,
@@ -768,16 +805,21 @@ def _subcall(self, prompt: str, model: str | None = None) -> RLMChatCompletion:
768805 # Propagate callbacks to children for nested tracking
769806 on_subcall_start = self .on_subcall_start ,
770807 on_subcall_complete = self .on_subcall_complete ,
808+ # Share parent's handler/client to avoid per-child TCP server overhead
809+ _shared_lm_handler_address = self ._active_handler_address ,
810+ _shared_client = self ._active_client ,
771811 )
772812 try :
773813 result = child .completion (prompt , root_prompt = None )
774814 # Track child's cost in parent's cumulative cost
775815 if result .usage_summary and result .usage_summary .total_cost :
776- self ._cumulative_cost += result .usage_summary .total_cost
816+ with self ._cumulative_cost_lock :
817+ self ._cumulative_cost += result .usage_summary .total_cost
777818 return result
778819 except BudgetExceededError as e :
779820 # Propagate child's spending to parent
780- self ._cumulative_cost += e .spent
821+ with self ._cumulative_cost_lock :
822+ self ._cumulative_cost += e .spent
781823 error_msg = f"Budget exceeded - { e } "
782824 return RLMChatCompletion (
783825 root_model = resolved_model ,
@@ -849,3 +891,43 @@ def __enter__(self) -> "RLM":
849891 def __exit__ (self , exc_type , exc_val , exc_tb ) -> bool :
850892 self .close ()
851893 return False
894+
895+
896+ class _SharedLMHandlerProxy :
897+ """Lightweight proxy that reuses a parent's BaseLM client without spawning a TCP server.
898+
899+ Exposes the same interface as LMHandler for completion() and get_usage_summary(),
900+ but start()/stop() are no-ops since the parent owns the actual handler lifecycle.
901+ """
902+
903+ def __init__ (self , client : BaseLM , address : tuple [str , int ]):
904+ self ._client = client
905+ self ._host , self ._port = address
906+
907+ @property
908+ def host (self ) -> str :
909+ return self ._host
910+
911+ @property
912+ def port (self ) -> int :
913+ return self ._port
914+
915+ @property
916+ def address (self ) -> tuple [str , int ]:
917+ return (self ._host , self ._port )
918+
919+ def start (self ) -> tuple [str , int ]:
920+ return self .address
921+
922+ def stop (self ) -> None :
923+ pass # Parent owns the handler
924+
925+ def completion (self , prompt : str , model : str | None = None ) -> str :
926+ # BaseLM.completion() does not accept model, but concrete implementations
927+ # (AnthropicClient, OpenAIClient) do. Forward when possible.
928+ if model is not None :
929+ return self ._client .completion (prompt , model = model )
930+ return self ._client .completion (prompt )
931+
932+ def get_usage_summary (self ) -> UsageSummary :
933+ return self ._client .get_usage_summary ()
0 commit comments