Skip to main content

lifeloop/
manifest_contract.rs

1//! Adapter manifest types and built-in manifest registry.
2
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6use crate::{
7    AdapterRole, FailureClass, IntegrationMode, LifecycleEventKind, SCHEMA_VERSION, SupportState,
8    ValidationError, require_non_empty,
9};
10
11/// Manifest placement classes — the trust-neutral, lifecycle-timing
12/// vocabulary the adapter manifest uses to declare placement support.
13///
14/// **Distinct from [`crate::PlacementClass`]**, which is the routing
15/// vocabulary the runtime uses on `acceptable_placements` for
16/// concrete payload delivery. The manifest declares *capability*;
17/// the payload envelope declares *routing intent*. A future
18/// revision may unify them; the current contract keeps them
19/// separate so manifest evolution does not churn payload routing.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum ManifestPlacementClass {
23    /// Before any frame opens; e.g. session-init context.
24    PreSession,
25    /// Leading edge of a frame, before user/task input arrives.
26    PreFrameLeading,
27    /// Trailing edge of a frame, after input but before model execution.
28    PreFrameTrailing,
29    /// Inside a tool-result envelope returned to the model.
30    ToolResult,
31    /// Through an operator or manual surface (skill, command, wrapper).
32    ManualOperator,
33}
34
35impl ManifestPlacementClass {
36    pub const ALL: &'static [Self] = &[
37        Self::PreSession,
38        Self::PreFrameLeading,
39        Self::PreFrameTrailing,
40        Self::ToolResult,
41        Self::ManualOperator,
42    ];
43}
44
45/// Per-event capability claim inside a manifest.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct ManifestLifecycleEventSupport {
49    pub support: SupportState,
50    /// Integration modes through which the adapter delivers this event.
51    /// May be empty when `support` is `unavailable`.
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub modes: Vec<IntegrationMode>,
54}
55
56/// Per-placement capability claim inside a manifest.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct ManifestPlacementSupport {
60    pub support: SupportState,
61    /// Placement size limit in bytes when the adapter declares one.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub max_bytes: Option<u64>,
64}
65
66/// Capability claim describing how the adapter surfaces
67/// `context.pressure_observed` lifecycle evidence.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct ManifestContextPressure {
71    pub support: SupportState,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub evidence: Option<String>,
74}
75
76/// Capability claim describing receipt emission and ledger support.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(deny_unknown_fields)]
79pub struct ManifestReceipts {
80    /// Adapter emits its own native receipts.
81    pub native: bool,
82    /// Lifeloop synthesizes receipts on the adapter's behalf.
83    pub lifeloop_synthesized: bool,
84    /// Durable cross-invocation receipt ledger.
85    pub receipt_ledger: SupportState,
86}
87
88/// Per-id support claims for harness identity correlation. Optional
89/// on the manifest because a telemetry-only adapter may not expose
90/// any of these.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(deny_unknown_fields)]
93pub struct ManifestSessionIdentity {
94    pub harness_session_id: SupportState,
95    pub harness_run_id: SupportState,
96    pub harness_task_id: SupportState,
97}
98
99/// Capability claim for the adapter's session-rename surface.
100/// Optional on the manifest; absent means "no rename concept."
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(deny_unknown_fields)]
103pub struct ManifestSessionRename {
104    pub support: SupportState,
105}
106
107/// Capability claim for reset/continuation renewal across a harness
108/// boundary. Lifeloop reports whether the adapter can prove the
109/// lifecycle path and delivery support; clients own renewal leases,
110/// continuation-token policy, and thread/session binding.
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(deny_unknown_fields)]
113pub struct ManifestRenewal {
114    pub reset: ManifestRenewalReset,
115    pub continuation: ManifestRenewalContinuation,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub evidence: Option<String>,
118}
119
120/// Reset-side renewal capability. `native`, `wrapper_mediated`, and
121/// `manual` are separate so a manifest can distinguish a real harness
122/// reset surface, a launcher/wrapper path, an operator-only path, and
123/// the all-`unavailable` "no safe reset path" case.
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(deny_unknown_fields)]
126pub struct ManifestRenewalReset {
127    pub native: SupportState,
128    pub wrapper_mediated: SupportState,
129    pub manual: SupportState,
130}
131
132/// Continuation-side renewal capability. Observation means the
133/// adapter can prove a continuation boundary happened; payload
134/// delivery means it can carry client-provided continuation facts
135/// across that boundary.
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(deny_unknown_fields)]
138pub struct ManifestRenewalContinuation {
139    pub observation: SupportState,
140    pub payload_delivery: SupportState,
141}
142
143/// Capability claim for operator approval/intervention surfaces.
144/// Optional; absent means "no operator surface."
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(deny_unknown_fields)]
147pub struct ManifestApprovalSurface {
148    pub support: SupportState,
149}
150
151/// One telemetry source the adapter exposes for lifecycle evidence.
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(deny_unknown_fields)]
154pub struct ManifestTelemetrySource {
155    pub source: String,
156    pub support: SupportState,
157}
158
159/// One pre-declared capability degradation the adapter ships with.
160/// Lets a manifest say "this build's `context_pressure` was native
161/// upstream but is currently `unavailable` here" without firing a
162/// runtime `capability.degraded` event.
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(deny_unknown_fields)]
165pub struct ManifestKnownDegradation {
166    pub capability: String,
167    pub previous_support: SupportState,
168    pub current_support: SupportState,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub evidence: Option<String>,
171}
172
173/// Adapter manifest. Issue #6 lands the full shape; pre-issue-#6
174/// drafts shipped a stub with only schema_version, adapter_id,
175/// adapter_version, display_name, roles, integration_modes, and
176/// lifecycle_events.
177///
178/// `contract_version` (this struct's first field) carries the
179/// Lifeloop contract version label (e.g. `lifeloop.v0.2`),
180/// independent of `adapter_version`. The two are separate so
181/// adapters can iterate without bumping the contract.
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(deny_unknown_fields)]
184pub struct AdapterManifest {
185    pub contract_version: String,
186    pub adapter_id: String,
187    pub adapter_version: String,
188    pub display_name: String,
189    pub role: AdapterRole,
190    pub integration_modes: Vec<IntegrationMode>,
191    pub lifecycle_events: BTreeMap<LifecycleEventKind, ManifestLifecycleEventSupport>,
192    pub placement: BTreeMap<ManifestPlacementClass, ManifestPlacementSupport>,
193    pub context_pressure: ManifestContextPressure,
194    pub receipts: ManifestReceipts,
195
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub session_identity: Option<ManifestSessionIdentity>,
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub session_rename: Option<ManifestSessionRename>,
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub renewal: Option<ManifestRenewal>,
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub approval_surface: Option<ManifestApprovalSurface>,
204    #[serde(default, skip_serializing_if = "Vec::is_empty")]
205    pub failure_modes: Vec<FailureClass>,
206    #[serde(default, skip_serializing_if = "Vec::is_empty")]
207    pub telemetry_sources: Vec<ManifestTelemetrySource>,
208    #[serde(default, skip_serializing_if = "Vec::is_empty")]
209    pub known_degradations: Vec<ManifestKnownDegradation>,
210}
211
212impl AdapterManifest {
213    pub fn validate(&self) -> Result<(), ValidationError> {
214        if self.contract_version != SCHEMA_VERSION {
215            return Err(ValidationError::SchemaVersionMismatch {
216                expected: SCHEMA_VERSION.to_string(),
217                found: self.contract_version.clone(),
218            });
219        }
220        require_non_empty(&self.adapter_id, "manifest.adapter_id")?;
221        require_non_empty(&self.adapter_version, "manifest.adapter_version")?;
222        require_non_empty(&self.display_name, "manifest.display_name")?;
223        if self.integration_modes.is_empty() {
224            return Err(ValidationError::InvalidManifest(
225                "manifest.integration_modes must declare at least one integration mode".into(),
226            ));
227        }
228        for deg in &self.known_degradations {
229            require_non_empty(&deg.capability, "manifest.known_degradations[].capability")?;
230        }
231        for src in &self.telemetry_sources {
232            require_non_empty(&src.source, "manifest.telemetry_sources[].source")?;
233        }
234        if let Some(renewal) = &self.renewal
235            && let Some(evidence) = &renewal.evidence
236        {
237            require_non_empty(evidence, "manifest.renewal.evidence")?;
238        }
239        Ok(())
240    }
241}
242
243// ----------------------------------------------------------------------------
244// Manifest registry
245// ----------------------------------------------------------------------------
246
247/// Conformance posture of a registered adapter.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
249#[serde(rename_all = "snake_case")]
250pub enum ConformanceLevel {
251    /// v1-conformance adapter: every capability claim that depends on
252    /// extracted code (asset rendering, telemetry, placement) is
253    /// paired with a test that verifies the claim.
254    V1Conformance,
255    /// Initial manifest shipped without full claim verification. The
256    /// claims describe expected behavior so clients can negotiate, but
257    /// the registry does not yet run capability-claim tests for them.
258    PreConformance,
259}
260
261/// Registry entry pairing an [`AdapterManifest`] with its
262/// [`ConformanceLevel`].
263#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct RegisteredAdapter {
265    pub manifest: AdapterManifest,
266    pub conformance: ConformanceLevel,
267}
268
269/// Built-in adapter manifest registry. Order is stable so callers
270/// that render a `lifeloop adapters` listing get a predictable
271/// output without sorting client-side.
272pub fn manifest_registry() -> Vec<RegisteredAdapter> {
273    vec![
274        RegisteredAdapter {
275            manifest: codex_manifest(),
276            conformance: ConformanceLevel::V1Conformance,
277        },
278        RegisteredAdapter {
279            manifest: claude_manifest(),
280            conformance: ConformanceLevel::V1Conformance,
281        },
282        RegisteredAdapter {
283            manifest: hermes_manifest(),
284            conformance: ConformanceLevel::PreConformance,
285        },
286        RegisteredAdapter {
287            manifest: openclaw_manifest(),
288            conformance: ConformanceLevel::PreConformance,
289        },
290        RegisteredAdapter {
291            manifest: gemini_manifest(),
292            conformance: ConformanceLevel::PreConformance,
293        },
294        RegisteredAdapter {
295            manifest: opencode_manifest(),
296            conformance: ConformanceLevel::PreConformance,
297        },
298    ]
299}
300
301/// Resolve a registered adapter by `adapter_id`. Returns `None` for
302/// unknown ids.
303pub fn lookup_manifest(adapter_id: &str) -> Option<RegisteredAdapter> {
304    manifest_registry()
305        .into_iter()
306        .find(|entry| entry.manifest.adapter_id == adapter_id)
307}
308
309fn synthesized() -> SupportState {
310    SupportState::Synthesized
311}
312
313fn native() -> SupportState {
314    SupportState::Native
315}
316
317fn unavailable() -> SupportState {
318    SupportState::Unavailable
319}
320
321fn manual() -> SupportState {
322    SupportState::Manual
323}
324
325/// Codex manifest. Native-hook integration covers Codex's stable hook
326/// surface, including `PreCompact` in Codex CLI 0.129+. Capability
327/// claims here are paired with verification tests in
328/// `tests/manifest_claims.rs`.
329pub fn codex_manifest() -> AdapterManifest {
330    let lifecycle_events = BTreeMap::from([
331        (
332            LifecycleEventKind::SessionStarting,
333            ManifestLifecycleEventSupport {
334                support: native(),
335                modes: vec![IntegrationMode::NativeHook],
336            },
337        ),
338        (
339            LifecycleEventKind::SessionStarted,
340            ManifestLifecycleEventSupport {
341                support: native(),
342                modes: vec![IntegrationMode::NativeHook],
343            },
344        ),
345        (
346            LifecycleEventKind::FrameOpening,
347            ManifestLifecycleEventSupport {
348                support: native(),
349                modes: vec![IntegrationMode::NativeHook],
350            },
351        ),
352        (
353            LifecycleEventKind::FrameOpened,
354            ManifestLifecycleEventSupport {
355                support: synthesized(),
356                modes: vec![IntegrationMode::NativeHook],
357            },
358        ),
359        (
360            LifecycleEventKind::ContextPressureObserved,
361            ManifestLifecycleEventSupport {
362                support: native(),
363                modes: vec![IntegrationMode::NativeHook],
364            },
365        ),
366        (
367            LifecycleEventKind::ContextCompacted,
368            ManifestLifecycleEventSupport {
369                support: native(),
370                modes: vec![IntegrationMode::NativeHook],
371            },
372        ),
373        (
374            LifecycleEventKind::FrameEnding,
375            ManifestLifecycleEventSupport {
376                support: native(),
377                modes: vec![IntegrationMode::NativeHook],
378            },
379        ),
380        (
381            LifecycleEventKind::FrameEnded,
382            ManifestLifecycleEventSupport {
383                support: native(),
384                modes: vec![IntegrationMode::NativeHook],
385            },
386        ),
387        (
388            LifecycleEventKind::SessionEnding,
389            ManifestLifecycleEventSupport {
390                support: unavailable(),
391                modes: Vec::new(),
392            },
393        ),
394        (
395            LifecycleEventKind::SessionEnded,
396            ManifestLifecycleEventSupport {
397                support: unavailable(),
398                modes: Vec::new(),
399            },
400        ),
401        (
402            LifecycleEventKind::SupervisorTick,
403            ManifestLifecycleEventSupport {
404                support: unavailable(),
405                modes: Vec::new(),
406            },
407        ),
408        (
409            LifecycleEventKind::CapabilityDegraded,
410            ManifestLifecycleEventSupport {
411                support: synthesized(),
412                modes: vec![IntegrationMode::NativeHook],
413            },
414        ),
415        (
416            LifecycleEventKind::ReceiptEmitted,
417            ManifestLifecycleEventSupport {
418                support: synthesized(),
419                modes: vec![IntegrationMode::NativeHook],
420            },
421        ),
422        (
423            LifecycleEventKind::ReceiptGapDetected,
424            ManifestLifecycleEventSupport {
425                support: unavailable(),
426                modes: Vec::new(),
427            },
428        ),
429    ]);
430
431    let placement = BTreeMap::from([
432        (
433            ManifestPlacementClass::PreSession,
434            ManifestPlacementSupport {
435                support: native(),
436                max_bytes: Some(8192),
437            },
438        ),
439        (
440            ManifestPlacementClass::PreFrameLeading,
441            ManifestPlacementSupport {
442                support: native(),
443                max_bytes: Some(8192),
444            },
445        ),
446        (
447            ManifestPlacementClass::PreFrameTrailing,
448            ManifestPlacementSupport {
449                support: unavailable(),
450                max_bytes: None,
451            },
452        ),
453        (
454            ManifestPlacementClass::ToolResult,
455            ManifestPlacementSupport {
456                support: unavailable(),
457                max_bytes: None,
458            },
459        ),
460        (
461            ManifestPlacementClass::ManualOperator,
462            ManifestPlacementSupport {
463                support: manual(),
464                max_bytes: None,
465            },
466        ),
467    ]);
468
469    AdapterManifest {
470        contract_version: SCHEMA_VERSION.to_string(),
471        adapter_id: "codex".into(),
472        adapter_version: "0.1.0".into(),
473        display_name: "Codex".into(),
474        role: AdapterRole::PrimaryWorker,
475        integration_modes: vec![IntegrationMode::NativeHook, IntegrationMode::ManualSkill],
476        lifecycle_events,
477        placement,
478        context_pressure: ManifestContextPressure {
479            support: native(),
480            evidence: Some(
481                "Codex CLI 0.129 exposes PreCompact before context pressure handling and PostCompact after context compacts"
482                    .into(),
483            ),
484        },
485        receipts: ManifestReceipts {
486            native: false,
487            lifeloop_synthesized: true,
488            receipt_ledger: unavailable(),
489        },
490        session_identity: Some(ManifestSessionIdentity {
491            harness_session_id: native(),
492            harness_run_id: synthesized(),
493            harness_task_id: unavailable(),
494        }),
495        session_rename: None,
496        renewal: None,
497        approval_surface: None,
498        failure_modes: vec![FailureClass::TransportError, FailureClass::PayloadTooLarge],
499        telemetry_sources: Vec::new(),
500        known_degradations: Vec::new(),
501    }
502}
503
504/// Claude manifest. Native-hook integration via `.claude/settings.json`.
505pub fn claude_manifest() -> AdapterManifest {
506    let lifecycle_events = BTreeMap::from([
507        (
508            LifecycleEventKind::SessionStarting,
509            ManifestLifecycleEventSupport {
510                support: native(),
511                modes: vec![IntegrationMode::NativeHook],
512            },
513        ),
514        (
515            LifecycleEventKind::SessionStarted,
516            ManifestLifecycleEventSupport {
517                support: native(),
518                modes: vec![IntegrationMode::NativeHook],
519            },
520        ),
521        (
522            LifecycleEventKind::FrameOpening,
523            ManifestLifecycleEventSupport {
524                support: native(),
525                modes: vec![IntegrationMode::NativeHook],
526            },
527        ),
528        (
529            LifecycleEventKind::FrameOpened,
530            ManifestLifecycleEventSupport {
531                support: native(),
532                modes: vec![IntegrationMode::NativeHook],
533            },
534        ),
535        (
536            LifecycleEventKind::ContextPressureObserved,
537            ManifestLifecycleEventSupport {
538                support: native(),
539                modes: vec![IntegrationMode::NativeHook],
540            },
541        ),
542        (
543            LifecycleEventKind::ContextCompacted,
544            ManifestLifecycleEventSupport {
545                support: unavailable(),
546                modes: Vec::new(),
547            },
548        ),
549        (
550            LifecycleEventKind::FrameEnding,
551            ManifestLifecycleEventSupport {
552                support: native(),
553                modes: vec![IntegrationMode::NativeHook],
554            },
555        ),
556        (
557            LifecycleEventKind::FrameEnded,
558            ManifestLifecycleEventSupport {
559                support: native(),
560                modes: vec![IntegrationMode::NativeHook],
561            },
562        ),
563        (
564            LifecycleEventKind::SessionEnding,
565            ManifestLifecycleEventSupport {
566                support: native(),
567                modes: vec![IntegrationMode::NativeHook],
568            },
569        ),
570        (
571            LifecycleEventKind::SessionEnded,
572            ManifestLifecycleEventSupport {
573                support: native(),
574                modes: vec![IntegrationMode::NativeHook],
575            },
576        ),
577        (
578            LifecycleEventKind::SupervisorTick,
579            ManifestLifecycleEventSupport {
580                support: unavailable(),
581                modes: Vec::new(),
582            },
583        ),
584        (
585            LifecycleEventKind::CapabilityDegraded,
586            ManifestLifecycleEventSupport {
587                support: synthesized(),
588                modes: vec![IntegrationMode::NativeHook],
589            },
590        ),
591        (
592            LifecycleEventKind::ReceiptEmitted,
593            ManifestLifecycleEventSupport {
594                support: synthesized(),
595                modes: vec![IntegrationMode::NativeHook],
596            },
597        ),
598        (
599            LifecycleEventKind::ReceiptGapDetected,
600            ManifestLifecycleEventSupport {
601                support: unavailable(),
602                modes: Vec::new(),
603            },
604        ),
605    ]);
606
607    let placement = BTreeMap::from([
608        (
609            ManifestPlacementClass::PreSession,
610            ManifestPlacementSupport {
611                support: native(),
612                max_bytes: Some(16_384),
613            },
614        ),
615        (
616            ManifestPlacementClass::PreFrameLeading,
617            ManifestPlacementSupport {
618                support: native(),
619                max_bytes: Some(16_384),
620            },
621        ),
622        (
623            ManifestPlacementClass::PreFrameTrailing,
624            ManifestPlacementSupport {
625                support: unavailable(),
626                max_bytes: None,
627            },
628        ),
629        (
630            ManifestPlacementClass::ToolResult,
631            ManifestPlacementSupport {
632                support: unavailable(),
633                max_bytes: None,
634            },
635        ),
636        (
637            ManifestPlacementClass::ManualOperator,
638            ManifestPlacementSupport {
639                support: manual(),
640                max_bytes: None,
641            },
642        ),
643    ]);
644
645    AdapterManifest {
646        contract_version: SCHEMA_VERSION.to_string(),
647        adapter_id: "claude".into(),
648        adapter_version: "0.1.0".into(),
649        display_name: "Claude".into(),
650        role: AdapterRole::PrimaryWorker,
651        integration_modes: vec![IntegrationMode::NativeHook],
652        lifecycle_events,
653        placement,
654        context_pressure: ManifestContextPressure {
655            support: native(),
656            evidence: Some(
657                "Claude emits PreCompact and SessionEnd events that map directly to context.pressure_observed"
658                    .into(),
659            ),
660        },
661        receipts: ManifestReceipts {
662            native: false,
663            lifeloop_synthesized: true,
664            receipt_ledger: unavailable(),
665        },
666        session_identity: Some(ManifestSessionIdentity {
667            harness_session_id: native(),
668            harness_run_id: synthesized(),
669            harness_task_id: unavailable(),
670        }),
671        session_rename: None,
672        renewal: None,
673        approval_surface: None,
674        failure_modes: vec![FailureClass::TransportError, FailureClass::PayloadTooLarge],
675        telemetry_sources: Vec::new(),
676        known_degradations: Vec::new(),
677    }
678}
679
680/// Hermes pre-conformance manifest. Reference-adapter integration
681/// supplied as a JSON descriptor at the path declared in
682/// [`crate::host_assets::HERMES_TARGET_ADAPTER`].
683pub fn hermes_manifest() -> AdapterManifest {
684    pre_conformance_reference_adapter_manifest("hermes", "Hermes")
685}
686
687/// OpenClaw pre-conformance manifest.
688pub fn openclaw_manifest() -> AdapterManifest {
689    pre_conformance_reference_adapter_manifest("openclaw", "OpenClaw")
690}
691
692/// Gemini pre-conformance manifest.
693pub fn gemini_manifest() -> AdapterManifest {
694    pre_conformance_telemetry_only_manifest("gemini", "Gemini")
695}
696
697/// OpenCode pre-conformance manifest.
698pub fn opencode_manifest() -> AdapterManifest {
699    pre_conformance_telemetry_only_manifest("opencode", "OpenCode")
700}
701
702fn pre_conformance_reference_adapter_manifest(
703    adapter_id: &str,
704    display_name: &str,
705) -> AdapterManifest {
706    let lifecycle_events = BTreeMap::from([
707        (
708            LifecycleEventKind::SessionStarting,
709            ManifestLifecycleEventSupport {
710                support: SupportState::Partial,
711                modes: vec![IntegrationMode::ReferenceAdapter],
712            },
713        ),
714        (
715            LifecycleEventKind::SessionStarted,
716            ManifestLifecycleEventSupport {
717                support: SupportState::Partial,
718                modes: vec![IntegrationMode::ReferenceAdapter],
719            },
720        ),
721        (
722            LifecycleEventKind::FrameOpening,
723            ManifestLifecycleEventSupport {
724                support: SupportState::Partial,
725                modes: vec![IntegrationMode::ReferenceAdapter],
726            },
727        ),
728        (
729            LifecycleEventKind::FrameEnded,
730            ManifestLifecycleEventSupport {
731                support: SupportState::Partial,
732                modes: vec![IntegrationMode::ReferenceAdapter],
733            },
734        ),
735        (
736            LifecycleEventKind::SessionEnded,
737            ManifestLifecycleEventSupport {
738                support: SupportState::Partial,
739                modes: vec![IntegrationMode::ReferenceAdapter],
740            },
741        ),
742    ]);
743
744    let placement = BTreeMap::from([
745        (
746            ManifestPlacementClass::PreSession,
747            ManifestPlacementSupport {
748                support: SupportState::Partial,
749                max_bytes: None,
750            },
751        ),
752        (
753            ManifestPlacementClass::PreFrameLeading,
754            ManifestPlacementSupport {
755                support: SupportState::Partial,
756                max_bytes: None,
757            },
758        ),
759        (
760            ManifestPlacementClass::ManualOperator,
761            ManifestPlacementSupport {
762                support: SupportState::Manual,
763                max_bytes: None,
764            },
765        ),
766    ]);
767
768    AdapterManifest {
769        contract_version: SCHEMA_VERSION.to_string(),
770        adapter_id: adapter_id.to_string(),
771        adapter_version: "0.0.1-pre".into(),
772        display_name: display_name.to_string(),
773        role: AdapterRole::Worker,
774        integration_modes: vec![IntegrationMode::ReferenceAdapter],
775        lifecycle_events,
776        placement,
777        context_pressure: ManifestContextPressure {
778            support: SupportState::Partial,
779            evidence: None,
780        },
781        receipts: ManifestReceipts {
782            native: false,
783            lifeloop_synthesized: true,
784            receipt_ledger: SupportState::Unavailable,
785        },
786        session_identity: None,
787        session_rename: None,
788        renewal: None,
789        approval_surface: None,
790        failure_modes: Vec::new(),
791        telemetry_sources: Vec::new(),
792        known_degradations: Vec::new(),
793    }
794}
795
796fn pre_conformance_telemetry_only_manifest(
797    adapter_id: &str,
798    display_name: &str,
799) -> AdapterManifest {
800    let lifecycle_events = BTreeMap::from([
801        (
802            LifecycleEventKind::SessionStarting,
803            ManifestLifecycleEventSupport {
804                support: SupportState::Partial,
805                modes: vec![IntegrationMode::TelemetryOnly],
806            },
807        ),
808        (
809            LifecycleEventKind::ContextPressureObserved,
810            ManifestLifecycleEventSupport {
811                support: SupportState::Partial,
812                modes: vec![IntegrationMode::TelemetryOnly],
813            },
814        ),
815        (
816            LifecycleEventKind::SessionEnded,
817            ManifestLifecycleEventSupport {
818                support: SupportState::Partial,
819                modes: vec![IntegrationMode::TelemetryOnly],
820            },
821        ),
822    ]);
823
824    let placement = BTreeMap::from([(
825        ManifestPlacementClass::ManualOperator,
826        ManifestPlacementSupport {
827            support: SupportState::Manual,
828            max_bytes: None,
829        },
830    )]);
831
832    AdapterManifest {
833        contract_version: SCHEMA_VERSION.to_string(),
834        adapter_id: adapter_id.to_string(),
835        adapter_version: "0.0.1-pre".into(),
836        display_name: display_name.to_string(),
837        role: AdapterRole::Observer,
838        integration_modes: vec![IntegrationMode::TelemetryOnly],
839        lifecycle_events,
840        placement,
841        context_pressure: ManifestContextPressure {
842            support: SupportState::Partial,
843            evidence: None,
844        },
845        receipts: ManifestReceipts {
846            native: false,
847            lifeloop_synthesized: true,
848            receipt_ledger: SupportState::Unavailable,
849        },
850        session_identity: None,
851        session_rename: None,
852        renewal: None,
853        approval_surface: None,
854        failure_modes: Vec::new(),
855        telemetry_sources: Vec::new(),
856        known_degradations: Vec::new(),
857    }
858}