@@ -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,
@@ -236,11 +245,9 @@ impl PyBash {
236245 /// Releases GIL before blocking on tokio to prevent deadlock with callbacks.
237246 fn execute_sync ( & self , py : Python < ' _ > , commands : String ) -> PyResult < ExecResult > {
238247 let inner = self . inner . clone ( ) ;
239- let rt = tokio:: runtime:: Runtime :: new ( )
240- . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {}" , e) ) ) ?;
241248
242249 py. detach ( || {
243- rt. block_on ( async move {
250+ self . rt . block_on ( async move {
244251 let mut bash = inner. lock ( ) . await ;
245252 match bash. exec ( & commands) . await {
246253 Ok ( result) => Ok ( ExecResult {
@@ -264,11 +271,9 @@ impl PyBash {
264271 /// Releases GIL before blocking on tokio to prevent deadlock.
265272 fn reset ( & self , py : Python < ' _ > ) -> PyResult < ( ) > {
266273 let inner = self . inner . clone ( ) ;
267- let rt = tokio:: runtime:: Runtime :: new ( )
268- . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {}" , e) ) ) ?;
269274
270275 py. detach ( || {
271- rt. block_on ( async move {
276+ self . rt . block_on ( async move {
272277 let mut bash = inner. lock ( ) . await ;
273278 let builder = Bash :: builder ( ) ;
274279 * bash = builder. build ( ) ;
@@ -318,6 +323,9 @@ impl PyBash {
318323#[ allow( dead_code) ]
319324pub struct BashTool {
320325 inner : Arc < Mutex < Bash > > ,
326+ /// Shared tokio runtime — reused across all sync calls to avoid
327+ /// per-call OS thread/fd exhaustion (issue #414).
328+ rt : tokio:: runtime:: Runtime ,
321329 username : Option < String > ,
322330 hostname : Option < String > ,
323331 max_commands : Option < u64 > ,
@@ -354,8 +362,14 @@ impl BashTool {
354362
355363 let bash = builder. build ( ) ;
356364
365+ let rt = tokio:: runtime:: Builder :: new_current_thread ( )
366+ . enable_all ( )
367+ . build ( )
368+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {e}" ) ) ) ?;
369+
357370 Ok ( Self {
358371 inner : Arc :: new ( Mutex :: new ( bash) ) ,
372+ rt,
359373 username,
360374 hostname,
361375 max_commands,
@@ -387,11 +401,9 @@ impl BashTool {
387401 /// Releases GIL before blocking on tokio to prevent deadlock with callbacks.
388402 fn execute_sync ( & self , py : Python < ' _ > , commands : String ) -> PyResult < ExecResult > {
389403 let inner = self . inner . clone ( ) ;
390- let rt = tokio:: runtime:: Runtime :: new ( )
391- . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {}" , e) ) ) ?;
392404
393405 py. detach ( || {
394- rt. block_on ( async move {
406+ self . rt . block_on ( async move {
395407 let mut bash = inner. lock ( ) . await ;
396408 match bash. exec ( & commands) . await {
397409 Ok ( result) => Ok ( ExecResult {
@@ -414,11 +426,9 @@ impl BashTool {
414426 /// Releases GIL before blocking on tokio to prevent deadlock.
415427 fn reset ( & self , py : Python < ' _ > ) -> PyResult < ( ) > {
416428 let inner = self . inner . clone ( ) ;
417- let rt = tokio:: runtime:: Runtime :: new ( )
418- . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {}" , e) ) ) ?;
419429
420430 py. detach ( || {
421- rt. block_on ( async move {
431+ self . rt . block_on ( async move {
422432 let mut bash = inner. lock ( ) . await ;
423433 let builder = Bash :: builder ( ) ;
424434 * bash = builder. build ( ) ;
@@ -521,6 +531,9 @@ pub struct ScriptedTool {
521531 short_desc : Option < String > ,
522532 tools : Vec < PyToolEntry > ,
523533 env_vars : Vec < ( String , String ) > ,
534+ /// Shared tokio runtime — reused across all sync calls to avoid
535+ /// per-call OS thread/fd exhaustion (issue #414).
536+ rt : tokio:: runtime:: Runtime ,
524537 max_commands : Option < u64 > ,
525538 max_loop_iterations : Option < u64 > ,
526539}
@@ -594,15 +607,21 @@ impl ScriptedTool {
594607 short_description : Option < String > ,
595608 max_commands : Option < u64 > ,
596609 max_loop_iterations : Option < u64 > ,
597- ) -> Self {
598- Self {
610+ ) -> PyResult < Self > {
611+ let rt = tokio:: runtime:: Builder :: new_current_thread ( )
612+ . enable_all ( )
613+ . build ( )
614+ . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {e}" ) ) ) ?;
615+
616+ Ok ( Self {
599617 name,
600618 short_desc : short_description,
601619 tools : Vec :: new ( ) ,
602620 env_vars : Vec :: new ( ) ,
621+ rt,
603622 max_commands,
604623 max_loop_iterations,
605- }
624+ } )
606625 }
607626
608627 /// Register a tool command.
@@ -667,11 +686,9 @@ impl ScriptedTool {
667686 /// Releases GIL before blocking on tokio to prevent deadlock with callbacks.
668687 fn execute_sync ( & self , py : Python < ' _ > , commands : String ) -> PyResult < ExecResult > {
669688 let mut tool = self . build_rust_tool ( ) ;
670- let rt = tokio:: runtime:: Runtime :: new ( )
671- . map_err ( |e| PyRuntimeError :: new_err ( format ! ( "Failed to create runtime: {}" , e) ) ) ?;
672689
673690 let resp = py. detach ( || {
674- rt. block_on ( async move {
691+ self . rt . block_on ( async move {
675692 tool. execute ( ToolRequest {
676693 commands,
677694 timeout_ms : None ,
0 commit comments