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 (idle) /
61            // InterruptYielding (running) so cooperative yielding points
62            // (e.g., `wait` tool) are interrupted when a peer message arrives.
63            ("peer_message", true) => pd(
64                ApplyMode::StageRunStart,
65                WakeMode::WakeIfIdle,
66                QueueMode::Fifo,
67                ConsumePoint::OnRunComplete,
68                true,
69            ),
70            ("peer_message", false) => pd(
71                ApplyMode::StageRunStart,
72                WakeMode::InterruptYielding,
73                QueueMode::Fifo,
74                ConsumePoint::OnRunComplete,
75                true,
76            ),
77
78            // PeerInput(Request) — same as Message
79            ("peer_request", true) => pd(
80                ApplyMode::StageRunStart,
81                WakeMode::WakeIfIdle,
82                QueueMode::Fifo,
83                ConsumePoint::OnRunComplete,
84                true,
85            ),
86            ("peer_request", false) => pd(
87                ApplyMode::StageRunStart,
88                WakeMode::InterruptYielding,
89                QueueMode::Fifo,
90                ConsumePoint::OnRunComplete,
91                true,
92            ),
93
94            // PeerInput(ResponseProgress) — StageRunBoundary, None, Coalesce
95            ("peer_response_progress", true) => pd(
96                ApplyMode::StageRunBoundary,
97                WakeMode::None,
98                QueueMode::Coalesce,
99                ConsumePoint::OnRunComplete,
100                true,
101            ),
102            ("peer_response_progress", false) => pd(
103                ApplyMode::StageRunBoundary,
104                WakeMode::None,
105                QueueMode::Coalesce,
106                ConsumePoint::OnRunComplete,
107                true,
108            ),
109
110            // PeerInput(ResponseTerminal) — StageRunStart, WakeIfIdle/None
111            ("peer_response_terminal", true) => pd(
112                ApplyMode::StageRunStart,
113                WakeMode::WakeIfIdle,
114                QueueMode::Fifo,
115                ConsumePoint::OnRunComplete,
116                true,
117            ),
118            ("peer_response_terminal", false) => pd(
119                ApplyMode::StageRunStart,
120                WakeMode::None,
121                QueueMode::Fifo,
122                ConsumePoint::OnRunComplete,
123                true,
124            ),
125
126            // FlowStepInput — StageRunStart, WakeIfIdle/None
127            ("flow_step", true) => pd(
128                ApplyMode::StageRunStart,
129                WakeMode::WakeIfIdle,
130                QueueMode::Fifo,
131                ConsumePoint::OnRunComplete,
132                true,
133            ),
134            ("flow_step", false) => pd(
135                ApplyMode::StageRunStart,
136                WakeMode::None,
137                QueueMode::Fifo,
138                ConsumePoint::OnRunComplete,
139                true,
140            ),
141
142            // ExternalEventInput — StageRunStart, WakeIfIdle/None
143            ("external_event", true) => pd(
144                ApplyMode::StageRunStart,
145                WakeMode::WakeIfIdle,
146                QueueMode::Fifo,
147                ConsumePoint::OnRunComplete,
148                true,
149            ),
150            ("external_event", false) => pd(
151                ApplyMode::StageRunStart,
152                WakeMode::None,
153                QueueMode::Fifo,
154                ConsumePoint::OnRunComplete,
155                true,
156            ),
157
158            // SystemGenerated — InjectNow, no wake, OnAccept
159            ("system_generated", true | false) => pd(
160                ApplyMode::InjectNow,
161                WakeMode::None,
162                QueueMode::None,
163                ConsumePoint::OnAccept,
164                true,
165            ),
166
167            // Projected — Ignore, None, None(queue), OnAccept, transcript=false
168            ("projected", true) => pd(
169                ApplyMode::Ignore,
170                WakeMode::None,
171                QueueMode::None,
172                ConsumePoint::OnAccept,
173                false,
174            ),
175            ("projected", false) => pd(
176                ApplyMode::Ignore,
177                WakeMode::None,
178                QueueMode::Coalesce,
179                ConsumePoint::OnAccept,
180                false,
181            ),
182
183            // Unknown kind — default conservative: StageRunStart, no wake
184            (_, _) => pd(
185                ApplyMode::StageRunStart,
186                WakeMode::None,
187                QueueMode::Fifo,
188                ConsumePoint::OnRunComplete,
189                true,
190            ),
191        }
192    }
193}
194
195#[cfg(test)]
196#[allow(clippy::unwrap_used)]
197mod tests {
198    use super::*;
199
200    fn assert_cell(
201        kind: &str,
202        idle: bool,
203        expected_apply: ApplyMode,
204        expected_wake: WakeMode,
205        expected_queue: QueueMode,
206        expected_consume: ConsumePoint,
207        expected_transcript: bool,
208    ) {
209        let decision = DefaultPolicyTable::resolve_by_kind(&KindId::new(kind), idle);
210        assert_eq!(
211            decision.apply_mode, expected_apply,
212            "kind={kind}, idle={idle}: apply_mode"
213        );
214        assert_eq!(
215            decision.wake_mode, expected_wake,
216            "kind={kind}, idle={idle}: wake_mode"
217        );
218        assert_eq!(
219            decision.queue_mode, expected_queue,
220            "kind={kind}, idle={idle}: queue_mode"
221        );
222        assert_eq!(
223            decision.consume_point, expected_consume,
224            "kind={kind}, idle={idle}: consume_point"
225        );
226        assert_eq!(
227            decision.record_transcript, expected_transcript,
228            "kind={kind}, idle={idle}: record_transcript"
229        );
230    }
231
232    #[test]
233    fn prompt_idle() {
234        assert_cell(
235            "prompt",
236            true,
237            ApplyMode::StageRunStart,
238            WakeMode::WakeIfIdle,
239            QueueMode::Fifo,
240            ConsumePoint::OnRunComplete,
241            true,
242        );
243    }
244    #[test]
245    fn prompt_running() {
246        assert_cell(
247            "prompt",
248            false,
249            ApplyMode::StageRunStart,
250            WakeMode::None,
251            QueueMode::Fifo,
252            ConsumePoint::OnRunComplete,
253            true,
254        );
255    }
256    #[test]
257    fn peer_message_idle() {
258        assert_cell(
259            "peer_message",
260            true,
261            ApplyMode::StageRunStart,
262            WakeMode::WakeIfIdle,
263            QueueMode::Fifo,
264            ConsumePoint::OnRunComplete,
265            true,
266        );
267    }
268    #[test]
269    fn peer_message_running() {
270        assert_cell(
271            "peer_message",
272            false,
273            ApplyMode::StageRunStart,
274            WakeMode::InterruptYielding,
275            QueueMode::Fifo,
276            ConsumePoint::OnRunComplete,
277            true,
278        );
279    }
280    #[test]
281    fn peer_request_idle() {
282        assert_cell(
283            "peer_request",
284            true,
285            ApplyMode::StageRunStart,
286            WakeMode::WakeIfIdle,
287            QueueMode::Fifo,
288            ConsumePoint::OnRunComplete,
289            true,
290        );
291    }
292    #[test]
293    fn peer_request_running() {
294        assert_cell(
295            "peer_request",
296            false,
297            ApplyMode::StageRunStart,
298            WakeMode::InterruptYielding,
299            QueueMode::Fifo,
300            ConsumePoint::OnRunComplete,
301            true,
302        );
303    }
304    #[test]
305    fn peer_response_progress_idle() {
306        assert_cell(
307            "peer_response_progress",
308            true,
309            ApplyMode::StageRunBoundary,
310            WakeMode::None,
311            QueueMode::Coalesce,
312            ConsumePoint::OnRunComplete,
313            true,
314        );
315    }
316    #[test]
317    fn peer_response_progress_running() {
318        assert_cell(
319            "peer_response_progress",
320            false,
321            ApplyMode::StageRunBoundary,
322            WakeMode::None,
323            QueueMode::Coalesce,
324            ConsumePoint::OnRunComplete,
325            true,
326        );
327    }
328    #[test]
329    fn peer_response_terminal_idle() {
330        assert_cell(
331            "peer_response_terminal",
332            true,
333            ApplyMode::StageRunStart,
334            WakeMode::WakeIfIdle,
335            QueueMode::Fifo,
336            ConsumePoint::OnRunComplete,
337            true,
338        );
339    }
340    #[test]
341    fn peer_response_terminal_running() {
342        assert_cell(
343            "peer_response_terminal",
344            false,
345            ApplyMode::StageRunStart,
346            WakeMode::None,
347            QueueMode::Fifo,
348            ConsumePoint::OnRunComplete,
349            true,
350        );
351    }
352    #[test]
353    fn flow_step_idle() {
354        assert_cell(
355            "flow_step",
356            true,
357            ApplyMode::StageRunStart,
358            WakeMode::WakeIfIdle,
359            QueueMode::Fifo,
360            ConsumePoint::OnRunComplete,
361            true,
362        );
363    }
364    #[test]
365    fn flow_step_running() {
366        assert_cell(
367            "flow_step",
368            false,
369            ApplyMode::StageRunStart,
370            WakeMode::None,
371            QueueMode::Fifo,
372            ConsumePoint::OnRunComplete,
373            true,
374        );
375    }
376    #[test]
377    fn external_event_idle() {
378        assert_cell(
379            "external_event",
380            true,
381            ApplyMode::StageRunStart,
382            WakeMode::WakeIfIdle,
383            QueueMode::Fifo,
384            ConsumePoint::OnRunComplete,
385            true,
386        );
387    }
388    #[test]
389    fn external_event_running() {
390        assert_cell(
391            "external_event",
392            false,
393            ApplyMode::StageRunStart,
394            WakeMode::None,
395            QueueMode::Fifo,
396            ConsumePoint::OnRunComplete,
397            true,
398        );
399    }
400    #[test]
401    fn system_generated_idle() {
402        assert_cell(
403            "system_generated",
404            true,
405            ApplyMode::InjectNow,
406            WakeMode::None,
407            QueueMode::None,
408            ConsumePoint::OnAccept,
409            true,
410        );
411    }
412    #[test]
413    fn system_generated_running() {
414        assert_cell(
415            "system_generated",
416            false,
417            ApplyMode::InjectNow,
418            WakeMode::None,
419            QueueMode::None,
420            ConsumePoint::OnAccept,
421            true,
422        );
423    }
424    #[test]
425    fn projected_idle() {
426        assert_cell(
427            "projected",
428            true,
429            ApplyMode::Ignore,
430            WakeMode::None,
431            QueueMode::None,
432            ConsumePoint::OnAccept,
433            false,
434        );
435    }
436    #[test]
437    fn projected_running() {
438        assert_cell(
439            "projected",
440            false,
441            ApplyMode::Ignore,
442            WakeMode::None,
443            QueueMode::Coalesce,
444            ConsumePoint::OnAccept,
445            false,
446        );
447    }
448
449    #[test]
450    fn resolve_via_input_object() {
451        use crate::input::*;
452        use chrono::Utc;
453        use meerkat_core::lifecycle::InputId;
454
455        let header = InputHeader {
456            id: InputId::new(),
457            timestamp: Utc::now(),
458            source: InputOrigin::Operator,
459            durability: InputDurability::Durable,
460            visibility: InputVisibility::default(),
461            idempotency_key: None,
462            supersession_key: None,
463            correlation_id: None,
464        };
465        let input = Input::Prompt(PromptInput {
466            header,
467            text: "hello".into(),
468            blocks: None,
469            turn_metadata: None,
470        });
471        let decision = DefaultPolicyTable::resolve(&input, true);
472        assert_eq!(decision.apply_mode, ApplyMode::StageRunStart);
473        assert_eq!(decision.wake_mode, WakeMode::WakeIfIdle);
474    }
475
476    #[test]
477    fn peer_message_running_interrupts_yielding() {
478        // Peer messages arriving while running should use InterruptYielding
479        // so cooperative yielding points (e.g., wait tool) are interrupted.
480        let decision = DefaultPolicyTable::resolve_by_kind(&KindId::new("peer_message"), false);
481        assert_eq!(
482            decision.wake_mode,
483            WakeMode::InterruptYielding,
484            "peer_message while running must use InterruptYielding"
485        );
486        // Must not set wake (only interrupt yielding points)
487        assert_ne!(
488            decision.wake_mode,
489            WakeMode::WakeIfIdle,
490            "peer_message while running must not use WakeIfIdle"
491        );
492    }
493
494    #[test]
495    fn peer_request_running_interrupts_yielding() {
496        // Peer requests arriving while running should use InterruptYielding.
497        let decision = DefaultPolicyTable::resolve_by_kind(&KindId::new("peer_request"), false);
498        assert_eq!(
499            decision.wake_mode,
500            WakeMode::InterruptYielding,
501            "peer_request while running must use InterruptYielding"
502        );
503    }
504
505    #[test]
506    fn peer_message_idle_still_wakes() {
507        // Peer messages while idle should still wake normally.
508        let decision = DefaultPolicyTable::resolve_by_kind(&KindId::new("peer_message"), true);
509        assert_eq!(
510            decision.wake_mode,
511            WakeMode::WakeIfIdle,
512            "peer_message while idle must use WakeIfIdle"
513        );
514    }
515
516    #[test]
517    fn peer_request_idle_still_wakes() {
518        // Peer requests while idle should still wake normally.
519        let decision = DefaultPolicyTable::resolve_by_kind(&KindId::new("peer_request"), true);
520        assert_eq!(
521            decision.wake_mode,
522            WakeMode::WakeIfIdle,
523            "peer_request while idle must use WakeIfIdle"
524        );
525    }
526}