Skip to main content

lifeloop/
wire.rs

1//! Core wire vocabularies, validation helpers, frame context, and payload shapes.
2
3use serde::{Deserialize, Serialize};
4
5use crate::SCHEMA_VERSION;
6
7// ============================================================================
8// Wire enums
9// ============================================================================
10
11#[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/// Support states for adapter capability claims.
32///
33/// Per `docs/specs/lifecycle-contract/body.md` ("Support states"), the
34/// pre-issue-#6 vocabulary distinguished `simulated` from `inferred`.
35/// Issue #6 simplified to one synthesizing state plus `partial`:
36///
37/// * `simulated` → renamed to `synthesized` (clearer about derivation).
38/// * `inferred` → folded into `partial` (telemetry-derived behavior is
39///   partial behavior).
40#[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    /// Default retry-class mapping per the spec's failure-to-retry table.
189    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// ============================================================================
302// Validation
303// ============================================================================
304
305/// Reasons a Lifeloop envelope, payload, or receipt failed validation.
306#[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// ============================================================================
347// Frame context
348// ============================================================================
349
350#[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// ============================================================================
394// Payload envelope and references
395// ============================================================================
396
397#[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// ============================================================================
501// Receipts
502// ============================================================================
503
504#[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}