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