use crate::ToolDefinition;
use crate::agent::StandardDefinition;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Clone, Serialize, Deserialize, Default, ToSchema, JsonSchema)]
pub struct AgentCloudMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_owner: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_workspace: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_slug: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
pub struct AgentConfigWithTools {
#[serde(flatten)]
#[schema(value_type = Object)]
pub agent: AgentConfig,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub resolved_tools: Vec<ToolDefinition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub markdown: Option<String>,
#[serde(flatten, default)]
pub cloud: AgentCloudMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
#[serde(tag = "agent_type", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant)]
pub enum AgentConfig {
#[schema(value_type = Object)]
StandardAgent(StandardDefinition),
#[schema(value_type = Object)]
WorkflowAgent(WorkflowAgentDefinition),
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct WorkflowAgentDefinition {
pub name: String,
pub description: String,
#[serde(default = "default_version")]
pub version: String,
pub definition: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_schema: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub triggers: Vec<crate::WorkflowTrigger>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channels: Option<crate::channel_commands::ChannelBindings>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub connections: Vec<crate::connections::ConnectionRequirement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools: Option<crate::ToolsConfig>,
}
fn default_version() -> String {
"0.1.0".to_string()
}
impl AgentConfig {
pub fn get_name(&self) -> &str {
match self {
AgentConfig::StandardAgent(def) => &def.name,
AgentConfig::WorkflowAgent(def) => &def.name,
}
}
pub fn get_definition(&self) -> &StandardDefinition {
match self {
AgentConfig::StandardAgent(def) => def,
AgentConfig::WorkflowAgent(_) => {
panic!("WorkflowAgent does not have a StandardDefinition")
}
}
}
pub fn version(&self) -> Option<String> {
match self {
AgentConfig::StandardAgent(def) => def
.version
.clone()
.or_else(crate::agent::default_agent_version),
AgentConfig::WorkflowAgent(def) => Some(def.version.clone()),
}
}
pub fn get_description(&self) -> &str {
match self {
AgentConfig::StandardAgent(def) => &def.description,
AgentConfig::WorkflowAgent(def) => &def.description,
}
}
pub fn get_tools_config(&self) -> Option<&crate::ToolsConfig> {
match self {
AgentConfig::StandardAgent(def) => def.tools.as_ref(),
AgentConfig::WorkflowAgent(_) => None,
}
}
pub fn get_schedule_triggers(&self) -> Vec<&crate::WorkflowTrigger> {
match self {
AgentConfig::StandardAgent(_) => vec![],
AgentConfig::WorkflowAgent(def) => def
.triggers
.iter()
.filter(|t| matches!(t, crate::WorkflowTrigger::Schedule { .. }))
.collect(),
}
}
pub fn validate(&self) -> anyhow::Result<()> {
match self {
AgentConfig::StandardAgent(def) => def.validate(),
AgentConfig::WorkflowAgent(_def) => Ok(()), }
}
pub fn to_card(&self, server_config: &crate::configuration::ServerConfig) -> distri_a2a::AgentCard {
let (name, description, version, icon_url, skills) = match self {
AgentConfig::StandardAgent(def) => (
def.name.clone(),
def.description.clone(),
def.version.clone(),
def.icon_url.clone(),
def.skills_description.clone(),
),
AgentConfig::WorkflowAgent(def) => (
def.name.clone(),
def.description.clone(),
Some(def.version.clone()),
None,
Vec::new(),
),
};
let version = version
.or_else(crate::agent::default_agent_version)
.unwrap_or_else(|| distri_a2a::A2A_VERSION.to_string());
distri_a2a::AgentCard {
version,
url: format!("{}/agents/{}", server_config.base_url, name),
name,
description,
icon_url,
documentation_url: server_config.documentation_url.clone(),
provider: Some(server_config.agent_provider.clone()),
preferred_transport: server_config.preferred_transport.clone(),
capabilities: server_config.capabilities.clone(),
default_input_modes: server_config.default_input_modes.clone(),
default_output_modes: server_config.default_output_modes.clone(),
skills,
security_schemes: server_config.security_schemes.clone(),
security: server_config.security.clone(),
}
}
}
#[cfg(test)]
mod channel_binding_tests {
use super::*;
#[test]
fn workflow_agent_accepts_channels_field() {
let json = serde_json::json!({
"name": "z", "description": "d",
"definition": {"id":"w","steps":[]},
"channels": {"telegram": {"web_app_base": "https://a.app"}}
});
let def: WorkflowAgentDefinition = serde_json::from_value(json).unwrap();
assert!(def.channels.is_some());
}
#[test]
fn workflow_agent_channels_optional() {
let json = serde_json::json!({
"name": "z", "description": "d", "definition": {"id":"w","steps":[]}
});
let def: WorkflowAgentDefinition = serde_json::from_value(json).unwrap();
assert!(def.channels.is_none());
}
}