Skip to main content

meerkat_runtime/
policy.rs

1//! §12 PolicyDecision — the output of the policy table.
2//!
3//! The runtime's policy table resolves each Input to a PolicyDecision
4//! that determines how and when the input is applied, whether it wakes
5//! the runtime, how it's queued, and when it's consumed.
6
7use serde::{Deserialize, Serialize};
8
9use crate::identifiers::PolicyVersion;
10
11/// How the input should be applied to the conversation.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14#[non_exhaustive]
15pub enum ApplyMode {
16    /// Stage for application at the start of the next run.
17    StageRunStart,
18    /// Stage for application at any run boundary (start or checkpoint).
19    StageRunBoundary,
20    /// Inject immediately (no run boundary required).
21    InjectNow,
22    /// Do not apply (input is informational only).
23    Ignore,
24}
25
26/// Whether the input should wake an idle runtime.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29#[non_exhaustive]
30pub enum WakeMode {
31    /// Wake the runtime if idle.
32    WakeIfIdle,
33    /// Do not wake (input will be processed at next natural run).
34    None,
35}
36
37/// Queue ordering discipline.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40#[non_exhaustive]
41pub enum QueueMode {
42    /// No queueing (immediate consumption).
43    None,
44    /// First-in, first-out ordering.
45    Fifo,
46    /// Coalesce with other inputs of the same type.
47    Coalesce,
48    /// Supersede earlier inputs with the same supersession key.
49    Supersede,
50    /// Priority ordering (higher priority first).
51    Priority,
52}
53
54/// When the input is considered consumed.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57#[non_exhaustive]
58pub enum ConsumePoint {
59    /// Consumed when the input is accepted.
60    OnAccept,
61    /// Consumed when the input is applied (boundary executed).
62    OnApply,
63    /// Consumed when the run starts.
64    OnRunStart,
65    /// Consumed when the run completes.
66    OnRunComplete,
67    /// Consumed only on explicit acknowledgment.
68    ExplicitAck,
69}
70
71/// How the runtime should drain admitted work.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
73#[serde(rename_all = "snake_case")]
74#[non_exhaustive]
75pub enum DrainPolicy {
76    #[default]
77    QueueNextTurn,
78    SteerBatch,
79    Immediate,
80    Ignore,
81}
82
83/// Where admitted work routes after policy resolution.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
85#[serde(rename_all = "snake_case")]
86#[non_exhaustive]
87pub enum RoutingDisposition {
88    #[default]
89    Queue,
90    Steer,
91    Immediate,
92    Drop,
93}
94
95/// Full policy decision for an input.
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct PolicyDecision {
98    /// How to apply the input.
99    pub apply_mode: ApplyMode,
100    /// Whether to wake the runtime.
101    pub wake_mode: WakeMode,
102    /// Queue ordering.
103    pub queue_mode: QueueMode,
104    /// When the input is consumed.
105    pub consume_point: ConsumePoint,
106    /// How runtime drain ownership should handle this work.
107    #[serde(default)]
108    pub drain_policy: DrainPolicy,
109    /// Where the work routes after admission.
110    #[serde(default)]
111    pub routing_disposition: RoutingDisposition,
112    /// Whether to record this input in the conversation transcript.
113    #[serde(default = "default_true")]
114    pub record_transcript: bool,
115    /// Whether to emit operator-visible content for this input.
116    #[serde(default = "default_true")]
117    pub emit_operator_content: bool,
118    /// Policy version that produced this decision.
119    pub policy_version: PolicyVersion,
120}
121
122fn default_true() -> bool {
123    true
124}
125
126#[cfg(test)]
127#[allow(clippy::unwrap_used)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn apply_mode_serde() {
133        for mode in [
134            ApplyMode::StageRunStart,
135            ApplyMode::StageRunBoundary,
136            ApplyMode::InjectNow,
137            ApplyMode::Ignore,
138        ] {
139            let json = serde_json::to_value(mode).unwrap();
140            let parsed: ApplyMode = serde_json::from_value(json).unwrap();
141            assert_eq!(mode, parsed);
142        }
143    }
144
145    #[test]
146    fn wake_mode_serde() {
147        for mode in [WakeMode::WakeIfIdle, WakeMode::None] {
148            let json = serde_json::to_value(mode).unwrap();
149            let parsed: WakeMode = serde_json::from_value(json).unwrap();
150            assert_eq!(mode, parsed);
151        }
152    }
153
154    #[test]
155    fn queue_mode_serde() {
156        for mode in [
157            QueueMode::None,
158            QueueMode::Fifo,
159            QueueMode::Coalesce,
160            QueueMode::Supersede,
161            QueueMode::Priority,
162        ] {
163            let json = serde_json::to_value(mode).unwrap();
164            let parsed: QueueMode = serde_json::from_value(json).unwrap();
165            assert_eq!(mode, parsed);
166        }
167    }
168
169    #[test]
170    fn consume_point_serde() {
171        for point in [
172            ConsumePoint::OnAccept,
173            ConsumePoint::OnApply,
174            ConsumePoint::OnRunStart,
175            ConsumePoint::OnRunComplete,
176            ConsumePoint::ExplicitAck,
177        ] {
178            let json = serde_json::to_value(point).unwrap();
179            let parsed: ConsumePoint = serde_json::from_value(json).unwrap();
180            assert_eq!(point, parsed);
181        }
182    }
183
184    #[test]
185    fn drain_policy_serde() {
186        for policy in [
187            DrainPolicy::QueueNextTurn,
188            DrainPolicy::SteerBatch,
189            DrainPolicy::Immediate,
190            DrainPolicy::Ignore,
191        ] {
192            let json = serde_json::to_value(policy).unwrap();
193            let parsed: DrainPolicy = serde_json::from_value(json).unwrap();
194            assert_eq!(policy, parsed);
195        }
196    }
197
198    #[test]
199    fn routing_disposition_serde() {
200        for disposition in [
201            RoutingDisposition::Queue,
202            RoutingDisposition::Steer,
203            RoutingDisposition::Immediate,
204            RoutingDisposition::Drop,
205        ] {
206            let json = serde_json::to_value(disposition).unwrap();
207            let parsed: RoutingDisposition = serde_json::from_value(json).unwrap();
208            assert_eq!(disposition, parsed);
209        }
210    }
211
212    #[test]
213    fn policy_decision_serde_roundtrip() {
214        let decision = PolicyDecision {
215            apply_mode: ApplyMode::StageRunStart,
216            wake_mode: WakeMode::WakeIfIdle,
217            queue_mode: QueueMode::Fifo,
218            consume_point: ConsumePoint::OnRunComplete,
219            drain_policy: DrainPolicy::QueueNextTurn,
220            routing_disposition: RoutingDisposition::Queue,
221            record_transcript: true,
222            emit_operator_content: true,
223            policy_version: PolicyVersion(1),
224        };
225        let json = serde_json::to_value(&decision).unwrap();
226        let parsed: PolicyDecision = serde_json::from_value(json).unwrap();
227        assert_eq!(decision, parsed);
228    }
229
230    #[test]
231    fn policy_decision_ignore_on_accept() {
232        let decision = PolicyDecision {
233            apply_mode: ApplyMode::Ignore,
234            wake_mode: WakeMode::None,
235            queue_mode: QueueMode::None,
236            consume_point: ConsumePoint::OnAccept,
237            drain_policy: DrainPolicy::Ignore,
238            routing_disposition: RoutingDisposition::Drop,
239            record_transcript: false,
240            emit_operator_content: false,
241            policy_version: PolicyVersion(1),
242        };
243        let json = serde_json::to_value(&decision).unwrap();
244        let parsed: PolicyDecision = serde_json::from_value(json).unwrap();
245        assert_eq!(decision, parsed);
246    }
247
248    #[test]
249    fn record_transcript_defaults_true() {
250        let json = serde_json::json!({
251            "apply_mode": "stage_run_start",
252            "wake_mode": "wake_if_idle",
253            "queue_mode": "fifo",
254            "consume_point": "on_run_complete",
255            "policy_version": 1
256        });
257        let parsed: PolicyDecision = serde_json::from_value(json).unwrap();
258        assert!(parsed.record_transcript);
259        assert!(parsed.emit_operator_content);
260    }
261}