1use 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#[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#[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 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#[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#[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#[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#[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 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
600impl<'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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
940#[serde(rename_all = "snake_case")]
941pub enum ManifestPlacementClass {
942 PreSession,
944 PreFrameLeading,
946 PreFrameTrailing,
948 ToolResult,
950 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
966#[serde(deny_unknown_fields)]
967pub struct ManifestLifecycleEventSupport {
968 pub support: SupportState,
969 #[serde(default, skip_serializing_if = "Vec::is_empty")]
972 pub modes: Vec<IntegrationMode>,
973}
974
975#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
977#[serde(deny_unknown_fields)]
978pub struct ManifestPlacementSupport {
979 pub support: SupportState,
980 #[serde(skip_serializing_if = "Option::is_none")]
982 pub max_bytes: Option<u64>,
983}
984
985#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
997#[serde(deny_unknown_fields)]
998pub struct ManifestReceipts {
999 pub native: bool,
1001 pub lifeloop_synthesized: bool,
1003 pub receipt_ledger: SupportState,
1005}
1006
1007#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1021#[serde(deny_unknown_fields)]
1022pub struct ManifestSessionRename {
1023 pub support: SupportState,
1024}
1025
1026#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1029#[serde(deny_unknown_fields)]
1030pub struct ManifestApprovalSurface {
1031 pub support: SupportState,
1032}
1033
1034#[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#[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#[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(°.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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1125#[serde(rename_all = "snake_case")]
1126pub enum ConformanceLevel {
1127 V1Conformance,
1131 PreConformance,
1135}
1136
1137#[derive(Debug, Clone, PartialEq, Eq)]
1140pub struct RegisteredAdapter {
1141 pub manifest: AdapterManifest,
1142 pub conformance: ConformanceLevel,
1143}
1144
1145pub 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
1177pub 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
1201pub 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
1379pub 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
1554pub fn hermes_manifest() -> AdapterManifest {
1558 pre_conformance_reference_adapter_manifest("hermes", "Hermes")
1559}
1560
1561pub fn openclaw_manifest() -> AdapterManifest {
1563 pre_conformance_reference_adapter_manifest("openclaw", "OpenClaw")
1564}
1565
1566pub fn gemini_manifest() -> AdapterManifest {
1568 pre_conformance_telemetry_only_manifest("gemini", "Gemini")
1569}
1570
1571pub 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#[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#[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 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 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
1979pub mod event {
1991 pub use crate::{LifecycleEventKind, lifecycle_event_kinds};
1992}
1993
1994pub 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
2004pub 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
2013pub mod capability {
2020 pub use crate::router::{
2021 CapabilityKind, CapabilityRequest, CapabilityRequirement, DefaultNegotiationStrategy,
2022 NegotiatedPlan, PayloadPlacementDecision, PlacementRejection, negotiate,
2023 };
2024}