1use serde::{Deserialize, Serialize};
4
5use crate::{
6 FailureClass, FrameContext, IntegrationMode, LifecycleEventKind, PayloadEnvelope, PayloadRef,
7 ReceiptStatus, RetryClass, SCHEMA_VERSION, ValidationError, Warning, require_non_empty,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct CallbackRequest {
17 pub schema_version: String,
18 pub event: LifecycleEventKind,
19 pub event_id: String,
20 pub adapter_id: String,
21 pub adapter_version: String,
22 pub integration_mode: IntegrationMode,
23 pub invocation_id: String,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub harness_session_id: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub harness_run_id: Option<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub harness_task_id: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub frame_context: Option<FrameContext>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub capability_snapshot_ref: Option<String>,
34 #[serde(default, skip_serializing_if = "Vec::is_empty")]
35 pub payload_refs: Vec<PayloadRef>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub sequence: Option<u64>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub idempotency_key: Option<String>,
40 #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
41 pub metadata: serde_json::Map<String, serde_json::Value>,
42}
43
44impl CallbackRequest {
45 pub fn validate(&self) -> Result<(), ValidationError> {
46 if self.schema_version != SCHEMA_VERSION {
47 return Err(ValidationError::SchemaVersionMismatch {
48 expected: SCHEMA_VERSION.to_string(),
49 found: self.schema_version.clone(),
50 });
51 }
52 require_non_empty(&self.event_id, "request.event_id")?;
53 require_non_empty(&self.adapter_id, "request.adapter_id")?;
54 require_non_empty(&self.adapter_version, "request.adapter_version")?;
55 require_non_empty(&self.invocation_id, "request.invocation_id")?;
56 if let Some(s) = &self.harness_session_id {
57 require_non_empty(s, "request.harness_session_id")?;
58 }
59 if let Some(s) = &self.harness_run_id {
60 require_non_empty(s, "request.harness_run_id")?;
61 }
62 if let Some(s) = &self.harness_task_id {
63 require_non_empty(s, "request.harness_task_id")?;
64 }
65 if let Some(s) = &self.capability_snapshot_ref {
66 require_non_empty(s, "request.capability_snapshot_ref")?;
67 }
68 if let Some(s) = &self.idempotency_key {
69 require_non_empty(s, "request.idempotency_key")?;
70 }
71 if let Some(fc) = &self.frame_context {
72 fc.validate()?;
73 }
74 for r in &self.payload_refs {
75 r.validate()?;
76 }
77 match self.event {
78 LifecycleEventKind::FrameOpening
79 | LifecycleEventKind::FrameOpened
80 | LifecycleEventKind::FrameEnding
81 | LifecycleEventKind::FrameEnded
82 if self.frame_context.is_none() =>
83 {
84 Err(ValidationError::InvalidRequest(
85 "frame.* events require frame_context".into(),
86 ))
87 }
88 LifecycleEventKind::ReceiptEmitted if self.idempotency_key.is_some() => {
89 Err(ValidationError::InvalidRequest(
90 "receipt.emitted is a notification event and must not carry an idempotency_key"
91 .into(),
92 ))
93 }
94 _ => Ok(()),
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(deny_unknown_fields)]
101pub struct CallbackResponse {
102 pub schema_version: String,
103 pub status: ReceiptStatus,
104 #[serde(default, skip_serializing_if = "Vec::is_empty")]
105 pub client_payloads: Vec<PayloadEnvelope>,
106 #[serde(default, skip_serializing_if = "Vec::is_empty")]
107 pub receipt_refs: Vec<String>,
108 #[serde(default, skip_serializing_if = "Vec::is_empty")]
109 pub warnings: Vec<Warning>,
110 pub failure_class: Option<FailureClass>,
111 pub retry_class: Option<RetryClass>,
112 #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
113 pub metadata: serde_json::Map<String, serde_json::Value>,
114}
115
116impl CallbackResponse {
117 pub fn ok(status: ReceiptStatus) -> Self {
118 Self {
119 schema_version: SCHEMA_VERSION.to_string(),
120 status,
121 client_payloads: Vec::new(),
122 receipt_refs: Vec::new(),
123 warnings: Vec::new(),
124 failure_class: None,
125 retry_class: None,
126 metadata: serde_json::Map::new(),
127 }
128 }
129
130 pub fn failed(failure: FailureClass) -> Self {
131 Self {
132 schema_version: SCHEMA_VERSION.to_string(),
133 status: ReceiptStatus::Failed,
134 client_payloads: Vec::new(),
135 receipt_refs: Vec::new(),
136 warnings: Vec::new(),
137 failure_class: Some(failure),
138 retry_class: Some(failure.default_retry()),
139 metadata: serde_json::Map::new(),
140 }
141 }
142
143 pub fn validate(&self) -> Result<(), ValidationError> {
144 if self.schema_version != SCHEMA_VERSION {
145 return Err(ValidationError::SchemaVersionMismatch {
146 expected: SCHEMA_VERSION.to_string(),
147 found: self.schema_version.clone(),
148 });
149 }
150 for p in &self.client_payloads {
151 p.validate()?;
152 }
153 for r in &self.receipt_refs {
154 require_non_empty(r, "response.receipt_refs[]")?;
155 }
156 for w in &self.warnings {
157 w.validate()?;
158 }
159 match (
160 matches!(self.status, ReceiptStatus::Failed),
161 self.failure_class.is_some(),
162 ) {
163 (true, false) => {
164 return Err(ValidationError::InvalidResponse(
165 "status=failed requires failure_class".into(),
166 ));
167 }
168 (false, true) => {
169 return Err(ValidationError::InvalidResponse(
170 "failure_class is only valid on status=failed responses".into(),
171 ));
172 }
173 _ => {}
174 }
175 if matches!(self.status, ReceiptStatus::Failed) && self.retry_class.is_none() {
176 return Err(ValidationError::InvalidResponse(
177 "status=failed requires retry_class (clients must declare retry posture)".into(),
178 ));
179 }
180 Ok(())
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(deny_unknown_fields)]
218pub struct DispatchEnvelope {
219 pub schema_version: String,
220 pub request: CallbackRequest,
221 #[serde(default, skip_serializing_if = "Vec::is_empty")]
222 pub payloads: Vec<PayloadEnvelope>,
223}
224
225impl DispatchEnvelope {
226 pub fn new(request: CallbackRequest, payloads: Vec<PayloadEnvelope>) -> Self {
228 Self {
229 schema_version: SCHEMA_VERSION.to_string(),
230 request,
231 payloads,
232 }
233 }
234
235 pub fn validate(&self) -> Result<(), ValidationError> {
243 if self.schema_version != SCHEMA_VERSION {
244 return Err(ValidationError::SchemaVersionMismatch {
245 expected: SCHEMA_VERSION.to_string(),
246 found: self.schema_version.clone(),
247 });
248 }
249 self.request.validate()?;
250 for p in &self.payloads {
251 p.validate()?;
252 }
253 Ok(())
254 }
255}