use std::collections::BTreeMap;
use std::process::ExitCode;
use clap::{Args, Subcommand};
use serde::{Deserialize, Serialize};
use crate::config::ResolvedContext;
use crate::output::OutputFormat;
mod inspect;
mod kill;
mod list;
pub mod resume;
pub mod suspend;
#[derive(Args)]
pub struct AgentArgs {
#[command(subcommand)]
pub command: AgentCommands,
}
#[derive(Subcommand)]
pub enum AgentCommands {
List(list::ListArgs),
Inspect(inspect::InspectArgs),
Kill(kill::KillArgs),
Suspend(suspend::SuspendArgs),
Resume(resume::ResumeArgs),
}
pub fn dispatch(args: AgentArgs, ctx: &ResolvedContext, output: OutputFormat) -> ExitCode {
match args.command {
AgentCommands::List(list_args) => list::run(list_args, ctx, output),
AgentCommands::Inspect(inspect_args) => inspect::run(inspect_args, ctx, output),
AgentCommands::Kill(kill_args) => kill::run(kill_args, ctx),
AgentCommands::Suspend(suspend_args) => suspend::run(suspend_args, ctx, output),
AgentCommands::Resume(resume_args) => resume::run(resume_args, ctx, output),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentResponse {
pub id: String,
pub name: String,
pub framework: String,
pub version: String,
pub status: String,
pub tool_names: Vec<String>,
pub metadata: BTreeMap<String, String>,
#[serde(default)]
pub pid: Option<u32>,
#[serde(default)]
pub session_count: Option<u32>,
#[serde(default)]
pub last_event: Option<String>,
#[serde(default)]
pub policy_violations_count: Option<u32>,
#[serde(default)]
pub active_sessions: Vec<ActiveSessionResponse>,
#[serde(default)]
pub recent_events: Vec<RecentEventResponse>,
#[serde(default)]
pub recent_traces: Vec<RecentTraceResponse>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActiveSessionResponse {
pub session_id: String,
pub started_at: String,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecentEventResponse {
pub event_type: String,
pub summary: String,
pub timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecentTraceResponse {
pub session_id: String,
pub timestamp: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PaginatedResponse<T> {
pub items: Vec<T>,
#[allow(dead_code)]
pub page: u32,
#[allow(dead_code)]
pub per_page: u32,
#[allow(dead_code)]
pub total: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_response_deserializes_from_json() {
let json = r#"{
"id": "aabbccdd00112233aabbccdd00112233",
"name": "my-agent",
"framework": "langgraph",
"version": "0.2.0",
"status": "Active",
"tool_names": ["search", "calculator"],
"metadata": {"env": "production"}
}"#;
let agent: AgentResponse = serde_json::from_str(json).unwrap();
assert_eq!(agent.id, "aabbccdd00112233aabbccdd00112233");
assert_eq!(agent.name, "my-agent");
assert_eq!(agent.framework, "langgraph");
assert_eq!(agent.version, "0.2.0");
assert_eq!(agent.status, "Active");
assert_eq!(agent.tool_names, vec!["search", "calculator"]);
assert_eq!(agent.metadata.get("env").unwrap(), "production");
}
#[test]
fn agent_response_round_trip() {
let agent = AgentResponse {
id: "00112233445566778899aabbccddeeff".to_string(),
name: "round-trip-agent".to_string(),
framework: "crewai".to_string(),
version: "1.0.0".to_string(),
status: "Suspended(PolicyViolation)".to_string(),
tool_names: vec![],
metadata: BTreeMap::new(),
pid: Some(1234),
session_count: Some(5),
last_event: Some("2025-01-01T00:00:00Z".to_string()),
policy_violations_count: Some(2),
active_sessions: vec![],
recent_events: vec![],
recent_traces: vec![],
};
let json = serde_json::to_string(&agent).unwrap();
let parsed: AgentResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.id, agent.id);
assert_eq!(parsed.status, agent.status);
assert!(parsed.tool_names.is_empty());
}
#[test]
fn paginated_response_deserializes() {
let json = r#"{
"items": [
{
"id": "aabb",
"name": "a1",
"framework": "f1",
"version": "0.1.0",
"status": "Active",
"tool_names": [],
"metadata": {}
}
],
"page": 1,
"per_page": 20,
"total": 1
}"#;
let resp: PaginatedResponse<AgentResponse> = serde_json::from_str(json).unwrap();
assert_eq!(resp.items.len(), 1);
assert_eq!(resp.items[0].name, "a1");
assert_eq!(resp.page, 1);
assert_eq!(resp.total, 1);
}
#[test]
fn agent_response_with_empty_metadata() {
let json = r#"{
"id": "ff",
"name": "empty-meta",
"framework": "custom",
"version": "0.0.1",
"status": "Deregistered",
"tool_names": [],
"metadata": {}
}"#;
let agent: AgentResponse = serde_json::from_str(json).unwrap();
assert!(agent.metadata.is_empty());
assert!(agent.tool_names.is_empty());
}
#[test]
fn new_fields_default_to_none_when_missing() {
let json = r#"{
"id": "aa",
"name": "old-server",
"framework": "custom",
"version": "0.0.1",
"status": "Active",
"tool_names": [],
"metadata": {}
}"#;
let agent: AgentResponse = serde_json::from_str(json).unwrap();
assert!(agent.pid.is_none());
assert!(agent.session_count.is_none());
assert!(agent.last_event.is_none());
assert!(agent.policy_violations_count.is_none());
assert!(agent.active_sessions.is_empty());
assert!(agent.recent_events.is_empty());
}
#[test]
fn new_fields_deserialize_when_present() {
let json = r#"{
"id": "bb",
"name": "full-agent",
"framework": "langgraph",
"version": "1.0.0",
"status": "Active",
"tool_names": ["search"],
"metadata": {},
"pid": 4567,
"session_count": 12,
"last_event": "2025-06-15T08:30:00Z",
"policy_violations_count": 3
}"#;
let agent: AgentResponse = serde_json::from_str(json).unwrap();
assert_eq!(agent.pid, Some(4567));
assert_eq!(agent.session_count, Some(12));
assert_eq!(agent.last_event.as_deref(), Some("2025-06-15T08:30:00Z"));
assert_eq!(agent.policy_violations_count, Some(3));
}
#[test]
fn round_trip_preserves_new_fields() {
let agent = AgentResponse {
id: "cc".to_string(),
name: "rt-agent".to_string(),
framework: "crewai".to_string(),
version: "2.0.0".to_string(),
status: "Active".to_string(),
tool_names: vec![],
metadata: BTreeMap::new(),
pid: Some(9999),
session_count: Some(42),
last_event: Some("2025-03-01T12:00:00Z".to_string()),
policy_violations_count: Some(0),
active_sessions: vec![],
recent_events: vec![],
recent_traces: vec![],
};
let json = serde_json::to_string(&agent).unwrap();
let parsed: AgentResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.pid, Some(9999));
assert_eq!(parsed.session_count, Some(42));
assert_eq!(parsed.last_event.as_deref(), Some("2025-03-01T12:00:00Z"));
assert_eq!(parsed.policy_violations_count, Some(0));
}
#[test]
fn active_sessions_and_recent_events_deserialize() {
let json = r#"{
"id": "dd",
"name": "session-agent",
"framework": "custom",
"version": "1.0.0",
"status": "Active",
"tool_names": [],
"metadata": {},
"active_sessions": [
{"session_id": "s1", "started_at": "2025-06-01T10:00:00Z", "status": "running"},
{"session_id": "s2", "started_at": "2025-06-01T11:00:00Z", "status": "idle"}
],
"recent_events": [
{"event_type": "violation", "summary": "blocked call", "timestamp": "2025-06-01T10:05:00Z"}
]
}"#;
let agent: AgentResponse = serde_json::from_str(json).unwrap();
assert_eq!(agent.active_sessions.len(), 2);
assert_eq!(agent.active_sessions[0].session_id, "s1");
assert_eq!(agent.active_sessions[0].status, "running");
assert_eq!(agent.active_sessions[1].session_id, "s2");
assert_eq!(agent.recent_events.len(), 1);
assert_eq!(agent.recent_events[0].event_type, "violation");
assert_eq!(agent.recent_events[0].summary, "blocked call");
}
#[test]
fn recent_traces_defaults_to_empty_when_missing() {
let json = r#"{
"id": "ee",
"name": "no-traces",
"framework": "custom",
"version": "1.0.0",
"status": "Active",
"tool_names": [],
"metadata": {}
}"#;
let agent: AgentResponse = serde_json::from_str(json).unwrap();
assert!(agent.recent_traces.is_empty());
}
#[test]
fn recent_traces_deserialize_when_present() {
let json = r#"{
"id": "ff",
"name": "traced-agent",
"framework": "langgraph",
"version": "1.0.0",
"status": "Active",
"tool_names": [],
"metadata": {},
"recent_traces": [
{"session_id": "sess-abc123", "timestamp": "2026-04-30T10:00:00Z"},
{"session_id": "sess-def456", "timestamp": "2026-04-30T09:30:00Z"}
]
}"#;
let agent: AgentResponse = serde_json::from_str(json).unwrap();
assert_eq!(agent.recent_traces.len(), 2);
assert_eq!(agent.recent_traces[0].session_id, "sess-abc123");
assert_eq!(agent.recent_traces[0].timestamp, "2026-04-30T10:00:00Z");
assert_eq!(agent.recent_traces[1].session_id, "sess-def456");
}
#[test]
fn recent_traces_round_trip() {
let agent = AgentResponse {
id: "gg".to_string(),
name: "rt-traces".to_string(),
framework: "crewai".to_string(),
version: "1.0.0".to_string(),
status: "Active".to_string(),
tool_names: vec![],
metadata: BTreeMap::new(),
pid: None,
session_count: None,
last_event: None,
policy_violations_count: None,
active_sessions: vec![],
recent_events: vec![],
recent_traces: vec![RecentTraceResponse {
session_id: "sess-111".to_string(),
timestamp: "2026-04-30T08:00:00Z".to_string(),
}],
};
let json = serde_json::to_string(&agent).unwrap();
let parsed: AgentResponse = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.recent_traces.len(), 1);
assert_eq!(parsed.recent_traces[0].session_id, "sess-111");
assert_eq!(parsed.recent_traces[0].timestamp, "2026-04-30T08:00:00Z");
}
}