1use serde::{Deserialize, Serialize};
4
5use crate::SCHEMA_VERSION;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum IntegrationMode {
14 ManualSkill,
15 LauncherWrapper,
16 NativeHook,
17 ReferenceAdapter,
18 TelemetryOnly,
19}
20
21impl IntegrationMode {
22 pub const ALL: &'static [Self] = &[
23 Self::ManualSkill,
24 Self::LauncherWrapper,
25 Self::NativeHook,
26 Self::ReferenceAdapter,
27 Self::TelemetryOnly,
28 ];
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum SupportState {
43 Native,
44 Synthesized,
45 Manual,
46 Partial,
47 Unavailable,
48}
49
50impl SupportState {
51 pub const ALL: &'static [Self] = &[
52 Self::Native,
53 Self::Synthesized,
54 Self::Manual,
55 Self::Partial,
56 Self::Unavailable,
57 ];
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum AdapterRole {
63 PrimaryWorker,
64 Worker,
65 Supervisor,
66 Observer,
67}
68
69impl AdapterRole {
70 pub const ALL: &'static [Self] = &[
71 Self::PrimaryWorker,
72 Self::Worker,
73 Self::Supervisor,
74 Self::Observer,
75 ];
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
79pub enum LifecycleEventKind {
80 #[serde(rename = "session.starting")]
81 SessionStarting,
82 #[serde(rename = "session.started")]
83 SessionStarted,
84 #[serde(rename = "frame.opening")]
85 FrameOpening,
86 #[serde(rename = "frame.opened")]
87 FrameOpened,
88 #[serde(rename = "context.pressure_observed")]
89 ContextPressureObserved,
90 #[serde(rename = "context.compacted")]
91 ContextCompacted,
92 #[serde(rename = "frame.ending")]
93 FrameEnding,
94 #[serde(rename = "frame.ended")]
95 FrameEnded,
96 #[serde(rename = "session.ending")]
97 SessionEnding,
98 #[serde(rename = "session.ended")]
99 SessionEnded,
100 #[serde(rename = "supervisor.tick")]
101 SupervisorTick,
102 #[serde(rename = "capability.degraded")]
103 CapabilityDegraded,
104 #[serde(rename = "receipt.emitted")]
105 ReceiptEmitted,
106 #[serde(rename = "receipt.gap_detected")]
107 ReceiptGapDetected,
108}
109
110impl LifecycleEventKind {
111 pub const ALL: &'static [Self] = &[
112 Self::SessionStarting,
113 Self::SessionStarted,
114 Self::FrameOpening,
115 Self::FrameOpened,
116 Self::ContextPressureObserved,
117 Self::ContextCompacted,
118 Self::FrameEnding,
119 Self::FrameEnded,
120 Self::SessionEnding,
121 Self::SessionEnded,
122 Self::SupervisorTick,
123 Self::CapabilityDegraded,
124 Self::ReceiptEmitted,
125 Self::ReceiptGapDetected,
126 ];
127}
128
129pub fn lifecycle_event_kinds() -> Vec<LifecycleEventKind> {
130 LifecycleEventKind::ALL.to_vec()
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum ReceiptStatus {
136 Observed,
137 Delivered,
138 Skipped,
139 Degraded,
140 Failed,
141}
142
143impl ReceiptStatus {
144 pub const ALL: &'static [Self] = &[
145 Self::Observed,
146 Self::Delivered,
147 Self::Skipped,
148 Self::Degraded,
149 Self::Failed,
150 ];
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
154#[serde(rename_all = "snake_case")]
155pub enum FailureClass {
156 AdapterUnavailable,
157 CapabilityUnsupported,
158 CapabilityDegraded,
159 PlacementUnavailable,
160 PayloadTooLarge,
161 PayloadRejected,
162 IdentityUnavailable,
163 TransportError,
164 Timeout,
165 OperatorRequired,
166 StateConflict,
167 InvalidRequest,
168 InternalError,
169}
170
171impl FailureClass {
172 pub const ALL: &'static [Self] = &[
173 Self::AdapterUnavailable,
174 Self::CapabilityUnsupported,
175 Self::CapabilityDegraded,
176 Self::PlacementUnavailable,
177 Self::PayloadTooLarge,
178 Self::PayloadRejected,
179 Self::IdentityUnavailable,
180 Self::TransportError,
181 Self::Timeout,
182 Self::OperatorRequired,
183 Self::StateConflict,
184 Self::InvalidRequest,
185 Self::InternalError,
186 ];
187
188 pub fn default_retry(self) -> RetryClass {
190 match self {
191 Self::AdapterUnavailable => RetryClass::RetryAfterReconfigure,
192 Self::CapabilityUnsupported => RetryClass::DoNotRetry,
193 Self::CapabilityDegraded => RetryClass::RetryAfterReread,
194 Self::PlacementUnavailable => RetryClass::RetryAfterReconfigure,
195 Self::PayloadTooLarge => RetryClass::DoNotRetry,
196 Self::PayloadRejected => RetryClass::RetryAfterReconfigure,
197 Self::IdentityUnavailable => RetryClass::RetryAfterReconfigure,
198 Self::TransportError => RetryClass::SafeRetry,
199 Self::Timeout => RetryClass::SafeRetry,
200 Self::OperatorRequired => RetryClass::RetryAfterOperator,
201 Self::StateConflict => RetryClass::RetryAfterReread,
202 Self::InvalidRequest => RetryClass::DoNotRetry,
203 Self::InternalError => RetryClass::RetryAfterReread,
204 }
205 }
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
209#[serde(rename_all = "snake_case")]
210pub enum RetryClass {
211 SafeRetry,
212 RetryAfterReread,
213 RetryAfterReconfigure,
214 RetryAfterOperator,
215 DoNotRetry,
216}
217
218impl RetryClass {
219 pub const ALL: &'static [Self] = &[
220 Self::SafeRetry,
221 Self::RetryAfterReread,
222 Self::RetryAfterReconfigure,
223 Self::RetryAfterOperator,
224 Self::DoNotRetry,
225 ];
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum PlacementClass {
231 DeveloperEquivalentFrame,
232 PrePromptFrame,
233 SideChannelContext,
234 ReceiptOnly,
235}
236
237impl PlacementClass {
238 pub const ALL: &'static [Self] = &[
239 Self::DeveloperEquivalentFrame,
240 Self::PrePromptFrame,
241 Self::SideChannelContext,
242 Self::ReceiptOnly,
243 ];
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
247#[serde(rename_all = "snake_case")]
248pub enum PlacementOutcome {
249 Delivered,
250 Skipped,
251 Degraded,
252 Failed,
253}
254
255impl PlacementOutcome {
256 pub const ALL: &'static [Self] =
257 &[Self::Delivered, Self::Skipped, Self::Degraded, Self::Failed];
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
261#[serde(rename_all = "snake_case")]
262pub enum RequirementLevel {
263 Required,
264 Preferred,
265 Optional,
266}
267
268impl RequirementLevel {
269 pub const ALL: &'static [Self] = &[Self::Required, Self::Preferred, Self::Optional];
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
273#[serde(rename_all = "snake_case")]
274pub enum NegotiationOutcome {
275 Satisfied,
276 Degraded,
277 Unsupported,
278 RequiresOperator,
279}
280
281impl NegotiationOutcome {
282 pub const ALL: &'static [Self] = &[
283 Self::Satisfied,
284 Self::Degraded,
285 Self::Unsupported,
286 Self::RequiresOperator,
287 ];
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
291#[serde(rename_all = "snake_case")]
292pub enum FrameClass {
293 TopLevel,
294 Subcall,
295}
296
297impl FrameClass {
298 pub const ALL: &'static [Self] = &[Self::TopLevel, Self::Subcall];
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
307#[serde(rename_all = "snake_case", tag = "kind", content = "detail")]
308pub enum ValidationError {
309 EmptyField(String),
310 SchemaVersionMismatch { expected: String, found: String },
311 InvalidFrameContext(String),
312 InvalidPayload(String),
313 InvalidReceipt(String),
314 InvalidRequest(String),
315 InvalidResponse(String),
316 InvalidManifest(String),
317}
318
319impl std::fmt::Display for ValidationError {
320 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321 match self {
322 Self::EmptyField(name) => write!(f, "empty sentinel string in field `{name}`"),
323 Self::SchemaVersionMismatch { expected, found } => write!(
324 f,
325 "schema_version mismatch: expected `{expected}`, found `{found}`"
326 ),
327 Self::InvalidFrameContext(msg) => write!(f, "invalid frame_context: {msg}"),
328 Self::InvalidPayload(msg) => write!(f, "invalid payload: {msg}"),
329 Self::InvalidReceipt(msg) => write!(f, "invalid receipt: {msg}"),
330 Self::InvalidRequest(msg) => write!(f, "invalid callback request: {msg}"),
331 Self::InvalidResponse(msg) => write!(f, "invalid callback response: {msg}"),
332 Self::InvalidManifest(msg) => write!(f, "invalid adapter manifest: {msg}"),
333 }
334 }
335}
336
337impl std::error::Error for ValidationError {}
338
339pub(crate) fn require_non_empty(value: &str, field: &'static str) -> Result<(), ValidationError> {
340 if value.is_empty() {
341 return Err(ValidationError::EmptyField(field.to_string()));
342 }
343 Ok(())
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
351#[serde(deny_unknown_fields)]
352pub struct FrameContext {
353 pub frame_id: String,
354 #[serde(skip_serializing_if = "Option::is_none")]
355 pub parent_frame_id: Option<String>,
356 pub frame_class: FrameClass,
357}
358
359impl FrameContext {
360 pub fn top_level(frame_id: impl Into<String>) -> Self {
361 Self {
362 frame_id: frame_id.into(),
363 parent_frame_id: None,
364 frame_class: FrameClass::TopLevel,
365 }
366 }
367
368 pub fn subcall(frame_id: impl Into<String>, parent_frame_id: impl Into<String>) -> Self {
369 Self {
370 frame_id: frame_id.into(),
371 parent_frame_id: Some(parent_frame_id.into()),
372 frame_class: FrameClass::Subcall,
373 }
374 }
375
376 pub fn validate(&self) -> Result<(), ValidationError> {
377 require_non_empty(&self.frame_id, "frame_context.frame_id")?;
378 if let Some(parent) = &self.parent_frame_id {
379 require_non_empty(parent, "frame_context.parent_frame_id")?;
380 }
381 match (self.frame_class, &self.parent_frame_id) {
382 (FrameClass::TopLevel, Some(_)) => Err(ValidationError::InvalidFrameContext(
383 "frame_class=top_level must not carry parent_frame_id".into(),
384 )),
385 (FrameClass::Subcall, None) => Err(ValidationError::InvalidFrameContext(
386 "frame_class=subcall requires parent_frame_id".into(),
387 )),
388 _ => Ok(()),
389 }
390 }
391}
392
393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
398#[serde(deny_unknown_fields)]
399pub struct AcceptablePlacement {
400 pub placement: PlacementClass,
401 pub requirement: RequirementLevel,
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
405#[serde(deny_unknown_fields)]
406pub struct PayloadRef {
407 pub payload_id: String,
408 pub payload_kind: String,
409 #[serde(skip_serializing_if = "Option::is_none")]
410 pub content_digest: Option<String>,
411 #[serde(skip_serializing_if = "Option::is_none")]
412 pub byte_size: Option<u64>,
413}
414
415impl PayloadRef {
416 pub fn validate(&self) -> Result<(), ValidationError> {
417 require_non_empty(&self.payload_id, "payload_ref.payload_id")?;
418 require_non_empty(&self.payload_kind, "payload_ref.payload_kind")?;
419 Ok(())
420 }
421}
422
423#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
424#[serde(deny_unknown_fields)]
425pub struct PayloadEnvelope {
426 pub schema_version: String,
427 pub payload_id: String,
428 pub client_id: String,
429 pub payload_kind: String,
430 pub format: String,
431 pub content_encoding: String,
432 #[serde(skip_serializing_if = "Option::is_none")]
433 pub body: Option<String>,
434 #[serde(skip_serializing_if = "Option::is_none")]
435 pub body_ref: Option<String>,
436 pub byte_size: u64,
437 #[serde(skip_serializing_if = "Option::is_none")]
438 pub content_digest: Option<String>,
439 pub acceptable_placements: Vec<AcceptablePlacement>,
440 #[serde(skip_serializing_if = "Option::is_none")]
441 pub idempotency_key: Option<String>,
442 #[serde(skip_serializing_if = "Option::is_none")]
443 pub expires_at_epoch_s: Option<u64>,
444 #[serde(skip_serializing_if = "Option::is_none")]
445 pub redaction: Option<String>,
446 #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
447 pub metadata: serde_json::Map<String, serde_json::Value>,
448}
449
450impl PayloadEnvelope {
451 pub fn effective_byte_size(&self) -> u64 {
452 self.body
453 .as_ref()
454 .map(|body| body.len() as u64)
455 .unwrap_or(self.byte_size)
456 }
457
458 pub fn validate(&self) -> Result<(), ValidationError> {
459 if self.schema_version != SCHEMA_VERSION {
460 return Err(ValidationError::SchemaVersionMismatch {
461 expected: SCHEMA_VERSION.to_string(),
462 found: self.schema_version.clone(),
463 });
464 }
465 require_non_empty(&self.payload_id, "payload.payload_id")?;
466 require_non_empty(&self.client_id, "payload.client_id")?;
467 require_non_empty(&self.payload_kind, "payload.payload_kind")?;
468 require_non_empty(&self.format, "payload.format")?;
469 require_non_empty(&self.content_encoding, "payload.content_encoding")?;
470 match (self.body.is_some(), self.body_ref.is_some()) {
471 (true, true) => Err(ValidationError::InvalidPayload(
472 "body and body_ref are mutually exclusive".into(),
473 )),
474 (false, false) => Err(ValidationError::InvalidPayload(
475 "exactly one of body or body_ref must be present".into(),
476 )),
477 _ => Ok(()),
478 }?;
479 if let Some(body) = &self.body {
480 let actual = body.len() as u64;
481 if actual != self.byte_size {
482 return Err(ValidationError::InvalidPayload(format!(
483 "body byte length {actual} does not match byte_size {}",
484 self.byte_size
485 )));
486 }
487 }
488 if let Some(idem) = &self.idempotency_key {
489 require_non_empty(idem, "payload.idempotency_key")?;
490 }
491 if self.acceptable_placements.is_empty() {
492 return Err(ValidationError::InvalidPayload(
493 "acceptable_placements must list at least one placement".into(),
494 ));
495 }
496 Ok(())
497 }
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
505#[serde(deny_unknown_fields)]
506pub struct PayloadReceipt {
507 pub payload_id: String,
508 pub payload_kind: String,
509 pub placement: PlacementClass,
510 pub status: PlacementOutcome,
511 pub byte_size: u64,
512 #[serde(skip_serializing_if = "Option::is_none")]
513 pub content_digest: Option<String>,
514}
515
516impl PayloadReceipt {
517 pub fn validate(&self) -> Result<(), ValidationError> {
518 require_non_empty(&self.payload_id, "payload_receipt.payload_id")?;
519 require_non_empty(&self.payload_kind, "payload_receipt.payload_kind")?;
520 Ok(())
521 }
522}
523
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
525#[serde(deny_unknown_fields)]
526pub struct CapabilityDegradation {
527 pub capability: String,
528 pub previous_support: SupportState,
529 pub current_support: SupportState,
530 #[serde(skip_serializing_if = "Option::is_none")]
531 pub evidence: Option<String>,
532 #[serde(skip_serializing_if = "Option::is_none")]
533 pub retry_class: Option<RetryClass>,
534}
535
536impl CapabilityDegradation {
537 pub fn validate(&self) -> Result<(), ValidationError> {
538 require_non_empty(&self.capability, "capability_degradation.capability")?;
539 Ok(())
540 }
541}
542
543#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
544#[serde(deny_unknown_fields)]
545pub struct Warning {
546 pub code: String,
547 pub message: String,
548 #[serde(skip_serializing_if = "Option::is_none")]
549 pub capability: Option<String>,
550}
551
552impl Warning {
553 pub fn validate(&self) -> Result<(), ValidationError> {
554 require_non_empty(&self.code, "warning.code")?;
555 Ok(())
556 }
557}