Skip to main content

meerkat_runtime/
ingress_types.rs

1//! Wire-type shells preserved from the deleted runtime ingress authority.
2//!
3//! These types name admission metadata that persists beyond the authority
4//! itself. They are pure data — no authority methods, no shadow state. The
5//! DSL owns ingress semantics (queue lanes, input phases, admission
6//! ordering); these types just carry content-shape / correlation metadata
7//! from the admission point to observability readers.
8
9use meerkat_core::lifecycle::RuntimeExecutionKind;
10use meerkat_core::lifecycle::run_primitive::{
11    ConversationAppend, ConversationContextAppend, PeerResponseTerminalApplyIntent,
12    RunApplyBoundary,
13};
14use serde::{Deserialize, Serialize};
15
16use crate::identifiers::{InputKind, KindId};
17use crate::policy::{ApplyMode, PolicyDecision};
18
19/// Content shape classification for admitted inputs.
20///
21/// Used by the admitted-input snapshot surface so callers can correlate
22/// admissions by content type without re-parsing the Input payload.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct ContentShape(InputKind);
25
26impl ContentShape {
27    pub const fn from_kind(kind: InputKind) -> Self {
28        Self(kind)
29    }
30
31    pub const fn from_kind_id(kind_id: KindId) -> Self {
32        Self(kind_id.kind())
33    }
34
35    pub const fn kind(self) -> InputKind {
36        self.0
37    }
38
39    pub fn as_str(self) -> &'static str {
40        self.0.as_str()
41    }
42}
43
44impl From<InputKind> for ContentShape {
45    fn from(kind: InputKind) -> Self {
46        Self::from_kind(kind)
47    }
48}
49
50impl From<KindId> for ContentShape {
51    fn from(kind_id: KindId) -> Self {
52        Self::from_kind_id(kind_id)
53    }
54}
55
56impl std::fmt::Display for ContentShape {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.write_str(self.as_str())
59    }
60}
61
62/// Reservation key for admitted inputs.
63#[derive(Debug, Clone, PartialEq, Eq, Hash)]
64pub struct ReservationKey(pub String);
65
66/// Request ID for correlation tracking.
67#[derive(Debug, Clone, PartialEq, Eq, Hash)]
68pub struct RequestId(pub String);
69
70/// Machine-owned runtime-loop semantics captured at admission.
71///
72/// The runtime loop must not re-read peer conventions, continuation payloads,
73/// or handling-mode hints to decide how a dequeued input runs. Admission has
74/// already resolved the typed policy/kind tuple; this record is the canonical
75/// carrier from that decision point to `RunPrimitive` construction.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77pub struct RuntimeInputSemantics {
78    pub boundary: RunApplyBoundary,
79    pub execution_kind: RuntimeExecutionKind,
80    pub peer_response_terminal_apply_intent: Option<PeerResponseTerminalApplyIntent>,
81}
82
83/// Admitted conversation projection for one input.
84///
85/// The raw input payload is still retained for durability/replay, but the
86/// runtime loop consumes this admitted projection when constructing
87/// `RunPrimitive` so dequeue mechanics do not reinterpret peer conventions or
88/// terminal status payloads.
89#[derive(Debug, Clone, Default, PartialEq, Eq)]
90pub struct RuntimeInputProjection {
91    pub append: Option<ConversationAppend>,
92    pub additional_appends: Vec<ConversationAppend>,
93    pub context_append: Option<ConversationContextAppend>,
94}
95
96impl RuntimeInputSemantics {
97    fn boundary_from_policy(policy: &PolicyDecision) -> RunApplyBoundary {
98        match policy.apply_mode {
99            ApplyMode::StageRunBoundary => RunApplyBoundary::RunCheckpoint,
100            ApplyMode::InjectNow => RunApplyBoundary::Immediate,
101            ApplyMode::StageRunStart | ApplyMode::Ignore => RunApplyBoundary::RunStart,
102        }
103    }
104
105    pub fn from_policy_and_execution_kind(
106        policy: &PolicyDecision,
107        execution_kind: RuntimeExecutionKind,
108        peer_response_terminal_apply_intent: Option<PeerResponseTerminalApplyIntent>,
109    ) -> Self {
110        Self {
111            boundary: Self::boundary_from_policy(policy),
112            execution_kind,
113            peer_response_terminal_apply_intent,
114        }
115    }
116
117    pub fn from_policy_and_kind(policy: &PolicyDecision, kind: InputKind) -> Self {
118        let execution_kind = match kind {
119            InputKind::Continuation => RuntimeExecutionKind::ResumePending,
120            InputKind::Prompt
121            | InputKind::PeerMessage
122            | InputKind::PeerRequest
123            | InputKind::PeerResponseProgress
124            | InputKind::PeerResponseTerminal
125            | InputKind::FlowStep
126            | InputKind::ExternalEvent
127            | InputKind::Operation => RuntimeExecutionKind::ContentTurn,
128        };
129        let peer_response_terminal_apply_intent = match kind {
130            InputKind::PeerResponseTerminal => {
131                Some(PeerResponseTerminalApplyIntent::AppendContextAndRun)
132            }
133            InputKind::Prompt
134            | InputKind::PeerMessage
135            | InputKind::PeerRequest
136            | InputKind::PeerResponseProgress
137            | InputKind::FlowStep
138            | InputKind::ExternalEvent
139            | InputKind::Continuation
140            | InputKind::Operation => None,
141        };
142        Self::from_policy_and_execution_kind(
143            policy,
144            execution_kind,
145            peer_response_terminal_apply_intent,
146        )
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::policy::{ConsumePoint, DrainPolicy, QueueMode, RoutingDisposition, WakeMode};
154
155    fn policy(apply_mode: ApplyMode) -> PolicyDecision {
156        PolicyDecision {
157            apply_mode,
158            wake_mode: WakeMode::WakeIfIdle,
159            queue_mode: QueueMode::Fifo,
160            consume_point: ConsumePoint::OnRunComplete,
161            drain_policy: DrainPolicy::QueueNextTurn,
162            routing_disposition: RoutingDisposition::Queue,
163            record_transcript: true,
164            emit_operator_content: true,
165            policy_version: crate::policy_table::DEFAULT_POLICY_VERSION,
166        }
167    }
168
169    #[test]
170    fn terminal_peer_response_keeps_content_turn_execution_kind() {
171        let semantics = RuntimeInputSemantics::from_policy_and_kind(
172            &policy(ApplyMode::StageRunBoundary),
173            InputKind::PeerResponseTerminal,
174        );
175
176        assert_eq!(semantics.boundary, RunApplyBoundary::RunCheckpoint);
177        assert_eq!(semantics.execution_kind, RuntimeExecutionKind::ContentTurn);
178        assert_eq!(
179            semantics.peer_response_terminal_apply_intent,
180            Some(PeerResponseTerminalApplyIntent::AppendContextAndRun)
181        );
182    }
183
184    #[test]
185    fn continuation_is_the_only_resume_pending_execution_kind() {
186        let semantics = RuntimeInputSemantics::from_policy_and_kind(
187            &policy(ApplyMode::StageRunBoundary),
188            InputKind::Continuation,
189        );
190
191        assert_eq!(semantics.boundary, RunApplyBoundary::RunCheckpoint);
192        assert_eq!(
193            semantics.execution_kind,
194            RuntimeExecutionKind::ResumePending
195        );
196        assert_eq!(semantics.peer_response_terminal_apply_intent, None);
197    }
198
199    #[test]
200    fn admitted_content_shape_is_closed_to_input_kind_contract() {
201        let shapes = [
202            (InputKind::Prompt, "prompt"),
203            (InputKind::PeerMessage, "peer_message"),
204            (InputKind::PeerRequest, "peer_request"),
205            (InputKind::PeerResponseProgress, "peer_response_progress"),
206            (InputKind::PeerResponseTerminal, "peer_response_terminal"),
207            (InputKind::FlowStep, "flow_step"),
208            (InputKind::ExternalEvent, "external_event"),
209            (InputKind::Continuation, "continuation"),
210            (InputKind::Operation, "operation"),
211        ];
212
213        for (kind, label) in shapes {
214            let shape = ContentShape::from_kind(kind);
215            assert_eq!(shape.kind(), kind);
216            assert_eq!(shape.as_str(), label);
217            assert_eq!(shape.to_string(), label);
218        }
219    }
220
221    #[test]
222    fn admitted_content_shape_source_has_no_string_newtype_contract() {
223        let source = std::fs::read_to_string(
224            std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
225                .join("src")
226                .join("ingress_types.rs"),
227        )
228        .expect("read ingress types source");
229
230        let forbidden = ["pub struct ContentShape", "(pub String)"].concat();
231        assert!(
232            !source.contains(&forbidden),
233            "runtime admitted-input ContentShape must not be a public arbitrary string newtype"
234        );
235    }
236}