use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ObjectMeta {
#[serde(default)]
pub name: String,
#[serde(default)]
pub namespace: String,
#[serde(default)]
pub labels: std::collections::HashMap<String, String>,
#[serde(default)]
pub annotations: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CrewCrd {
pub api_version: String,
pub kind: String,
pub metadata: ObjectMeta,
pub spec: CrewCrdSpec,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CrewCrdSpec {
pub name: String,
#[serde(default)]
pub agents: Vec<AgentCrdSpec>,
#[serde(default)]
pub tasks: Vec<TaskCrdSpec>,
#[serde(default = "default_process_mode")]
pub process_mode: String,
#[serde(default = "default_trust")]
pub trust_level: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentCrdSpec {
pub agent_key: String,
pub name: String,
pub role: String,
pub goal: String,
#[serde(default)]
pub tools: Vec<String>,
#[serde(default = "default_complexity")]
pub complexity: String,
#[serde(default)]
pub domain: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct TaskCrdSpec {
pub description: String,
#[serde(default)]
pub expected_output: Option<String>,
#[serde(default = "default_priority")]
pub priority: String,
#[serde(default = "default_risk")]
pub risk: String,
#[serde(default)]
pub dependencies: Vec<usize>,
}
fn default_process_mode() -> String {
"sequential".into()
}
fn default_trust() -> String {
"basic".into()
}
fn default_complexity() -> String {
"medium".into()
}
fn default_priority() -> String {
"normal".into()
}
fn default_risk() -> String {
"low".into()
}
pub const API_GROUP: &str = "agnosai.io";
pub const API_VERSION: &str = "agnosai.io/v1";
impl CrewCrd {
#[must_use]
pub fn new(name: impl Into<String>, spec: CrewCrdSpec) -> Self {
Self {
api_version: API_VERSION.into(),
kind: "Crew".into(),
metadata: ObjectMeta {
name: name.into(),
..Default::default()
},
spec,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crew_crd_serde_roundtrip() {
let crd = CrewCrd::new(
"test-crew",
CrewCrdSpec {
name: "Test Crew".into(),
agents: vec![AgentCrdSpec {
agent_key: "agent-a".into(),
name: "Agent A".into(),
role: "tester".into(),
goal: "test things".into(),
tools: vec!["echo".into()],
complexity: "medium".into(),
domain: None,
}],
tasks: vec![TaskCrdSpec {
description: "do something".into(),
expected_output: None,
priority: "normal".into(),
risk: "low".into(),
dependencies: vec![],
}],
process_mode: "sequential".into(),
trust_level: "basic".into(),
},
);
let json = serde_json::to_string_pretty(&crd).unwrap();
assert!(json.contains("agnosai.io/v1"));
assert!(json.contains("Crew"));
let restored: CrewCrd = serde_json::from_str(&json).unwrap();
assert_eq!(restored.spec.name, "Test Crew");
assert_eq!(restored.spec.agents.len(), 1);
assert_eq!(restored.spec.tasks.len(), 1);
}
#[test]
fn crew_crd_yaml_compatible() {
let crd = CrewCrd::new(
"my-crew",
CrewCrdSpec {
name: "My Crew".into(),
agents: vec![],
tasks: vec![],
process_mode: "parallel".into(),
trust_level: "strict".into(),
},
);
let json = serde_json::to_value(&crd).unwrap();
assert_eq!(json["apiVersion"], "agnosai.io/v1");
assert_eq!(json["kind"], "Crew");
assert_eq!(json["metadata"]["name"], "my-crew");
}
#[test]
fn defaults_applied() {
let json = r#"{
"apiVersion": "agnosai.io/v1",
"kind": "Crew",
"metadata": {"name": "minimal"},
"spec": {"name": "Minimal"}
}"#;
let crd: CrewCrd = serde_json::from_str(json).unwrap();
assert_eq!(crd.spec.process_mode, "sequential");
assert_eq!(crd.spec.trust_level, "basic");
}
#[test]
fn api_constants() {
assert_eq!(API_GROUP, "agnosai.io");
assert_eq!(API_VERSION, "agnosai.io/v1");
}
}