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