@@ -105,6 +105,7 @@ impl ToolSpec {
105105 | "discover_datasets"
106106 | "get_traces"
107107 | "get_trace"
108+ | "get_trace_activity"
108109 | "list_log_errors"
109110 | "list_trace_errors" => ToolEffect :: ExpensiveRead ,
110111 "create_alert"
@@ -135,7 +136,10 @@ pub fn validate_tool_arguments(schema: &Value, arguments: &Value) -> Result<(),
135136
136137fn validate_schema_value ( schema : & Value , value : & Value , path : & str ) -> Result < ( ) , String > {
137138 if let Some ( result) = validate_any_of ( schema, value, path) {
138- return result;
139+ result?;
140+ }
141+ if let Some ( result) = validate_one_of ( schema, value, path) {
142+ result?;
139143 }
140144
141145 validate_enum ( schema, value, path) ?;
@@ -146,6 +150,21 @@ fn validate_schema_value(schema: &Value, value: &Value, path: &str) -> Result<()
146150 validate_number ( schema, value, path)
147151}
148152
153+ fn validate_one_of ( schema : & Value , value : & Value , path : & str ) -> Option < Result < ( ) , String > > {
154+ schema
155+ . get ( "oneOf" )
156+ . and_then ( Value :: as_array)
157+ . map ( |variants| {
158+ let matches = variants
159+ . iter ( )
160+ . filter ( |variant| validate_schema_value ( variant, value, path) . is_ok ( ) )
161+ . count ( ) ;
162+ ( matches == 1 )
163+ . then_some ( ( ) )
164+ . ok_or_else ( || format ! ( "{path} must match exactly one allowed schema" ) )
165+ } )
166+ }
167+
149168fn validate_any_of ( schema : & Value , value : & Value , path : & str ) -> Option < Result < ( ) , String > > {
150169 schema
151170 . get ( "anyOf" )
@@ -336,7 +355,9 @@ pub fn oss_read_tool_specs() -> Vec<ToolSpec> {
336355 json!( {
337356 "query" : string_property( "User request or dataset search text." ) ,
338357 "selectedDataset" : { "type" : "string" } ,
339- "contextDataset" : { "type" : "string" }
358+ "contextDataset" : { "type" : "string" } ,
359+ "telemetryType" : { "type" : "string" , "enum" : [ "logs" , "metrics" , "traces" ] } ,
360+ "limit" : { "type" : "integer" , "minimum" : 1 , "maximum" : 20 , "default" : 5 }
340361 } ) ,
341362 & [ "query" ] ,
342363 ) ,
@@ -380,6 +401,8 @@ pub fn oss_read_tool_specs() -> Vec<ToolSpec> {
380401 "dataset" : string_property( "Dataset name." ) ,
381402 "limit" : { "type" : "integer" , "minimum" : 1 , "maximum" : 100 , "default" : 10 } ,
382403 "minutes" : { "type" : "integer" , "minimum" : 1 , "maximum" : 1440 , "default" : 60 } ,
404+ "startTime" : string_property( "Optional explicit RFC3339 range start; provide with endTime." ) ,
405+ "endTime" : string_property( "Optional explicit RFC3339 range end; provide with startTime." ) ,
383406 } ) ,
384407 & [ "dataset" ] ,
385408 ) ,
@@ -388,21 +411,29 @@ pub fn oss_read_tool_specs() -> Vec<ToolSpec> {
388411 "get_log_context" ,
389412 "Get surrounding log context" ,
390413 "Fetch records around an exact anchor log row." ,
391- object_schema(
392- json!( {
393- "dataset" : string_property( "Dataset containing the anchor row." ) ,
394- "pTimestamp" : string_property( "Exact p_timestamp from the anchor row." ) ,
395- "log" : { "type" : "string" } ,
396- "body" : { "type" : "string" } ,
397- "message" : { "type" : "string" } ,
398- "contextWindow" : { "type" : "string" } ,
399- "contextStartTime" : { "type" : "string" } ,
400- "contextEndTime" : { "type" : "string" } ,
401- "conditions" : { "type" : "object" , "additionalProperties" : true } ,
402- "pageSize" : { "type" : "integer" , "minimum" : 1 , "maximum" : 500 , "default" : 100 } ,
403- } ) ,
404- & [ "dataset" , "pTimestamp" ] ,
405- ) ,
414+ {
415+ let mut schema = object_schema(
416+ json!( {
417+ "dataset" : string_property( "Dataset containing the anchor row." ) ,
418+ "pTimestamp" : string_property( "Exact p_timestamp from the anchor row." ) ,
419+ "log" : string_property( "Exact log value from the anchor row. Provide exactly one of log, body, or message." ) ,
420+ "body" : string_property( "Exact body value from the anchor row. Provide exactly one of log, body, or message." ) ,
421+ "message" : string_property( "Exact message value from the anchor row. Provide exactly one of log, body, or message." ) ,
422+ "contextWindow" : { "type" : "string" } ,
423+ "contextStartTime" : { "type" : "string" } ,
424+ "contextEndTime" : { "type" : "string" } ,
425+ "conditions" : { "type" : "object" , "additionalProperties" : true } ,
426+ "pageSize" : { "type" : "integer" , "minimum" : 1 , "maximum" : 500 , "default" : 100 } ,
427+ } ) ,
428+ & [ "dataset" , "pTimestamp" ] ,
429+ ) ;
430+ schema[ "oneOf" ] = json!( [
431+ { "required" : [ "log" ] } ,
432+ { "required" : [ "body" ] } ,
433+ { "required" : [ "message" ] }
434+ ] ) ;
435+ schema
436+ } ,
406437 ) ,
407438 ToolSpec :: new(
408439 "list_log_errors" ,
@@ -504,6 +535,22 @@ pub fn oss_read_tool_specs() -> Vec<ToolSpec> {
504535 & [ "traceId" , "dataset" , "startTime" , "endTime" ] ,
505536 ) ,
506537 ) ,
538+ ToolSpec :: new(
539+ "get_trace_activity" ,
540+ "Get trace activity" ,
541+ "Return a bounded trace-activity time series using Parseable's canonical semantics: span_count is COUNT(DISTINCT span_span_id) and trace_count is COUNT(DISTINCT span_trace_id). Use this for span or trace volume, trends, charts, and service breakdowns instead of deriving counts from raw trace rows. Returns the exact executed SQL with its rows so clients can reuse it unchanged." ,
542+ object_schema(
543+ json!( {
544+ "dataset" : string_property( "One traces dataset." ) ,
545+ "startTime" : string_property( "RFC3339 range start." ) ,
546+ "endTime" : string_property( "RFC3339 range end." ) ,
547+ "groupBy" : { "type" : "array" , "maxItems" : 5 , "items" : { "type" : "string" } , "description" : "Optional exact trace fields, such as service.name, used to split the activity series." } ,
548+ "filters" : { "type" : "object" , "description" : "Optional exact trace-field filters." , "additionalProperties" : true } ,
549+ "bucketCount" : { "type" : "integer" , "minimum" : 1 , "maximum" : 100 , "default" : 60 , "description" : "Approximate number of time buckets." }
550+ } ) ,
551+ & [ "dataset" , "startTime" , "endTime" ] ,
552+ ) ,
553+ ) ,
507554 ToolSpec :: new(
508555 "ping" ,
509556 "Ping Parseable server" ,
@@ -854,7 +901,7 @@ mod tests {
854901 let tools = oss_tool_specs ( ) ;
855902 let names = tools. iter ( ) . map ( |tool| tool. name ) . collect :: < HashSet < _ > > ( ) ;
856903
857- assert_eq ! ( tools. len( ) , 49 ) ;
904+ assert_eq ! ( tools. len( ) , 50 ) ;
858905 assert_eq ! ( tools. len( ) , names. len( ) ) ;
859906 }
860907
@@ -893,7 +940,7 @@ mod tests {
893940 let read_tools = oss_read_tool_specs ( ) ;
894941 let action_tools = oss_action_tool_specs ( ) ;
895942
896- assert_eq ! ( read_tools. len( ) , 36 ) ;
943+ assert_eq ! ( read_tools. len( ) , 37 ) ;
897944 assert_eq ! ( action_tools. len( ) , 13 ) ;
898945 assert ! ( read_tools. iter( ) . all( |tool| matches!(
899946 tool. effect( ) ,
@@ -977,6 +1024,18 @@ mod tests {
9771024 ) ;
9781025 }
9791026
1027+ #[ actix_web:: test]
1028+ async fn oss_catalog_exposes_trace_activity ( ) {
1029+ let tool = oss_read_tool_specs ( )
1030+ . into_iter ( )
1031+ . find ( |tool| tool. name == "get_trace_activity" )
1032+ . expect ( "get_trace_activity must be available in OSS" ) ;
1033+
1034+ assert_eq ! ( tool. effect( ) , ToolEffect :: ExpensiveRead ) ;
1035+ assert_eq ! ( tool. input_schema[ "properties" ] [ "dataset" ] [ "type" ] , "string" ) ;
1036+ assert_eq ! ( tool. input_schema[ "properties" ] [ "groupBy" ] [ "maxItems" ] , 5 ) ;
1037+ }
1038+
9801039 #[ actix_web:: test]
9811040 async fn tool_call_defaults_to_empty_arguments ( ) {
9821041 let call: ToolCallRequest = serde_json:: from_value ( serde_json:: json!( {
@@ -1012,6 +1071,52 @@ mod tests {
10121071 . validate_arguments( & serde_json:: json!( { "dataset" : "logs" } ) )
10131072 . is_ok( )
10141073 ) ;
1074+
1075+ let resolve_tool = oss_tool_specs ( )
1076+ . into_iter ( )
1077+ . find ( |tool| tool. name == "resolve_dataset" )
1078+ . unwrap ( ) ;
1079+ assert ! (
1080+ resolve_tool
1081+ . validate_arguments( & serde_json:: json!( {
1082+ "query" : "metrics" ,
1083+ "telemetryType" : "metrics" ,
1084+ "limit" : 10
1085+ } ) )
1086+ . is_ok( )
1087+ ) ;
1088+
1089+ let context_tool = oss_tool_specs ( )
1090+ . into_iter ( )
1091+ . find ( |tool| tool. name == "get_log_context" )
1092+ . unwrap ( ) ;
1093+ assert ! (
1094+ context_tool
1095+ . validate_arguments( & serde_json:: json!( {
1096+ "dataset" : "logs" ,
1097+ "pTimestamp" : "2026-10-22T00:00:00Z"
1098+ } ) )
1099+ . is_err( )
1100+ ) ;
1101+ assert ! (
1102+ context_tool
1103+ . validate_arguments( & serde_json:: json!( {
1104+ "dataset" : "logs" ,
1105+ "pTimestamp" : "2026-10-22T00:00:00Z" ,
1106+ "message" : "failed"
1107+ } ) )
1108+ . is_ok( )
1109+ ) ;
1110+ assert ! (
1111+ context_tool
1112+ . validate_arguments( & serde_json:: json!( {
1113+ "dataset" : "logs" ,
1114+ "pTimestamp" : "2026-10-22T00:00:00Z" ,
1115+ "log" : "failed" ,
1116+ "message" : "failed"
1117+ } ) )
1118+ . is_err( )
1119+ ) ;
10151120 }
10161121
10171122 #[ actix_web:: test]
0 commit comments