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