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/// Full policy decision for an input.
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct PolicyDecision {
74    /// How to apply the input.
75    pub apply_mode: ApplyMode,
76    /// Whether to wake the runtime.
77    pub wake_mode: WakeMode,
78    /// Queue ordering.
79    pub queue_mode: QueueMode,
80    /// When the input is consumed.
81    pub consume_point: ConsumePoint,
82    /// Whether to record this input in the conversation transcript.
83    #[serde(default = "default_true")]
84    pub record_transcript: bool,
85    /// Whether to emit operator-visible content for this input.
86    #[serde(default = "default_true")]
87    pub emit_operator_content: bool,
88    /// Policy version that produced this decision.
89    pub policy_version: PolicyVersion,
90}
91
92fn default_true() -> bool {
93    true
94}
95
96#[cfg(test)]
97#[allow(clippy::unwrap_used)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn apply_mode_serde() {
103        for mode in [
104            ApplyMode::StageRunStart,
105            ApplyMode::StageRunBoundary,
106            ApplyMode::InjectNow,
107            ApplyMode::Ignore,
108        ] {
109            let json = serde_json::to_value(mode).unwrap();
110            let parsed: ApplyMode = serde_json::from_value(json).unwrap();
111            assert_eq!(mode, parsed);
112        }
113    }
114
115    #[test]
116    fn wake_mode_serde() {
117        for mode in [WakeMode::WakeIfIdle, WakeMode::None] {
118            let json = serde_json::to_value(mode).unwrap();
119            let parsed: WakeMode = serde_json::from_value(json).unwrap();
120            assert_eq!(mode, parsed);
121        }
122    }
123
124    #[test]
125    fn queue_mode_serde() {
126        for mode in [
127            QueueMode::None,
128            QueueMode::Fifo,
129            QueueMode::Coalesce,
130            QueueMode::Supersede,
131            QueueMode::Priority,
132        ] {
133            let json = serde_json::to_value(mode).unwrap();
134            let parsed: QueueMode = serde_json::from_value(json).unwrap();
135            assert_eq!(mode, parsed);
136        }
137    }
138
139    #[test]
140    fn consume_point_serde() {
141        for point in [
142            ConsumePoint::OnAccept,
143            ConsumePoint::OnApply,
144            ConsumePoint::OnRunStart,
145            ConsumePoint::OnRunComplete,
146            ConsumePoint::ExplicitAck,
147        ] {
148            let json = serde_json::to_value(point).unwrap();
149            let parsed: ConsumePoint = serde_json::from_value(json).unwrap();
150            assert_eq!(point, parsed);
151        }
152    }
153
154    #[test]
155    fn policy_decision_serde_roundtrip() {
156        let decision = PolicyDecision {
157            apply_mode: ApplyMode::StageRunStart,
158            wake_mode: WakeMode::WakeIfIdle,
159            queue_mode: QueueMode::Fifo,
160            consume_point: ConsumePoint::OnRunComplete,
161            record_transcript: true,
162            emit_operator_content: true,
163            policy_version: PolicyVersion(1),
164        };
165        let json = serde_json::to_value(&decision).unwrap();
166        let parsed: PolicyDecision = serde_json::from_value(json).unwrap();
167        assert_eq!(decision, parsed);
168    }
169
170    #[test]
171    fn policy_decision_ignore_on_accept() {
172        let decision = PolicyDecision {
173            apply_mode: ApplyMode::Ignore,
174            wake_mode: WakeMode::None,
175            queue_mode: QueueMode::None,
176            consume_point: ConsumePoint::OnAccept,
177            record_transcript: false,
178            emit_operator_content: false,
179            policy_version: PolicyVersion(1),
180        };
181        let json = serde_json::to_value(&decision).unwrap();
182        let parsed: PolicyDecision = serde_json::from_value(json).unwrap();
183        assert_eq!(decision, parsed);
184    }
185
186    #[test]
187    fn record_transcript_defaults_true() {
188        let json = serde_json::json!({
189            "apply_mode": "stage_run_start",
190            "wake_mode": "wake_if_idle",
191            "queue_mode": "fifo",
192            "consume_point": "on_run_complete",
193            "policy_version": 1
194        });
195        let parsed: PolicyDecision = serde_json::from_value(json).unwrap();
196        assert!(parsed.record_transcript);
197        assert!(parsed.emit_operator_content);
198    }
199}