pub trait Lifecycle {
type Profile: Copy + Debug + PartialEq;
type Version: Copy + Display + PartialEq;
// Required methods
fn classify(
&self,
state: SessionState,
profile: Option<Self::Profile>,
method: &str,
) -> Classification;
fn resolve(
&self,
declaration: Declaration<'_>,
) -> Result<Resolution<Self::Profile, Self::Version>, DeclarationError>;
}Expand description
The vocabulary one protocol brings to the session engine.
Declared by the engine, which is its only consumer, and implemented by whichever protocol module is in play. See the module docs for why it is two methods and two associated types rather than a wider interface.
§Examples
A protocol with no handshake, which is what A2A’s per-request version header amounts to. Nothing in this module changes for it.
use protocol_droid::application::session::{
Classification, Declaration, DeclarationError, Lifecycle, MetaView, Resolution,
};
use protocol_droid::domain::session::SessionState;
/// A2A speaks `Major.Minor`; MCP speaks dates. Nothing here assumes either.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct MajorMinor(u8, u8);
impl std::fmt::Display for MajorMinor {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}.{}", self.0, self.1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct A2AProfile {
version: MajorMinor,
}
struct A2ALifecycle {
supported: Vec<MajorMinor>,
}
impl Lifecycle for A2ALifecycle {
type Profile = A2AProfile;
type Version = MajorMinor;
fn classify(
&self,
state: SessionState,
_profile: Option<Self::Profile>,
_method: &str,
) -> Classification {
// No handshake, so nothing is ever a handshake — and the engine
// does not need to be told what "initialize" means.
let _ = state;
Classification::Ordinary
}
fn resolve(&self, declaration: Declaration<'_>) -> Result<Resolution<Self::Profile, Self::Version>, DeclarationError> {
// A2A negotiates out of its own metadata rather than a version
// string, so both arms are read but neither is the version. That
// difference is the point: the engine never supplies it.
match declaration {
Declaration::Stateless { .. } | Declaration::Handshake { .. } => {}
}
let version = self.supported.first().copied().ok_or(DeclarationError::NoVersion)?;
Ok(Resolution {
profile: A2AProfile { version },
version,
echoed: true,
})
}
}
let lifecycle = A2ALifecycle { supported: vec![MajorMinor(0, 3)] };
let resolution = lifecycle
.resolve(Declaration::Stateless { meta: MetaView::default() })
.expect("a supported version is answered");
assert_eq!(resolution.version, MajorMinor(0, 3));
assert_eq!(
lifecycle.classify(SessionState::Stateless, Some(resolution.profile), "message/send"),
Classification::Ordinary,
);Required Associated Types§
Required Methods§
Sourcefn classify(
&self,
state: SessionState,
profile: Option<Self::Profile>,
method: &str,
) -> Classification
fn classify( &self, state: SessionState, profile: Option<Self::Profile>, method: &str, ) -> Classification
What a message naming method means for a session in state.
profile is None before anything has been agreed. A protocol that
gates a method on a feature has to say so here rather than by
inspecting a version, because the version is not this engine’s to
interpret.
The engine refuses a message in a closing or closed session whatever this answers, so a protocol does not have to repeat that check.
Sourcefn resolve(
&self,
declaration: Declaration<'_>,
) -> Result<Resolution<Self::Profile, Self::Version>, DeclarationError>
fn resolve( &self, declaration: Declaration<'_>, ) -> Result<Resolution<Self::Profile, Self::Version>, DeclarationError>
Reads what a peer declared and answers with what this side agrees to.
§Errors
DeclarationError when the declaration is not one this protocol can
turn into a profile. Never for an unsupported but well-formed version:
answering with something else is the specified behaviour, and an error
there would stop a server working the day a revision is published.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".