@@ -131,7 +131,7 @@ impl MetaTool for InvokeToolTool {
131131 "format" : {
132132 "type" : "string" ,
133133 "enum" : [ "summary" , "full" ] ,
134- "description" : "summary keeps metadata plus a bounded sample; full returns truncated rows"
134+ "description" : "When max_rows is set: summary caps the sample at min(max_rows, 5) ; full returns up to max_rows rows. Ignored when max_rows is omitted. "
135135 }
136136 }
137137 }
@@ -246,12 +246,12 @@ impl MetaTool for InvokeToolTool {
246246 ) ) ) ;
247247 }
248248
249- let routing = call
249+ let backend = call
250250 . ctx
251- . routing_service
251+ . invoke_backend
252252 . as_ref ( )
253253 . ok_or_else ( || MetaToolError :: Internal ( "invoke routing not configured" . into ( ) ) ) ?;
254- match routing
254+ match backend
255255 . call_tool (
256256 space_id,
257257 & resolved. feature_set_ids ,
@@ -473,6 +473,18 @@ fn invoke_error(message: String) -> CallToolResult {
473473mod tests {
474474 use super :: * ;
475475
476+ fn issue_rows ( count : usize ) -> Vec < Value > {
477+ ( 0 ..count)
478+ . map ( |i| {
479+ json ! ( {
480+ "id" : i,
481+ "title" : format!( "issue-{i}" ) ,
482+ "body" : format!( "body-{i}" )
483+ } )
484+ } )
485+ . collect ( )
486+ }
487+
476488 #[ test]
477489 fn no_filter_passes_through_large_array ( ) {
478490 let items: Vec < Value > = ( 0 ..100 ) . map ( |i| json ! ( { "id" : i, "name" : format!( "n{i}" ) } ) ) . collect ( ) ;
@@ -481,8 +493,8 @@ mod tests {
481493 }
482494
483495 #[ test]
484- fn explicit_max_rows_truncates ( ) {
485- let items: Vec < Value > = ( 0 .. 20 ) . map ( |i| json ! ( { "id" : i } ) ) . collect ( ) ;
496+ fn explicit_max_rows_truncates_top_level_array ( ) {
497+ let items: Vec < Value > = issue_rows ( 20 ) ;
486498 let filter = InvokeResultFilter {
487499 max_rows : Some ( 3 ) ,
488500 ..Default :: default ( )
@@ -491,6 +503,70 @@ mod tests {
491503 assert_eq ! ( shaped. get( "returned" ) , Some ( & json!( 3 ) ) ) ;
492504 assert_eq ! ( shaped. get( "total" ) , Some ( & json!( 20 ) ) ) ;
493505 assert_eq ! ( shaped. get( "truncated" ) , Some ( & json!( true ) ) ) ;
506+ let sample = shaped. get ( "items" ) . and_then ( |v| v. as_array ( ) ) . unwrap ( ) ;
507+ assert_eq ! ( sample. len( ) , 3 ) ;
508+ }
509+
510+ #[ test]
511+ fn explicit_max_rows_truncates_nested_issues_key ( ) {
512+ let issues = issue_rows ( 20 ) ;
513+ let filter = InvokeResultFilter {
514+ max_rows : Some ( 3 ) ,
515+ ..Default :: default ( )
516+ } ;
517+ let shaped = shape_json_value ( json ! ( { "issues" : issues } ) , & filter) ;
518+ assert_eq ! ( shaped. get( "returned" ) , Some ( & json!( 3 ) ) ) ;
519+ assert_eq ! ( shaped. get( "total" ) , Some ( & json!( 20 ) ) ) ;
520+ assert_eq ! ( shaped. get( "truncated" ) , Some ( & json!( true ) ) ) ;
521+ let sample = shaped. get ( "issues" ) . and_then ( |v| v. as_array ( ) ) . unwrap ( ) ;
522+ assert_eq ! ( sample. len( ) , 3 ) ;
523+ }
524+
525+ #[ test]
526+ fn json_in_text_block_truncates_with_metadata ( ) {
527+ let rows: Vec < Value > = ( 0 ..80 ) . map ( |i| json ! ( { "n" : i } ) ) . collect ( ) ;
528+ let content = vec ! [ json!( {
529+ "type" : "text" ,
530+ "text" : json!( { "results" : rows } ) . to_string( ) ,
531+ } ) ] ;
532+ let filter = parse_invoke_filter ( Some ( & json ! ( { "max_rows" : 10 } ) ) ) . unwrap ( ) ;
533+
534+ let ( shaped_content, _) = apply_invoke_result_filter ( content, None , & filter) ;
535+ let text = shaped_content[ 0 ] . get ( "text" ) . and_then ( |t| t. as_str ( ) ) . unwrap ( ) ;
536+ let parsed: Value = serde_json:: from_str ( text) . unwrap ( ) ;
537+
538+ assert_eq ! ( parsed. get( "returned" ) , Some ( & json!( 10 ) ) ) ;
539+ assert_eq ! ( parsed. get( "total" ) , Some ( & json!( 80 ) ) ) ;
540+ assert_eq ! ( parsed. get( "truncated" ) , Some ( & json!( true ) ) ) ;
541+ }
542+
543+ #[ test]
544+ fn structured_content_and_text_both_shaped ( ) {
545+ let items = issue_rows ( 20 ) ;
546+ let structured = json ! ( { "items" : items } ) ;
547+ let content = vec ! [ json!( {
548+ "type" : "text" ,
549+ "text" : structured. to_string( ) ,
550+ } ) ] ;
551+ let filter = InvokeResultFilter {
552+ max_rows : Some ( 5 ) ,
553+ fields : Some ( vec ! [ "id" . into( ) , "title" . into( ) ] ) ,
554+ ..Default :: default ( )
555+ } ;
556+
557+ let ( shaped_content, shaped_structured) =
558+ apply_invoke_result_filter ( content, Some ( structured) , & filter) ;
559+
560+ let parsed_text: Value =
561+ serde_json:: from_str ( shaped_content[ 0 ] . get ( "text" ) . and_then ( |t| t. as_str ( ) ) . unwrap ( ) )
562+ . unwrap ( ) ;
563+ assert_eq ! ( parsed_text. get( "returned" ) , Some ( & json!( 5 ) ) ) ;
564+ assert_eq ! ( parsed_text. get( "total" ) , Some ( & json!( 20 ) ) ) ;
565+
566+ let shaped = shaped_structured. unwrap ( ) ;
567+ let structured_sample = shaped. get ( "items" ) . and_then ( |v| v. as_array ( ) ) . unwrap ( ) ;
568+ assert_eq ! ( structured_sample. len( ) , 5 ) ;
569+ assert_eq ! ( structured_sample[ 0 ] , json!( { "id" : 0 , "title" : "issue-0" } ) ) ;
494570 }
495571
496572 #[ test]
@@ -509,6 +585,83 @@ mod tests {
509585 assert_eq ! ( kept[ 1 ] , json!( { "id" : 2 , "name" : "b" } ) ) ;
510586 }
511587
588+ #[ test]
589+ fn max_rows_and_fields_together ( ) {
590+ let items: Vec < Value > = ( 0 ..30 )
591+ . map ( |i| json ! ( { "id" : i, "label" : format!( "row-{i}" ) } ) )
592+ . collect ( ) ;
593+ let filter = parse_invoke_filter ( Some ( & json ! ( { "max_rows" : 5 , "fields" : [ "id" ] } ) ) ) . unwrap ( ) ;
594+ let shaped = shape_json_value ( Value :: Array ( items) , & filter) ;
595+
596+ assert_eq ! ( shaped. get( "returned" ) , Some ( & json!( 5 ) ) ) ;
597+ assert_eq ! ( shaped. get( "total" ) , Some ( & json!( 30 ) ) ) ;
598+ assert_eq ! ( shaped. get( "truncated" ) , Some ( & json!( true ) ) ) ;
599+ let sample = shaped. get ( "items" ) . and_then ( |v| v. as_array ( ) ) . unwrap ( ) ;
600+ assert_eq ! ( sample. len( ) , 5 ) ;
601+ assert_eq ! ( sample[ 0 ] , json!( { "id" : 0 } ) ) ;
602+ }
603+
604+ #[ test]
605+ fn summary_format_no_op_when_max_rows_at_most_five ( ) {
606+ let items = issue_rows ( 20 ) ;
607+ let filter = InvokeResultFilter {
608+ max_rows : Some ( 3 ) ,
609+ format : Some ( "summary" . into ( ) ) ,
610+ ..Default :: default ( )
611+ } ;
612+ let shaped = shape_json_value ( Value :: Array ( items) , & filter) ;
613+ assert_eq ! ( shaped. get( "returned" ) , Some ( & json!( 3 ) ) ) ;
614+ }
615+
616+ #[ test]
617+ fn summary_format_caps_sample_at_five ( ) {
618+ let items = issue_rows ( 20 ) ;
619+ let filter = InvokeResultFilter {
620+ max_rows : Some ( 10 ) ,
621+ format : Some ( "summary" . into ( ) ) ,
622+ ..Default :: default ( )
623+ } ;
624+ let shaped = shape_json_value ( Value :: Array ( items) , & filter) ;
625+ assert_eq ! ( shaped. get( "returned" ) , Some ( & json!( 5 ) ) ) ;
626+ assert_eq ! ( shaped. get( "total" ) , Some ( & json!( 20 ) ) ) ;
627+ }
628+
629+ #[ test]
630+ fn full_format_returns_up_to_max_rows ( ) {
631+ let items = issue_rows ( 20 ) ;
632+ let filter = InvokeResultFilter {
633+ max_rows : Some ( 10 ) ,
634+ format : Some ( "full" . into ( ) ) ,
635+ ..Default :: default ( )
636+ } ;
637+ let shaped = shape_json_value ( Value :: Array ( items) , & filter) ;
638+ assert_eq ! ( shaped. get( "returned" ) , Some ( & json!( 10 ) ) ) ;
639+ let sample = shaped. get ( "items" ) . and_then ( |v| v. as_array ( ) ) . unwrap ( ) ;
640+ assert_eq ! ( sample. len( ) , 10 ) ;
641+ }
642+
643+ #[ test]
644+ fn parse_invoke_filter_ignores_invalid_types ( ) {
645+ let filter = parse_invoke_filter ( Some ( & json ! ( {
646+ "max_rows" : "not-a-number" ,
647+ "max_bytes" : true ,
648+ "fields" : "id" ,
649+ "format" : 123
650+ } ) ) )
651+ . unwrap ( ) ;
652+ assert_eq ! ( filter. max_rows, None ) ;
653+ assert_eq ! ( filter. max_bytes, None ) ;
654+ assert_eq ! ( filter. fields, None ) ;
655+ assert_eq ! ( filter. format, None ) ;
656+ }
657+
658+ #[ test]
659+ fn parse_invoke_filter_accepts_partial_objects ( ) {
660+ let filter = parse_invoke_filter ( Some ( & json ! ( { "max_rows" : 3 } ) ) ) . unwrap ( ) ;
661+ assert_eq ! ( filter. max_rows, Some ( 3 ) ) ;
662+ assert_eq ! ( filter. max_bytes, None ) ;
663+ }
664+
512665 #[ test]
513666 fn plain_text_byte_trunc_includes_metadata ( ) {
514667 let text = "x" . repeat ( 100 ) ;
0 commit comments