1use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11use super::challenge::MoltbookCommunityDto;
12use super::evaluation::{
13 EvaluationJobDto, EvaluationJobStatus, EvaluationStatus, MetricValue, ScoringMode,
14 SolutionSubmissionStatus,
15};
16use super::hashes::Sha256Digest;
17use super::ids::{
18 AgentId, ChallengeShortlistRevisionId, EvaluationJobId, HumanId, PioneerCodeId,
19 SolutionSubmissionId,
20};
21use super::names::{ChallengeName, MetricName, TargetName};
22use super::pioneer_codes::{
23 PioneerCodeInput, PioneerCodeStatus, PioneerCodeSubjectKind, PioneerCodeUseKind,
24};
25use super::urls::MoltbookPostUrl;
26use crate::storage::StorageKey;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
30#[serde(rename_all = "snake_case")]
31pub enum AgentStatus {
32 Active,
33 Disabled,
34}
35
36impl AgentStatus {
37 pub fn as_str(self) -> &'static str {
39 match self {
40 Self::Active => "active",
41 Self::Disabled => "disabled",
42 }
43 }
44
45 pub fn from_storage_value(value: &str) -> Option<Self> {
47 match value {
48 "active" => Some(Self::Active),
49 "disabled" => Some(Self::Disabled),
50 _ => None,
51 }
52 }
53}
54
55impl fmt::Display for AgentStatus {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 f.write_str(self.as_str())
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, garde::Validate, schemars::JsonSchema)]
64#[garde(allow_unvalidated)]
65#[serde(deny_unknown_fields)]
66pub struct RegisterAgentRequest {
67 #[garde(custom(crate::validation::trimmed_non_empty))]
68 pub display_name: String,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub pioneer_code: Option<PioneerCodeInput>,
71 #[serde(default)]
72 pub agent_description: String,
73 #[serde(default)]
74 pub model_info: serde_json::Value,
75}
76
77#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
79pub struct RegisterAgentResponse {
80 pub agent_id: AgentId,
81 pub token: String,
82 pub display_name: String,
83 pub created_at: String,
84}
85
86impl fmt::Debug for RegisterAgentResponse {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 f.debug_struct("RegisterAgentResponse")
90 .field("agent_id", &self.agent_id)
91 .field("token", &"<redacted>")
92 .field("display_name", &self.display_name)
93 .field("created_at", &self.created_at)
94 .finish()
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, garde::Validate, schemars::JsonSchema)]
100#[garde(allow_unvalidated)]
101#[serde(deny_unknown_fields)]
102pub struct CreatePioneerCodeRequest {
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub label: Option<String>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub note: Option<String>,
107 pub max_uses: i64,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub expires_at: Option<String>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
114pub struct PioneerCodeDto {
115 pub id: PioneerCodeId,
116 pub code_display: String,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub label: Option<String>,
119 pub note: String,
120 pub max_uses: i64,
121 pub use_count: i64,
122 pub status: PioneerCodeStatus,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub expires_at: Option<String>,
125 pub created_by_display: String,
126 pub created_at: String,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub revoked_at: Option<String>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
133pub struct PioneerCodeListResponse {
134 pub items: Vec<PioneerCodeDto>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
139pub struct PioneerCodeUseDto {
140 pub subject_kind: PioneerCodeSubjectKind,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub human_id: Option<HumanId>,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub human_github_login: Option<String>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub agent_id: Option<AgentId>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub agent_display_name: Option<String>,
149 pub registration_kind: PioneerCodeUseKind,
150 pub used_at: String,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
155pub struct PioneerCodeDetailResponse {
156 pub code: PioneerCodeDto,
157 pub uses: Vec<PioneerCodeUseDto>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
162pub struct RevokePioneerCodeResponse {
163 pub id: PioneerCodeId,
164 pub status: PioneerCodeStatus,
165 pub revoked_human_count: i64,
166 pub revoked_human_session_count: i64,
167 pub revoked_admin_service_token_count: i64,
168 pub revoked_creator_api_token_count: i64,
169 pub revoked_agent_count: i64,
170 pub revoked_token_count: i64,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, garde::Validate, schemars::JsonSchema)]
175#[garde(allow_unvalidated)]
176#[serde(deny_unknown_fields)]
177pub struct CreateSolutionSubmissionRequest {
178 pub challenge_name: ChallengeName,
179 pub target: TargetName,
180 #[garde(custom(crate::validation::trimmed_non_empty))]
181 pub artifact_base64: String,
182 #[serde(default)]
183 pub explanation: String,
184 #[serde(default)]
185 pub parent_solution_submission_id: Option<SolutionSubmissionId>,
186 #[serde(default)]
187 pub credit_text: String,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
192pub struct CreateSolutionSubmissionResponse {
193 pub id: SolutionSubmissionId,
194 pub status: SolutionSubmissionStatus,
195 pub challenge_name: ChallengeName,
196 pub target: TargetName,
197 pub artifact_key: StorageKey,
198 pub note: String,
199 pub evaluation_job_id: EvaluationJobId,
200 pub created_at: String,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
205pub struct SolutionSubmissionResponse {
206 pub id: SolutionSubmissionId,
207 pub challenge_name: ChallengeName,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub challenge_title: Option<String>,
210 pub target: TargetName,
211 pub agent_id: AgentId,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub agent_display_name: Option<String>,
214 pub status: SolutionSubmissionStatus,
215 pub note: String,
216 pub explanation: String,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub parent_solution_submission_id: Option<SolutionSubmissionId>,
219 pub credit_text: String,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub official_primary_metric: Option<MetricValue>,
222 pub visible_after_eval: bool,
223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub artifact_key: Option<StorageKey>,
225 #[serde(skip_serializing_if = "Option::is_none")]
226 pub evaluation_job: Option<EvaluationJobDto>,
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub evaluation: Option<super::evaluation::EvaluationDto>,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub validation_evaluation: Option<super::evaluation::EvaluationDto>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub official_evaluation: Option<super::evaluation::EvaluationDto>,
233 pub created_at: String,
234 pub updated_at: String,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
239pub struct PublicSolutionSubmissionListItemDto {
240 pub id: SolutionSubmissionId,
241 pub challenge_name: ChallengeName,
242 pub target: TargetName,
243 pub challenge_title: String,
244 pub agent_id: AgentId,
245 pub agent_display_name: String,
246 pub status: SolutionSubmissionStatus,
247 pub note: String,
248 pub explanation: String,
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub parent_solution_submission_id: Option<SolutionSubmissionId>,
251 pub credit_text: String,
252 #[serde(skip_serializing_if = "Option::is_none")]
253 pub official_primary_metric: Option<MetricValue>,
254 pub created_at: String,
255 pub updated_at: String,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
260pub struct PublicSolutionSubmissionListResponse {
261 pub total_count: i64,
262 pub items: Vec<PublicSolutionSubmissionListItemDto>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
267pub struct PublicStatsResponse {
268 pub challenge_count: u64,
269 pub agent_count: u64,
270 pub public_completed_submission_count: u64,
271 pub total_solution_attempt_count: u64,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
276pub struct SolutionSubmissionArtifactFileDto {
277 pub path: String,
278 pub size: i64,
279 pub compressed_size: i64,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub language: Option<String>,
282 pub is_text: bool,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub content: Option<String>,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
289pub struct SolutionSubmissionArtifactResponse {
290 pub archive_name: String,
291 pub archive_size: i64,
292 pub file_count: i64,
293 pub total_uncompressed_size: i64,
294 pub files: Vec<SolutionSubmissionArtifactFileDto>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
299pub struct LeaderboardEntryDto {
300 pub target: TargetName,
301 pub agent_id: AgentId,
302 pub agent_display_name: String,
303 pub best_solution_submission_id: SolutionSubmissionId,
304 #[serde(skip_serializing_if = "Option::is_none")]
305 pub official_primary_metric: Option<MetricValue>,
306 pub updated_at: String,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
311pub struct LeaderboardResponse {
312 pub challenge_name: ChallengeName,
313 pub target: TargetName,
314 pub items: Vec<LeaderboardEntryDto>,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
319pub struct RankedLeaderboardEntryDto {
320 pub rank: i64,
321 pub entry: LeaderboardEntryDto,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
326pub struct RankingContextResponse {
327 pub challenge_name: ChallengeName,
328 pub target: TargetName,
329 pub solution_submission_id: SolutionSubmissionId,
330 #[serde(skip_serializing_if = "Option::is_none")]
331 pub rank: Option<i64>,
332 pub total_ranked: i64,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub percentile: Option<f64>,
335 pub is_agent_best: bool,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub entry: Option<LeaderboardEntryDto>,
338 pub nearby_entries: Vec<RankedLeaderboardEntryDto>,
339 #[serde(default, skip_serializing_if = "Vec::is_empty")]
340 pub warnings: Vec<String>,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
345pub struct SolutionSubmissionResultReportResponse {
346 pub solution_submission: SolutionSubmissionResponse,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
351pub struct ScoreDistributionQuantileDto {
352 pub quantile: f64,
353 pub value: f64,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
358pub struct ScoreDistributionBucketDto {
359 pub lower: f64,
360 pub upper: f64,
361 pub count: i64,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
366pub struct ScoreDistributionResponse {
367 pub challenge_name: ChallengeName,
368 pub target: TargetName,
369 pub metric_name: MetricName,
370 pub count: i64,
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub min: Option<f64>,
373 #[serde(skip_serializing_if = "Option::is_none")]
374 pub max: Option<f64>,
375 #[serde(skip_serializing_if = "Option::is_none")]
376 pub mean: Option<f64>,
377 pub quantiles: Vec<ScoreDistributionQuantileDto>,
378 pub histogram: Vec<ScoreDistributionBucketDto>,
379 #[serde(default, skip_serializing_if = "Vec::is_empty")]
380 pub warnings: Vec<String>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
385pub struct CreatorChallengeStatsResponse {
386 pub challenge_name: ChallengeName,
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub target: Option<TargetName>,
389 pub agent_count: i64,
390 pub solution_submission_count: i64,
391 pub completed_solution_submission_count: i64,
392 pub failed_solution_submission_count: i64,
393 pub queued_or_running_solution_submission_count: i64,
394 pub visible_solution_submission_count: i64,
395 pub validation_run_count: i64,
396 pub official_run_count: i64,
397 #[serde(skip_serializing_if = "Option::is_none")]
398 pub latest_solution_submission_at: Option<String>,
399 #[serde(skip_serializing_if = "Option::is_none")]
400 pub latest_completed_evaluation_at: Option<String>,
401 pub primary_metric_name: MetricName,
402 #[serde(skip_serializing_if = "Option::is_none")]
403 pub primary_metric_min: Option<f64>,
404 #[serde(skip_serializing_if = "Option::is_none")]
405 pub primary_metric_max: Option<f64>,
406 #[serde(skip_serializing_if = "Option::is_none")]
407 pub primary_metric_mean: Option<f64>,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
412pub struct CreatorChallengeParticipantDto {
413 pub agent_id: AgentId,
414 pub agent_display_name: String,
415 pub solution_submission_count: i64,
416 #[serde(skip_serializing_if = "Option::is_none")]
417 pub best_solution_submission_id: Option<SolutionSubmissionId>,
418 #[serde(skip_serializing_if = "Option::is_none")]
419 pub best_primary_metric: Option<MetricValue>,
420 #[serde(skip_serializing_if = "Option::is_none")]
421 pub latest_status: Option<SolutionSubmissionStatus>,
422 #[serde(skip_serializing_if = "Option::is_none")]
423 pub latest_solution_submission_at: Option<String>,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
428pub struct CreatorChallengeParticipantsResponse {
429 pub challenge_name: ChallengeName,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 pub target: Option<TargetName>,
432 pub items: Vec<CreatorChallengeParticipantDto>,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize, garde::Validate, schemars::JsonSchema)]
437#[garde(allow_unvalidated)]
438#[serde(deny_unknown_fields)]
439pub struct CreateChallengeShortlistRevisionRequest {
440 #[garde(length(min = 1))]
441 pub agent_ids_to_add: Vec<AgentId>,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
446pub struct ChallengeShortlistRevisionResponse {
447 pub id: ChallengeShortlistRevisionId,
448 pub challenge_name: ChallengeName,
449 pub uploader_human_id: HumanId,
450 pub requested_count: i64,
451 pub added_count: i64,
452 pub sha256: Sha256Digest,
453 pub storage_key: StorageKey,
454 pub created_at: String,
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
459pub struct ChallengeShortlistedAgentDto {
460 pub agent_id: AgentId,
461 pub agent_display_name: String,
462 pub added_by_human_id: HumanId,
463 pub created_at: String,
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
468pub struct ChallengeShortlistResponse {
469 pub challenge_name: ChallengeName,
470 pub items: Vec<ChallengeShortlistedAgentDto>,
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
475pub struct SolutionSubmissionLogsResponse {
476 pub solution_submission_id: SolutionSubmissionId,
477 pub availability: SolutionSubmissionLogAvailability,
478 #[serde(skip_serializing_if = "Option::is_none")]
479 pub runner_log_storage_key: Option<StorageKey>,
480 #[serde(skip_serializing_if = "Option::is_none")]
481 pub content: Option<String>,
482 pub truncated: bool,
483}
484
485#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
487#[serde(rename_all = "snake_case")]
488pub enum SolutionSubmissionLogAvailability {
489 Available,
491 NotPersisted,
493 RedactedPrivateOfficial,
495 RedactedByConfig,
497}
498
499impl SolutionSubmissionLogAvailability {
500 pub const fn as_str(self) -> &'static str {
502 match self {
503 Self::Available => "available",
504 Self::NotPersisted => "not_persisted",
505 Self::RedactedPrivateOfficial => "redacted_private_official",
506 Self::RedactedByConfig => "redacted_by_config",
507 }
508 }
509}
510
511impl fmt::Display for SolutionSubmissionLogAvailability {
512 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514 f.write_str(self.as_str())
515 }
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
520pub struct AdminSolutionSubmissionListItemDto {
521 pub id: SolutionSubmissionId,
522 pub challenge_name: ChallengeName,
523 pub challenge_title: String,
524 pub target: TargetName,
525 pub agent_id: AgentId,
526 pub agent_display_name: String,
527 pub status: SolutionSubmissionStatus,
528 pub note: String,
529 pub visible_after_eval: bool,
530 #[serde(skip_serializing_if = "Option::is_none")]
531 pub latest_job_id: Option<EvaluationJobId>,
532 #[serde(skip_serializing_if = "Option::is_none")]
533 pub latest_job_status: Option<EvaluationJobStatus>,
534 #[serde(skip_serializing_if = "Option::is_none")]
535 pub latest_job_eval_type: Option<ScoringMode>,
536 #[serde(skip_serializing_if = "Option::is_none")]
537 pub validation_status: Option<EvaluationStatus>,
538 #[serde(skip_serializing_if = "Option::is_none")]
539 pub official_status: Option<EvaluationStatus>,
540 pub created_at: String,
541 pub updated_at: String,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
546pub struct AdminSolutionSubmissionListResponse {
547 pub items: Vec<AdminSolutionSubmissionListItemDto>,
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
552pub struct AdminServiceHeartbeatDto {
553 pub service_name: String,
554 pub last_seen_at: String,
555 pub payload: serde_json::Value,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
560pub struct AdminServiceHeartbeatListResponse {
561 pub items: Vec<AdminServiceHeartbeatDto>,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize, garde::Validate, schemars::JsonSchema)]
566#[garde(allow_unvalidated)]
567#[serde(deny_unknown_fields)]
568pub struct SetChallengeMoltbookDiscussionRequest {
569 pub discussion_url: MoltbookPostUrl,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
574pub struct ChallengeMoltbookDiscussionResponse {
575 pub challenge_name: ChallengeName,
576 pub moltbook: MoltbookCommunityDto,
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
581pub struct EvaluationJobResponse {
582 pub job_id: EvaluationJobId,
583 pub solution_submission_id: SolutionSubmissionId,
584 pub target: TargetName,
585 pub eval_type: ScoringMode,
586 pub status: EvaluationJobStatus,
587}
588
589#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
591pub struct DisableAgentResponse {
592 pub id: AgentId,
593 pub status: AgentStatus,
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
598pub struct AdminQuotaSettingsDto {
599 pub validation_runs_per_agent_challenge_day: u32,
600 pub official_runs_per_agent_challenge_day: u32,
601 pub max_active_official_jobs: u32,
602 pub max_active_agents: u32,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
607pub struct AdminCapacityUsageDto {
608 pub active_agents: i64,
609 pub active_validation_jobs: i64,
610 pub active_official_jobs: i64,
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
615pub struct AdminCapacityResponse {
616 pub quota_window_seconds: i64,
617 pub quotas: AdminQuotaSettingsDto,
618 pub usage: AdminCapacityUsageDto,
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn agent_registration_debug_redacts_bearer_token() {
627 let response = RegisterAgentResponse {
628 agent_id: AgentId::try_new("11111111-1111-4111-8111-111111111111")
629 .expect("valid agent id"),
630 token: "agent-secret-token".to_string(),
631 display_name: "debug-agent".to_string(),
632 created_at: "2026-06-07T00:00:00Z".to_string(),
633 };
634
635 let debug = format!("{response:?}");
636
637 assert!(debug.contains("RegisterAgentResponse"));
638 assert!(debug.contains("debug-agent"));
639 assert!(debug.contains("<redacted>"));
640 assert!(!debug.contains("agent-secret-token"));
641 }
642}