1use crate::{
12 AdapterManifest, CallbackRequest, FrameContext, IntegrationMode, LifecycleEventKind,
13 PayloadRef, SCHEMA_VERSION,
14};
15
16use super::validation::{AdapterRegistry, AdapterResolution, RouteError, manifest_of};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct RoutingPlan {
31 pub event: LifecycleEventKind,
33 pub event_id: String,
35 pub invocation_id: String,
37 pub adapter: AdapterManifest,
40 pub integration_mode: IntegrationMode,
45 pub harness_session_id: Option<String>,
47 pub harness_run_id: Option<String>,
49 pub harness_task_id: Option<String>,
51 pub frame_context: Option<FrameContext>,
53 pub payload_refs: Vec<PayloadRef>,
56 pub capability_snapshot_ref: Option<String>,
58 pub sequence: Option<u64>,
60 pub idempotency_key: Option<String>,
62 pub metadata: serde_json::Map<String, serde_json::Value>,
64}
65
66pub fn route<R: AdapterRegistry>(
79 req: &CallbackRequest,
80 registry: &R,
81) -> Result<RoutingPlan, RouteError> {
82 if req.schema_version != SCHEMA_VERSION {
84 return Err(RouteError::SchemaVersionMismatch {
85 expected: SCHEMA_VERSION.to_string(),
86 found: req.schema_version.clone(),
87 });
88 }
89
90 require_non_empty(&req.event_id, "request.event_id")?;
95 require_non_empty(&req.adapter_id, "request.adapter_id")?;
96 require_non_empty(&req.adapter_version, "request.adapter_version")?;
97 require_non_empty(&req.invocation_id, "request.invocation_id")?;
98 if let Some(s) = &req.harness_session_id {
99 require_non_empty(s, "request.harness_session_id")?;
100 }
101 if let Some(s) = &req.harness_run_id {
102 require_non_empty(s, "request.harness_run_id")?;
103 }
104 if let Some(s) = &req.harness_task_id {
105 require_non_empty(s, "request.harness_task_id")?;
106 }
107 if let Some(s) = &req.capability_snapshot_ref {
108 require_non_empty(s, "request.capability_snapshot_ref")?;
109 }
110 if let Some(s) = &req.idempotency_key {
111 require_non_empty(s, "request.idempotency_key")?;
112 }
113
114 if let Some(fc) = &req.frame_context {
116 validate_frame_context(fc)?;
117 }
118 require_frame_context_for_event(req)?;
119
120 if matches!(req.event, LifecycleEventKind::ReceiptEmitted) && req.idempotency_key.is_some() {
122 return Err(RouteError::InvalidEventEnvelope {
123 detail: "receipt.emitted is a notification event and must not carry \
124 an idempotency_key"
125 .into(),
126 });
127 }
128
129 for (idx, r) in req.payload_refs.iter().enumerate() {
131 if r.payload_id.is_empty() {
132 return Err(RouteError::InvalidPayloadRef {
133 index: idx,
134 detail: "payload_ref.payload_id is empty".into(),
135 });
136 }
137 if r.payload_kind.is_empty() {
138 return Err(RouteError::InvalidPayloadRef {
139 index: idx,
140 detail: "payload_ref.payload_kind is empty".into(),
141 });
142 }
143 }
144
145 let resolution = registry.resolve(&req.adapter_id, &req.adapter_version);
147 let manifest = match &resolution {
148 AdapterResolution::Found(_) => manifest_of(&resolution).expect("Found carries manifest"),
149 AdapterResolution::UnknownId => {
150 return Err(RouteError::AdapterIdNotFound {
151 adapter_id: req.adapter_id.clone(),
152 });
153 }
154 AdapterResolution::VersionMismatch { registered_version } => {
155 return Err(RouteError::AdapterVersionMismatch {
156 adapter_id: req.adapter_id.clone(),
157 requested: req.adapter_version.clone(),
158 registered: registered_version.clone(),
159 });
160 }
161 };
162
163 Ok(RoutingPlan {
164 event: req.event,
165 event_id: req.event_id.clone(),
166 invocation_id: req.invocation_id.clone(),
167 adapter: manifest.clone(),
168 integration_mode: req.integration_mode,
169 harness_session_id: req.harness_session_id.clone(),
170 harness_run_id: req.harness_run_id.clone(),
171 harness_task_id: req.harness_task_id.clone(),
172 frame_context: req.frame_context.clone(),
173 payload_refs: req.payload_refs.clone(),
174 capability_snapshot_ref: req.capability_snapshot_ref.clone(),
175 sequence: req.sequence,
176 idempotency_key: req.idempotency_key.clone(),
177 metadata: req.metadata.clone(),
178 })
179}
180
181fn require_non_empty(value: &str, field: &'static str) -> Result<(), RouteError> {
182 if value.is_empty() {
183 Err(RouteError::EmptySentinel { field })
184 } else {
185 Ok(())
186 }
187}
188
189fn validate_frame_context(fc: &FrameContext) -> Result<(), RouteError> {
204 if fc.frame_id.is_empty() {
205 return Err(RouteError::InvalidFrameContext {
206 detail: "frame_id is empty".into(),
207 });
208 }
209 if let Some(parent) = &fc.parent_frame_id
210 && parent.is_empty()
211 {
212 return Err(RouteError::InvalidFrameContext {
213 detail: "parent_frame_id is empty".into(),
214 });
215 }
216 match (fc.frame_class, &fc.parent_frame_id) {
217 (crate::FrameClass::TopLevel, Some(_)) => Err(RouteError::InvalidFrameContext {
218 detail: "frame_class=top_level must not carry parent_frame_id".into(),
219 }),
220 (crate::FrameClass::Subcall, None) => Err(RouteError::InvalidFrameContext {
221 detail: "frame_class=subcall requires parent_frame_id".into(),
222 }),
223 _ => Ok(()),
224 }
225}
226
227fn require_frame_context_for_event(req: &CallbackRequest) -> Result<(), RouteError> {
228 let needs_frame = matches!(
229 req.event,
230 LifecycleEventKind::FrameOpening
231 | LifecycleEventKind::FrameOpened
232 | LifecycleEventKind::FrameEnding
233 | LifecycleEventKind::FrameEnded
234 );
235 if needs_frame && req.frame_context.is_none() {
236 return Err(RouteError::InvalidFrameContext {
237 detail: "frame.* events require frame_context".into(),
238 });
239 }
240 Ok(())
241}