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 meerkat_core::types::HandlingMode;
15use serde::{Deserialize, Serialize};
16
17use crate::identifiers::{InputKind, KindId};
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(crate) boundary: RunApplyBoundary,
79    pub(crate) execution_kind: RuntimeExecutionKind,
80    pub(crate) execution_handling_mode: Option<HandlingMode>,
81    pub(crate) peer_response_terminal_apply_intent: Option<PeerResponseTerminalApplyIntent>,
82    /// #338: machine-owned verdict that this admitted input requires a live
83    /// channel interrupt (true iff the admitted lane is `Steer`). Carried
84    /// end-to-end so the live-projection consumer reads the typed fact instead
85    /// of re-scanning `handling_mode == Steer`.
86    #[serde(default)]
87    pub(crate) live_interrupt_required: bool,
88}
89
90/// Admitted conversation projection for one input.
91///
92/// The raw input payload is still retained for durability/replay, but the
93/// runtime loop consumes this admitted projection when constructing
94/// `RunPrimitive` so dequeue mechanics do not reinterpret peer conventions or
95/// terminal status payloads.
96// Cannot derive `Eq`: `peer_response_terminal` carries a typed fact whose
97// render payload is a `serde_json::Value`, which is `PartialEq` but not `Eq`.
98#[derive(Debug, Clone, Default, PartialEq)]
99pub struct RuntimeInputProjection {
100    /// Host-attached injected-context appends staged immediately BEFORE
101    /// `append` (the input's own transcript append) when the batch primitive
102    /// is constructed. A distinct slot — not `additional_appends` — because
103    /// the ordering invariant is "injected context lands before the turn's
104    /// user/peer append" and `additional_appends` chain AFTER it.
105    pub injected_context_appends: Vec<ConversationAppend>,
106    pub append: Option<ConversationAppend>,
107    pub additional_appends: Vec<ConversationAppend>,
108    pub context_append: Option<ConversationContextAppend>,
109    /// Typed terminal-peer-response fact this projection carries when the
110    /// admitted input is a peer terminal response. The producer threads the
111    /// typed [`meerkat_core::PeerResponseTerminalFact`] all the way to the
112    /// realtime/live consumer instead of re-deriving it from the flattened
113    /// `context_append` prose text.
114    pub peer_response_terminal: Option<meerkat_core::PeerResponseTerminalFact>,
115}
116
117impl RuntimeInputSemantics {
118    pub fn try_from_generated_admission(
119        input: &crate::input::Input,
120        runtime_idle: bool,
121    ) -> Result<Self, String> {
122        crate::policy_table::generated_admission_projection_for_input(input, runtime_idle)
123            .map(|projection| projection.runtime_semantics)
124    }
125
126    pub fn boundary(&self) -> RunApplyBoundary {
127        self.boundary
128    }
129
130    pub fn execution_kind(&self) -> RuntimeExecutionKind {
131        self.execution_kind
132    }
133
134    pub fn peer_response_terminal_apply_intent(&self) -> Option<PeerResponseTerminalApplyIntent> {
135        self.peer_response_terminal_apply_intent
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn terminal_peer_response_keeps_content_turn_execution_kind() {
145        let semantics = crate::policy_table::generated_admission_projection_for_kind(
146            KindId::new(InputKind::PeerResponseTerminal),
147            false,
148        )
149        .expect("generated admission projection")
150        .runtime_semantics;
151
152        assert_eq!(semantics.boundary, RunApplyBoundary::RunStart);
153        assert_eq!(semantics.execution_kind, RuntimeExecutionKind::ContentTurn);
154        assert_eq!(semantics.execution_handling_mode, None);
155        assert_eq!(
156            semantics.peer_response_terminal_apply_intent,
157            Some(PeerResponseTerminalApplyIntent::AppendContextAndRun)
158        );
159    }
160
161    #[test]
162    fn continuation_is_the_only_resume_pending_execution_kind() {
163        let semantics = crate::policy_table::generated_admission_projection_for_kind(
164            KindId::new(InputKind::Continuation),
165            false,
166        )
167        .expect("generated admission projection")
168        .runtime_semantics;
169
170        assert_eq!(semantics.boundary, RunApplyBoundary::RunCheckpoint);
171        assert_eq!(
172            semantics.execution_kind,
173            RuntimeExecutionKind::ResumePending
174        );
175        assert_eq!(semantics.execution_handling_mode, None);
176        assert_eq!(semantics.peer_response_terminal_apply_intent, None);
177    }
178
179    #[test]
180    fn admitted_content_shape_is_closed_to_input_kind_contract() {
181        let shapes = [
182            (InputKind::Prompt, "prompt"),
183            (InputKind::PeerMessage, "peer_message"),
184            (InputKind::PeerRequest, "peer_request"),
185            (InputKind::PeerResponseProgress, "peer_response_progress"),
186            (InputKind::PeerResponseTerminal, "peer_response_terminal"),
187            (InputKind::FlowStep, "flow_step"),
188            (InputKind::ExternalEvent, "external_event"),
189            (InputKind::Continuation, "continuation"),
190            (InputKind::Operation, "operation"),
191        ];
192
193        for (kind, label) in shapes {
194            let shape = ContentShape::from_kind(kind);
195            assert_eq!(shape.kind(), kind);
196            assert_eq!(shape.as_str(), label);
197            assert_eq!(shape.to_string(), label);
198        }
199    }
200
201    #[test]
202    fn admitted_content_shape_source_has_no_string_newtype_contract() {
203        let source = std::fs::read_to_string(
204            std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
205                .join("src")
206                .join("ingress_types.rs"),
207        )
208        .expect("read ingress types source");
209
210        let forbidden = ["pub struct ContentShape", "(pub String)"].concat();
211        assert!(
212            !source.contains(&forbidden),
213            "runtime admitted-input ContentShape must not be a public arbitrary string newtype"
214        );
215    }
216}