1use {
4 crate::{
5 scheduler::{FailurePolicy, OccurrenceRef},
6 sui,
7 },
8 serde::{Deserialize, Serialize},
9 std::time::Duration,
10};
11
12const DEFAULT_WATCH_TIMEOUT: Duration = Duration::from_secs(60 * 60);
13const DEFAULT_WATCH_POLL_INTERVAL: Duration = Duration::from_secs(1);
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(tag = "kind", rename_all = "snake_case")]
18pub enum TaskController {
19 Address {
21 address: sui::types::Address,
23 },
24 Agent {
26 agent_id: sui::types::Address,
28 },
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum TaskStatus {
35 Active,
37 Paused,
39 Canceled,
41 Rejected {
43 reason: TaskRejectionReason,
45 },
46 Finalized,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum TaskRejectionReason {
54 UnsupportedWorkAdmission,
56 StaleSkillContract,
58 MutableDAG,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(tag = "kind", rename_all = "snake_case")]
65pub enum OccurrenceSource {
66 Standalone,
68 Recurring {
70 iteration: u64,
72 },
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum WithdrawalReason {
79 RecurrenceReplaced,
81 RecurrenceCleared,
83 TaskCanceled,
85 TaskRejected,
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(tag = "status", rename_all = "snake_case")]
92pub enum OccurrenceStatus {
93 Pending,
95 Advertised,
97 Executing,
99 Finished,
101 Settled {
103 succeeded: bool,
105 },
106 Missed {
108 missed_at_ms: u64,
110 },
111 Withdrawn {
113 reason: WithdrawalReason,
115 },
116}
117
118impl OccurrenceStatus {
119 pub const fn is_terminal(self) -> bool {
121 matches!(
122 self,
123 Self::Settled { .. } | Self::Missed { .. } | Self::Withdrawn { .. }
124 )
125 }
126}
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
130pub struct TaskPointer {
131 task_pointer_id: sui::types::Address,
132 task_id: sui::types::Address,
133}
134
135impl TaskPointer {
136 pub(crate) const fn new(
137 task_pointer_id: sui::types::Address,
138 task_id: sui::types::Address,
139 ) -> Self {
140 Self {
141 task_pointer_id,
142 task_id,
143 }
144 }
145
146 pub const fn task_pointer_id(&self) -> sui::types::Address {
148 self.task_pointer_id
149 }
150
151 pub const fn task_id(&self) -> sui::types::Address {
153 self.task_id
154 }
155}
156
157#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
159pub struct TaskPointerPage {
160 task_pointers: Vec<TaskPointer>,
161 next_cursor: Option<Vec<u8>>,
162}
163
164impl TaskPointerPage {
165 pub(crate) fn new(task_pointers: Vec<TaskPointer>, next_cursor: Option<Vec<u8>>) -> Self {
166 Self {
167 task_pointers,
168 next_cursor,
169 }
170 }
171
172 pub fn task_pointers(&self) -> &[TaskPointer] {
174 &self.task_pointers
175 }
176
177 pub fn next_cursor(&self) -> Option<&[u8]> {
179 self.next_cursor.as_deref()
180 }
181
182 pub fn into_parts(self) -> (Vec<TaskPointer>, Option<Vec<u8>>) {
184 (self.task_pointers, self.next_cursor)
185 }
186}
187
188#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
190pub struct ExecutionSnapshot {
191 execution_id: sui::types::Address,
192 created_at_ms: Option<u64>,
193 active_walks: Option<u64>,
194 pending_abort_walks: Option<u64>,
195 pending_settlement_walks: Option<u64>,
196 successful_walks: Option<u64>,
197 failed_walks: Option<u64>,
198 aborted_walks: Option<u64>,
199}
200
201pub(crate) struct ExecutionObservation {
202 pub created_at_ms: u64,
203 pub active_walks: u64,
204 pub pending_abort_walks: u64,
205 pub pending_settlement_walks: u64,
206 pub successful_walks: u64,
207 pub failed_walks: u64,
208 pub aborted_walks: u64,
209}
210
211impl ExecutionSnapshot {
212 pub(crate) const fn unavailable(execution_id: sui::types::Address) -> Self {
213 Self {
214 execution_id,
215 created_at_ms: None,
216 active_walks: None,
217 pending_abort_walks: None,
218 pending_settlement_walks: None,
219 successful_walks: None,
220 failed_walks: None,
221 aborted_walks: None,
222 }
223 }
224
225 pub(crate) const fn observed(
226 execution_id: sui::types::Address,
227 observation: ExecutionObservation,
228 ) -> Self {
229 Self {
230 execution_id,
231 created_at_ms: Some(observation.created_at_ms),
232 active_walks: Some(observation.active_walks),
233 pending_abort_walks: Some(observation.pending_abort_walks),
234 pending_settlement_walks: Some(observation.pending_settlement_walks),
235 successful_walks: Some(observation.successful_walks),
236 failed_walks: Some(observation.failed_walks),
237 aborted_walks: Some(observation.aborted_walks),
238 }
239 }
240
241 pub const fn execution_id(&self) -> sui::types::Address {
243 self.execution_id
244 }
245
246 pub const fn created_at_ms(&self) -> Option<u64> {
248 self.created_at_ms
249 }
250
251 pub const fn active_walks(&self) -> Option<u64> {
253 self.active_walks
254 }
255
256 pub const fn pending_abort_walks(&self) -> Option<u64> {
258 self.pending_abort_walks
259 }
260
261 pub const fn pending_settlement_walks(&self) -> Option<u64> {
263 self.pending_settlement_walks
264 }
265
266 pub const fn successful_walks(&self) -> Option<u64> {
268 self.successful_walks
269 }
270
271 pub const fn failed_walks(&self) -> Option<u64> {
273 self.failed_walks
274 }
275
276 pub const fn aborted_walks(&self) -> Option<u64> {
278 self.aborted_walks
279 }
280}
281
282#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
284pub struct OccurrenceSnapshot {
285 pub(crate) reference: OccurrenceRef,
286 pub(crate) source: OccurrenceSource,
287 pub(crate) requested_start_time_ms: u64,
288 pub(crate) effective_start_time_ms: Option<u64>,
289 pub(crate) deadline_ms: Option<u64>,
290 pub(crate) priority_fee_percentage: u64,
291 pub(crate) dispatched_at_ms: Option<u64>,
292 pub(crate) settled_at_ms: Option<u64>,
293 pub(crate) status: OccurrenceStatus,
294 pub(crate) execution: Option<ExecutionSnapshot>,
295 pub(crate) observed_task_version: sui::types::Version,
296}
297
298impl OccurrenceSnapshot {
299 pub const fn reference(&self) -> OccurrenceRef {
301 self.reference
302 }
303
304 pub const fn source(&self) -> OccurrenceSource {
306 self.source
307 }
308
309 pub const fn requested_start_time_ms(&self) -> u64 {
311 self.requested_start_time_ms
312 }
313
314 pub const fn effective_start_time_ms(&self) -> Option<u64> {
316 self.effective_start_time_ms
317 }
318
319 pub const fn deadline_ms(&self) -> Option<u64> {
321 self.deadline_ms
322 }
323
324 pub const fn priority_fee_percentage(&self) -> u64 {
326 self.priority_fee_percentage
327 }
328
329 pub const fn dispatched_at_ms(&self) -> Option<u64> {
331 self.dispatched_at_ms
332 }
333
334 pub const fn settled_at_ms(&self) -> Option<u64> {
336 self.settled_at_ms
337 }
338
339 pub const fn status(&self) -> OccurrenceStatus {
341 self.status
342 }
343
344 pub const fn execution(&self) -> Option<&ExecutionSnapshot> {
346 self.execution.as_ref()
347 }
348
349 pub const fn observed_task_version(&self) -> sui::types::Version {
351 self.observed_task_version
352 }
353}
354
355#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
357pub struct TaskSnapshot {
358 pub(crate) task_id: sui::types::Address,
359 pub(crate) controller: TaskController,
360 pub(crate) status: TaskStatus,
361 pub(crate) failure_policy: FailurePolicy,
362 pub(crate) advertised: Option<OccurrenceRef>,
363 pub(crate) allocated_occurrences: u64,
364 pub(crate) pending_occurrences: u64,
365 pub(crate) dispatched_occurrences: u64,
366 pub(crate) in_flight_occurrences: u64,
367 pub(crate) observed_version: sui::types::Version,
368}
369
370impl TaskSnapshot {
371 pub const fn task_id(&self) -> sui::types::Address {
373 self.task_id
374 }
375
376 pub const fn controller(&self) -> TaskController {
378 self.controller
379 }
380
381 pub const fn status(&self) -> TaskStatus {
383 self.status
384 }
385
386 pub const fn failure_policy(&self) -> FailurePolicy {
388 self.failure_policy
389 }
390
391 pub const fn advertised(&self) -> Option<OccurrenceRef> {
393 self.advertised
394 }
395
396 pub const fn allocated_occurrences(&self) -> u64 {
398 self.allocated_occurrences
399 }
400
401 pub const fn pending_occurrences(&self) -> u64 {
403 self.pending_occurrences
404 }
405
406 pub const fn dispatched_occurrences(&self) -> u64 {
408 self.dispatched_occurrences
409 }
410
411 pub const fn in_flight_occurrences(&self) -> u64 {
413 self.in_flight_occurrences
414 }
415
416 pub const fn observed_version(&self) -> sui::types::Version {
418 self.observed_version
419 }
420}
421
422#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
424pub struct OccurrencePage {
425 occurrences: Vec<OccurrenceSnapshot>,
426 next_cursor: Option<Vec<u8>>,
427}
428
429impl OccurrencePage {
430 pub(crate) fn new(occurrences: Vec<OccurrenceSnapshot>, next_cursor: Option<Vec<u8>>) -> Self {
432 Self {
433 occurrences,
434 next_cursor,
435 }
436 }
437
438 pub fn occurrences(&self) -> &[OccurrenceSnapshot] {
440 &self.occurrences
441 }
442
443 pub fn next_cursor(&self) -> Option<&[u8]> {
445 self.next_cursor.as_deref()
446 }
447
448 pub fn into_parts(self) -> (Vec<OccurrenceSnapshot>, Option<Vec<u8>>) {
450 (self.occurrences, self.next_cursor)
451 }
452}
453
454#[derive(Clone, Copy, Debug, PartialEq, Eq)]
456pub struct WatchOptions {
457 timeout: Duration,
458 poll_interval: Duration,
459}
460
461impl WatchOptions {
462 pub const fn new(timeout: Duration, poll_interval: Duration) -> Self {
464 Self {
465 timeout,
466 poll_interval,
467 }
468 }
469
470 pub const fn timeout(self) -> Duration {
472 self.timeout
473 }
474
475 pub const fn poll_interval(self) -> Duration {
477 self.poll_interval
478 }
479}
480
481impl Default for WatchOptions {
482 fn default() -> Self {
483 Self::new(DEFAULT_WATCH_TIMEOUT, DEFAULT_WATCH_POLL_INTERVAL)
484 }
485}
486
487#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
489pub struct OccurrenceCost {
490 pub(crate) payment_id: sui::types::Address,
491 pub(crate) max_budget_mist: u64,
492 pub(crate) locked_budget_mist: u64,
493 pub(crate) consumed_mist: u64,
494 pub(crate) outstanding_locks: u64,
495 #[serde(default)]
496 pub(crate) outstanding_invocation_ids: Vec<sui::types::Address>,
497 pub(crate) accomplished: bool,
498 pub(crate) refunded: bool,
499}
500
501impl OccurrenceCost {
502 pub const fn payment_id(&self) -> sui::types::Address {
504 self.payment_id
505 }
506
507 pub const fn max_budget_mist(&self) -> u64 {
509 self.max_budget_mist
510 }
511
512 pub const fn locked_budget_mist(&self) -> u64 {
514 self.locked_budget_mist
515 }
516
517 pub const fn consumed_mist(&self) -> u64 {
519 self.consumed_mist
520 }
521
522 pub const fn outstanding_locks(&self) -> u64 {
524 self.outstanding_locks
525 }
526
527 pub fn outstanding_invocation_ids(&self) -> &[sui::types::Address] {
529 &self.outstanding_invocation_ids
530 }
531
532 pub const fn accomplished(&self) -> bool {
534 self.accomplished
535 }
536
537 pub const fn refunded(&self) -> bool {
539 self.refunded
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 fn address(value: &'static str) -> sui::types::Address {
548 sui::types::Address::from_static(value)
549 }
550
551 #[test]
552 fn occurrence_terminality_matches_scheduler_completion() {
553 for status in [
554 OccurrenceStatus::Pending,
555 OccurrenceStatus::Advertised,
556 OccurrenceStatus::Executing,
557 OccurrenceStatus::Finished,
558 ] {
559 assert!(!status.is_terminal());
560 }
561 for status in [
562 OccurrenceStatus::Settled { succeeded: true },
563 OccurrenceStatus::Missed { missed_at_ms: 10 },
564 OccurrenceStatus::Withdrawn {
565 reason: WithdrawalReason::TaskCanceled,
566 },
567 ] {
568 assert!(status.is_terminal());
569 }
570 }
571
572 #[test]
573 fn execution_snapshots_distinguish_missing_and_observed_runtime_state() {
574 let execution_id = address("0x10");
575 let unavailable = ExecutionSnapshot::unavailable(execution_id);
576 assert_eq!(unavailable.execution_id(), execution_id);
577 assert_eq!(unavailable.created_at_ms(), None);
578 assert_eq!(unavailable.active_walks(), None);
579 assert_eq!(unavailable.pending_abort_walks(), None);
580 assert_eq!(unavailable.pending_settlement_walks(), None);
581 assert_eq!(unavailable.successful_walks(), None);
582 assert_eq!(unavailable.failed_walks(), None);
583 assert_eq!(unavailable.aborted_walks(), None);
584
585 let observed = ExecutionSnapshot::observed(
586 execution_id,
587 ExecutionObservation {
588 created_at_ms: 1,
589 active_walks: 2,
590 pending_abort_walks: 3,
591 pending_settlement_walks: 4,
592 successful_walks: 5,
593 failed_walks: 6,
594 aborted_walks: 7,
595 },
596 );
597 assert_eq!(observed.execution_id(), execution_id);
598 assert_eq!(observed.created_at_ms(), Some(1));
599 assert_eq!(observed.active_walks(), Some(2));
600 assert_eq!(observed.pending_abort_walks(), Some(3));
601 assert_eq!(observed.pending_settlement_walks(), Some(4));
602 assert_eq!(observed.successful_walks(), Some(5));
603 assert_eq!(observed.failed_walks(), Some(6));
604 assert_eq!(observed.aborted_walks(), Some(7));
605 }
606
607 #[test]
608 fn occurrence_and_task_snapshots_expose_one_coherent_object_view() {
609 let task_id = address("0x11");
610 let execution_id = address("0x12");
611 let reference = OccurrenceRef::new(task_id, 8);
612 let execution = ExecutionSnapshot::observed(
613 execution_id,
614 ExecutionObservation {
615 created_at_ms: 10,
616 active_walks: 0,
617 pending_abort_walks: 0,
618 pending_settlement_walks: 0,
619 successful_walks: 1,
620 failed_walks: 0,
621 aborted_walks: 0,
622 },
623 );
624 let occurrence = OccurrenceSnapshot {
625 reference,
626 source: OccurrenceSource::Recurring { iteration: 2 },
627 requested_start_time_ms: 20,
628 effective_start_time_ms: Some(21),
629 deadline_ms: Some(30),
630 priority_fee_percentage: 40,
631 dispatched_at_ms: Some(22),
632 settled_at_ms: Some(29),
633 status: OccurrenceStatus::Settled { succeeded: true },
634 execution: Some(execution),
635 observed_task_version: 7,
636 };
637
638 assert_eq!(occurrence.reference(), reference);
639 assert_eq!(
640 occurrence.source(),
641 OccurrenceSource::Recurring { iteration: 2 }
642 );
643 assert_eq!(occurrence.requested_start_time_ms(), 20);
644 assert_eq!(occurrence.effective_start_time_ms(), Some(21));
645 assert_eq!(occurrence.deadline_ms(), Some(30));
646 assert_eq!(occurrence.priority_fee_percentage(), 40);
647 assert_eq!(occurrence.dispatched_at_ms(), Some(22));
648 assert_eq!(occurrence.settled_at_ms(), Some(29));
649 assert_eq!(
650 occurrence.status(),
651 OccurrenceStatus::Settled { succeeded: true }
652 );
653 assert_eq!(
654 occurrence.execution().map(ExecutionSnapshot::execution_id),
655 Some(execution_id)
656 );
657 assert_eq!(occurrence.observed_task_version(), 7);
658
659 let task = TaskSnapshot {
660 task_id,
661 controller: TaskController::Address {
662 address: address("0x13"),
663 },
664 status: TaskStatus::Paused,
665 failure_policy: FailurePolicy::Pause,
666 advertised: Some(reference),
667 allocated_occurrences: 9,
668 pending_occurrences: 3,
669 dispatched_occurrences: 6,
670 in_flight_occurrences: 2,
671 observed_version: 8,
672 };
673 assert_eq!(task.task_id(), task_id);
674 assert_eq!(
675 task.controller(),
676 TaskController::Address {
677 address: address("0x13")
678 }
679 );
680 assert_eq!(task.status(), TaskStatus::Paused);
681 assert_eq!(task.failure_policy(), FailurePolicy::Pause);
682 assert_eq!(task.advertised(), Some(reference));
683 assert_eq!(task.allocated_occurrences(), 9);
684 assert_eq!(task.pending_occurrences(), 3);
685 assert_eq!(task.dispatched_occurrences(), 6);
686 assert_eq!(task.in_flight_occurrences(), 2);
687 assert_eq!(task.observed_version(), 8);
688 }
689
690 #[test]
691 fn occurrence_pages_preserve_records_and_opaque_cursors() {
692 let page = OccurrencePage::new(Vec::new(), Some(vec![1, 2, 3]));
693 assert!(page.occurrences().is_empty());
694 assert_eq!(page.next_cursor(), Some([1, 2, 3].as_slice()));
695
696 let (occurrences, cursor) = page.into_parts();
697 assert!(occurrences.is_empty());
698 assert_eq!(cursor, Some(vec![1, 2, 3]));
699 }
700
701 #[test]
702 fn task_pointer_pages_preserve_identity_and_opaque_cursors() {
703 let pointer = TaskPointer::new(address("0x20"), address("0x21"));
704 assert_eq!(pointer.task_pointer_id(), address("0x20"));
705 assert_eq!(pointer.task_id(), address("0x21"));
706
707 let page = TaskPointerPage::new(vec![pointer], Some(vec![1, 2, 3]));
708 assert_eq!(page.task_pointers(), &[pointer]);
709 assert_eq!(page.next_cursor(), Some([1, 2, 3].as_slice()));
710
711 let (pointers, cursor) = page.into_parts();
712 assert_eq!(pointers, vec![pointer]);
713 assert_eq!(cursor, Some(vec![1, 2, 3]));
714 }
715
716 #[test]
717 fn watch_options_and_costs_expose_their_complete_policy() {
718 let options = WatchOptions::new(Duration::from_secs(4), Duration::from_millis(250));
719 assert_eq!(options.timeout(), Duration::from_secs(4));
720 assert_eq!(options.poll_interval(), Duration::from_millis(250));
721
722 let defaults = WatchOptions::default();
723 assert_eq!(defaults.timeout(), DEFAULT_WATCH_TIMEOUT);
724 assert_eq!(defaults.poll_interval(), DEFAULT_WATCH_POLL_INTERVAL);
725
726 let cost = OccurrenceCost {
727 payment_id: address("0x14"),
728 max_budget_mist: 100,
729 locked_budget_mist: 30,
730 consumed_mist: 40,
731 outstanding_locks: 2,
732 outstanding_invocation_ids: vec![address("0x15"), address("0x16")],
733 accomplished: true,
734 refunded: false,
735 };
736 assert_eq!(cost.payment_id(), address("0x14"));
737 assert_eq!(cost.max_budget_mist(), 100);
738 assert_eq!(cost.locked_budget_mist(), 30);
739 assert_eq!(cost.consumed_mist(), 40);
740 assert_eq!(cost.outstanding_locks(), 2);
741 assert_eq!(
742 cost.outstanding_invocation_ids(),
743 [address("0x15"), address("0x16")]
744 );
745 let mut json = serde_json::to_value(&cost).unwrap();
746 assert_eq!(
747 serde_json::from_value::<OccurrenceCost>(json.clone()).unwrap(),
748 cost
749 );
750 json.as_object_mut()
751 .unwrap()
752 .remove("outstanding_invocation_ids");
753 assert!(serde_json::from_value::<OccurrenceCost>(json)
754 .unwrap()
755 .outstanding_invocation_ids()
756 .is_empty());
757 assert!(cost.accomplished());
758 assert!(!cost.refunded());
759 }
760}