@@ -165,6 +165,9 @@ impl ExecResult {
165165#[ allow( dead_code) ]
166166pub struct PyBash {
167167 inner : Arc < Mutex < Bash > > ,
168+ /// Shared tokio runtime — reused across all sync calls to avoid
169+ /// per-call OS thread/fd exhaustion (issue #414).
170+ rt : tokio:: runtime:: Runtime ,
168171 username : Option < String > ,
169172 hostname : Option < String > ,
170173 max_commands : Option < u64 > ,
@@ -201,8 +204,14 @@ impl PyBash {
201204
202205 let bash = builder. build ( ) ;
203206
207+ let rt = tokio:: runtime:: Builder :: new_current_thread ( )
208+ . enable_all ( )
209+ . build ( )
210+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {e}" ) ) ) ?;
211+
204212 Ok ( Self {
205213 inner : Arc :: new ( Mutex :: new ( bash) ) ,
214+ rt,
206215 username,
207216 hostname,
208217 max_commands,
@@ -235,10 +244,8 @@ impl PyBash {
235244 /// Execute commands synchronously (blocking).
236245 fn execute_sync ( & self , commands : String ) -> PyResult < ExecResult > {
237246 let inner = self . inner . clone ( ) ;
238- let rt = tokio:: runtime:: Runtime :: new ( )
239- . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {}" , e) ) ) ?;
240247
241- rt. block_on ( async move {
248+ self . rt . block_on ( async move {
242249 let mut bash = inner. lock ( ) . await ;
243250 match bash. exec ( & commands) . await {
244251 Ok ( result) => Ok ( ExecResult {
@@ -260,10 +267,8 @@ impl PyBash {
260267 /// Reset interpreter to fresh state.
261268 fn reset ( & self ) -> PyResult < ( ) > {
262269 let inner = self . inner . clone ( ) ;
263- let rt = tokio:: runtime:: Runtime :: new ( )
264- . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {}" , e) ) ) ?;
265270
266- rt. block_on ( async move {
271+ self . rt . block_on ( async move {
267272 let mut bash = inner. lock ( ) . await ;
268273 let builder = Bash :: builder ( ) ;
269274 * bash = builder. build ( ) ;
@@ -312,6 +317,9 @@ impl PyBash {
312317#[ allow( dead_code) ]
313318pub struct BashTool {
314319 inner : Arc < Mutex < Bash > > ,
320+ /// Shared tokio runtime — reused across all sync calls to avoid
321+ /// per-call OS thread/fd exhaustion (issue #414).
322+ rt : tokio:: runtime:: Runtime ,
315323 username : Option < String > ,
316324 hostname : Option < String > ,
317325 max_commands : Option < u64 > ,
@@ -348,8 +356,14 @@ impl BashTool {
348356
349357 let bash = builder. build ( ) ;
350358
359+ let rt = tokio:: runtime:: Builder :: new_current_thread ( )
360+ . enable_all ( )
361+ . build ( )
362+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {e}" ) ) ) ?;
363+
351364 Ok ( Self {
352365 inner : Arc :: new ( Mutex :: new ( bash) ) ,
366+ rt,
353367 username,
354368 hostname,
355369 max_commands,
@@ -380,10 +394,8 @@ impl BashTool {
380394
381395 fn execute_sync ( & self , commands : String ) -> PyResult < ExecResult > {
382396 let inner = self . inner . clone ( ) ;
383- let rt = tokio:: runtime:: Runtime :: new ( )
384- . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {}" , e) ) ) ?;
385397
386- rt. block_on ( async move {
398+ self . rt . block_on ( async move {
387399 let mut bash = inner. lock ( ) . await ;
388400 match bash. exec ( & commands) . await {
389401 Ok ( result) => Ok ( ExecResult {
@@ -404,10 +416,8 @@ impl BashTool {
404416
405417 fn reset ( & self ) -> PyResult < ( ) > {
406418 let inner = self . inner . clone ( ) ;
407- let rt = tokio:: runtime:: Runtime :: new ( )
408- . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {}" , e) ) ) ?;
409419
410- rt. block_on ( async move {
420+ self . rt . block_on ( async move {
411421 let mut bash = inner. lock ( ) . await ;
412422 let builder = Bash :: builder ( ) ;
413423 * bash = builder. build ( ) ;
@@ -509,6 +519,9 @@ pub struct ScriptedTool {
509519 short_desc : Option < String > ,
510520 tools : Vec < PyToolEntry > ,
511521 env_vars : Vec < ( String , String ) > ,
522+ /// Shared tokio runtime — reused across all sync calls to avoid
523+ /// per-call OS thread/fd exhaustion (issue #414).
524+ rt : tokio:: runtime:: Runtime ,
512525 max_commands : Option < u64 > ,
513526 max_loop_iterations : Option < u64 > ,
514527}
@@ -582,15 +595,21 @@ impl ScriptedTool {
582595 short_description : Option < String > ,
583596 max_commands : Option < u64 > ,
584597 max_loop_iterations : Option < u64 > ,
585- ) -> Self {
586- Self {
598+ ) -> PyResult < Self > {
599+ let rt = tokio:: runtime:: Builder :: new_current_thread ( )
600+ . enable_all ( )
601+ . build ( )
602+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {e}" ) ) ) ?;
603+
604+ Ok ( Self {
587605 name,
588606 short_desc : short_description,
589607 tools : Vec :: new ( ) ,
590608 env_vars : Vec :: new ( ) ,
609+ rt,
591610 max_commands,
592611 max_loop_iterations,
593- }
612+ } )
594613 }
595614
596615 /// Register a tool command.
@@ -654,10 +673,8 @@ impl ScriptedTool {
654673 /// Execute a bash script synchronously (blocking).
655674 fn execute_sync ( & self , commands : String ) -> PyResult < ExecResult > {
656675 let mut tool = self . build_rust_tool ( ) ;
657- let rt = tokio:: runtime:: Runtime :: new ( )
658- . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {}" , e) ) ) ?;
659676
660- let resp = rt. block_on ( async move {
677+ let resp = self . rt . block_on ( async move {
661678 tool. execute ( ToolRequest {
662679 commands,
663680 timeout_ms : None ,
0 commit comments