use serde::{Deserialize, Serialize};
use crate::SCHEMA_VERSION;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IntegrationMode {
ManualSkill,
LauncherWrapper,
NativeHook,
ReferenceAdapter,
TelemetryOnly,
}
impl IntegrationMode {
pub const ALL: &'static [Self] = &[
Self::ManualSkill,
Self::LauncherWrapper,
Self::NativeHook,
Self::ReferenceAdapter,
Self::TelemetryOnly,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SupportState {
Native,
Synthesized,
Manual,
Partial,
Unavailable,
}
impl SupportState {
pub const ALL: &'static [Self] = &[
Self::Native,
Self::Synthesized,
Self::Manual,
Self::Partial,
Self::Unavailable,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AdapterRole {
PrimaryWorker,
Worker,
Supervisor,
Observer,
}
impl AdapterRole {
pub const ALL: &'static [Self] = &[
Self::PrimaryWorker,
Self::Worker,
Self::Supervisor,
Self::Observer,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
pub enum LifecycleEventKind {
#[serde(rename = "session.starting")]
SessionStarting,
#[serde(rename = "session.started")]
SessionStarted,
#[serde(rename = "frame.opening")]
FrameOpening,
#[serde(rename = "frame.opened")]
FrameOpened,
#[serde(rename = "context.pressure_observed")]
ContextPressureObserved,
#[serde(rename = "context.compacted")]
ContextCompacted,
#[serde(rename = "frame.ending")]
FrameEnding,
#[serde(rename = "frame.ended")]
FrameEnded,
#[serde(rename = "session.ending")]
SessionEnding,
#[serde(rename = "session.ended")]
SessionEnded,
#[serde(rename = "supervisor.tick")]
SupervisorTick,
#[serde(rename = "capability.degraded")]
CapabilityDegraded,
#[serde(rename = "receipt.emitted")]
ReceiptEmitted,
#[serde(rename = "receipt.gap_detected")]
ReceiptGapDetected,
}
impl LifecycleEventKind {
pub const ALL: &'static [Self] = &[
Self::SessionStarting,
Self::SessionStarted,
Self::FrameOpening,
Self::FrameOpened,
Self::ContextPressureObserved,
Self::ContextCompacted,
Self::FrameEnding,
Self::FrameEnded,
Self::SessionEnding,
Self::SessionEnded,
Self::SupervisorTick,
Self::CapabilityDegraded,
Self::ReceiptEmitted,
Self::ReceiptGapDetected,
];
}
pub fn lifecycle_event_kinds() -> Vec<LifecycleEventKind> {
LifecycleEventKind::ALL.to_vec()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReceiptStatus {
Observed,
Delivered,
Skipped,
Degraded,
Failed,
}
impl ReceiptStatus {
pub const ALL: &'static [Self] = &[
Self::Observed,
Self::Delivered,
Self::Skipped,
Self::Degraded,
Self::Failed,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FailureClass {
AdapterUnavailable,
CapabilityUnsupported,
CapabilityDegraded,
PlacementUnavailable,
PayloadTooLarge,
PayloadRejected,
IdentityUnavailable,
TransportError,
Timeout,
OperatorRequired,
StateConflict,
InvalidRequest,
InternalError,
}
impl FailureClass {
pub const ALL: &'static [Self] = &[
Self::AdapterUnavailable,
Self::CapabilityUnsupported,
Self::CapabilityDegraded,
Self::PlacementUnavailable,
Self::PayloadTooLarge,
Self::PayloadRejected,
Self::IdentityUnavailable,
Self::TransportError,
Self::Timeout,
Self::OperatorRequired,
Self::StateConflict,
Self::InvalidRequest,
Self::InternalError,
];
pub fn default_retry(self) -> RetryClass {
match self {
Self::AdapterUnavailable => RetryClass::RetryAfterReconfigure,
Self::CapabilityUnsupported => RetryClass::DoNotRetry,
Self::CapabilityDegraded => RetryClass::RetryAfterReread,
Self::PlacementUnavailable => RetryClass::RetryAfterReconfigure,
Self::PayloadTooLarge => RetryClass::DoNotRetry,
Self::PayloadRejected => RetryClass::RetryAfterReconfigure,
Self::IdentityUnavailable => RetryClass::RetryAfterReconfigure,
Self::TransportError => RetryClass::SafeRetry,
Self::Timeout => RetryClass::SafeRetry,
Self::OperatorRequired => RetryClass::RetryAfterOperator,
Self::StateConflict => RetryClass::RetryAfterReread,
Self::InvalidRequest => RetryClass::DoNotRetry,
Self::InternalError => RetryClass::RetryAfterReread,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RetryClass {
SafeRetry,
RetryAfterReread,
RetryAfterReconfigure,
RetryAfterOperator,
DoNotRetry,
}
impl RetryClass {
pub const ALL: &'static [Self] = &[
Self::SafeRetry,
Self::RetryAfterReread,
Self::RetryAfterReconfigure,
Self::RetryAfterOperator,
Self::DoNotRetry,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PlacementClass {
DeveloperEquivalentFrame,
PrePromptFrame,
SideChannelContext,
ReceiptOnly,
}
impl PlacementClass {
pub const ALL: &'static [Self] = &[
Self::DeveloperEquivalentFrame,
Self::PrePromptFrame,
Self::SideChannelContext,
Self::ReceiptOnly,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PlacementOutcome {
Delivered,
Skipped,
Degraded,
Failed,
}
impl PlacementOutcome {
pub const ALL: &'static [Self] =
&[Self::Delivered, Self::Skipped, Self::Degraded, Self::Failed];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RequirementLevel {
Required,
Preferred,
Optional,
}
impl RequirementLevel {
pub const ALL: &'static [Self] = &[Self::Required, Self::Preferred, Self::Optional];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NegotiationOutcome {
Satisfied,
Degraded,
Unsupported,
RequiresOperator,
}
impl NegotiationOutcome {
pub const ALL: &'static [Self] = &[
Self::Satisfied,
Self::Degraded,
Self::Unsupported,
Self::RequiresOperator,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrameClass {
TopLevel,
Subcall,
}
impl FrameClass {
pub const ALL: &'static [Self] = &[Self::TopLevel, Self::Subcall];
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind", content = "detail")]
pub enum ValidationError {
EmptyField(String),
SchemaVersionMismatch { expected: String, found: String },
InvalidFrameContext(String),
InvalidPayload(String),
InvalidReceipt(String),
InvalidRequest(String),
InvalidResponse(String),
InvalidManifest(String),
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyField(name) => write!(f, "empty sentinel string in field `{name}`"),
Self::SchemaVersionMismatch { expected, found } => write!(
f,
"schema_version mismatch: expected `{expected}`, found `{found}`"
),
Self::InvalidFrameContext(msg) => write!(f, "invalid frame_context: {msg}"),
Self::InvalidPayload(msg) => write!(f, "invalid payload: {msg}"),
Self::InvalidReceipt(msg) => write!(f, "invalid receipt: {msg}"),
Self::InvalidRequest(msg) => write!(f, "invalid callback request: {msg}"),
Self::InvalidResponse(msg) => write!(f, "invalid callback response: {msg}"),
Self::InvalidManifest(msg) => write!(f, "invalid adapter manifest: {msg}"),
}
}
}
impl std::error::Error for ValidationError {}
pub(crate) fn require_non_empty(value: &str, field: &'static str) -> Result<(), ValidationError> {
if value.is_empty() {
return Err(ValidationError::EmptyField(field.to_string()));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FrameContext {
pub frame_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_frame_id: Option<String>,
pub frame_class: FrameClass,
}
impl FrameContext {
pub fn top_level(frame_id: impl Into<String>) -> Self {
Self {
frame_id: frame_id.into(),
parent_frame_id: None,
frame_class: FrameClass::TopLevel,
}
}
pub fn subcall(frame_id: impl Into<String>, parent_frame_id: impl Into<String>) -> Self {
Self {
frame_id: frame_id.into(),
parent_frame_id: Some(parent_frame_id.into()),
frame_class: FrameClass::Subcall,
}
}
pub fn validate(&self) -> Result<(), ValidationError> {
require_non_empty(&self.frame_id, "frame_context.frame_id")?;
if let Some(parent) = &self.parent_frame_id {
require_non_empty(parent, "frame_context.parent_frame_id")?;
}
match (self.frame_class, &self.parent_frame_id) {
(FrameClass::TopLevel, Some(_)) => Err(ValidationError::InvalidFrameContext(
"frame_class=top_level must not carry parent_frame_id".into(),
)),
(FrameClass::Subcall, None) => Err(ValidationError::InvalidFrameContext(
"frame_class=subcall requires parent_frame_id".into(),
)),
_ => Ok(()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AcceptablePlacement {
pub placement: PlacementClass,
pub requirement: RequirementLevel,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PayloadRef {
pub payload_id: String,
pub payload_kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_digest: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub byte_size: Option<u64>,
}
impl PayloadRef {
pub fn validate(&self) -> Result<(), ValidationError> {
require_non_empty(&self.payload_id, "payload_ref.payload_id")?;
require_non_empty(&self.payload_kind, "payload_ref.payload_kind")?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PayloadEnvelope {
pub schema_version: String,
pub payload_id: String,
pub client_id: String,
pub payload_kind: String,
pub format: String,
pub content_encoding: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body_ref: Option<String>,
pub byte_size: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_digest: Option<String>,
pub acceptable_placements: Vec<AcceptablePlacement>,
#[serde(skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at_epoch_s: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub redaction: Option<String>,
#[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
pub metadata: serde_json::Map<String, serde_json::Value>,
}
impl PayloadEnvelope {
pub fn effective_byte_size(&self) -> u64 {
self.body
.as_ref()
.map(|body| body.len() as u64)
.unwrap_or(self.byte_size)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.schema_version != SCHEMA_VERSION {
return Err(ValidationError::SchemaVersionMismatch {
expected: SCHEMA_VERSION.to_string(),
found: self.schema_version.clone(),
});
}
require_non_empty(&self.payload_id, "payload.payload_id")?;
require_non_empty(&self.client_id, "payload.client_id")?;
require_non_empty(&self.payload_kind, "payload.payload_kind")?;
require_non_empty(&self.format, "payload.format")?;
require_non_empty(&self.content_encoding, "payload.content_encoding")?;
match (self.body.is_some(), self.body_ref.is_some()) {
(true, true) => Err(ValidationError::InvalidPayload(
"body and body_ref are mutually exclusive".into(),
)),
(false, false) => Err(ValidationError::InvalidPayload(
"exactly one of body or body_ref must be present".into(),
)),
_ => Ok(()),
}?;
if let Some(body) = &self.body {
let actual = body.len() as u64;
if actual != self.byte_size {
return Err(ValidationError::InvalidPayload(format!(
"body byte length {actual} does not match byte_size {}",
self.byte_size
)));
}
}
if let Some(idem) = &self.idempotency_key {
require_non_empty(idem, "payload.idempotency_key")?;
}
if self.acceptable_placements.is_empty() {
return Err(ValidationError::InvalidPayload(
"acceptable_placements must list at least one placement".into(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PayloadReceipt {
pub payload_id: String,
pub payload_kind: String,
pub placement: PlacementClass,
pub status: PlacementOutcome,
pub byte_size: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_digest: Option<String>,
}
impl PayloadReceipt {
pub fn validate(&self) -> Result<(), ValidationError> {
require_non_empty(&self.payload_id, "payload_receipt.payload_id")?;
require_non_empty(&self.payload_kind, "payload_receipt.payload_kind")?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CapabilityDegradation {
pub capability: String,
pub previous_support: SupportState,
pub current_support: SupportState,
#[serde(skip_serializing_if = "Option::is_none")]
pub evidence: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_class: Option<RetryClass>,
}
impl CapabilityDegradation {
pub fn validate(&self) -> Result<(), ValidationError> {
require_non_empty(&self.capability, "capability_degradation.capability")?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Warning {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub capability: Option<String>,
}
impl Warning {
pub fn validate(&self) -> Result<(), ValidationError> {
require_non_empty(&self.code, "warning.code")?;
Ok(())
}
}