Skip to main content

agentics_domain/models/challenge/
targets.rs

1use std::borrow::Cow;
2use std::fmt;
3
4use schemars::{Schema, SchemaGenerator, json_schema};
5use serde::de::{Error as DeError, Visitor};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8use super::super::images::ChallengeImageReference;
9use super::super::names::{ResourceProfileName, TargetName};
10use crate::zip_project::ZipProjectNetworkAccess;
11
12/// Supported Docker platforms for targets.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
14pub enum DockerPlatform {
15    #[serde(rename = "linux/arm64")]
16    LinuxArm64,
17    #[serde(rename = "linux/amd64")]
18    LinuxAmd64,
19}
20
21impl DockerPlatform {
22    /// Canonical Docker platform string used in Docker API requests.
23    pub fn as_str(self) -> &'static str {
24        match self {
25            Self::LinuxArm64 => "linux/arm64",
26            Self::LinuxAmd64 => "linux/amd64",
27        }
28    }
29}
30
31/// Accelerator selection used by a target.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum TargetAccelerator {
34    None,
35    Gpu,
36}
37
38impl TargetAccelerator {
39    /// Stable string form used in user-facing summaries.
40    pub fn as_str(self) -> &'static str {
41        match self {
42            Self::None => "none",
43            Self::Gpu => "gpu",
44        }
45    }
46
47    /// Parse a stable database string for required worker accelerator scheduling.
48    pub fn from_storage_value(value: &str) -> Option<Self> {
49        match value {
50            "none" => Some(Self::None),
51            "gpu" => Some(Self::Gpu),
52            _ => None,
53        }
54    }
55}
56
57impl Serialize for TargetAccelerator {
58    /// Serialize no accelerator as explicit JSON null and GPU as the only accelerator string.
59    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
60    where
61        S: Serializer,
62    {
63        match self {
64            Self::None => serializer.serialize_none(),
65            Self::Gpu => serializer.serialize_str("gpu"),
66        }
67    }
68}
69
70impl<'de> Deserialize<'de> for TargetAccelerator {
71    /// Deserialize required nullable accelerator policy from challenge configs.
72    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
73    where
74        D: Deserializer<'de>,
75    {
76        struct TargetAcceleratorVisitor;
77
78        impl<'de> Visitor<'de> for TargetAcceleratorVisitor {
79            type Value = TargetAccelerator;
80
81            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82                formatter.write_str("null for no accelerator or \"gpu\" for GPU acceleration")
83            }
84
85            fn visit_none<E>(self) -> std::result::Result<Self::Value, E>
86            where
87                E: DeError,
88            {
89                Ok(TargetAccelerator::None)
90            }
91
92            fn visit_unit<E>(self) -> std::result::Result<Self::Value, E>
93            where
94                E: DeError,
95            {
96                Ok(TargetAccelerator::None)
97            }
98
99            fn visit_some<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
100            where
101                D: Deserializer<'de>,
102            {
103                deserializer.deserialize_any(self)
104            }
105
106            fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
107            where
108                E: DeError,
109            {
110                match value {
111                    "gpu" => Ok(TargetAccelerator::Gpu),
112                    "cpu" => Err(E::custom(
113                        "accelerator must be explicit null when no accelerator is required, not \"cpu\"",
114                    )),
115                    other => Err(E::unknown_variant(other, &["gpu"])),
116                }
117            }
118        }
119
120        deserializer.deserialize_any(TargetAcceleratorVisitor)
121    }
122}
123
124impl schemars::JsonSchema for TargetAccelerator {
125    /// Target accelerator is an inline required nullable field in target specs.
126    fn inline_schema() -> bool {
127        true
128    }
129
130    /// Stable schema name for target accelerator.
131    fn schema_name() -> Cow<'static, str> {
132        Cow::Borrowed("TargetAccelerator")
133    }
134
135    /// JSON schema for `null | "gpu"`.
136    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
137        json_schema!({
138            "x-agentics-preserve-null": true,
139            "oneOf": [
140                { "type": "null" },
141                { "type": "string", "enum": ["gpu"] }
142            ]
143        })
144    }
145}
146
147/// One execution and ranking target declared by a challenge.
148#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
149#[serde(deny_unknown_fields)]
150pub struct ChallengeTargetSpec {
151    pub name: TargetName,
152    pub docker_platform: DockerPlatform,
153    /// Required nullable field: JSON null means no accelerator, "gpu" means GPU acceleration.
154    pub accelerator: TargetAccelerator,
155    pub validation_enabled: bool,
156    pub resource_profile: ResourceProfileSpec,
157}
158
159/// Resource envelope and Docker images declared by a challenge.
160#[derive(Debug, Clone, Serialize, Deserialize, garde::Validate, schemars::JsonSchema)]
161#[garde(allow_unvalidated)]
162#[serde(deny_unknown_fields)]
163pub struct ResourceProfileSpec {
164    pub name: ResourceProfileName,
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    #[garde(custom(crate::validation::optional_trimmed_non_empty))]
167    pub resource_description: Option<String>,
168    pub solution_image: ChallengeImageReference,
169    pub evaluator_image: ChallengeImageReference,
170    pub solution: SolutionStageProfiles,
171    pub evaluator: EvaluatorStageProfiles,
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub hardware_metadata: Option<HardwareProfileSpec>,
174}
175
176/// Resource limits for participant-owned solution stages.
177#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
178#[serde(deny_unknown_fields)]
179pub struct SolutionStageProfiles {
180    pub setup: StageResourceProfile,
181    pub build: StageResourceProfile,
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub run: Option<StageResourceProfile>,
184}
185
186/// Resource limits for trusted challenge-owned evaluator stages.
187#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
188#[serde(deny_unknown_fields)]
189pub struct EvaluatorStageProfiles {
190    pub setup: StageResourceProfile,
191    pub run: StageResourceProfile,
192}
193
194/// Resource envelope for one Docker-executed stage.
195#[derive(Debug, Clone, Serialize, Deserialize, garde::Validate, schemars::JsonSchema)]
196#[serde(deny_unknown_fields)]
197pub struct StageResourceProfile {
198    #[garde(range(min = 1))]
199    pub timeout_sec: u64,
200    #[garde(range(min = 1))]
201    pub memory_limit_mb: u64,
202    #[garde(range(min = 1))]
203    pub cpu_limit_millis: u32,
204    #[garde(range(min = 1))]
205    pub disk_limit_mb: u64,
206    #[garde(skip)]
207    pub network_access: ZipProjectNetworkAccess,
208}
209
210/// Optional hardware metadata advertised with a resource profile.
211#[derive(Debug, Clone, Serialize, Deserialize, garde::Validate, schemars::JsonSchema)]
212#[garde(allow_unvalidated)]
213pub struct HardwareProfileSpec {
214    #[garde(custom(crate::validation::trimmed_non_empty))]
215    pub kind: String,
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    #[garde(custom(crate::validation::optional_trimmed_non_empty))]
218    pub gpu_model: Option<String>,
219    #[serde(default, skip_serializing_if = "Option::is_none")]
220    #[garde(range(min = 1))]
221    pub gpu_count: Option<u32>,
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    #[garde(range(min = 1))]
224    pub gpu_memory_gb: Option<u64>,
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    #[garde(custom(crate::validation::optional_trimmed_non_empty))]
227    pub cuda_variant: Option<String>,
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    #[garde(custom(crate::validation::optional_trimmed_non_empty))]
230    pub cuda_version: Option<String>,
231    #[serde(default, skip_serializing_if = "Option::is_none")]
232    #[garde(custom(crate::validation::optional_trimmed_non_empty))]
233    pub driver_minimum: Option<String>,
234}