@@ -141,6 +141,24 @@ function runSkillConstraintCoverage(trace, config) {
141141 } ) ;
142142}
143143
144+ const toolOutputConsumptionRateScriptMatch = fs . readFileSync ( path . join ( __dirname , "../../../.github/workflows/shared/graders/tool-output-consumption-rate.md" ) , "utf8" ) . match ( / s c r i p t : \| \n ( [ \s \S ] * ?) \n ^ - - - \s * $ / m) ;
145+ if ( ! toolOutputConsumptionRateScriptMatch ?. [ 1 ] ) {
146+ throw new Error ( "unable to extract tool-output-consumption-rate grader script" ) ;
147+ }
148+ const toolOutputConsumptionRateScript = toolOutputConsumptionRateScriptMatch [ 1 ]
149+ . split ( "\n" )
150+ . map ( line => line . slice ( 6 ) )
151+ . join ( "\n" ) ;
152+
153+ function runToolOutputConsumptionRate ( trace ) {
154+ return runCustomGrader ( "tool-output-consumption-rate" , toolOutputConsumptionRateScript , makeTrace ( trace ) , {
155+ name : "Tool Output Consumption Rate" ,
156+ unit : "ratio" ,
157+ direction : "higher_is_better" ,
158+ source : "inline" ,
159+ } ) ;
160+ }
161+
144162describe ( "trace_graders" , ( ) => {
145163 describe ( "buildGradersSummaryBody" , ( ) => {
146164 it ( "renders all computed grader values without emojis" , ( ) => {
@@ -1008,6 +1026,98 @@ describe("trace_graders", () => {
10081026 } ) ;
10091027 } ) ;
10101028
1029+ describe ( "tool-output-consumption-rate custom grader" , ( ) => {
1030+ it ( "scores the fraction of matching tool observations that were consumed" , ( ) => {
1031+ const result = runToolOutputConsumptionRate ( {
1032+ trajectoryIR : {
1033+ toolCalls : [ { id : "tc-1" } , { id : "tc-2" } ] ,
1034+ observations : [
1035+ { id : "obs-1" , sourceToolCallId : "tc-1" , consumedByActionIds : [ "act-1" ] } ,
1036+ { id : "obs-2" , sourceToolCallId : "tc-2" , consumedByActionIds : [ ] } ,
1037+ { id : "obs-3" , sourceToolCallId : null , consumedByActionIds : [ "act-2" ] } ,
1038+ { id : "obs-4" , sourceToolCallId : "unknown" , consumedByActionIds : [ "act-3" ] } ,
1039+ ] ,
1040+ } ,
1041+ } ) ;
1042+
1043+ expect ( result . value ) . toBeCloseTo ( 0.5 ) ;
1044+ expect ( result . details ) . toContain ( "toolObservations=2 consumed=1" ) ;
1045+ expect ( result . details ) . toContain ( "unconsumed: obs-2" ) ;
1046+ } ) ;
1047+
1048+ it ( "treats malformed consumption metadata as unconsumed" , ( ) => {
1049+ const result = runToolOutputConsumptionRate ( {
1050+ trajectoryIR : {
1051+ toolCalls : [ { id : "tc-1" } ] ,
1052+ observations : [ { id : "obs-1" , sourceToolCallId : "tc-1" , consumedByActionIds : "act-1" } ] ,
1053+ } ,
1054+ } ) ;
1055+
1056+ expect ( result . value ) . toBe ( 0 ) ;
1057+ expect ( result . details ) . toContain ( "toolObservations=1 consumed=0" ) ;
1058+ } ) ;
1059+
1060+ it ( "treats consumedByActionIds arrays containing only non-string/empty entries as unconsumed" , ( ) => {
1061+ const result = runToolOutputConsumptionRate ( {
1062+ trajectoryIR : {
1063+ toolCalls : [ { id : "tc-1" } , { id : "tc-2" } ] ,
1064+ observations : [
1065+ { id : "obs-1" , sourceToolCallId : "tc-1" , consumedByActionIds : [ null ] } ,
1066+ { id : "obs-2" , sourceToolCallId : "tc-2" , consumedByActionIds : [ 42 , "" ] } ,
1067+ ] ,
1068+ } ,
1069+ } ) ;
1070+
1071+ expect ( result . value ) . toBe ( 0 ) ;
1072+ expect ( result . details ) . toContain ( "toolObservations=2 consumed=0" ) ;
1073+ expect ( result . details ) . toContain ( "unconsumed: obs-1, obs-2" ) ;
1074+ } ) ;
1075+
1076+ it ( "reads observations/toolCalls from the root trace object" , ( ) => {
1077+ const result = runToolOutputConsumptionRate ( {
1078+ toolCalls : [ { id : "tc-1" } ] ,
1079+ observations : [ { id : "obs-1" , sourceToolCallId : "tc-1" , consumedByActionIds : [ "act-1" ] } ] ,
1080+ } ) ;
1081+
1082+ expect ( result . value ) . toBe ( 1 ) ;
1083+ } ) ;
1084+
1085+ it ( "reads a complete IR nested in agentOutput" , ( ) => {
1086+ const result = runToolOutputConsumptionRate ( {
1087+ agentOutput : {
1088+ trajectoryIR : {
1089+ toolCalls : [ { id : "tc-1" } ] ,
1090+ observations : [ { id : "obs-1" , sourceToolCallId : "tc-1" , consumedByActionIds : [ "act-1" ] } ] ,
1091+ } ,
1092+ } ,
1093+ } ) ;
1094+
1095+ expect ( result . value ) . toBe ( 1 ) ;
1096+ } ) ;
1097+
1098+ it . each ( [
1099+ [ "no observations" , { trajectoryIR : { toolCalls : [ { id : "tc-1" } ] } } , "no observations" ] ,
1100+ [ "no tool calls" , { trajectoryIR : { observations : [ { id : "obs-1" , sourceToolCallId : "tc-1" , consumedByActionIds : [ "act-1" ] } ] } } , "no tool-originated observations" ] ,
1101+ [
1102+ "no matching tool call" ,
1103+ {
1104+ trajectoryIR : {
1105+ toolCalls : [ { id : "tc-1" } ] ,
1106+ observations : [ { id : "obs-1" , sourceToolCallId : "unknown" , consumedByActionIds : [ "act-1" ] } ] ,
1107+ } ,
1108+ } ,
1109+ "no tool-originated observations" ,
1110+ ] ,
1111+ ] ) ( "normalizes %s as unavailable" , ( _name , trace , message ) => {
1112+ const result = runToolOutputConsumptionRate ( trace ) ;
1113+
1114+ expect ( result . value ) . toBeNull ( ) ;
1115+ expect ( result . passed ) . toBeNull ( ) ;
1116+ expect ( result . status ) . toBe ( "unavailable" ) ;
1117+ expect ( result . message ) . toContain ( message ) ;
1118+ } ) ;
1119+ } ) ;
1120+
10111121 // --- Hostile data ---
10121122 describe ( "hostile data handling" , ( ) => {
10131123 it ( "handles hostile strings in tool call names" , ( ) => {
0 commit comments