1use crate::{
14 AgentId, ArtifactId, DelegationId, EnumParseError, NoteId, ScopeId, TenantId, Timestamp,
15 TrajectoryId,
16};
17use serde::{Deserialize, Serialize};
18use std::fmt;
19use std::marker::PhantomData;
20use std::str::FromStr;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
30pub enum DelegationStatus {
31 Pending,
33 Accepted,
35 InProgress,
37 Completed,
39 Rejected,
41 Failed,
43}
44
45impl DelegationStatus {
46 pub fn as_db_str(&self) -> &'static str {
48 match self {
49 DelegationStatus::Pending => "pending",
50 DelegationStatus::Accepted => "accepted",
51 DelegationStatus::InProgress => "in_progress",
52 DelegationStatus::Completed => "completed",
53 DelegationStatus::Rejected => "rejected",
54 DelegationStatus::Failed => "failed",
55 }
56 }
57
58 pub fn from_db_str(s: &str) -> Result<Self, EnumParseError> {
60 match s.to_lowercase().as_str() {
61 "pending" => Ok(DelegationStatus::Pending),
62 "accepted" => Ok(DelegationStatus::Accepted),
63 "inprogress" | "in_progress" | "in-progress" => Ok(DelegationStatus::InProgress),
64 "completed" | "complete" => Ok(DelegationStatus::Completed),
65 "rejected" => Ok(DelegationStatus::Rejected),
66 "failed" | "failure" => Ok(DelegationStatus::Failed),
67 _ => Err(EnumParseError {
68 enum_name: "DelegationStatus",
69 input: s.to_string(),
70 }),
71 }
72 }
73
74 pub fn is_terminal(&self) -> bool {
76 matches!(
77 self,
78 DelegationStatus::Completed | DelegationStatus::Rejected | DelegationStatus::Failed
79 )
80 }
81}
82
83impl fmt::Display for DelegationStatus {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 write!(f, "{}", self.as_db_str())
86 }
87}
88
89impl FromStr for DelegationStatus {
90 type Err = EnumParseError;
91
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
93 Self::from_db_str(s)
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
103#[serde(rename_all = "snake_case")]
104#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
105pub enum DelegationResultStatus {
106 Success,
108 Partial,
110 Failure,
112}
113
114impl DelegationResultStatus {
115 pub fn as_db_str(&self) -> &'static str {
117 match self {
118 DelegationResultStatus::Success => "success",
119 DelegationResultStatus::Partial => "partial",
120 DelegationResultStatus::Failure => "failure",
121 }
122 }
123
124 pub fn from_db_str(s: &str) -> Result<Self, EnumParseError> {
126 match s.to_lowercase().as_str() {
127 "success" => Ok(DelegationResultStatus::Success),
128 "partial" => Ok(DelegationResultStatus::Partial),
129 "failure" | "failed" => Ok(DelegationResultStatus::Failure),
130 _ => Err(EnumParseError {
131 enum_name: "DelegationResultStatus",
132 input: s.to_string(),
133 }),
134 }
135 }
136}
137
138impl fmt::Display for DelegationResultStatus {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 write!(f, "{}", self.as_db_str())
141 }
142}
143
144impl FromStr for DelegationResultStatus {
145 type Err = EnumParseError;
146
147 fn from_str(s: &str) -> Result<Self, Self::Err> {
148 Self::from_db_str(s)
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
159pub struct DelegationResult {
160 pub status: DelegationResultStatus,
162 #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
164 pub produced_artifacts: Vec<ArtifactId>,
165 #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
167 pub produced_notes: Vec<NoteId>,
168 pub summary: String,
170 pub error: Option<String>,
172}
173
174impl DelegationResult {
175 pub fn success(summary: &str, artifacts: Vec<ArtifactId>) -> Self {
177 Self {
178 status: DelegationResultStatus::Success,
179 produced_artifacts: artifacts,
180 produced_notes: Vec::new(),
181 summary: summary.to_string(),
182 error: None,
183 }
184 }
185
186 pub fn partial(summary: &str, artifacts: Vec<ArtifactId>) -> Self {
188 Self {
189 status: DelegationResultStatus::Partial,
190 produced_artifacts: artifacts,
191 produced_notes: Vec::new(),
192 summary: summary.to_string(),
193 error: None,
194 }
195 }
196
197 pub fn failure(error: &str) -> Self {
199 Self {
200 status: DelegationResultStatus::Failure,
201 produced_artifacts: Vec::new(),
202 produced_notes: Vec::new(),
203 summary: String::new(),
204 error: Some(error.to_string()),
205 }
206 }
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
217pub struct DelegationRecord {
218 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
219 pub delegation_id: DelegationId,
220 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
221 pub tenant_id: TenantId,
222 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
223 pub from_agent_id: AgentId,
224 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
225 pub to_agent_id: AgentId,
226 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
227 pub trajectory_id: TrajectoryId,
228 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
229 pub scope_id: ScopeId,
230 pub task_description: String,
231 #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
232 pub created_at: Timestamp,
233 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
234 pub accepted_at: Option<Timestamp>,
235 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
236 pub started_at: Option<Timestamp>,
237 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
238 pub completed_at: Option<Timestamp>,
239 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
240 pub expected_completion: Option<Timestamp>,
241 pub result: Option<DelegationResult>,
242 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
243 pub context: Option<serde_json::Value>,
244 pub rejection_reason: Option<String>,
246 pub failure_reason: Option<String>,
248}
249
250pub trait DelegationState: private::Sealed + Send + Sync {}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub struct Pending;
260impl DelegationState for Pending {}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub struct DelegationAccepted;
265impl DelegationState for DelegationAccepted {}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub struct InProgress;
270impl DelegationState for InProgress {}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub struct DelegationCompleted;
275impl DelegationState for DelegationCompleted {}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub struct DelegationRejected;
280impl DelegationState for DelegationRejected {}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284pub struct DelegationFailed;
285impl DelegationState for DelegationFailed {}
286
287mod private {
288 pub trait Sealed {}
289 impl Sealed for super::Pending {}
290 impl Sealed for super::DelegationAccepted {}
291 impl Sealed for super::InProgress {}
292 impl Sealed for super::DelegationCompleted {}
293 impl Sealed for super::DelegationRejected {}
294 impl Sealed for super::DelegationFailed {}
295}
296
297#[derive(Debug, Clone)]
312pub struct Delegation<S: DelegationState> {
313 data: DelegationRecord,
314 _state: PhantomData<S>,
315}
316
317impl<S: DelegationState> Delegation<S> {
318 pub fn data(&self) -> &DelegationRecord {
320 &self.data
321 }
322
323 pub fn delegation_id(&self) -> DelegationId {
325 self.data.delegation_id
326 }
327
328 pub fn tenant_id(&self) -> TenantId {
330 self.data.tenant_id
331 }
332
333 pub fn from_agent_id(&self) -> AgentId {
335 self.data.from_agent_id
336 }
337
338 pub fn to_agent_id(&self) -> AgentId {
340 self.data.to_agent_id
341 }
342
343 pub fn trajectory_id(&self) -> TrajectoryId {
345 self.data.trajectory_id
346 }
347
348 pub fn scope_id(&self) -> ScopeId {
350 self.data.scope_id
351 }
352
353 pub fn task_description(&self) -> &str {
355 &self.data.task_description
356 }
357
358 pub fn created_at(&self) -> Timestamp {
360 self.data.created_at
361 }
362
363 pub fn expected_completion(&self) -> Option<Timestamp> {
365 self.data.expected_completion
366 }
367
368 pub fn context(&self) -> Option<&serde_json::Value> {
370 self.data.context.as_ref()
371 }
372
373 pub fn into_data(self) -> DelegationRecord {
375 self.data
376 }
377}
378
379impl Delegation<Pending> {
380 pub fn new(data: DelegationRecord) -> Self {
382 Delegation {
383 data,
384 _state: PhantomData,
385 }
386 }
387
388 pub fn accept(mut self, accepted_at: Timestamp) -> Delegation<DelegationAccepted> {
393 self.data.accepted_at = Some(accepted_at);
394 Delegation {
395 data: self.data,
396 _state: PhantomData,
397 }
398 }
399
400 pub fn reject(mut self, reason: String) -> Delegation<DelegationRejected> {
405 self.data.rejection_reason = Some(reason);
406 Delegation {
407 data: self.data,
408 _state: PhantomData,
409 }
410 }
411
412 pub fn fail(mut self, reason: String) -> Delegation<DelegationFailed> {
417 self.data.failure_reason = Some(reason);
418 Delegation {
419 data: self.data,
420 _state: PhantomData,
421 }
422 }
423}
424
425impl Delegation<DelegationAccepted> {
426 pub fn accepted_at(&self) -> Timestamp {
428 self.data
429 .accepted_at
430 .expect("Accepted delegation must have accepted_at")
431 }
432
433 pub fn start(mut self, started_at: Timestamp) -> Delegation<InProgress> {
438 self.data.started_at = Some(started_at);
439 Delegation {
440 data: self.data,
441 _state: PhantomData,
442 }
443 }
444
445 pub fn fail(mut self, reason: String) -> Delegation<DelegationFailed> {
450 self.data.failure_reason = Some(reason);
451 Delegation {
452 data: self.data,
453 _state: PhantomData,
454 }
455 }
456}
457
458impl Delegation<InProgress> {
459 pub fn accepted_at(&self) -> Timestamp {
461 self.data
462 .accepted_at
463 .expect("In-progress delegation must have accepted_at")
464 }
465
466 pub fn started_at(&self) -> Timestamp {
468 self.data
469 .started_at
470 .expect("In-progress delegation must have started_at")
471 }
472
473 pub fn complete(
478 mut self,
479 completed_at: Timestamp,
480 result: DelegationResult,
481 ) -> Delegation<DelegationCompleted> {
482 self.data.completed_at = Some(completed_at);
483 self.data.result = Some(result);
484 Delegation {
485 data: self.data,
486 _state: PhantomData,
487 }
488 }
489
490 pub fn fail(mut self, reason: String) -> Delegation<DelegationFailed> {
495 self.data.failure_reason = Some(reason);
496 Delegation {
497 data: self.data,
498 _state: PhantomData,
499 }
500 }
501}
502
503impl Delegation<DelegationCompleted> {
504 pub fn accepted_at(&self) -> Timestamp {
506 self.data
507 .accepted_at
508 .expect("Completed delegation must have accepted_at")
509 }
510
511 pub fn started_at(&self) -> Timestamp {
513 self.data
514 .started_at
515 .expect("Completed delegation must have started_at")
516 }
517
518 pub fn completed_at(&self) -> Timestamp {
520 self.data
521 .completed_at
522 .expect("Completed delegation must have completed_at")
523 }
524
525 pub fn result(&self) -> &DelegationResult {
527 self.data
528 .result
529 .as_ref()
530 .expect("Completed delegation must have result")
531 }
532}
533
534impl Delegation<DelegationRejected> {
535 pub fn rejection_reason(&self) -> &str {
537 self.data
538 .rejection_reason
539 .as_deref()
540 .unwrap_or("No reason provided")
541 }
542}
543
544impl Delegation<DelegationFailed> {
545 pub fn failure_reason(&self) -> &str {
547 self.data
548 .failure_reason
549 .as_deref()
550 .unwrap_or("No reason provided")
551 }
552}
553
554#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
563pub struct StoredDelegation {
564 pub data: DelegationRecord,
565 pub status: DelegationStatus,
566}
567
568#[derive(Debug, Clone)]
571pub enum LoadedDelegation {
572 Pending(Delegation<Pending>),
573 Accepted(Delegation<DelegationAccepted>),
574 InProgress(Delegation<InProgress>),
575 Completed(Delegation<DelegationCompleted>),
576 Rejected(Delegation<DelegationRejected>),
577 Failed(Delegation<DelegationFailed>),
578}
579
580impl StoredDelegation {
581 pub fn into_typed(self) -> LoadedDelegation {
583 match self.status {
584 DelegationStatus::Pending => LoadedDelegation::Pending(Delegation {
585 data: self.data,
586 _state: PhantomData,
587 }),
588 DelegationStatus::Accepted => LoadedDelegation::Accepted(Delegation {
589 data: self.data,
590 _state: PhantomData,
591 }),
592 DelegationStatus::InProgress => LoadedDelegation::InProgress(Delegation {
593 data: self.data,
594 _state: PhantomData,
595 }),
596 DelegationStatus::Completed => LoadedDelegation::Completed(Delegation {
597 data: self.data,
598 _state: PhantomData,
599 }),
600 DelegationStatus::Rejected => LoadedDelegation::Rejected(Delegation {
601 data: self.data,
602 _state: PhantomData,
603 }),
604 DelegationStatus::Failed => LoadedDelegation::Failed(Delegation {
605 data: self.data,
606 _state: PhantomData,
607 }),
608 }
609 }
610
611 pub fn into_pending(self) -> Result<Delegation<Pending>, DelegationStateError> {
613 if self.status != DelegationStatus::Pending {
614 return Err(DelegationStateError::WrongState {
615 delegation_id: self.data.delegation_id,
616 expected: DelegationStatus::Pending,
617 actual: self.status,
618 });
619 }
620 Ok(Delegation {
621 data: self.data,
622 _state: PhantomData,
623 })
624 }
625
626 pub fn into_accepted(self) -> Result<Delegation<DelegationAccepted>, DelegationStateError> {
628 if self.status != DelegationStatus::Accepted {
629 return Err(DelegationStateError::WrongState {
630 delegation_id: self.data.delegation_id,
631 expected: DelegationStatus::Accepted,
632 actual: self.status,
633 });
634 }
635 Ok(Delegation {
636 data: self.data,
637 _state: PhantomData,
638 })
639 }
640
641 pub fn into_in_progress(self) -> Result<Delegation<InProgress>, DelegationStateError> {
643 if self.status != DelegationStatus::InProgress {
644 return Err(DelegationStateError::WrongState {
645 delegation_id: self.data.delegation_id,
646 expected: DelegationStatus::InProgress,
647 actual: self.status,
648 });
649 }
650 Ok(Delegation {
651 data: self.data,
652 _state: PhantomData,
653 })
654 }
655
656 pub fn data(&self) -> &DelegationRecord {
658 &self.data
659 }
660
661 pub fn status(&self) -> DelegationStatus {
663 self.status
664 }
665}
666
667#[derive(Debug, Clone, PartialEq, Eq)]
669pub enum DelegationStateError {
670 WrongState {
672 delegation_id: DelegationId,
673 expected: DelegationStatus,
674 actual: DelegationStatus,
675 },
676}
677
678impl fmt::Display for DelegationStateError {
679 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
680 match self {
681 DelegationStateError::WrongState {
682 delegation_id,
683 expected,
684 actual,
685 } => {
686 write!(
687 f,
688 "Delegation {} is in state {} but expected {}",
689 delegation_id, actual, expected
690 )
691 }
692 }
693 }
694}
695
696impl std::error::Error for DelegationStateError {}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701 use crate::EntityIdType;
702 use chrono::Utc;
703
704 fn make_delegation_data() -> DelegationRecord {
705 let now = Utc::now();
706 DelegationRecord {
707 delegation_id: DelegationId::now_v7(),
708 tenant_id: TenantId::now_v7(),
709 from_agent_id: AgentId::now_v7(),
710 to_agent_id: AgentId::now_v7(),
711 trajectory_id: TrajectoryId::now_v7(),
712 scope_id: ScopeId::now_v7(),
713 task_description: "Analyze codebase".to_string(),
714 created_at: now,
715 accepted_at: None,
716 started_at: None,
717 completed_at: None,
718 expected_completion: Some(now + chrono::Duration::hours(1)),
719 result: None,
720 context: None,
721 rejection_reason: None,
722 failure_reason: None,
723 }
724 }
725
726 #[test]
727 fn test_delegation_status_roundtrip() {
728 for status in [
729 DelegationStatus::Pending,
730 DelegationStatus::Accepted,
731 DelegationStatus::InProgress,
732 DelegationStatus::Completed,
733 DelegationStatus::Rejected,
734 DelegationStatus::Failed,
735 ] {
736 let db_str = status.as_db_str();
737 let parsed = DelegationStatus::from_db_str(db_str)
738 .expect("DelegationStatus roundtrip should succeed");
739 assert_eq!(status, parsed);
740 }
741 }
742
743 #[test]
744 fn test_delegation_happy_path() {
745 let now = Utc::now();
746 let data = make_delegation_data();
747 let delegation = Delegation::<Pending>::new(data);
748
749 let accepted = delegation.accept(now);
750 assert_eq!(accepted.accepted_at(), now);
751
752 let in_progress = accepted.start(now);
753 assert_eq!(in_progress.started_at(), now);
754
755 let result = DelegationResult::success("Done", vec![]);
756 let completed = in_progress.complete(now, result);
757 assert_eq!(completed.result().status, DelegationResultStatus::Success);
758 }
759
760 #[test]
761 fn test_delegation_result_constructors() {
762 let success = DelegationResult::success("Done", vec![]);
763 assert_eq!(success.status, DelegationResultStatus::Success);
764 assert!(success.error.is_none());
765
766 let failure = DelegationResult::failure("Oops");
767 assert_eq!(failure.status, DelegationResultStatus::Failure);
768 assert_eq!(failure.error, Some("Oops".to_string()));
769 }
770
771 #[test]
772 fn test_delegation_reject() {
773 let data = make_delegation_data();
774 let delegation = Delegation::<Pending>::new(data);
775
776 let rejected = delegation.reject("Not available".to_string());
777 assert_eq!(rejected.rejection_reason(), "Not available");
778 }
779
780 #[test]
781 fn test_delegation_fail() {
782 let now = Utc::now();
783 let data = make_delegation_data();
784 let delegation = Delegation::<Pending>::new(data);
785
786 let accepted = delegation.accept(now);
787 let in_progress = accepted.start(now);
788 let failed = in_progress.fail("Timeout".to_string());
789 assert_eq!(failed.failure_reason(), "Timeout");
790 }
791
792 #[test]
793 fn test_stored_delegation_conversion() {
794 let data = make_delegation_data();
795 let stored = StoredDelegation {
796 data: data.clone(),
797 status: DelegationStatus::Pending,
798 };
799
800 let pending = stored
801 .into_pending()
802 .expect("pending delegation should convert successfully");
803 assert_eq!(pending.delegation_id(), data.delegation_id);
804 }
805
806 #[test]
807 fn test_stored_delegation_wrong_state() {
808 let data = make_delegation_data();
809 let stored = StoredDelegation {
810 data,
811 status: DelegationStatus::Accepted,
812 };
813
814 assert!(matches!(
815 stored.into_pending(),
816 Err(DelegationStateError::WrongState { .. })
817 ));
818 }
819}