Skip to main content

lifeloop/
lib.rs

1//! Provider-neutral lifecycle contracts for AI harnesses.
2//!
3//! `docs/specs/lifecycle-contract/body.md` is the normative target. This module
4//! implements the `lifeloop.v0.1` slice of that contract: the wire enums,
5//! lifecycle receipt, payload envelope, and the callback request/response
6//! envelopes that clients implement.
7
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11pub mod host_assets;
12pub mod protocol;
13pub mod router;
14pub mod source_files;
15pub mod telemetry;
16
17pub const SCHEMA_VERSION: &str = "lifeloop.v0.1";
18
19// ============================================================================
20// Wire enums
21// ============================================================================
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum IntegrationMode {
26    ManualSkill,
27    LauncherWrapper,
28    NativeHook,
29    ReferenceAdapter,
30    TelemetryOnly,
31}
32
33impl IntegrationMode {
34    pub const ALL: &'static [Self] = &[
35        Self::ManualSkill,
36        Self::LauncherWrapper,
37        Self::NativeHook,
38        Self::ReferenceAdapter,
39        Self::TelemetryOnly,
40    ];
41}
42
43/// Support states for adapter capability claims.
44///
45/// Per `docs/specs/lifecycle-contract/body.md` ("Support states"), the
46/// pre-issue-#6 vocabulary distinguished `simulated` from `inferred`.
47/// Issue #6 simplified to one synthesizing state plus `partial`:
48///
49/// * `simulated` → renamed to `synthesized` (clearer about derivation).
50/// * `inferred` → folded into `partial` (telemetry-derived behavior is
51///   partial behavior).
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum SupportState {
55    Native,
56    Synthesized,
57    Manual,
58    Partial,
59    Unavailable,
60}
61
62impl SupportState {
63    pub const ALL: &'static [Self] = &[
64        Self::Native,
65        Self::Synthesized,
66        Self::Manual,
67        Self::Partial,
68        Self::Unavailable,
69    ];
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum AdapterRole {
75    PrimaryWorker,
76    Worker,
77    Supervisor,
78    Observer,
79}
80
81impl AdapterRole {
82    pub const ALL: &'static [Self] = &[
83        Self::PrimaryWorker,
84        Self::Worker,
85        Self::Supervisor,
86        Self::Observer,
87    ];
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
91pub enum LifecycleEventKind {
92    #[serde(rename = "session.starting")]
93    SessionStarting,
94    #[serde(rename = "session.started")]
95    SessionStarted,
96    #[serde(rename = "frame.opening")]
97    FrameOpening,
98    #[serde(rename = "frame.opened")]
99    FrameOpened,
100    #[serde(rename = "context.pressure_observed")]
101    ContextPressureObserved,
102    #[serde(rename = "context.compacted")]
103    ContextCompacted,
104    #[serde(rename = "frame.ending")]
105    FrameEnding,
106    #[serde(rename = "frame.ended")]
107    FrameEnded,
108    #[serde(rename = "session.ending")]
109    SessionEnding,
110    #[serde(rename = "session.ended")]
111    SessionEnded,
112    #[serde(rename = "supervisor.tick")]
113    SupervisorTick,
114    #[serde(rename = "capability.degraded")]
115    CapabilityDegraded,
116    #[serde(rename = "receipt.emitted")]
117    ReceiptEmitted,
118    #[serde(rename = "receipt.gap_detected")]
119    ReceiptGapDetected,
120}
121
122impl LifecycleEventKind {
123    pub const ALL: &'static [Self] = &[
124        Self::SessionStarting,
125        Self::SessionStarted,
126        Self::FrameOpening,
127        Self::FrameOpened,
128        Self::ContextPressureObserved,
129        Self::ContextCompacted,
130        Self::FrameEnding,
131        Self::FrameEnded,
132        Self::SessionEnding,
133        Self::SessionEnded,
134        Self::SupervisorTick,
135        Self::CapabilityDegraded,
136        Self::ReceiptEmitted,
137        Self::ReceiptGapDetected,
138    ];
139}
140
141pub fn lifecycle_event_kinds() -> Vec<LifecycleEventKind> {
142    LifecycleEventKind::ALL.to_vec()
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
146#[serde(rename_all = "snake_case")]
147pub enum ReceiptStatus {
148    Observed,
149    Delivered,
150    Skipped,
151    Degraded,
152    Failed,
153}
154
155impl ReceiptStatus {
156    pub const ALL: &'static [Self] = &[
157        Self::Observed,
158        Self::Delivered,
159        Self::Skipped,
160        Self::Degraded,
161        Self::Failed,
162    ];
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
166#[serde(rename_all = "snake_case")]
167pub enum FailureClass {
168    AdapterUnavailable,
169    CapabilityUnsupported,
170    CapabilityDegraded,
171    PlacementUnavailable,
172    PayloadTooLarge,
173    PayloadRejected,
174    IdentityUnavailable,
175    TransportError,
176    Timeout,
177    OperatorRequired,
178    StateConflict,
179    InvalidRequest,
180    InternalError,
181}
182
183impl FailureClass {
184    pub const ALL: &'static [Self] = &[
185        Self::AdapterUnavailable,
186        Self::CapabilityUnsupported,
187        Self::CapabilityDegraded,
188        Self::PlacementUnavailable,
189        Self::PayloadTooLarge,
190        Self::PayloadRejected,
191        Self::IdentityUnavailable,
192        Self::TransportError,
193        Self::Timeout,
194        Self::OperatorRequired,
195        Self::StateConflict,
196        Self::InvalidRequest,
197        Self::InternalError,
198    ];
199
200    /// Default retry-class mapping per the spec's failure-to-retry table.
201    pub fn default_retry(self) -> RetryClass {
202        match self {
203            Self::AdapterUnavailable => RetryClass::RetryAfterReconfigure,
204            Self::CapabilityUnsupported => RetryClass::DoNotRetry,
205            Self::CapabilityDegraded => RetryClass::RetryAfterReread,
206            Self::PlacementUnavailable => RetryClass::RetryAfterReconfigure,
207            Self::PayloadTooLarge => RetryClass::DoNotRetry,
208            Self::PayloadRejected => RetryClass::RetryAfterReconfigure,
209            Self::IdentityUnavailable => RetryClass::RetryAfterReconfigure,
210            Self::TransportError => RetryClass::SafeRetry,
211            Self::Timeout => RetryClass::SafeRetry,
212            Self::OperatorRequired => RetryClass::RetryAfterOperator,
213            Self::StateConflict => RetryClass::RetryAfterReread,
214            Self::InvalidRequest => RetryClass::DoNotRetry,
215            Self::InternalError => RetryClass::RetryAfterReread,
216        }
217    }
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
221#[serde(rename_all = "snake_case")]
222pub enum RetryClass {
223    SafeRetry,
224    RetryAfterReread,
225    RetryAfterReconfigure,
226    RetryAfterOperator,
227    DoNotRetry,
228}
229
230impl RetryClass {
231    pub const ALL: &'static [Self] = &[
232        Self::SafeRetry,
233        Self::RetryAfterReread,
234        Self::RetryAfterReconfigure,
235        Self::RetryAfterOperator,
236        Self::DoNotRetry,
237    ];
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
241#[serde(rename_all = "snake_case")]
242pub enum PlacementClass {
243    DeveloperEquivalentFrame,
244    PrePromptFrame,
245    SideChannelContext,
246    ReceiptOnly,
247}
248
249impl PlacementClass {
250    pub const ALL: &'static [Self] = &[
251        Self::DeveloperEquivalentFrame,
252        Self::PrePromptFrame,
253        Self::SideChannelContext,
254        Self::ReceiptOnly,
255    ];
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
259#[serde(rename_all = "snake_case")]
260pub enum PlacementOutcome {
261    Delivered,
262    Skipped,
263    Degraded,
264    Failed,
265}
266
267impl PlacementOutcome {
268    pub const ALL: &'static [Self] =
269        &[Self::Delivered, Self::Skipped, Self::Degraded, Self::Failed];
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
273#[serde(rename_all = "snake_case")]
274pub enum RequirementLevel {
275    Required,
276    Preferred,
277    Optional,
278}
279
280impl RequirementLevel {
281    pub const ALL: &'static [Self] = &[Self::Required, Self::Preferred, Self::Optional];
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286pub enum NegotiationOutcome {
287    Satisfied,
288    Degraded,
289    Unsupported,
290    RequiresOperator,
291}
292
293impl NegotiationOutcome {
294    pub const ALL: &'static [Self] = &[
295        Self::Satisfied,
296        Self::Degraded,
297        Self::Unsupported,
298        Self::RequiresOperator,
299    ];
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
303#[serde(rename_all = "snake_case")]
304pub enum FrameClass {
305    TopLevel,
306    Subcall,
307}
308
309impl FrameClass {
310    pub const ALL: &'static [Self] = &[Self::TopLevel, Self::Subcall];
311}
312
313// ============================================================================
314// Validation
315// ============================================================================
316
317/// Reasons a Lifeloop envelope, payload, or receipt failed validation.
318#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
319#[serde(rename_all = "snake_case", tag = "kind", content = "detail")]
320pub enum ValidationError {
321    EmptyField(String),
322    SchemaVersionMismatch { expected: String, found: String },
323    InvalidFrameContext(String),
324    InvalidPayload(String),
325    InvalidReceipt(String),
326    InvalidRequest(String),
327    InvalidResponse(String),
328    InvalidManifest(String),
329}
330
331impl std::fmt::Display for ValidationError {
332    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333        match self {
334            Self::EmptyField(name) => write!(f, "empty sentinel string in field `{name}`"),
335            Self::SchemaVersionMismatch { expected, found } => write!(
336                f,
337                "schema_version mismatch: expected `{expected}`, found `{found}`"
338            ),
339            Self::InvalidFrameContext(msg) => write!(f, "invalid frame_context: {msg}"),
340            Self::InvalidPayload(msg) => write!(f, "invalid payload: {msg}"),
341            Self::InvalidReceipt(msg) => write!(f, "invalid receipt: {msg}"),
342            Self::InvalidRequest(msg) => write!(f, "invalid callback request: {msg}"),
343            Self::InvalidResponse(msg) => write!(f, "invalid callback response: {msg}"),
344            Self::InvalidManifest(msg) => write!(f, "invalid adapter manifest: {msg}"),
345        }
346    }
347}
348
349impl std::error::Error for ValidationError {}
350
351fn require_non_empty(value: &str, field: &'static str) -> Result<(), ValidationError> {
352    if value.is_empty() {
353        return Err(ValidationError::EmptyField(field.to_string()));
354    }
355    Ok(())
356}
357
358// ============================================================================
359// Frame context
360// ============================================================================
361
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(deny_unknown_fields)]
364pub struct FrameContext {
365    pub frame_id: String,
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub parent_frame_id: Option<String>,
368    pub frame_class: FrameClass,
369}
370
371impl FrameContext {
372    pub fn top_level(frame_id: impl Into<String>) -> Self {
373        Self {
374            frame_id: frame_id.into(),
375            parent_frame_id: None,
376            frame_class: FrameClass::TopLevel,
377        }
378    }
379
380    pub fn subcall(frame_id: impl Into<String>, parent_frame_id: impl Into<String>) -> Self {
381        Self {
382            frame_id: frame_id.into(),
383            parent_frame_id: Some(parent_frame_id.into()),
384            frame_class: FrameClass::Subcall,
385        }
386    }
387
388    pub fn validate(&self) -> Result<(), ValidationError> {
389        require_non_empty(&self.frame_id, "frame_context.frame_id")?;
390        if let Some(parent) = &self.parent_frame_id {
391            require_non_empty(parent, "frame_context.parent_frame_id")?;
392        }
393        match (self.frame_class, &self.parent_frame_id) {
394            (FrameClass::TopLevel, Some(_)) => Err(ValidationError::InvalidFrameContext(
395                "frame_class=top_level must not carry parent_frame_id".into(),
396            )),
397            (FrameClass::Subcall, None) => Err(ValidationError::InvalidFrameContext(
398                "frame_class=subcall requires parent_frame_id".into(),
399            )),
400            _ => Ok(()),
401        }
402    }
403}
404
405// ============================================================================
406// Payload envelope and references
407// ============================================================================
408
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
410#[serde(deny_unknown_fields)]
411pub struct AcceptablePlacement {
412    pub placement: PlacementClass,
413    pub requirement: RequirementLevel,
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
417#[serde(deny_unknown_fields)]
418pub struct PayloadRef {
419    pub payload_id: String,
420    pub payload_kind: String,
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub content_digest: Option<String>,
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub byte_size: Option<u64>,
425}
426
427impl PayloadRef {
428    pub fn validate(&self) -> Result<(), ValidationError> {
429        require_non_empty(&self.payload_id, "payload_ref.payload_id")?;
430        require_non_empty(&self.payload_kind, "payload_ref.payload_kind")?;
431        Ok(())
432    }
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436#[serde(deny_unknown_fields)]
437pub struct PayloadEnvelope {
438    pub schema_version: String,
439    pub payload_id: String,
440    pub client_id: String,
441    pub payload_kind: String,
442    pub format: String,
443    pub content_encoding: String,
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub body: Option<String>,
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub body_ref: Option<String>,
448    pub byte_size: u64,
449    #[serde(skip_serializing_if = "Option::is_none")]
450    pub content_digest: Option<String>,
451    pub acceptable_placements: Vec<AcceptablePlacement>,
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub idempotency_key: Option<String>,
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub expires_at_epoch_s: Option<u64>,
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub redaction: Option<String>,
458    #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
459    pub metadata: serde_json::Map<String, serde_json::Value>,
460}
461
462impl PayloadEnvelope {
463    pub fn validate(&self) -> Result<(), ValidationError> {
464        if self.schema_version != SCHEMA_VERSION {
465            return Err(ValidationError::SchemaVersionMismatch {
466                expected: SCHEMA_VERSION.to_string(),
467                found: self.schema_version.clone(),
468            });
469        }
470        require_non_empty(&self.payload_id, "payload.payload_id")?;
471        require_non_empty(&self.client_id, "payload.client_id")?;
472        require_non_empty(&self.payload_kind, "payload.payload_kind")?;
473        require_non_empty(&self.format, "payload.format")?;
474        require_non_empty(&self.content_encoding, "payload.content_encoding")?;
475        match (self.body.is_some(), self.body_ref.is_some()) {
476            (true, true) => Err(ValidationError::InvalidPayload(
477                "body and body_ref are mutually exclusive".into(),
478            )),
479            (false, false) => Err(ValidationError::InvalidPayload(
480                "exactly one of body or body_ref must be present".into(),
481            )),
482            _ => Ok(()),
483        }?;
484        if let Some(idem) = &self.idempotency_key {
485            require_non_empty(idem, "payload.idempotency_key")?;
486        }
487        if self.acceptable_placements.is_empty() {
488            return Err(ValidationError::InvalidPayload(
489                "acceptable_placements must list at least one placement".into(),
490            ));
491        }
492        Ok(())
493    }
494}
495
496// ============================================================================
497// Receipts
498// ============================================================================
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
501#[serde(deny_unknown_fields)]
502pub struct PayloadReceipt {
503    pub payload_id: String,
504    pub placement: PlacementClass,
505    pub status: PlacementOutcome,
506    pub byte_size: u64,
507}
508
509impl PayloadReceipt {
510    pub fn validate(&self) -> Result<(), ValidationError> {
511        require_non_empty(&self.payload_id, "payload_receipt.payload_id")?;
512        Ok(())
513    }
514}
515
516#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
517#[serde(deny_unknown_fields)]
518pub struct CapabilityDegradation {
519    pub capability: String,
520    pub previous_support: SupportState,
521    pub current_support: SupportState,
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub evidence: Option<String>,
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub retry_class: Option<RetryClass>,
526}
527
528impl CapabilityDegradation {
529    pub fn validate(&self) -> Result<(), ValidationError> {
530        require_non_empty(&self.capability, "capability_degradation.capability")?;
531        Ok(())
532    }
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
536#[serde(deny_unknown_fields)]
537pub struct Warning {
538    pub code: String,
539    pub message: String,
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub capability: Option<String>,
542}
543
544impl Warning {
545    pub fn validate(&self) -> Result<(), ValidationError> {
546        require_non_empty(&self.code, "warning.code")?;
547        Ok(())
548    }
549}
550
551#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
552#[serde(deny_unknown_fields)]
553pub struct LifecycleReceipt {
554    pub schema_version: String,
555    pub receipt_id: String,
556    pub idempotency_key: Option<String>,
557    pub client_id: String,
558    pub adapter_id: String,
559    pub invocation_id: String,
560    pub event: LifecycleEventKind,
561    pub event_id: String,
562    pub sequence: Option<u64>,
563    pub parent_receipt_id: Option<String>,
564    pub integration_mode: IntegrationMode,
565    pub status: ReceiptStatus,
566    pub at_epoch_s: u64,
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub harness_session_id: Option<String>,
569    #[serde(skip_serializing_if = "Option::is_none")]
570    pub harness_run_id: Option<String>,
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub harness_task_id: Option<String>,
573    #[serde(default, skip_serializing_if = "Vec::is_empty")]
574    pub payload_receipts: Vec<PayloadReceipt>,
575    #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
576    pub telemetry_summary: serde_json::Map<String, serde_json::Value>,
577    #[serde(default, skip_serializing_if = "Vec::is_empty")]
578    pub capability_degradations: Vec<CapabilityDegradation>,
579    pub failure_class: Option<FailureClass>,
580    pub retry_class: Option<RetryClass>,
581    #[serde(default, skip_serializing_if = "Vec::is_empty")]
582    pub warnings: Vec<Warning>,
583}
584
585impl LifecycleReceipt {
586    /// Wire keys that are required *and* nullable: the JSON object MUST carry
587    /// them even when the value is `null`. A producer that omits one of these
588    /// keys is rejected at deserialize time. See
589    /// `docs/specs/lifecycle-contract/body.md` ("required and nullable") and
590    /// `docs/specs/README.md` for the field-presence taxonomy.
591    pub const REQUIRED_NULLABLE_FIELDS: &'static [&'static str] = &[
592        "idempotency_key",
593        "sequence",
594        "parent_receipt_id",
595        "failure_class",
596        "retry_class",
597    ];
598}
599
600// `Option<T>` defaults to "missing key → None" under serde, which would let an
601// inbound receipt drop a required-nullable key entirely and still deserialize.
602// The validator runs after that, so the omission goes silent.
603//
604// The fix is a parent-level intercept: a serde Visitor that walks the input
605// map generically, tracks which keys appeared, and only then constructs the
606// receipt. Required-nullable keys missing from the map → multi-key error so a
607// draft client hears about all omissions on the first try. Required-non-null
608// keys → standard `missing_field`. Optional keys default to `None`/empty.
609//
610// The Visitor stays format-agnostic (no dependency on `serde_json` types in
611// the Deserialize signature) so a non-JSON serde backend — bincode, YAML,
612// CBOR — can deserialize a `LifecycleReceipt` whenever the contract is
613// represented in that format.
614impl<'de> Deserialize<'de> for LifecycleReceipt {
615    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
616    where
617        D: serde::Deserializer<'de>,
618    {
619        deserializer.deserialize_struct(
620            "LifecycleReceipt",
621            LIFECYCLE_RECEIPT_FIELDS,
622            LifecycleReceiptVisitor,
623        )
624    }
625}
626
627const LIFECYCLE_RECEIPT_FIELDS: &[&str] = &[
628    "schema_version",
629    "receipt_id",
630    "idempotency_key",
631    "client_id",
632    "adapter_id",
633    "invocation_id",
634    "event",
635    "event_id",
636    "sequence",
637    "parent_receipt_id",
638    "integration_mode",
639    "status",
640    "at_epoch_s",
641    "harness_session_id",
642    "harness_run_id",
643    "harness_task_id",
644    "payload_receipts",
645    "telemetry_summary",
646    "capability_degradations",
647    "failure_class",
648    "retry_class",
649    "warnings",
650];
651
652#[derive(Deserialize)]
653#[serde(field_identifier, rename_all = "snake_case")]
654enum LifecycleReceiptField {
655    SchemaVersion,
656    ReceiptId,
657    IdempotencyKey,
658    ClientId,
659    AdapterId,
660    InvocationId,
661    Event,
662    EventId,
663    Sequence,
664    ParentReceiptId,
665    IntegrationMode,
666    Status,
667    AtEpochS,
668    HarnessSessionId,
669    HarnessRunId,
670    HarnessTaskId,
671    PayloadReceipts,
672    TelemetrySummary,
673    CapabilityDegradations,
674    FailureClass,
675    RetryClass,
676    Warnings,
677}
678
679struct LifecycleReceiptVisitor;
680
681impl<'de> serde::de::Visitor<'de> for LifecycleReceiptVisitor {
682    type Value = LifecycleReceipt;
683
684    fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
685        f.write_str("a LifecycleReceipt object")
686    }
687
688    fn visit_map<A>(self, mut map: A) -> Result<LifecycleReceipt, A::Error>
689    where
690        A: serde::de::MapAccess<'de>,
691    {
692        // For `Option<T>` fields we use `Option<Option<T>>` so the outer
693        // layer encodes "did the key appear" and the inner layer encodes the
694        // wire value. For required non-null fields we use `Option<T>` and
695        // emit `missing_field` at the end. For default-able collection
696        // fields we use `Option<...>` and fall back to `Default::default()`.
697        let mut schema_version: Option<String> = None;
698        let mut receipt_id: Option<String> = None;
699        let mut idempotency_key: Option<Option<String>> = None;
700        let mut client_id: Option<String> = None;
701        let mut adapter_id: Option<String> = None;
702        let mut invocation_id: Option<String> = None;
703        let mut event: Option<LifecycleEventKind> = None;
704        let mut event_id: Option<String> = None;
705        let mut sequence: Option<Option<u64>> = None;
706        let mut parent_receipt_id: Option<Option<String>> = None;
707        let mut integration_mode: Option<IntegrationMode> = None;
708        let mut status: Option<ReceiptStatus> = None;
709        let mut at_epoch_s: Option<u64> = None;
710        let mut harness_session_id: Option<Option<String>> = None;
711        let mut harness_run_id: Option<Option<String>> = None;
712        let mut harness_task_id: Option<Option<String>> = None;
713        let mut payload_receipts: Option<Vec<PayloadReceipt>> = None;
714        let mut telemetry_summary: Option<serde_json::Map<String, serde_json::Value>> = None;
715        let mut capability_degradations: Option<Vec<CapabilityDegradation>> = None;
716        let mut failure_class: Option<Option<FailureClass>> = None;
717        let mut retry_class: Option<Option<RetryClass>> = None;
718        let mut warnings: Option<Vec<Warning>> = None;
719
720        while let Some(field) = map.next_key::<LifecycleReceiptField>()? {
721            match field {
722                LifecycleReceiptField::SchemaVersion => {
723                    set_once(&mut schema_version, &mut map, "schema_version")?;
724                }
725                LifecycleReceiptField::ReceiptId => {
726                    set_once(&mut receipt_id, &mut map, "receipt_id")?;
727                }
728                LifecycleReceiptField::IdempotencyKey => {
729                    set_once(&mut idempotency_key, &mut map, "idempotency_key")?;
730                }
731                LifecycleReceiptField::ClientId => {
732                    set_once(&mut client_id, &mut map, "client_id")?;
733                }
734                LifecycleReceiptField::AdapterId => {
735                    set_once(&mut adapter_id, &mut map, "adapter_id")?;
736                }
737                LifecycleReceiptField::InvocationId => {
738                    set_once(&mut invocation_id, &mut map, "invocation_id")?;
739                }
740                LifecycleReceiptField::Event => {
741                    set_once(&mut event, &mut map, "event")?;
742                }
743                LifecycleReceiptField::EventId => {
744                    set_once(&mut event_id, &mut map, "event_id")?;
745                }
746                LifecycleReceiptField::Sequence => {
747                    set_once(&mut sequence, &mut map, "sequence")?;
748                }
749                LifecycleReceiptField::ParentReceiptId => {
750                    set_once(&mut parent_receipt_id, &mut map, "parent_receipt_id")?;
751                }
752                LifecycleReceiptField::IntegrationMode => {
753                    set_once(&mut integration_mode, &mut map, "integration_mode")?;
754                }
755                LifecycleReceiptField::Status => {
756                    set_once(&mut status, &mut map, "status")?;
757                }
758                LifecycleReceiptField::AtEpochS => {
759                    set_once(&mut at_epoch_s, &mut map, "at_epoch_s")?;
760                }
761                LifecycleReceiptField::HarnessSessionId => {
762                    set_once(&mut harness_session_id, &mut map, "harness_session_id")?;
763                }
764                LifecycleReceiptField::HarnessRunId => {
765                    set_once(&mut harness_run_id, &mut map, "harness_run_id")?;
766                }
767                LifecycleReceiptField::HarnessTaskId => {
768                    set_once(&mut harness_task_id, &mut map, "harness_task_id")?;
769                }
770                LifecycleReceiptField::PayloadReceipts => {
771                    set_once(&mut payload_receipts, &mut map, "payload_receipts")?;
772                }
773                LifecycleReceiptField::TelemetrySummary => {
774                    set_once(&mut telemetry_summary, &mut map, "telemetry_summary")?;
775                }
776                LifecycleReceiptField::CapabilityDegradations => {
777                    set_once(
778                        &mut capability_degradations,
779                        &mut map,
780                        "capability_degradations",
781                    )?;
782                }
783                LifecycleReceiptField::FailureClass => {
784                    set_once(&mut failure_class, &mut map, "failure_class")?;
785                }
786                LifecycleReceiptField::RetryClass => {
787                    set_once(&mut retry_class, &mut map, "retry_class")?;
788                }
789                LifecycleReceiptField::Warnings => {
790                    set_once(&mut warnings, &mut map, "warnings")?;
791                }
792            }
793        }
794
795        // Required-nullable presence check. Collect every missing key so a
796        // draft client that drops several at once gets one error.
797        let mut missing_required_nullable: Vec<&'static str> = Vec::new();
798        if idempotency_key.is_none() {
799            missing_required_nullable.push("idempotency_key");
800        }
801        if sequence.is_none() {
802            missing_required_nullable.push("sequence");
803        }
804        if parent_receipt_id.is_none() {
805            missing_required_nullable.push("parent_receipt_id");
806        }
807        if failure_class.is_none() {
808            missing_required_nullable.push("failure_class");
809        }
810        if retry_class.is_none() {
811            missing_required_nullable.push("retry_class");
812        }
813        if !missing_required_nullable.is_empty() {
814            return Err(serde::de::Error::custom(format!(
815                "LifecycleReceipt is missing required-nullable field(s): {}; \
816                 these keys MUST be present even when their value is null",
817                missing_required_nullable.join(", ")
818            )));
819        }
820
821        Ok(LifecycleReceipt {
822            schema_version: schema_version
823                .ok_or_else(|| serde::de::Error::missing_field("schema_version"))?,
824            receipt_id: receipt_id.ok_or_else(|| serde::de::Error::missing_field("receipt_id"))?,
825            idempotency_key: idempotency_key.expect("checked above"),
826            client_id: client_id.ok_or_else(|| serde::de::Error::missing_field("client_id"))?,
827            adapter_id: adapter_id.ok_or_else(|| serde::de::Error::missing_field("adapter_id"))?,
828            invocation_id: invocation_id
829                .ok_or_else(|| serde::de::Error::missing_field("invocation_id"))?,
830            event: event.ok_or_else(|| serde::de::Error::missing_field("event"))?,
831            event_id: event_id.ok_or_else(|| serde::de::Error::missing_field("event_id"))?,
832            sequence: sequence.expect("checked above"),
833            parent_receipt_id: parent_receipt_id.expect("checked above"),
834            integration_mode: integration_mode
835                .ok_or_else(|| serde::de::Error::missing_field("integration_mode"))?,
836            status: status.ok_or_else(|| serde::de::Error::missing_field("status"))?,
837            at_epoch_s: at_epoch_s.ok_or_else(|| serde::de::Error::missing_field("at_epoch_s"))?,
838            harness_session_id: harness_session_id.unwrap_or(None),
839            harness_run_id: harness_run_id.unwrap_or(None),
840            harness_task_id: harness_task_id.unwrap_or(None),
841            payload_receipts: payload_receipts.unwrap_or_default(),
842            telemetry_summary: telemetry_summary.unwrap_or_default(),
843            capability_degradations: capability_degradations.unwrap_or_default(),
844            failure_class: failure_class.expect("checked above"),
845            retry_class: retry_class.expect("checked above"),
846            warnings: warnings.unwrap_or_default(),
847        })
848    }
849}
850
851fn set_once<'de, T, A>(
852    slot: &mut Option<T>,
853    map: &mut A,
854    field: &'static str,
855) -> Result<(), A::Error>
856where
857    T: serde::Deserialize<'de>,
858    A: serde::de::MapAccess<'de>,
859{
860    if slot.is_some() {
861        return Err(serde::de::Error::duplicate_field(field));
862    }
863    *slot = Some(map.next_value()?);
864    Ok(())
865}
866
867impl LifecycleReceipt {
868    pub fn validate(&self) -> Result<(), ValidationError> {
869        if self.schema_version != SCHEMA_VERSION {
870            return Err(ValidationError::SchemaVersionMismatch {
871                expected: SCHEMA_VERSION.to_string(),
872                found: self.schema_version.clone(),
873            });
874        }
875        require_non_empty(&self.receipt_id, "receipt.receipt_id")?;
876        require_non_empty(&self.client_id, "receipt.client_id")?;
877        require_non_empty(&self.adapter_id, "receipt.adapter_id")?;
878        require_non_empty(&self.invocation_id, "receipt.invocation_id")?;
879        require_non_empty(&self.event_id, "receipt.event_id")?;
880        if let Some(idem) = &self.idempotency_key {
881            require_non_empty(idem, "receipt.idempotency_key")?;
882        }
883        if let Some(parent) = &self.parent_receipt_id {
884            require_non_empty(parent, "receipt.parent_receipt_id")?;
885        }
886        if matches!(self.event, LifecycleEventKind::ReceiptEmitted) {
887            return Err(ValidationError::InvalidReceipt(
888                "receipt.emitted is a notification event and must not itself produce a receipt"
889                    .into(),
890            ));
891        }
892        for pr in &self.payload_receipts {
893            pr.validate()?;
894        }
895        for deg in &self.capability_degradations {
896            deg.validate()?;
897        }
898        for w in &self.warnings {
899            w.validate()?;
900        }
901        match (
902            matches!(self.status, ReceiptStatus::Failed),
903            self.failure_class.is_some(),
904        ) {
905            (true, false) => {
906                return Err(ValidationError::InvalidReceipt(
907                    "status=failed requires failure_class".into(),
908                ));
909            }
910            (false, true) => {
911                return Err(ValidationError::InvalidReceipt(
912                    "failure_class is only valid on status=failed receipts".into(),
913                ));
914            }
915            _ => {}
916        }
917        if matches!(self.status, ReceiptStatus::Failed) && self.retry_class.is_none() {
918            return Err(ValidationError::InvalidReceipt(
919                "status=failed requires retry_class (clients must declare retry posture)".into(),
920            ));
921        }
922        Ok(())
923    }
924}
925
926// ============================================================================
927// Adapter manifest (issue #6: full registry)
928// ============================================================================
929
930/// Manifest placement classes — the trust-neutral, lifecycle-timing
931/// vocabulary the adapter manifest uses to declare placement support.
932///
933/// **Distinct from [`PlacementClass`]**, which is the routing
934/// vocabulary the runtime uses on `acceptable_placements` for
935/// concrete payload delivery. The manifest declares *capability*;
936/// the payload envelope declares *routing intent*. A future
937/// revision may unify them; the current contract keeps them
938/// separate so manifest evolution does not churn payload routing.
939#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
940#[serde(rename_all = "snake_case")]
941pub enum ManifestPlacementClass {
942    /// Before any frame opens; e.g. session-init context.
943    PreSession,
944    /// Leading edge of a frame, before user/task input arrives.
945    PreFrameLeading,
946    /// Trailing edge of a frame, after input but before model execution.
947    PreFrameTrailing,
948    /// Inside a tool-result envelope returned to the model.
949    ToolResult,
950    /// Through an operator or manual surface (skill, command, wrapper).
951    ManualOperator,
952}
953
954impl ManifestPlacementClass {
955    pub const ALL: &'static [Self] = &[
956        Self::PreSession,
957        Self::PreFrameLeading,
958        Self::PreFrameTrailing,
959        Self::ToolResult,
960        Self::ManualOperator,
961    ];
962}
963
964/// Per-event capability claim inside a manifest.
965#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
966#[serde(deny_unknown_fields)]
967pub struct ManifestLifecycleEventSupport {
968    pub support: SupportState,
969    /// Integration modes through which the adapter delivers this event.
970    /// May be empty when `support` is `unavailable`.
971    #[serde(default, skip_serializing_if = "Vec::is_empty")]
972    pub modes: Vec<IntegrationMode>,
973}
974
975/// Per-placement capability claim inside a manifest.
976#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
977#[serde(deny_unknown_fields)]
978pub struct ManifestPlacementSupport {
979    pub support: SupportState,
980    /// Placement size limit in bytes when the adapter declares one.
981    #[serde(skip_serializing_if = "Option::is_none")]
982    pub max_bytes: Option<u64>,
983}
984
985/// Capability claim describing how the adapter surfaces
986/// `context.pressure_observed` lifecycle evidence.
987#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
988#[serde(deny_unknown_fields)]
989pub struct ManifestContextPressure {
990    pub support: SupportState,
991    #[serde(skip_serializing_if = "Option::is_none")]
992    pub evidence: Option<String>,
993}
994
995/// Capability claim describing receipt emission and ledger support.
996#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
997#[serde(deny_unknown_fields)]
998pub struct ManifestReceipts {
999    /// Adapter emits its own native receipts.
1000    pub native: bool,
1001    /// Lifeloop synthesizes receipts on the adapter's behalf.
1002    pub lifeloop_synthesized: bool,
1003    /// Durable cross-invocation receipt ledger.
1004    pub receipt_ledger: SupportState,
1005}
1006
1007/// Per-id support claims for harness identity correlation. Optional
1008/// on the manifest because a telemetry-only adapter may not expose
1009/// any of these.
1010#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1011#[serde(deny_unknown_fields)]
1012pub struct ManifestSessionIdentity {
1013    pub harness_session_id: SupportState,
1014    pub harness_run_id: SupportState,
1015    pub harness_task_id: SupportState,
1016}
1017
1018/// Capability claim for the adapter's session-rename surface.
1019/// Optional on the manifest; absent means "no rename concept."
1020#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1021#[serde(deny_unknown_fields)]
1022pub struct ManifestSessionRename {
1023    pub support: SupportState,
1024}
1025
1026/// Capability claim for operator approval/intervention surfaces.
1027/// Optional; absent means "no operator surface."
1028#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1029#[serde(deny_unknown_fields)]
1030pub struct ManifestApprovalSurface {
1031    pub support: SupportState,
1032}
1033
1034/// One telemetry source the adapter exposes for lifecycle evidence.
1035#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1036#[serde(deny_unknown_fields)]
1037pub struct ManifestTelemetrySource {
1038    pub source: String,
1039    pub support: SupportState,
1040}
1041
1042/// One pre-declared capability degradation the adapter ships with.
1043/// Lets a manifest say "this build's `context_pressure` was native
1044/// upstream but is currently `unavailable` here" without firing a
1045/// runtime `capability.degraded` event.
1046#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1047#[serde(deny_unknown_fields)]
1048pub struct ManifestKnownDegradation {
1049    pub capability: String,
1050    pub previous_support: SupportState,
1051    pub current_support: SupportState,
1052    #[serde(skip_serializing_if = "Option::is_none")]
1053    pub evidence: Option<String>,
1054}
1055
1056/// Adapter manifest. Issue #6 lands the full shape; pre-issue-#6
1057/// drafts shipped a stub with only schema_version, adapter_id,
1058/// adapter_version, display_name, roles, integration_modes, and
1059/// lifecycle_events.
1060///
1061/// `contract_version` (this struct's first field) carries the
1062/// Lifeloop contract version label (e.g. `lifeloop.v0.1`),
1063/// independent of `adapter_version`. The two are separate so
1064/// adapters can iterate without bumping the contract.
1065#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1066#[serde(deny_unknown_fields)]
1067pub struct AdapterManifest {
1068    pub contract_version: String,
1069    pub adapter_id: String,
1070    pub adapter_version: String,
1071    pub display_name: String,
1072    pub role: AdapterRole,
1073    pub integration_modes: Vec<IntegrationMode>,
1074    pub lifecycle_events: BTreeMap<LifecycleEventKind, ManifestLifecycleEventSupport>,
1075    pub placement: BTreeMap<ManifestPlacementClass, ManifestPlacementSupport>,
1076    pub context_pressure: ManifestContextPressure,
1077    pub receipts: ManifestReceipts,
1078
1079    #[serde(default, skip_serializing_if = "Option::is_none")]
1080    pub session_identity: Option<ManifestSessionIdentity>,
1081    #[serde(default, skip_serializing_if = "Option::is_none")]
1082    pub session_rename: Option<ManifestSessionRename>,
1083    #[serde(default, skip_serializing_if = "Option::is_none")]
1084    pub approval_surface: Option<ManifestApprovalSurface>,
1085    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1086    pub failure_modes: Vec<FailureClass>,
1087    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1088    pub telemetry_sources: Vec<ManifestTelemetrySource>,
1089    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1090    pub known_degradations: Vec<ManifestKnownDegradation>,
1091}
1092
1093impl AdapterManifest {
1094    pub fn validate(&self) -> Result<(), ValidationError> {
1095        if self.contract_version != SCHEMA_VERSION {
1096            return Err(ValidationError::SchemaVersionMismatch {
1097                expected: SCHEMA_VERSION.to_string(),
1098                found: self.contract_version.clone(),
1099            });
1100        }
1101        require_non_empty(&self.adapter_id, "manifest.adapter_id")?;
1102        require_non_empty(&self.adapter_version, "manifest.adapter_version")?;
1103        require_non_empty(&self.display_name, "manifest.display_name")?;
1104        if self.integration_modes.is_empty() {
1105            return Err(ValidationError::InvalidManifest(
1106                "manifest.integration_modes must declare at least one integration mode".into(),
1107            ));
1108        }
1109        for deg in &self.known_degradations {
1110            require_non_empty(&deg.capability, "manifest.known_degradations[].capability")?;
1111        }
1112        for src in &self.telemetry_sources {
1113            require_non_empty(&src.source, "manifest.telemetry_sources[].source")?;
1114        }
1115        Ok(())
1116    }
1117}
1118
1119// ----------------------------------------------------------------------------
1120// Manifest registry
1121// ----------------------------------------------------------------------------
1122
1123/// Conformance posture of a registered adapter.
1124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1125#[serde(rename_all = "snake_case")]
1126pub enum ConformanceLevel {
1127    /// v1-conformance adapter: every capability claim that depends on
1128    /// extracted code (asset rendering, telemetry, placement) is
1129    /// paired with a test that verifies the claim.
1130    V1Conformance,
1131    /// Initial manifest shipped without full claim verification. The
1132    /// claims describe expected behavior so clients can negotiate, but
1133    /// the registry does not yet run capability-claim tests for them.
1134    PreConformance,
1135}
1136
1137/// Registry entry pairing an [`AdapterManifest`] with its
1138/// [`ConformanceLevel`].
1139#[derive(Debug, Clone, PartialEq, Eq)]
1140pub struct RegisteredAdapter {
1141    pub manifest: AdapterManifest,
1142    pub conformance: ConformanceLevel,
1143}
1144
1145/// Built-in adapter manifest registry. Order is stable so callers
1146/// that render a `lifeloop adapters` listing get a predictable
1147/// output without sorting client-side.
1148pub fn manifest_registry() -> Vec<RegisteredAdapter> {
1149    vec![
1150        RegisteredAdapter {
1151            manifest: codex_manifest(),
1152            conformance: ConformanceLevel::V1Conformance,
1153        },
1154        RegisteredAdapter {
1155            manifest: claude_manifest(),
1156            conformance: ConformanceLevel::V1Conformance,
1157        },
1158        RegisteredAdapter {
1159            manifest: hermes_manifest(),
1160            conformance: ConformanceLevel::PreConformance,
1161        },
1162        RegisteredAdapter {
1163            manifest: openclaw_manifest(),
1164            conformance: ConformanceLevel::PreConformance,
1165        },
1166        RegisteredAdapter {
1167            manifest: gemini_manifest(),
1168            conformance: ConformanceLevel::PreConformance,
1169        },
1170        RegisteredAdapter {
1171            manifest: opencode_manifest(),
1172            conformance: ConformanceLevel::PreConformance,
1173        },
1174    ]
1175}
1176
1177/// Resolve a registered adapter by `adapter_id`. Returns `None` for
1178/// unknown ids.
1179pub fn lookup_manifest(adapter_id: &str) -> Option<RegisteredAdapter> {
1180    manifest_registry()
1181        .into_iter()
1182        .find(|entry| entry.manifest.adapter_id == adapter_id)
1183}
1184
1185fn synthesized() -> SupportState {
1186    SupportState::Synthesized
1187}
1188
1189fn native() -> SupportState {
1190    SupportState::Native
1191}
1192
1193fn unavailable() -> SupportState {
1194    SupportState::Unavailable
1195}
1196
1197fn manual() -> SupportState {
1198    SupportState::Manual
1199}
1200
1201/// Codex manifest. Native-hook integration covers Codex's stable hook
1202/// surface, including `PreCompact` in Codex CLI 0.129+. Capability
1203/// claims here are paired with verification tests in
1204/// `tests/manifest_claims.rs`.
1205pub fn codex_manifest() -> AdapterManifest {
1206    let lifecycle_events = BTreeMap::from([
1207        (
1208            LifecycleEventKind::SessionStarting,
1209            ManifestLifecycleEventSupport {
1210                support: native(),
1211                modes: vec![IntegrationMode::NativeHook],
1212            },
1213        ),
1214        (
1215            LifecycleEventKind::SessionStarted,
1216            ManifestLifecycleEventSupport {
1217                support: native(),
1218                modes: vec![IntegrationMode::NativeHook],
1219            },
1220        ),
1221        (
1222            LifecycleEventKind::FrameOpening,
1223            ManifestLifecycleEventSupport {
1224                support: native(),
1225                modes: vec![IntegrationMode::NativeHook],
1226            },
1227        ),
1228        (
1229            LifecycleEventKind::FrameOpened,
1230            ManifestLifecycleEventSupport {
1231                support: synthesized(),
1232                modes: vec![IntegrationMode::NativeHook],
1233            },
1234        ),
1235        (
1236            LifecycleEventKind::ContextPressureObserved,
1237            ManifestLifecycleEventSupport {
1238                support: native(),
1239                modes: vec![IntegrationMode::NativeHook],
1240            },
1241        ),
1242        (
1243            LifecycleEventKind::ContextCompacted,
1244            ManifestLifecycleEventSupport {
1245                support: native(),
1246                modes: vec![IntegrationMode::NativeHook],
1247            },
1248        ),
1249        (
1250            LifecycleEventKind::FrameEnding,
1251            ManifestLifecycleEventSupport {
1252                support: native(),
1253                modes: vec![IntegrationMode::NativeHook],
1254            },
1255        ),
1256        (
1257            LifecycleEventKind::FrameEnded,
1258            ManifestLifecycleEventSupport {
1259                support: native(),
1260                modes: vec![IntegrationMode::NativeHook],
1261            },
1262        ),
1263        (
1264            LifecycleEventKind::SessionEnding,
1265            ManifestLifecycleEventSupport {
1266                support: unavailable(),
1267                modes: Vec::new(),
1268            },
1269        ),
1270        (
1271            LifecycleEventKind::SessionEnded,
1272            ManifestLifecycleEventSupport {
1273                support: unavailable(),
1274                modes: Vec::new(),
1275            },
1276        ),
1277        (
1278            LifecycleEventKind::SupervisorTick,
1279            ManifestLifecycleEventSupport {
1280                support: unavailable(),
1281                modes: Vec::new(),
1282            },
1283        ),
1284        (
1285            LifecycleEventKind::CapabilityDegraded,
1286            ManifestLifecycleEventSupport {
1287                support: synthesized(),
1288                modes: vec![IntegrationMode::NativeHook],
1289            },
1290        ),
1291        (
1292            LifecycleEventKind::ReceiptEmitted,
1293            ManifestLifecycleEventSupport {
1294                support: synthesized(),
1295                modes: vec![IntegrationMode::NativeHook],
1296            },
1297        ),
1298        (
1299            LifecycleEventKind::ReceiptGapDetected,
1300            ManifestLifecycleEventSupport {
1301                support: unavailable(),
1302                modes: Vec::new(),
1303            },
1304        ),
1305    ]);
1306
1307    let placement = BTreeMap::from([
1308        (
1309            ManifestPlacementClass::PreSession,
1310            ManifestPlacementSupport {
1311                support: native(),
1312                max_bytes: Some(8192),
1313            },
1314        ),
1315        (
1316            ManifestPlacementClass::PreFrameLeading,
1317            ManifestPlacementSupport {
1318                support: native(),
1319                max_bytes: Some(8192),
1320            },
1321        ),
1322        (
1323            ManifestPlacementClass::PreFrameTrailing,
1324            ManifestPlacementSupport {
1325                support: unavailable(),
1326                max_bytes: None,
1327            },
1328        ),
1329        (
1330            ManifestPlacementClass::ToolResult,
1331            ManifestPlacementSupport {
1332                support: unavailable(),
1333                max_bytes: None,
1334            },
1335        ),
1336        (
1337            ManifestPlacementClass::ManualOperator,
1338            ManifestPlacementSupport {
1339                support: manual(),
1340                max_bytes: None,
1341            },
1342        ),
1343    ]);
1344
1345    AdapterManifest {
1346        contract_version: SCHEMA_VERSION.to_string(),
1347        adapter_id: "codex".into(),
1348        adapter_version: "0.1.0".into(),
1349        display_name: "Codex".into(),
1350        role: AdapterRole::PrimaryWorker,
1351        integration_modes: vec![IntegrationMode::NativeHook, IntegrationMode::ManualSkill],
1352        lifecycle_events,
1353        placement,
1354        context_pressure: ManifestContextPressure {
1355            support: native(),
1356            evidence: Some(
1357                "Codex CLI 0.129 exposes PreCompact before context pressure handling and PostCompact after context compacts"
1358                    .into(),
1359            ),
1360        },
1361        receipts: ManifestReceipts {
1362            native: false,
1363            lifeloop_synthesized: true,
1364            receipt_ledger: unavailable(),
1365        },
1366        session_identity: Some(ManifestSessionIdentity {
1367            harness_session_id: native(),
1368            harness_run_id: synthesized(),
1369            harness_task_id: unavailable(),
1370        }),
1371        session_rename: None,
1372        approval_surface: None,
1373        failure_modes: vec![FailureClass::TransportError, FailureClass::PayloadTooLarge],
1374        telemetry_sources: Vec::new(),
1375        known_degradations: Vec::new(),
1376    }
1377}
1378
1379/// Claude manifest. Native-hook integration via `.claude/settings.json`.
1380pub fn claude_manifest() -> AdapterManifest {
1381    let lifecycle_events = BTreeMap::from([
1382        (
1383            LifecycleEventKind::SessionStarting,
1384            ManifestLifecycleEventSupport {
1385                support: native(),
1386                modes: vec![IntegrationMode::NativeHook],
1387            },
1388        ),
1389        (
1390            LifecycleEventKind::SessionStarted,
1391            ManifestLifecycleEventSupport {
1392                support: native(),
1393                modes: vec![IntegrationMode::NativeHook],
1394            },
1395        ),
1396        (
1397            LifecycleEventKind::FrameOpening,
1398            ManifestLifecycleEventSupport {
1399                support: native(),
1400                modes: vec![IntegrationMode::NativeHook],
1401            },
1402        ),
1403        (
1404            LifecycleEventKind::FrameOpened,
1405            ManifestLifecycleEventSupport {
1406                support: native(),
1407                modes: vec![IntegrationMode::NativeHook],
1408            },
1409        ),
1410        (
1411            LifecycleEventKind::ContextPressureObserved,
1412            ManifestLifecycleEventSupport {
1413                support: native(),
1414                modes: vec![IntegrationMode::NativeHook],
1415            },
1416        ),
1417        (
1418            LifecycleEventKind::ContextCompacted,
1419            ManifestLifecycleEventSupport {
1420                support: unavailable(),
1421                modes: Vec::new(),
1422            },
1423        ),
1424        (
1425            LifecycleEventKind::FrameEnding,
1426            ManifestLifecycleEventSupport {
1427                support: native(),
1428                modes: vec![IntegrationMode::NativeHook],
1429            },
1430        ),
1431        (
1432            LifecycleEventKind::FrameEnded,
1433            ManifestLifecycleEventSupport {
1434                support: native(),
1435                modes: vec![IntegrationMode::NativeHook],
1436            },
1437        ),
1438        (
1439            LifecycleEventKind::SessionEnding,
1440            ManifestLifecycleEventSupport {
1441                support: native(),
1442                modes: vec![IntegrationMode::NativeHook],
1443            },
1444        ),
1445        (
1446            LifecycleEventKind::SessionEnded,
1447            ManifestLifecycleEventSupport {
1448                support: native(),
1449                modes: vec![IntegrationMode::NativeHook],
1450            },
1451        ),
1452        (
1453            LifecycleEventKind::SupervisorTick,
1454            ManifestLifecycleEventSupport {
1455                support: unavailable(),
1456                modes: Vec::new(),
1457            },
1458        ),
1459        (
1460            LifecycleEventKind::CapabilityDegraded,
1461            ManifestLifecycleEventSupport {
1462                support: synthesized(),
1463                modes: vec![IntegrationMode::NativeHook],
1464            },
1465        ),
1466        (
1467            LifecycleEventKind::ReceiptEmitted,
1468            ManifestLifecycleEventSupport {
1469                support: synthesized(),
1470                modes: vec![IntegrationMode::NativeHook],
1471            },
1472        ),
1473        (
1474            LifecycleEventKind::ReceiptGapDetected,
1475            ManifestLifecycleEventSupport {
1476                support: unavailable(),
1477                modes: Vec::new(),
1478            },
1479        ),
1480    ]);
1481
1482    let placement = BTreeMap::from([
1483        (
1484            ManifestPlacementClass::PreSession,
1485            ManifestPlacementSupport {
1486                support: native(),
1487                max_bytes: Some(16_384),
1488            },
1489        ),
1490        (
1491            ManifestPlacementClass::PreFrameLeading,
1492            ManifestPlacementSupport {
1493                support: native(),
1494                max_bytes: Some(16_384),
1495            },
1496        ),
1497        (
1498            ManifestPlacementClass::PreFrameTrailing,
1499            ManifestPlacementSupport {
1500                support: unavailable(),
1501                max_bytes: None,
1502            },
1503        ),
1504        (
1505            ManifestPlacementClass::ToolResult,
1506            ManifestPlacementSupport {
1507                support: unavailable(),
1508                max_bytes: None,
1509            },
1510        ),
1511        (
1512            ManifestPlacementClass::ManualOperator,
1513            ManifestPlacementSupport {
1514                support: manual(),
1515                max_bytes: None,
1516            },
1517        ),
1518    ]);
1519
1520    AdapterManifest {
1521        contract_version: SCHEMA_VERSION.to_string(),
1522        adapter_id: "claude".into(),
1523        adapter_version: "0.1.0".into(),
1524        display_name: "Claude".into(),
1525        role: AdapterRole::PrimaryWorker,
1526        integration_modes: vec![IntegrationMode::NativeHook],
1527        lifecycle_events,
1528        placement,
1529        context_pressure: ManifestContextPressure {
1530            support: native(),
1531            evidence: Some(
1532                "Claude emits PreCompact and SessionEnd events that map directly to context.pressure_observed"
1533                    .into(),
1534            ),
1535        },
1536        receipts: ManifestReceipts {
1537            native: false,
1538            lifeloop_synthesized: true,
1539            receipt_ledger: unavailable(),
1540        },
1541        session_identity: Some(ManifestSessionIdentity {
1542            harness_session_id: native(),
1543            harness_run_id: synthesized(),
1544            harness_task_id: unavailable(),
1545        }),
1546        session_rename: None,
1547        approval_surface: None,
1548        failure_modes: vec![FailureClass::TransportError, FailureClass::PayloadTooLarge],
1549        telemetry_sources: Vec::new(),
1550        known_degradations: Vec::new(),
1551    }
1552}
1553
1554/// Hermes pre-conformance manifest. Reference-adapter integration
1555/// supplied as a JSON descriptor at the path declared in
1556/// [`crate::host_assets::HERMES_TARGET_ADAPTER`].
1557pub fn hermes_manifest() -> AdapterManifest {
1558    pre_conformance_reference_adapter_manifest("hermes", "Hermes")
1559}
1560
1561/// OpenClaw pre-conformance manifest.
1562pub fn openclaw_manifest() -> AdapterManifest {
1563    pre_conformance_reference_adapter_manifest("openclaw", "OpenClaw")
1564}
1565
1566/// Gemini pre-conformance manifest.
1567pub fn gemini_manifest() -> AdapterManifest {
1568    pre_conformance_telemetry_only_manifest("gemini", "Gemini")
1569}
1570
1571/// OpenCode pre-conformance manifest.
1572pub fn opencode_manifest() -> AdapterManifest {
1573    pre_conformance_telemetry_only_manifest("opencode", "OpenCode")
1574}
1575
1576fn pre_conformance_reference_adapter_manifest(
1577    adapter_id: &str,
1578    display_name: &str,
1579) -> AdapterManifest {
1580    let lifecycle_events = BTreeMap::from([
1581        (
1582            LifecycleEventKind::SessionStarting,
1583            ManifestLifecycleEventSupport {
1584                support: SupportState::Partial,
1585                modes: vec![IntegrationMode::ReferenceAdapter],
1586            },
1587        ),
1588        (
1589            LifecycleEventKind::SessionStarted,
1590            ManifestLifecycleEventSupport {
1591                support: SupportState::Partial,
1592                modes: vec![IntegrationMode::ReferenceAdapter],
1593            },
1594        ),
1595        (
1596            LifecycleEventKind::FrameOpening,
1597            ManifestLifecycleEventSupport {
1598                support: SupportState::Partial,
1599                modes: vec![IntegrationMode::ReferenceAdapter],
1600            },
1601        ),
1602        (
1603            LifecycleEventKind::FrameEnded,
1604            ManifestLifecycleEventSupport {
1605                support: SupportState::Partial,
1606                modes: vec![IntegrationMode::ReferenceAdapter],
1607            },
1608        ),
1609        (
1610            LifecycleEventKind::SessionEnded,
1611            ManifestLifecycleEventSupport {
1612                support: SupportState::Partial,
1613                modes: vec![IntegrationMode::ReferenceAdapter],
1614            },
1615        ),
1616    ]);
1617
1618    let placement = BTreeMap::from([
1619        (
1620            ManifestPlacementClass::PreSession,
1621            ManifestPlacementSupport {
1622                support: SupportState::Partial,
1623                max_bytes: None,
1624            },
1625        ),
1626        (
1627            ManifestPlacementClass::PreFrameLeading,
1628            ManifestPlacementSupport {
1629                support: SupportState::Partial,
1630                max_bytes: None,
1631            },
1632        ),
1633        (
1634            ManifestPlacementClass::ManualOperator,
1635            ManifestPlacementSupport {
1636                support: SupportState::Manual,
1637                max_bytes: None,
1638            },
1639        ),
1640    ]);
1641
1642    AdapterManifest {
1643        contract_version: SCHEMA_VERSION.to_string(),
1644        adapter_id: adapter_id.to_string(),
1645        adapter_version: "0.0.1-pre".into(),
1646        display_name: display_name.to_string(),
1647        role: AdapterRole::Worker,
1648        integration_modes: vec![IntegrationMode::ReferenceAdapter],
1649        lifecycle_events,
1650        placement,
1651        context_pressure: ManifestContextPressure {
1652            support: SupportState::Partial,
1653            evidence: None,
1654        },
1655        receipts: ManifestReceipts {
1656            native: false,
1657            lifeloop_synthesized: true,
1658            receipt_ledger: SupportState::Unavailable,
1659        },
1660        session_identity: None,
1661        session_rename: None,
1662        approval_surface: None,
1663        failure_modes: Vec::new(),
1664        telemetry_sources: Vec::new(),
1665        known_degradations: Vec::new(),
1666    }
1667}
1668
1669fn pre_conformance_telemetry_only_manifest(
1670    adapter_id: &str,
1671    display_name: &str,
1672) -> AdapterManifest {
1673    let lifecycle_events = BTreeMap::from([
1674        (
1675            LifecycleEventKind::SessionStarting,
1676            ManifestLifecycleEventSupport {
1677                support: SupportState::Partial,
1678                modes: vec![IntegrationMode::TelemetryOnly],
1679            },
1680        ),
1681        (
1682            LifecycleEventKind::ContextPressureObserved,
1683            ManifestLifecycleEventSupport {
1684                support: SupportState::Partial,
1685                modes: vec![IntegrationMode::TelemetryOnly],
1686            },
1687        ),
1688        (
1689            LifecycleEventKind::SessionEnded,
1690            ManifestLifecycleEventSupport {
1691                support: SupportState::Partial,
1692                modes: vec![IntegrationMode::TelemetryOnly],
1693            },
1694        ),
1695    ]);
1696
1697    let placement = BTreeMap::from([(
1698        ManifestPlacementClass::ManualOperator,
1699        ManifestPlacementSupport {
1700            support: SupportState::Manual,
1701            max_bytes: None,
1702        },
1703    )]);
1704
1705    AdapterManifest {
1706        contract_version: SCHEMA_VERSION.to_string(),
1707        adapter_id: adapter_id.to_string(),
1708        adapter_version: "0.0.1-pre".into(),
1709        display_name: display_name.to_string(),
1710        role: AdapterRole::Observer,
1711        integration_modes: vec![IntegrationMode::TelemetryOnly],
1712        lifecycle_events,
1713        placement,
1714        context_pressure: ManifestContextPressure {
1715            support: SupportState::Partial,
1716            evidence: None,
1717        },
1718        receipts: ManifestReceipts {
1719            native: false,
1720            lifeloop_synthesized: true,
1721            receipt_ledger: SupportState::Unavailable,
1722        },
1723        session_identity: None,
1724        session_rename: None,
1725        approval_surface: None,
1726        failure_modes: Vec::new(),
1727        telemetry_sources: Vec::new(),
1728        known_degradations: Vec::new(),
1729    }
1730}
1731
1732// ============================================================================
1733// Callback request and response envelopes
1734// ============================================================================
1735
1736#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1737#[serde(deny_unknown_fields)]
1738pub struct CallbackRequest {
1739    pub schema_version: String,
1740    pub event: LifecycleEventKind,
1741    pub event_id: String,
1742    pub adapter_id: String,
1743    pub adapter_version: String,
1744    pub integration_mode: IntegrationMode,
1745    pub invocation_id: String,
1746    #[serde(skip_serializing_if = "Option::is_none")]
1747    pub harness_session_id: Option<String>,
1748    #[serde(skip_serializing_if = "Option::is_none")]
1749    pub harness_run_id: Option<String>,
1750    #[serde(skip_serializing_if = "Option::is_none")]
1751    pub harness_task_id: Option<String>,
1752    #[serde(skip_serializing_if = "Option::is_none")]
1753    pub frame_context: Option<FrameContext>,
1754    #[serde(skip_serializing_if = "Option::is_none")]
1755    pub capability_snapshot_ref: Option<String>,
1756    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1757    pub payload_refs: Vec<PayloadRef>,
1758    #[serde(skip_serializing_if = "Option::is_none")]
1759    pub sequence: Option<u64>,
1760    #[serde(skip_serializing_if = "Option::is_none")]
1761    pub idempotency_key: Option<String>,
1762    #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
1763    pub metadata: serde_json::Map<String, serde_json::Value>,
1764}
1765
1766impl CallbackRequest {
1767    pub fn validate(&self) -> Result<(), ValidationError> {
1768        if self.schema_version != SCHEMA_VERSION {
1769            return Err(ValidationError::SchemaVersionMismatch {
1770                expected: SCHEMA_VERSION.to_string(),
1771                found: self.schema_version.clone(),
1772            });
1773        }
1774        require_non_empty(&self.event_id, "request.event_id")?;
1775        require_non_empty(&self.adapter_id, "request.adapter_id")?;
1776        require_non_empty(&self.adapter_version, "request.adapter_version")?;
1777        require_non_empty(&self.invocation_id, "request.invocation_id")?;
1778        if let Some(s) = &self.harness_session_id {
1779            require_non_empty(s, "request.harness_session_id")?;
1780        }
1781        if let Some(s) = &self.harness_run_id {
1782            require_non_empty(s, "request.harness_run_id")?;
1783        }
1784        if let Some(s) = &self.harness_task_id {
1785            require_non_empty(s, "request.harness_task_id")?;
1786        }
1787        if let Some(s) = &self.capability_snapshot_ref {
1788            require_non_empty(s, "request.capability_snapshot_ref")?;
1789        }
1790        if let Some(s) = &self.idempotency_key {
1791            require_non_empty(s, "request.idempotency_key")?;
1792        }
1793        if let Some(fc) = &self.frame_context {
1794            fc.validate()?;
1795        }
1796        for r in &self.payload_refs {
1797            r.validate()?;
1798        }
1799        match self.event {
1800            LifecycleEventKind::FrameOpening
1801            | LifecycleEventKind::FrameOpened
1802            | LifecycleEventKind::FrameEnding
1803            | LifecycleEventKind::FrameEnded
1804                if self.frame_context.is_none() =>
1805            {
1806                Err(ValidationError::InvalidRequest(
1807                    "frame.* events require frame_context".into(),
1808                ))
1809            }
1810            LifecycleEventKind::ReceiptEmitted if self.idempotency_key.is_some() => {
1811                Err(ValidationError::InvalidRequest(
1812                    "receipt.emitted is a notification event and must not carry an idempotency_key"
1813                        .into(),
1814                ))
1815            }
1816            _ => Ok(()),
1817        }
1818    }
1819}
1820
1821#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1822#[serde(deny_unknown_fields)]
1823pub struct CallbackResponse {
1824    pub schema_version: String,
1825    pub status: ReceiptStatus,
1826    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1827    pub client_payloads: Vec<PayloadEnvelope>,
1828    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1829    pub receipt_refs: Vec<String>,
1830    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1831    pub warnings: Vec<Warning>,
1832    pub failure_class: Option<FailureClass>,
1833    pub retry_class: Option<RetryClass>,
1834    #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
1835    pub metadata: serde_json::Map<String, serde_json::Value>,
1836}
1837
1838impl CallbackResponse {
1839    pub fn ok(status: ReceiptStatus) -> Self {
1840        Self {
1841            schema_version: SCHEMA_VERSION.to_string(),
1842            status,
1843            client_payloads: Vec::new(),
1844            receipt_refs: Vec::new(),
1845            warnings: Vec::new(),
1846            failure_class: None,
1847            retry_class: None,
1848            metadata: serde_json::Map::new(),
1849        }
1850    }
1851
1852    pub fn failed(failure: FailureClass) -> Self {
1853        Self {
1854            schema_version: SCHEMA_VERSION.to_string(),
1855            status: ReceiptStatus::Failed,
1856            client_payloads: Vec::new(),
1857            receipt_refs: Vec::new(),
1858            warnings: Vec::new(),
1859            failure_class: Some(failure),
1860            retry_class: Some(failure.default_retry()),
1861            metadata: serde_json::Map::new(),
1862        }
1863    }
1864
1865    pub fn validate(&self) -> Result<(), ValidationError> {
1866        if self.schema_version != SCHEMA_VERSION {
1867            return Err(ValidationError::SchemaVersionMismatch {
1868                expected: SCHEMA_VERSION.to_string(),
1869                found: self.schema_version.clone(),
1870            });
1871        }
1872        for p in &self.client_payloads {
1873            p.validate()?;
1874        }
1875        for r in &self.receipt_refs {
1876            require_non_empty(r, "response.receipt_refs[]")?;
1877        }
1878        for w in &self.warnings {
1879            w.validate()?;
1880        }
1881        match (
1882            matches!(self.status, ReceiptStatus::Failed),
1883            self.failure_class.is_some(),
1884        ) {
1885            (true, false) => {
1886                return Err(ValidationError::InvalidResponse(
1887                    "status=failed requires failure_class".into(),
1888                ));
1889            }
1890            (false, true) => {
1891                return Err(ValidationError::InvalidResponse(
1892                    "failure_class is only valid on status=failed responses".into(),
1893                ));
1894            }
1895            _ => {}
1896        }
1897        if matches!(self.status, ReceiptStatus::Failed) && self.retry_class.is_none() {
1898            return Err(ValidationError::InvalidResponse(
1899                "status=failed requires retry_class (clients must declare retry posture)".into(),
1900            ));
1901        }
1902        Ok(())
1903    }
1904}
1905
1906// ============================================================================
1907// Dispatch envelope (transport boundary)
1908// ============================================================================
1909
1910/// Wire shape carrying a [`CallbackRequest`] and the opaque
1911/// [`PayloadEnvelope`] bodies a dispatch is delivering with.
1912///
1913/// The lifecycle contract distinguishes two concerns:
1914///
1915/// * the *request* a client receives describing what is happening
1916///   (event kind, frame context, [`CallbackRequest::payload_refs`]
1917///   pointing at named/sized/digested payloads), and
1918/// * the *envelope bodies* the request refers to.
1919///
1920/// Until issue #22 the CLI and the subprocess invoker only transported
1921/// the request — the envelopes were not delivered, so subprocess clients
1922/// could not reach payload bodies and negotiation never saw real
1923/// placement inputs. `DispatchEnvelope` is the transport-boundary shape
1924/// that carries both:
1925///
1926/// ```json
1927/// {
1928///   "schema_version": "lifeloop.v0.1",
1929///   "request": { "...CallbackRequest...": "..." },
1930///   "payloads": [ { "...PayloadEnvelope...": "..." } ]
1931/// }
1932/// ```
1933///
1934/// Lifeloop does not parse `payloads[].body` — it is transported
1935/// verbatim, consistent with the spec rule that bodies are opaque
1936/// (`docs/specs/lifecycle-contract/body.md`, "Opaque Payload Envelope").
1937/// `payloads` is omitted on the wire when empty.
1938#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1939#[serde(deny_unknown_fields)]
1940pub struct DispatchEnvelope {
1941    pub schema_version: String,
1942    pub request: CallbackRequest,
1943    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1944    pub payloads: Vec<PayloadEnvelope>,
1945}
1946
1947impl DispatchEnvelope {
1948    /// Construct a dispatch envelope at the canonical schema version.
1949    pub fn new(request: CallbackRequest, payloads: Vec<PayloadEnvelope>) -> Self {
1950        Self {
1951            schema_version: SCHEMA_VERSION.to_string(),
1952            request,
1953            payloads,
1954        }
1955    }
1956
1957    /// Validate the envelope: schema version, the inner request, and
1958    /// each carried payload. Cross-correlation between
1959    /// `request.payload_refs` and `payloads[]` is intentionally *not*
1960    /// enforced here — a request may declare refs that are delivered
1961    /// out-of-band, and clients may receive bodies the request did not
1962    /// list (e.g. degraded fallback bodies). Cross-correlation belongs
1963    /// in negotiation/receipt synthesis, not the transport boundary.
1964    pub fn validate(&self) -> Result<(), ValidationError> {
1965        if self.schema_version != SCHEMA_VERSION {
1966            return Err(ValidationError::SchemaVersionMismatch {
1967                expected: SCHEMA_VERSION.to_string(),
1968                found: self.schema_version.clone(),
1969            });
1970        }
1971        self.request.validate()?;
1972        for p in &self.payloads {
1973            p.validate()?;
1974        }
1975        Ok(())
1976    }
1977}
1978
1979// ============================================================================
1980// Domain module aliases (added 0.1.1)
1981//
1982// These submodules are pure re-exports of the canonical top-level surface
1983// they exist so consumers can write `use lifeloop::event::LifecycleEventKind`
1984// instead of `use lifeloop::LifecycleEventKind`. The canonical paths remain
1985// authoritative; aliases must stay in lockstep. See
1986// `tests/domain_modules.rs` for the alias-identity contract.
1987// ============================================================================
1988
1989/// Lifecycle event vocabulary. Re-exports of canonical crate-root items.
1990pub mod event {
1991    pub use crate::{LifecycleEventKind, lifecycle_event_kinds};
1992}
1993
1994/// Adapter manifest types and registry. Re-exports of canonical crate-root items.
1995pub mod manifest {
1996    pub use crate::{
1997        AdapterManifest, ConformanceLevel, ManifestApprovalSurface, ManifestContextPressure,
1998        ManifestKnownDegradation, ManifestLifecycleEventSupport, ManifestPlacementClass,
1999        ManifestPlacementSupport, ManifestReceipts, ManifestSessionIdentity, ManifestSessionRename,
2000        ManifestTelemetrySource, RegisteredAdapter, lookup_manifest, manifest_registry,
2001    };
2002}
2003
2004/// Built-in adapter registry and per-harness manifest constructors.
2005/// Re-exports of canonical crate-root items.
2006pub mod adapters {
2007    pub use crate::{
2008        claude_manifest, codex_manifest, gemini_manifest, hermes_manifest, lookup_manifest,
2009        manifest_registry, openclaw_manifest, opencode_manifest,
2010    };
2011}
2012
2013/// Capability vocabulary and negotiation entry point. Re-exports the canonical
2014/// router items so consumers can use `lifeloop::capability::*` without taking
2015/// a dependency on the `router` module path. The `CapabilityKind` /
2016/// `CapabilityRequest` split is preserved deliberately — `CapabilityKind` is
2017/// the identity used as a manifest lookup key, and `CapabilityRequest` adds
2018/// policy (`desired`, `level`).
2019pub mod capability {
2020    pub use crate::router::{
2021        CapabilityKind, CapabilityRequest, CapabilityRequirement, DefaultNegotiationStrategy,
2022        NegotiatedPlan, PayloadPlacementDecision, PlacementRejection, negotiate,
2023    };
2024}