1010
1111use candid:: Principal ;
1212use serde:: { Deserialize , Serialize } ;
13- use serde_json:: json;
13+ use serde_json:: { Map , json} ;
1414use std:: collections:: BTreeMap ;
1515
1616use crate :: Json ;
@@ -291,6 +291,7 @@ impl<T> ToolOutput<T> {
291291#[ derive( Debug , Clone , Default , Deserialize , Serialize ) ]
292292pub struct RequestMeta {
293293 /// The target engine principal for the request.
294+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
294295 pub engine : Option < Principal > ,
295296
296297 /// Gets the username from request context.
@@ -299,6 +300,11 @@ pub struct RequestMeta {
299300 /// of the user interacting with the bot.
300301 #[ serde( skip_serializing_if = "Option::is_none" ) ]
301302 pub user : Option < String > ,
303+
304+ /// Extra metadata key-value pairs.
305+ #[ serde( flatten) ]
306+ #[ serde( skip_serializing_if = "Map::is_empty" ) ]
307+ pub extra : Map < String , Json > ,
302308}
303309
304310/// Represents the usage statistics for the agent or tool execution.
@@ -742,4 +748,65 @@ mod tests {
742748 assert_eq ! ( calls[ 1 ] . name, "echo" ) ;
743749 assert_eq ! ( calls[ 1 ] . args, serde_json:: json!( { "text" : "hi" } ) ) ;
744750 }
751+
752+ #[ test]
753+ fn test_request_meta_extra_flatten_serde ( ) {
754+ // empty extra should not serialize
755+ let meta = RequestMeta {
756+ engine : None ,
757+ user : None ,
758+ extra : Map :: new ( ) ,
759+ } ;
760+ let v = serde_json:: to_value ( & meta) . unwrap ( ) ;
761+ assert_eq ! ( v, serde_json:: json!( { } ) ) ;
762+
763+ // extra should be flattened into the top-level object
764+ let mut extra = Map :: new ( ) ;
765+ extra. insert ( "foo" . into ( ) , serde_json:: json!( "bar" ) ) ;
766+ extra. insert ( "n" . into ( ) , serde_json:: json!( 1 ) ) ;
767+ extra. insert ( "obj" . into ( ) , serde_json:: json!( { "x" : true } ) ) ;
768+
769+ let meta2 = RequestMeta {
770+ engine : Some ( Principal :: from_text ( "aaaaa-aa" ) . unwrap ( ) ) ,
771+ user : Some ( "alice" . into ( ) ) ,
772+ extra,
773+ } ;
774+
775+ let v2 = serde_json:: to_value ( & meta2) . unwrap ( ) ;
776+ assert_eq ! ( v2. get( "engine" ) . unwrap( ) , "aaaaa-aa" ) ;
777+ assert_eq ! ( v2. get( "user" ) . unwrap( ) , "alice" ) ;
778+ assert_eq ! ( v2. get( "foo" ) . unwrap( ) , "bar" ) ;
779+ assert_eq ! ( v2. get( "n" ) . unwrap( ) , 1 ) ;
780+ assert_eq ! ( v2. get( "obj" ) . unwrap( ) , & serde_json:: json!( { "x" : true } ) ) ;
781+ assert ! ( v2. get( "extra" ) . is_none( ) ) ;
782+
783+ // deserialization: unknown fields go into extra
784+ let input = serde_json:: json!( {
785+ "engine" : "aaaaa-aa" ,
786+ "user" : "bob" ,
787+ "k1" : "v1" ,
788+ "k2" : 2 ,
789+ "nested" : { "a" : 1 }
790+ } ) ;
791+ let back: RequestMeta = serde_json:: from_value ( input) . unwrap ( ) ;
792+ assert_eq ! ( back. engine. unwrap( ) . to_text( ) , "aaaaa-aa" ) ;
793+ assert_eq ! ( back. user. as_deref( ) , Some ( "bob" ) ) ;
794+ assert_eq ! ( back. extra. get( "k1" ) . unwrap( ) , "v1" ) ;
795+ assert_eq ! ( back. extra. get( "k2" ) . unwrap( ) , 2 ) ;
796+ assert_eq ! (
797+ back. extra. get( "nested" ) . unwrap( ) ,
798+ & serde_json:: json!( { "a" : 1 } )
799+ ) ;
800+
801+ // round-trip (field-by-field)
802+ let back2: RequestMeta = serde_json:: from_value ( v2) . unwrap ( ) ;
803+ assert_eq ! ( back2. engine. unwrap( ) . to_text( ) , "aaaaa-aa" ) ;
804+ assert_eq ! ( back2. user. as_deref( ) , Some ( "alice" ) ) ;
805+ assert_eq ! ( back2. extra. get( "foo" ) . unwrap( ) , "bar" ) ;
806+ assert_eq ! ( back2. extra. get( "n" ) . unwrap( ) , 1 ) ;
807+ assert_eq ! (
808+ back2. extra. get( "obj" ) . unwrap( ) ,
809+ & serde_json:: json!( { "x" : true } )
810+ ) ;
811+ }
745812}
0 commit comments