use car_engine::Runtime;
use std::collections::HashMap;
use crate::types::{
AgentCapabilities, AgentCard, AgentInterface, AgentProvider, AgentSkill, TransportProtocol,
};
#[derive(Debug, Clone)]
pub struct AgentCardConfig {
pub name: String,
pub description: String,
pub url: String,
pub provider: AgentProvider,
pub capabilities: Option<AgentCapabilities>,
pub extra_interfaces: Vec<AgentInterface>,
pub default_input_modes: Vec<String>,
pub default_output_modes: Vec<String>,
}
impl AgentCardConfig {
pub fn minimal(
name: impl Into<String>,
description: impl Into<String>,
url: impl Into<String>,
provider: AgentProvider,
) -> Self {
Self {
name: name.into(),
description: description.into(),
url: url.into(),
provider,
capabilities: None,
extra_interfaces: Vec::new(),
default_input_modes: vec!["text".into(), "data".into()],
default_output_modes: vec!["text".into(), "data".into()],
}
}
}
pub async fn build_default_agent_card(
runtime: &Runtime,
config: AgentCardConfig,
) -> AgentCard {
let schemas = runtime.tool_schemas().await;
let skills: Vec<AgentSkill> = schemas
.into_iter()
.map(|s| {
let mut tags = Vec::new();
if s.idempotent {
tags.push("idempotent".into());
}
if s.cache_ttl_secs.is_some() {
tags.push("cacheable".into());
}
if s.rate_limit.is_some() {
tags.push("rate-limited".into());
}
AgentSkill {
id: s.name.clone(),
name: s.name,
description: s.description,
tags,
examples: Vec::new(),
input_modes: vec!["data".into()],
output_modes: vec!["data".into()],
}
})
.collect();
let primary_interface = AgentInterface {
url: config.url.clone(),
protocol_binding: "JSONRPC".into(),
transport: Some(TransportProtocol::JsonRpc),
tenant: None,
protocol_version: "1.0".into(),
};
let mut supported_interfaces = vec![primary_interface.clone()];
supported_interfaces.extend(config.extra_interfaces.clone());
let additional_interfaces: Vec<AgentInterface> = config.extra_interfaces;
AgentCard {
name: config.name,
description: config.description,
url: config.url,
version: "1.0.0".into(),
protocol_version: "1.0".into(),
preferred_transport: Some("JSONRPC".into()),
provider: config.provider,
capabilities: config.capabilities.unwrap_or(AgentCapabilities {
streaming: true,
push_notifications: false,
state_transition_history: false,
extended_agent_card: false,
extensions: Vec::new(),
}),
default_input_modes: config.default_input_modes,
default_output_modes: config.default_output_modes,
skills,
documentation_url: None,
icon_url: None,
supported_interfaces,
additional_interfaces,
security_schemes: HashMap::new(),
supports_authenticated_extended_card: false,
security_requirements: vec![],
signatures: vec![],
}
}
#[cfg(test)]
mod tests {
use super::*;
use car_ir::ToolSchema;
#[tokio::test]
async fn skills_mirror_registered_tools() {
let runtime = Runtime::new();
runtime
.register_tool_schema(ToolSchema {
name: "echo".into(),
description: "Echo input".into(),
parameters: serde_json::json!({"type": "object"}),
returns: None,
idempotent: true,
cache_ttl_secs: Some(60),
rate_limit: None,
})
.await;
let card = build_default_agent_card(
&runtime,
AgentCardConfig::minimal(
"test",
"test agent",
"https://example.test",
AgentProvider {
organization: "Parslee".into(),
url: None,
},
),
)
.await;
assert_eq!(card.skills.len(), 1);
let skill = &card.skills[0];
assert_eq!(skill.id, "echo");
assert!(skill.tags.contains(&"idempotent".to_string()));
assert!(skill.tags.contains(&"cacheable".to_string()));
assert_eq!(card.version, "1.0.0");
assert!(card.capabilities.streaming);
assert!(!card.capabilities.push_notifications);
}
}