Skip to main content

meerkat_runtime/
accept.rs

1//! §14 AcceptOutcome — result of accepting an input.
2
3use meerkat_core::lifecycle::InputId;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7use crate::input_state::InputState;
8use crate::policy::PolicyDecision;
9
10/// Typed reason why an input was rejected at the accept boundary.
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(tag = "reject_type", rename_all = "snake_case")]
13#[non_exhaustive]
14pub enum RejectReason {
15    /// Runtime is not in a state that accepts input (e.g. stopped, destroyed).
16    NotReady {
17        /// The runtime state that caused the rejection.
18        state: String,
19    },
20    /// Input failed durability validation.
21    DurabilityViolation {
22        /// Description of the violation.
23        detail: String,
24    },
25    /// Peer input carried a forbidden handling_mode.
26    PeerHandlingModeInvalid {
27        /// Description of the violation.
28        detail: String,
29    },
30}
31
32impl fmt::Display for RejectReason {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::NotReady { state } => {
36                write!(f, "runtime not accepting input while in state: {state}")
37            }
38            Self::DurabilityViolation { detail } => write!(f, "{detail}"),
39            Self::PeerHandlingModeInvalid { detail } => write!(f, "{detail}"),
40        }
41    }
42}
43
44/// Outcome of `RuntimeDriver::accept_input()`.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(tag = "outcome_type", rename_all = "snake_case")]
47#[non_exhaustive]
48#[allow(clippy::large_enum_variant)]
49pub enum AcceptOutcome {
50    /// Input was accepted and processing has begun.
51    Accepted {
52        /// The assigned input ID.
53        input_id: InputId,
54        /// The policy decision applied to this input.
55        policy: PolicyDecision,
56        /// Current input state.
57        state: InputState,
58    },
59    /// Input was deduplicated (idempotency key matched an existing input).
60    Deduplicated {
61        /// The new input ID that was deduplicated.
62        input_id: InputId,
63        /// The existing input ID that was matched.
64        existing_id: InputId,
65    },
66    /// Input was rejected (validation failed, durability violation, etc.).
67    Rejected {
68        /// Why the input was rejected.
69        reason: RejectReason,
70    },
71}
72
73impl AcceptOutcome {
74    /// Check if the input was accepted.
75    pub fn is_accepted(&self) -> bool {
76        matches!(self, Self::Accepted { .. })
77    }
78
79    /// Check if the input was deduplicated.
80    pub fn is_deduplicated(&self) -> bool {
81        matches!(self, Self::Deduplicated { .. })
82    }
83
84    /// Check if the input was rejected.
85    pub fn is_rejected(&self) -> bool {
86        matches!(self, Self::Rejected { .. })
87    }
88}
89
90#[cfg(test)]
91#[allow(clippy::unwrap_used)]
92mod tests {
93    use super::*;
94    use crate::identifiers::PolicyVersion;
95    use crate::policy::{
96        ApplyMode, ConsumePoint, DrainPolicy, QueueMode, RoutingDisposition, WakeMode,
97    };
98
99    #[test]
100    fn accepted_serde() {
101        let outcome = AcceptOutcome::Accepted {
102            input_id: InputId::new(),
103            policy: PolicyDecision {
104                apply_mode: ApplyMode::StageRunStart,
105                wake_mode: WakeMode::WakeIfIdle,
106                queue_mode: QueueMode::Fifo,
107                consume_point: ConsumePoint::OnRunComplete,
108                drain_policy: DrainPolicy::QueueNextTurn,
109                routing_disposition: RoutingDisposition::Queue,
110                record_transcript: true,
111                emit_operator_content: true,
112                policy_version: PolicyVersion(1),
113            },
114            state: InputState::new_accepted(InputId::new()),
115        };
116        let json = serde_json::to_value(&outcome).unwrap();
117        assert_eq!(json["outcome_type"], "accepted");
118        let parsed: AcceptOutcome = serde_json::from_value(json).unwrap();
119        assert!(parsed.is_accepted());
120        assert!(!parsed.is_deduplicated());
121        assert!(!parsed.is_rejected());
122    }
123
124    #[test]
125    fn deduplicated_serde() {
126        let outcome = AcceptOutcome::Deduplicated {
127            input_id: InputId::new(),
128            existing_id: InputId::new(),
129        };
130        let json = serde_json::to_value(&outcome).unwrap();
131        assert_eq!(json["outcome_type"], "deduplicated");
132        let parsed: AcceptOutcome = serde_json::from_value(json).unwrap();
133        assert!(parsed.is_deduplicated());
134    }
135
136    #[test]
137    fn rejected_serde() {
138        let outcome = AcceptOutcome::Rejected {
139            reason: RejectReason::DurabilityViolation {
140                detail: "durability violation".into(),
141            },
142        };
143        let json = serde_json::to_value(&outcome).unwrap();
144        assert_eq!(json["outcome_type"], "rejected");
145        assert_eq!(json["reason"]["reject_type"], "durability_violation");
146        let parsed: AcceptOutcome = serde_json::from_value(json).unwrap();
147        assert!(parsed.is_rejected());
148    }
149
150    #[test]
151    fn reject_reason_display() {
152        let not_ready = RejectReason::NotReady {
153            state: "Stopped".into(),
154        };
155        assert_eq!(
156            not_ready.to_string(),
157            "runtime not accepting input while in state: Stopped"
158        );
159
160        let durability = RejectReason::DurabilityViolation {
161            detail: "Derived durability forbidden for prompt".into(),
162        };
163        assert_eq!(
164            durability.to_string(),
165            "Derived durability forbidden for prompt"
166        );
167
168        let peer = RejectReason::PeerHandlingModeInvalid {
169            detail: "handling_mode is forbidden on ResponseProgress peer inputs".into(),
170        };
171        assert_eq!(
172            peer.to_string(),
173            "handling_mode is forbidden on ResponseProgress peer inputs"
174        );
175    }
176
177    #[test]
178    fn reject_reason_serde_round_trip() {
179        let reasons = vec![
180            RejectReason::NotReady {
181                state: "Destroyed".into(),
182            },
183            RejectReason::DurabilityViolation {
184                detail: "external derived".into(),
185            },
186            RejectReason::PeerHandlingModeInvalid {
187                detail: "forbidden".into(),
188            },
189        ];
190        for reason in reasons {
191            let json = serde_json::to_value(&reason).unwrap();
192            let parsed: RejectReason = serde_json::from_value(json).unwrap();
193            assert_eq!(parsed, reason);
194        }
195    }
196}