1use 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#[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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
64pub struct ReservationKey(pub String);
65
66#[derive(Debug, Clone, PartialEq, Eq, Hash)]
68pub struct RequestId(pub String);
69
70#[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#[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}