@@ -506,7 +506,12 @@ impl EngineLoop {
506506 continue ;
507507 }
508508 }
509- let signature = tool_signature ( & tool_key, & args) ;
509+ let signature = if tool_key == "batch" {
510+ batch_tool_signature ( & args)
511+ . unwrap_or_else ( || tool_signature ( & tool_key, & args) )
512+ } else {
513+ tool_signature ( & tool_key, & args)
514+ } ;
510515 if is_shell_tool_name ( & tool_key)
511516 && shell_mismatch_signatures. contains ( & signature)
512517 {
@@ -517,7 +522,9 @@ impl EngineLoop {
517522 continue ;
518523 }
519524 let mut signature_count = 1usize ;
520- if is_read_only_tool ( & tool_key) {
525+ if is_read_only_tool ( & tool_key)
526+ || ( tool_key == "batch" && is_read_only_batch_call ( & args) )
527+ {
521528 let count = readonly_signature_counts
522529 . entry ( signature. clone ( ) )
523530 . and_modify ( |v| * v = v. saturating_add ( 1 ) )
@@ -1358,11 +1365,69 @@ fn is_read_only_tool(tool_name: &str) -> bool {
13581365 )
13591366}
13601367
1368+ fn is_batch_wrapper_tool_name ( name : & str ) -> bool {
1369+ matches ! (
1370+ normalize_tool_name( name) . as_str( ) ,
1371+ "default_api" | "default" | "api" | "function" | "functions" | "tool" | "tools"
1372+ )
1373+ }
1374+
1375+ fn extract_batch_calls ( args : & Value ) -> Vec < ( String , Value ) > {
1376+ let calls = args
1377+ . get ( "tool_calls" )
1378+ . and_then ( |v| v. as_array ( ) )
1379+ . cloned ( )
1380+ . unwrap_or_default ( ) ;
1381+ calls
1382+ . into_iter ( )
1383+ . filter_map ( |call| {
1384+ let obj = call. as_object ( ) ?;
1385+ let tool_raw = obj
1386+ . get ( "tool" )
1387+ . and_then ( |v| v. as_str ( ) )
1388+ . map ( str:: trim)
1389+ . filter ( |s| !s. is_empty ( ) ) ;
1390+ let name_raw = obj
1391+ . get ( "name" )
1392+ . and_then ( |v| v. as_str ( ) )
1393+ . map ( str:: trim)
1394+ . filter ( |s| !s. is_empty ( ) ) ;
1395+ let effective = match ( tool_raw, name_raw) {
1396+ ( Some ( t) , Some ( n) ) if is_batch_wrapper_tool_name ( t) => n,
1397+ ( Some ( t) , _) => t,
1398+ ( None , Some ( n) ) => n,
1399+ ( None , None ) => return None ,
1400+ } ;
1401+ let normalized = normalize_tool_name ( effective) ;
1402+ let call_args = obj. get ( "args" ) . cloned ( ) . unwrap_or_else ( || json ! ( { } ) ) ;
1403+ Some ( ( normalized, call_args) )
1404+ } )
1405+ . collect ( )
1406+ }
1407+
1408+ fn is_read_only_batch_call ( args : & Value ) -> bool {
1409+ let calls = extract_batch_calls ( args) ;
1410+ !calls. is_empty ( ) && calls. iter ( ) . all ( |( tool, _) | is_read_only_tool ( tool) )
1411+ }
1412+
1413+ fn batch_tool_signature ( args : & Value ) -> Option < String > {
1414+ let calls = extract_batch_calls ( args) ;
1415+ if calls. is_empty ( ) {
1416+ return None ;
1417+ }
1418+ let parts = calls
1419+ . into_iter ( )
1420+ . map ( |( tool, call_args) | tool_signature ( & tool, & call_args) )
1421+ . collect :: < Vec < _ > > ( ) ;
1422+ Some ( format ! ( "batch:{}" , parts. join( "|" ) ) )
1423+ }
1424+
13611425fn tool_budget_for ( tool_name : & str ) -> usize {
13621426 match normalize_tool_name ( tool_name) . as_str ( ) {
13631427 "glob" => 4 ,
13641428 "read" => 8 ,
13651429 "websearch" => 3 ,
1430+ "batch" => 4 ,
13661431 "grep" | "search" | "codesearch" => 6 ,
13671432 _ => 10 ,
13681433 }
@@ -3589,6 +3654,24 @@ Call: todowrite(task_id=3, status="in_progress")
35893654 assert_eq ! ( normalize_tool_name( "functions.shell" ) , "bash" ) ;
35903655 }
35913656
3657+ #[ test]
3658+ fn batch_helpers_use_name_when_tool_is_wrapper ( ) {
3659+ let args = json ! ( {
3660+ "tool_calls" : [
3661+ { "tool" : "default_api" , "name" : "read" , "args" : { "path" : "CONCEPT.md" } } ,
3662+ { "tool" : "default_api:glob" , "args" : { "pattern" : "*.md" } }
3663+ ]
3664+ } ) ;
3665+ let calls = extract_batch_calls ( & args) ;
3666+ assert_eq ! ( calls. len( ) , 2 ) ;
3667+ assert_eq ! ( calls[ 0 ] . 0 , "read" ) ;
3668+ assert_eq ! ( calls[ 1 ] . 0 , "glob" ) ;
3669+ assert ! ( is_read_only_batch_call( & args) ) ;
3670+ let sig = batch_tool_signature ( & args) . unwrap_or_default ( ) ;
3671+ assert ! ( sig. contains( "read:" ) ) ;
3672+ assert ! ( sig. contains( "glob:" ) ) ;
3673+ }
3674+
35923675 #[ test]
35933676 fn runtime_prompt_includes_execution_environment_block ( ) {
35943677 let prompt = tandem_runtime_system_prompt ( & HostRuntimeContext {
0 commit comments