Skip to main content

meerkat_runtime/
policy_table.rs

1//! §17 DefaultPolicyTable — resolves Input × runtime_idle to PolicyDecision.
2//!
3//! All input kinds × 2 idle states, exact per spec §17.
4
5use crate::identifiers::{KindId, PolicyVersion};
6use crate::input::Input;
7use crate::policy::{ApplyMode, ConsumePoint, PolicyDecision, QueueMode, WakeMode};
8
9/// The default policy version for the built-in table.
10pub const DEFAULT_POLICY_VERSION: PolicyVersion = PolicyVersion(1);
11
12/// Helper to construct a PolicyDecision with transcript defaults.
13fn pd(
14    apply_mode: ApplyMode,
15    wake_mode: WakeMode,
16    queue_mode: QueueMode,
17    consume_point: ConsumePoint,
18    record_transcript: bool,
19) -> PolicyDecision {
20    PolicyDecision {
21        apply_mode,
22        wake_mode,
23        queue_mode,
24        consume_point,
25        record_transcript,
26        emit_operator_content: record_transcript,
27        policy_version: DEFAULT_POLICY_VERSION,
28    }
29}
30
31/// Default policy table implementing §17.
32pub struct DefaultPolicyTable;
33
34impl DefaultPolicyTable {
35    /// Resolve a policy decision for the given input and runtime state.
36    pub fn resolve(input: &Input, runtime_idle: bool) -> PolicyDecision {
37        let kind = input.kind_id();
38        Self::resolve_by_kind(&kind, runtime_idle)
39    }
40
41    /// Resolve by kind ID (for testing and extensibility).
42    pub fn resolve_by_kind(kind: &KindId, runtime_idle: bool) -> PolicyDecision {
43        match (kind.0.as_str(), runtime_idle) {
44            // PromptInput — StageRunStart, WakeIfIdle (idle) / None (running)
45            ("prompt", true) => pd(
46                ApplyMode::StageRunStart,
47                WakeMode::WakeIfIdle,
48                QueueMode::Fifo,
49                ConsumePoint::OnRunComplete,
50                true,
51            ),
52            ("prompt", false) => pd(
53                ApplyMode::StageRunStart,
54                WakeMode::None,
55                QueueMode::Fifo,
56                ConsumePoint::OnRunComplete,
57                true,
58            ),
59
60            // PeerInput(Message) — StageRunStart, WakeIfIdle/None
61            ("peer_message", true) => pd(
62                ApplyMode::StageRunStart,
63                WakeMode::WakeIfIdle,
64                QueueMode::Fifo,
65                ConsumePoint::OnRunComplete,
66                true,
67            ),
68            ("peer_message", false) => pd(
69                ApplyMode::StageRunStart,
70                WakeMode::None,
71                QueueMode::Fifo,
72                ConsumePoint::OnRunComplete,
73                true,
74            ),
75
76            // PeerInput(Request) — same as Message
77            ("peer_request", true) => pd(
78                ApplyMode::StageRunStart,
79                WakeMode::WakeIfIdle,
80                QueueMode::Fifo,
81                ConsumePoint::OnRunComplete,
82                true,
83            ),
84            ("peer_request", false) => pd(
85                ApplyMode::StageRunStart,
86                WakeMode::None,
87                QueueMode::Fifo,
88                ConsumePoint::OnRunComplete,
89                true,
90            ),
91
92            // PeerInput(ResponseProgress) — StageRunBoundary, None, Coalesce
93            ("peer_response_progress", true) => pd(
94                ApplyMode::StageRunBoundary,
95                WakeMode::None,
96                QueueMode::Coalesce,
97                ConsumePoint::OnRunComplete,
98                true,
99            ),
100            ("peer_response_progress", false) => pd(
101                ApplyMode::StageRunBoundary,
102                WakeMode::None,
103                QueueMode::Coalesce,
104                ConsumePoint::OnRunComplete,
105                true,
106            ),
107
108            // PeerInput(ResponseTerminal) — StageRunStart, WakeIfIdle/None
109            ("peer_response_terminal", true) => pd(
110                ApplyMode::StageRunStart,
111                WakeMode::WakeIfIdle,
112                QueueMode::Fifo,
113                ConsumePoint::OnRunComplete,
114                true,
115            ),
116            ("peer_response_terminal", false) => pd(
117                ApplyMode::StageRunStart,
118                WakeMode::None,
119                QueueMode::Fifo,
120                ConsumePoint::OnRunComplete,
121                true,
122            ),
123
124            // FlowStepInput — StageRunStart, WakeIfIdle/None
125            ("flow_step", true) => pd(
126                ApplyMode::StageRunStart,
127                WakeMode::WakeIfIdle,
128                QueueMode::Fifo,
129                ConsumePoint::OnRunComplete,
130                true,
131            ),
132            ("flow_step", false) => pd(
133                ApplyMode::StageRunStart,
134                WakeMode::None,
135                QueueMode::Fifo,
136                ConsumePoint::OnRunComplete,
137                true,
138            ),
139
140            // ExternalEventInput — StageRunStart, WakeIfIdle/None
141            ("external_event", true) => pd(
142                ApplyMode::StageRunStart,
143                WakeMode::WakeIfIdle,
144                QueueMode::Fifo,
145                ConsumePoint::OnRunComplete,
146                true,
147            ),
148            ("external_event", false) => pd(
149                ApplyMode::StageRunStart,
150                WakeMode::None,
151                QueueMode::Fifo,
152                ConsumePoint::OnRunComplete,
153                true,
154            ),
155
156            // SystemGenerated — InjectNow, no wake, OnAccept
157            ("system_generated", true | false) => pd(
158                ApplyMode::InjectNow,
159                WakeMode::None,
160                QueueMode::None,
161                ConsumePoint::OnAccept,
162                true,
163            ),
164
165            // Projected — Ignore, None, None(queue), OnAccept, transcript=false
166            ("projected", true) => pd(
167                ApplyMode::Ignore,
168                WakeMode::None,
169                QueueMode::None,
170                ConsumePoint::OnAccept,
171                false,
172            ),
173            ("projected", false) => pd(
174                ApplyMode::Ignore,
175                WakeMode::None,
176                QueueMode::Coalesce,
177                ConsumePoint::OnAccept,
178                false,
179            ),
180
181            // Unknown kind — default conservative: StageRunStart, no wake
182            (_, _) => pd(
183                ApplyMode::StageRunStart,
184                WakeMode::None,
185                QueueMode::Fifo,
186                ConsumePoint::OnRunComplete,
187                true,
188            ),
189        }
190    }
191}
192
193#[cfg(test)]
194#[allow(clippy::unwrap_used)]
195mod tests {
196    use super::*;
197
198    fn assert_cell(
199        kind: &str,
200        idle: bool,
201        expected_apply: ApplyMode,
202        expected_wake: WakeMode,
203        expected_queue: QueueMode,
204        expected_consume: ConsumePoint,
205        expected_transcript: bool,
206    ) {
207        let decision = DefaultPolicyTable::resolve_by_kind(&KindId::new(kind), idle);
208        assert_eq!(
209            decision.apply_mode, expected_apply,
210            "kind={kind}, idle={idle}: apply_mode"
211        );
212        assert_eq!(
213            decision.wake_mode, expected_wake,
214            "kind={kind}, idle={idle}: wake_mode"
215        );
216        assert_eq!(
217            decision.queue_mode, expected_queue,
218            "kind={kind}, idle={idle}: queue_mode"
219        );
220        assert_eq!(
221            decision.consume_point, expected_consume,
222            "kind={kind}, idle={idle}: consume_point"
223        );
224        assert_eq!(
225            decision.record_transcript, expected_transcript,
226            "kind={kind}, idle={idle}: record_transcript"
227        );
228    }
229
230    #[test]
231    fn prompt_idle() {
232        assert_cell(
233            "prompt",
234            true,
235            ApplyMode::StageRunStart,
236            WakeMode::WakeIfIdle,
237            QueueMode::Fifo,
238            ConsumePoint::OnRunComplete,
239            true,
240        );
241    }
242    #[test]
243    fn prompt_running() {
244        assert_cell(
245            "prompt",
246            false,
247            ApplyMode::StageRunStart,
248            WakeMode::None,
249            QueueMode::Fifo,
250            ConsumePoint::OnRunComplete,
251            true,
252        );
253    }
254    #[test]
255    fn peer_message_idle() {
256        assert_cell(
257            "peer_message",
258            true,
259            ApplyMode::StageRunStart,
260            WakeMode::WakeIfIdle,
261            QueueMode::Fifo,
262            ConsumePoint::OnRunComplete,
263            true,
264        );
265    }
266    #[test]
267    fn peer_message_running() {
268        assert_cell(
269            "peer_message",
270            false,
271            ApplyMode::StageRunStart,
272            WakeMode::None,
273            QueueMode::Fifo,
274            ConsumePoint::OnRunComplete,
275            true,
276        );
277    }
278    #[test]
279    fn peer_request_idle() {
280        assert_cell(
281            "peer_request",
282            true,
283            ApplyMode::StageRunStart,
284            WakeMode::WakeIfIdle,
285            QueueMode::Fifo,
286            ConsumePoint::OnRunComplete,
287            true,
288        );
289    }
290    #[test]
291    fn peer_request_running() {
292        assert_cell(
293            "peer_request",
294            false,
295            ApplyMode::StageRunStart,
296            WakeMode::None,
297            QueueMode::Fifo,
298            ConsumePoint::OnRunComplete,
299            true,
300        );
301    }
302    #[test]
303    fn peer_response_progress_idle() {
304        assert_cell(
305            "peer_response_progress",
306            true,
307            ApplyMode::StageRunBoundary,
308            WakeMode::None,
309            QueueMode::Coalesce,
310            ConsumePoint::OnRunComplete,
311            true,
312        );
313    }
314    #[test]
315    fn peer_response_progress_running() {
316        assert_cell(
317            "peer_response_progress",
318            false,
319            ApplyMode::StageRunBoundary,
320            WakeMode::None,
321            QueueMode::Coalesce,
322            ConsumePoint::OnRunComplete,
323            true,
324        );
325    }
326    #[test]
327    fn peer_response_terminal_idle() {
328        assert_cell(
329            "peer_response_terminal",
330            true,
331            ApplyMode::StageRunStart,
332            WakeMode::WakeIfIdle,
333            QueueMode::Fifo,
334            ConsumePoint::OnRunComplete,
335            true,
336        );
337    }
338    #[test]
339    fn peer_response_terminal_running() {
340        assert_cell(
341            "peer_response_terminal",
342            false,
343            ApplyMode::StageRunStart,
344            WakeMode::None,
345            QueueMode::Fifo,
346            ConsumePoint::OnRunComplete,
347            true,
348        );
349    }
350    #[test]
351    fn flow_step_idle() {
352        assert_cell(
353            "flow_step",
354            true,
355            ApplyMode::StageRunStart,
356            WakeMode::WakeIfIdle,
357            QueueMode::Fifo,
358            ConsumePoint::OnRunComplete,
359            true,
360        );
361    }
362    #[test]
363    fn flow_step_running() {
364        assert_cell(
365            "flow_step",
366            false,
367            ApplyMode::StageRunStart,
368            WakeMode::None,
369            QueueMode::Fifo,
370            ConsumePoint::OnRunComplete,
371            true,
372        );
373    }
374    #[test]
375    fn external_event_idle() {
376        assert_cell(
377            "external_event",
378            true,
379            ApplyMode::StageRunStart,
380            WakeMode::WakeIfIdle,
381            QueueMode::Fifo,
382            ConsumePoint::OnRunComplete,
383            true,
384        );
385    }
386    #[test]
387    fn external_event_running() {
388        assert_cell(
389            "external_event",
390            false,
391            ApplyMode::StageRunStart,
392            WakeMode::None,
393            QueueMode::Fifo,
394            ConsumePoint::OnRunComplete,
395            true,
396        );
397    }
398    #[test]
399    fn system_generated_idle() {
400        assert_cell(
401            "system_generated",
402            true,
403            ApplyMode::InjectNow,
404            WakeMode::None,
405            QueueMode::None,
406            ConsumePoint::OnAccept,
407            true,
408        );
409    }
410    #[test]
411    fn system_generated_running() {
412        assert_cell(
413            "system_generated",
414            false,
415            ApplyMode::InjectNow,
416            WakeMode::None,
417            QueueMode::None,
418            ConsumePoint::OnAccept,
419            true,
420        );
421    }
422    #[test]
423    fn projected_idle() {
424        assert_cell(
425            "projected",
426            true,
427            ApplyMode::Ignore,
428            WakeMode::None,
429            QueueMode::None,
430            ConsumePoint::OnAccept,
431            false,
432        );
433    }
434    #[test]
435    fn projected_running() {
436        assert_cell(
437            "projected",
438            false,
439            ApplyMode::Ignore,
440            WakeMode::None,
441            QueueMode::Coalesce,
442            ConsumePoint::OnAccept,
443            false,
444        );
445    }
446
447    #[test]
448    fn resolve_via_input_object() {
449        use crate::input::*;
450        use chrono::Utc;
451        use meerkat_core::lifecycle::InputId;
452
453        let header = InputHeader {
454            id: InputId::new(),
455            timestamp: Utc::now(),
456            source: InputOrigin::Operator,
457            durability: InputDurability::Durable,
458            visibility: InputVisibility::default(),
459            idempotency_key: None,
460            supersession_key: None,
461            correlation_id: None,
462        };
463        let input = Input::Prompt(PromptInput {
464            header,
465            text: "hello".into(),
466            turn_metadata: None,
467        });
468        let decision = DefaultPolicyTable::resolve(&input, true);
469        assert_eq!(decision.apply_mode, ApplyMode::StageRunStart);
470        assert_eq!(decision.wake_mode, WakeMode::WakeIfIdle);
471    }
472}