@@ -112,6 +112,71 @@ pub struct AgentGroupsResponse {
112112 pub groups : Vec < AgentGroupSummary > ,
113113}
114114
115+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Serialize , Deserialize ) ]
116+ #[ serde( rename_all = "lowercase" ) ]
117+ pub enum AgentGroupMemberRole {
118+ Hub ,
119+ Spoke ,
120+ Peer ,
121+ }
122+
123+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Serialize , Deserialize ) ]
124+ #[ serde( rename_all = "lowercase" ) ]
125+ pub enum AgentGroupMemberState {
126+ Running ,
127+ Waiting ,
128+ Idle ,
129+ Ended ,
130+ }
131+
132+ #[ derive( Debug , Clone , PartialEq , Serialize , Deserialize ) ]
133+ #[ serde( rename_all = "camelCase" ) ]
134+ pub struct AgentGroupMemberStatus {
135+ pub agent_id : String ,
136+ pub name : String ,
137+ pub role : AgentGroupMemberRole ,
138+ pub status : AgentGroupMemberState ,
139+ pub running_turns : u32 ,
140+ pub pending_asks : u32 ,
141+ pub waiting_on_peer : u32 ,
142+ pub background_tasks : u32 ,
143+ pub tokens_in : u64 ,
144+ pub tokens_out : u64 ,
145+ pub cost_usd : f64 ,
146+ pub last_activity_at : Option < String > ,
147+ }
148+
149+ #[ derive( Debug , Clone , PartialEq , Serialize , Deserialize ) ]
150+ #[ serde( rename_all = "camelCase" ) ]
151+ pub struct AgentGroupRosterTotals {
152+ pub members : u32 ,
153+ pub hubs : u32 ,
154+ pub spokes : u32 ,
155+ pub running : u32 ,
156+ pub waiting : u32 ,
157+ pub ended : u32 ,
158+ pub background_tasks : u32 ,
159+ pub cost_usd : f64 ,
160+ }
161+
162+ #[ derive( Debug , Clone , PartialEq , Serialize , Deserialize ) ]
163+ pub struct AgentGroupRoster {
164+ pub members : Vec < AgentGroupMemberStatus > ,
165+ pub totals : AgentGroupRosterTotals ,
166+ }
167+
168+ #[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
169+ #[ serde( rename_all = "camelCase" ) ]
170+ pub struct EndAgentGroupResponse {
171+ pub threads_ended : u32 ,
172+ pub tasks_cancelled : u32 ,
173+ }
174+
175+ #[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
176+ pub struct CancelSubagentTaskResponse {
177+ pub cancelled : bool ,
178+ }
179+
115180/// One saved MCP/service preset on an agent, as the connect and status
116181/// endpoints return it. `authorization_url` is the provider page to open when
117182/// `status` is `needs_auth`.
@@ -322,6 +387,36 @@ pub enum MessageDirection {
322387 Outbound ,
323388}
324389
390+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Serialize , Deserialize ) ]
391+ #[ serde( rename_all = "lowercase" ) ]
392+ pub enum ConversationLiveness {
393+ Working ,
394+ Waiting ,
395+ }
396+
397+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Serialize , Deserialize ) ]
398+ #[ serde( rename_all = "lowercase" ) ]
399+ pub enum SubagentTaskStatus {
400+ Running ,
401+ Suspended ,
402+ Completed ,
403+ Error ,
404+ Cancelled ,
405+ }
406+
407+ #[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
408+ #[ serde( rename_all = "camelCase" ) ]
409+ pub struct SubagentTaskSummary {
410+ pub id : String ,
411+ pub subagent : String ,
412+ pub status : SubagentTaskStatus ,
413+ pub request : String ,
414+ pub resumptions : u32 ,
415+ pub trace_id : Option < String > ,
416+ pub created_at : String ,
417+ pub ended_at : Option < String > ,
418+ }
419+
325420impl MessageDirection {
326421 pub fn as_str ( self ) -> & ' static str {
327422 match self {
@@ -345,6 +440,8 @@ pub struct ConversationSummary {
345440 pub message_count : u32 ,
346441 pub is_active : bool ,
347442 pub has_errors : bool ,
443+ #[ serde( default ) ]
444+ pub liveness : Option < ConversationLiveness > ,
348445 /// The asker behind the thread — set only for inter-agent (`agent:`)
349446 /// conversations, where another agent is the contact.
350447 #[ serde( default ) ]
@@ -397,6 +494,7 @@ impl std::fmt::Debug for ConversationSummary {
397494 . field ( "message_count" , & self . message_count )
398495 . field ( "is_active" , & self . is_active )
399496 . field ( "has_errors" , & self . has_errors )
497+ . field ( "liveness" , & self . liveness )
400498 . field ( "peer" , & self . peer )
401499 . finish ( )
402500 }
@@ -447,6 +545,10 @@ pub struct ConversationDetail {
447545 /// while the agent is idle. Older backends omit it.
448546 #[ serde( default ) ]
449547 pub active_turn_started_at : Option < String > ,
548+ #[ serde( default ) ]
549+ pub liveness : Option < ConversationLiveness > ,
550+ #[ serde( default ) ]
551+ pub tasks : Vec < SubagentTaskSummary > ,
450552 /// The asker behind the thread when this is an inter-agent
451553 /// (`agent:`) conversation.
452554 #[ serde( default ) ]
@@ -855,4 +957,130 @@ mod tests {
855957 assert ! ( !format!( "{agentless:?}" ) . contains( "agentless-secret" ) ) ;
856958 assert ! ( !format!( "{locator:?}" ) . contains( "agentless-secret" ) ) ;
857959 }
960+
961+ #[ test]
962+ fn swarm_observability_models_round_trip ( ) {
963+ let liveness = ConversationLiveness :: Waiting ;
964+ assert_eq ! (
965+ serde_json:: from_value:: <ConversationLiveness >( serde_json:: to_value( liveness) . unwrap( ) )
966+ . unwrap( ) ,
967+ liveness
968+ ) ;
969+
970+ let task = SubagentTaskSummary {
971+ id : "task-1" . into ( ) ,
972+ subagent : "worker" . into ( ) ,
973+ status : SubagentTaskStatus :: Suspended ,
974+ request : "Inspect the deployment" . into ( ) ,
975+ resumptions : 2 ,
976+ trace_id : Some ( "trace-1" . into ( ) ) ,
977+ created_at : "2026-08-28T12:00:00Z" . into ( ) ,
978+ ended_at : None ,
979+ } ;
980+ assert_eq ! (
981+ serde_json:: from_value:: <SubagentTaskSummary >( serde_json:: to_value( & task) . unwrap( ) )
982+ . unwrap( ) ,
983+ task
984+ ) ;
985+
986+ let roster = AgentGroupRoster {
987+ members : vec ! [ AgentGroupMemberStatus {
988+ agent_id: "agent-1" . into( ) ,
989+ name: "Worker" . into( ) ,
990+ role: AgentGroupMemberRole :: Spoke ,
991+ status: AgentGroupMemberState :: Running ,
992+ running_turns: 1 ,
993+ pending_asks: 0 ,
994+ waiting_on_peer: 0 ,
995+ background_tasks: 2 ,
996+ tokens_in: 10 ,
997+ tokens_out: 20 ,
998+ cost_usd: 0.42 ,
999+ last_activity_at: Some ( "2026-08-28T12:00:00Z" . into( ) ) ,
1000+ } ] ,
1001+ totals : AgentGroupRosterTotals {
1002+ members : 1 ,
1003+ hubs : 0 ,
1004+ spokes : 1 ,
1005+ running : 1 ,
1006+ waiting : 0 ,
1007+ ended : 0 ,
1008+ background_tasks : 2 ,
1009+ cost_usd : 0.42 ,
1010+ } ,
1011+ } ;
1012+ assert_eq ! (
1013+ serde_json:: from_value:: <AgentGroupRoster >( serde_json:: to_value( & roster) . unwrap( ) )
1014+ . unwrap( ) ,
1015+ roster
1016+ ) ;
1017+
1018+ for response in [
1019+ EndAgentGroupResponse {
1020+ threads_ended : 2 ,
1021+ tasks_cancelled : 1 ,
1022+ } ,
1023+ EndAgentGroupResponse {
1024+ threads_ended : 0 ,
1025+ tasks_cancelled : 0 ,
1026+ } ,
1027+ ] {
1028+ assert_eq ! (
1029+ serde_json:: from_value:: <EndAgentGroupResponse >(
1030+ serde_json:: to_value( & response) . unwrap( )
1031+ )
1032+ . unwrap( ) ,
1033+ response
1034+ ) ;
1035+ }
1036+ let response = CancelSubagentTaskResponse { cancelled : true } ;
1037+ assert_eq ! (
1038+ serde_json:: from_value:: <CancelSubagentTaskResponse >(
1039+ serde_json:: to_value( & response) . unwrap( )
1040+ )
1041+ . unwrap( ) ,
1042+ response
1043+ ) ;
1044+ }
1045+
1046+ #[ test]
1047+ fn missing_swarm_observability_fields_default ( ) {
1048+ let summary: ConversationSummary = serde_json:: from_value ( serde_json:: json!( {
1049+ "phone" : "tg:123" ,
1050+ "contactName" : null,
1051+ "agentName" : "Orchid" ,
1052+ "agentId" : "agent-1" ,
1053+ "apiKey" : "secret" ,
1054+ "lastMessage" : "Latest" ,
1055+ "lastMessageAt" : "2026-08-28T12:00:00Z" ,
1056+ "lastDirection" : "inbound" ,
1057+ "messageCount" : 1 ,
1058+ "isActive" : true ,
1059+ "hasErrors" : false
1060+ } ) )
1061+ . unwrap ( ) ;
1062+ assert_eq ! ( summary. liveness, None ) ;
1063+
1064+ let detail: ConversationDetail = serde_json:: from_value ( serde_json:: json!( {
1065+ "phone" : "tg:123" ,
1066+ "meta" : {
1067+ "contactName" : null,
1068+ "contactEmail" : null,
1069+ "agentName" : "Orchid" ,
1070+ "agentId" : "agent-1" ,
1071+ "apiKey" : "secret" ,
1072+ "messageCount" : 0 ,
1073+ "firstSeen" : "2026-08-28T11:00:00Z" ,
1074+ "lastSeen" : "2026-08-28T12:00:00Z"
1075+ } ,
1076+ "messages" : [ ] ,
1077+ "spans" : { } ,
1078+ "agent" : null,
1079+ "blocked" : false ,
1080+ "takeover" : null
1081+ } ) )
1082+ . unwrap ( ) ;
1083+ assert_eq ! ( detail. liveness, None ) ;
1084+ assert ! ( detail. tasks. is_empty( ) ) ;
1085+ }
8581086}
0 commit comments