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
250define_state_machine! {
255 pub trait DelegationState;
257 Pending,
259 DelegationAccepted,
261 InProgress,
263 DelegationCompleted,
265 DelegationRejected,
267 DelegationFailed,
269}
270
271define_typestate_wrapper! {
272 pub struct Delegation<DelegationState> wraps DelegationRecord;
283 initial_state: Pending;
284}
285
286impl<S: DelegationState> Delegation<S> {
287 pub fn delegation_id(&self) -> DelegationId {
289 self.data.delegation_id
290 }
291
292 pub fn tenant_id(&self) -> TenantId {
294 self.data.tenant_id
295 }
296
297 pub fn from_agent_id(&self) -> AgentId {
299 self.data.from_agent_id
300 }
301
302 pub fn to_agent_id(&self) -> AgentId {
304 self.data.to_agent_id
305 }
306
307 pub fn trajectory_id(&self) -> TrajectoryId {
309 self.data.trajectory_id
310 }
311
312 pub fn scope_id(&self) -> ScopeId {
314 self.data.scope_id
315 }
316
317 pub fn task_description(&self) -> &str {
319 &self.data.task_description
320 }
321
322 pub fn created_at(&self) -> Timestamp {
324 self.data.created_at
325 }
326
327 pub fn expected_completion(&self) -> Option<Timestamp> {
329 self.data.expected_completion
330 }
331
332 pub fn context(&self) -> Option<&serde_json::Value> {
334 self.data.context.as_ref()
335 }
336}
337
338impl Delegation<Pending> {
339 pub fn accept(mut self, accepted_at: Timestamp) -> Delegation<DelegationAccepted> {
344 self.data.accepted_at = Some(accepted_at);
345 Delegation {
346 data: self.data,
347 _state: PhantomData,
348 }
349 }
350
351 pub fn reject(mut self, reason: String) -> Delegation<DelegationRejected> {
356 self.data.rejection_reason = Some(reason);
357 Delegation {
358 data: self.data,
359 _state: PhantomData,
360 }
361 }
362
363 pub fn fail(mut self, reason: String) -> Delegation<DelegationFailed> {
368 self.data.failure_reason = Some(reason);
369 Delegation {
370 data: self.data,
371 _state: PhantomData,
372 }
373 }
374}
375
376impl Delegation<DelegationAccepted> {
377 pub fn accepted_at(&self) -> Timestamp {
379 self.data
380 .accepted_at
381 .expect("Accepted delegation must have accepted_at")
382 }
383
384 pub fn start(mut self, started_at: Timestamp) -> Delegation<InProgress> {
389 self.data.started_at = Some(started_at);
390 Delegation {
391 data: self.data,
392 _state: PhantomData,
393 }
394 }
395
396 pub fn fail(mut self, reason: String) -> Delegation<DelegationFailed> {
401 self.data.failure_reason = Some(reason);
402 Delegation {
403 data: self.data,
404 _state: PhantomData,
405 }
406 }
407}
408
409impl Delegation<InProgress> {
410 pub fn accepted_at(&self) -> Timestamp {
412 self.data
413 .accepted_at
414 .expect("In-progress delegation must have accepted_at")
415 }
416
417 pub fn started_at(&self) -> Timestamp {
419 self.data
420 .started_at
421 .expect("In-progress delegation must have started_at")
422 }
423
424 pub fn complete(
429 mut self,
430 completed_at: Timestamp,
431 result: DelegationResult,
432 ) -> Delegation<DelegationCompleted> {
433 self.data.completed_at = Some(completed_at);
434 self.data.result = Some(result);
435 Delegation {
436 data: self.data,
437 _state: PhantomData,
438 }
439 }
440
441 pub fn fail(mut self, reason: String) -> Delegation<DelegationFailed> {
446 self.data.failure_reason = Some(reason);
447 Delegation {
448 data: self.data,
449 _state: PhantomData,
450 }
451 }
452}
453
454impl Delegation<DelegationCompleted> {
455 pub fn accepted_at(&self) -> Timestamp {
457 self.data
458 .accepted_at
459 .expect("Completed delegation must have accepted_at")
460 }
461
462 pub fn started_at(&self) -> Timestamp {
464 self.data
465 .started_at
466 .expect("Completed delegation must have started_at")
467 }
468
469 pub fn completed_at(&self) -> Timestamp {
471 self.data
472 .completed_at
473 .expect("Completed delegation must have completed_at")
474 }
475
476 pub fn result(&self) -> &DelegationResult {
478 self.data
479 .result
480 .as_ref()
481 .expect("Completed delegation must have result")
482 }
483}
484
485impl Delegation<DelegationRejected> {
486 pub fn rejection_reason(&self) -> &str {
488 self.data
489 .rejection_reason
490 .as_deref()
491 .unwrap_or("No reason provided")
492 }
493}
494
495impl Delegation<DelegationFailed> {
496 pub fn failure_reason(&self) -> &str {
498 self.data
499 .failure_reason
500 .as_deref()
501 .unwrap_or("No reason provided")
502 }
503}
504
505#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
514pub struct StoredDelegation {
515 pub data: DelegationRecord,
516 pub status: DelegationStatus,
517}
518
519#[derive(Debug, Clone)]
522pub enum LoadedDelegation {
523 Pending(Delegation<Pending>),
524 Accepted(Delegation<DelegationAccepted>),
525 InProgress(Delegation<InProgress>),
526 Completed(Delegation<DelegationCompleted>),
527 Rejected(Delegation<DelegationRejected>),
528 Failed(Delegation<DelegationFailed>),
529}
530
531impl StoredDelegation {
532 pub fn into_typed(self) -> LoadedDelegation {
534 match self.status {
535 DelegationStatus::Pending => LoadedDelegation::Pending(Delegation {
536 data: self.data,
537 _state: PhantomData,
538 }),
539 DelegationStatus::Accepted => LoadedDelegation::Accepted(Delegation {
540 data: self.data,
541 _state: PhantomData,
542 }),
543 DelegationStatus::InProgress => LoadedDelegation::InProgress(Delegation {
544 data: self.data,
545 _state: PhantomData,
546 }),
547 DelegationStatus::Completed => LoadedDelegation::Completed(Delegation {
548 data: self.data,
549 _state: PhantomData,
550 }),
551 DelegationStatus::Rejected => LoadedDelegation::Rejected(Delegation {
552 data: self.data,
553 _state: PhantomData,
554 }),
555 DelegationStatus::Failed => LoadedDelegation::Failed(Delegation {
556 data: self.data,
557 _state: PhantomData,
558 }),
559 }
560 }
561
562 pub fn into_pending(self) -> Result<Delegation<Pending>, DelegationStateError> {
564 if self.status != DelegationStatus::Pending {
565 return Err(DelegationStateError::WrongState {
566 delegation_id: self.data.delegation_id,
567 expected: DelegationStatus::Pending,
568 actual: self.status,
569 });
570 }
571 Ok(Delegation {
572 data: self.data,
573 _state: PhantomData,
574 })
575 }
576
577 pub fn into_accepted(self) -> Result<Delegation<DelegationAccepted>, DelegationStateError> {
579 if self.status != DelegationStatus::Accepted {
580 return Err(DelegationStateError::WrongState {
581 delegation_id: self.data.delegation_id,
582 expected: DelegationStatus::Accepted,
583 actual: self.status,
584 });
585 }
586 Ok(Delegation {
587 data: self.data,
588 _state: PhantomData,
589 })
590 }
591
592 pub fn into_in_progress(self) -> Result<Delegation<InProgress>, DelegationStateError> {
594 if self.status != DelegationStatus::InProgress {
595 return Err(DelegationStateError::WrongState {
596 delegation_id: self.data.delegation_id,
597 expected: DelegationStatus::InProgress,
598 actual: self.status,
599 });
600 }
601 Ok(Delegation {
602 data: self.data,
603 _state: PhantomData,
604 })
605 }
606
607 pub fn data(&self) -> &DelegationRecord {
609 &self.data
610 }
611
612 pub fn status(&self) -> DelegationStatus {
614 self.status
615 }
616}
617
618#[derive(Debug, Clone, PartialEq, Eq)]
620pub enum DelegationStateError {
621 WrongState {
623 delegation_id: DelegationId,
624 expected: DelegationStatus,
625 actual: DelegationStatus,
626 },
627}
628
629impl fmt::Display for DelegationStateError {
630 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
631 match self {
632 DelegationStateError::WrongState {
633 delegation_id,
634 expected,
635 actual,
636 } => {
637 write!(
638 f,
639 "Delegation {} is in state {} but expected {}",
640 delegation_id, actual, expected
641 )
642 }
643 }
644 }
645}
646
647impl std::error::Error for DelegationStateError {}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652 use crate::EntityIdType;
653 use chrono::Utc;
654
655 fn make_delegation_data() -> DelegationRecord {
656 let now = Utc::now();
657 DelegationRecord {
658 delegation_id: DelegationId::now_v7(),
659 tenant_id: TenantId::now_v7(),
660 from_agent_id: AgentId::now_v7(),
661 to_agent_id: AgentId::now_v7(),
662 trajectory_id: TrajectoryId::now_v7(),
663 scope_id: ScopeId::now_v7(),
664 task_description: "Analyze codebase".to_string(),
665 created_at: now,
666 accepted_at: None,
667 started_at: None,
668 completed_at: None,
669 expected_completion: Some(now + chrono::Duration::hours(1)),
670 result: None,
671 context: None,
672 rejection_reason: None,
673 failure_reason: None,
674 }
675 }
676
677 #[test]
678 fn test_delegation_status_roundtrip() {
679 for status in [
680 DelegationStatus::Pending,
681 DelegationStatus::Accepted,
682 DelegationStatus::InProgress,
683 DelegationStatus::Completed,
684 DelegationStatus::Rejected,
685 DelegationStatus::Failed,
686 ] {
687 let db_str = status.as_db_str();
688 let parsed = DelegationStatus::from_db_str(db_str)
689 .expect("DelegationStatus roundtrip should succeed");
690 assert_eq!(status, parsed);
691 }
692 }
693
694 #[test]
695 fn test_delegation_happy_path() {
696 let now = Utc::now();
697 let data = make_delegation_data();
698 let delegation = Delegation::<Pending>::new(data);
699
700 let accepted = delegation.accept(now);
701 assert_eq!(accepted.accepted_at(), now);
702
703 let in_progress = accepted.start(now);
704 assert_eq!(in_progress.started_at(), now);
705
706 let result = DelegationResult::success("Done", vec![]);
707 let completed = in_progress.complete(now, result);
708 assert_eq!(completed.result().status, DelegationResultStatus::Success);
709 }
710
711 #[test]
712 fn test_delegation_result_constructors() {
713 let success = DelegationResult::success("Done", vec![]);
714 assert_eq!(success.status, DelegationResultStatus::Success);
715 assert!(success.error.is_none());
716
717 let failure = DelegationResult::failure("Oops");
718 assert_eq!(failure.status, DelegationResultStatus::Failure);
719 assert_eq!(failure.error, Some("Oops".to_string()));
720 }
721
722 #[test]
723 fn test_delegation_reject() {
724 let data = make_delegation_data();
725 let delegation = Delegation::<Pending>::new(data);
726
727 let rejected = delegation.reject("Not available".to_string());
728 assert_eq!(rejected.rejection_reason(), "Not available");
729 }
730
731 #[test]
732 fn test_delegation_fail() {
733 let now = Utc::now();
734 let data = make_delegation_data();
735 let delegation = Delegation::<Pending>::new(data);
736
737 let accepted = delegation.accept(now);
738 let in_progress = accepted.start(now);
739 let failed = in_progress.fail("Timeout".to_string());
740 assert_eq!(failed.failure_reason(), "Timeout");
741 }
742
743 #[test]
744 fn test_stored_delegation_conversion() {
745 let data = make_delegation_data();
746 let stored = StoredDelegation {
747 data: data.clone(),
748 status: DelegationStatus::Pending,
749 };
750
751 let pending = stored
752 .into_pending()
753 .expect("pending delegation should convert successfully");
754 assert_eq!(pending.delegation_id(), data.delegation_id);
755 }
756
757 #[test]
758 fn test_stored_delegation_wrong_state() {
759 let data = make_delegation_data();
760 let stored = StoredDelegation {
761 data,
762 status: DelegationStatus::Accepted,
763 };
764
765 assert!(matches!(
766 stored.into_pending(),
767 Err(DelegationStateError::WrongState { .. })
768 ));
769 }
770}