protocol-droid 0.5.1

A Model Context Protocol toolkit that speaks every published spec revision, with one canonical IR behind many per-revision codecs.
Documentation
//! `A2aFeatureProfile` — the per-revision capability bitset, derived
//! from the same four inputs as the MCP profile.
//!
//! # Why a separate profile type from MCP
//!
//! MCP's `FeatureProfile` and A2A's `A2aFeatureProfile` are
//! independent types because the features they enumerate are
//! independent. A bridge that translates MCP↔A2A maps the *union*
//! of the two — the bridge's domain code carries both profiles
//! side-by-side; it does not unify them into one bitset.
//!
//! # What this is not
//!
//! This is not the place to enumerate every A2A feature. The
//! enumeration in `Feature` below is the v1.0 minimum: the
//! methods the spec lists, the streaming shape, and the
//! push-notification delivery model. A future revision that adds
//! a new method gains a new variant and a row in the matrix; the
//! type stays the same.

use crate::domain::a2a::version::A2aVersion;

/// A capability an A2A peer can negotiate. Each variant has a
/// row in `tests::feature_matrix_golden` that names the
/// A2A spec section that introduced it.
///
/// # Why a `Copy` newtype for the matrix
///
/// The matrix is a `[[bool; N_FEATURES]; N_VERSIONS]` — a
/// per-revision row, indexed by `Feature::ALL`. The `ALL`
/// array is the source of truth for the column count, so a new
/// variant appended to the enum is the only edit the matrix
/// needs to pick it up. The `Copy` derive keeps the
/// `Feature::ALL` array a stack-local constant.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Feature {
    /// `SendMessage` — the unary message-send method.
    SendMessage,
    /// `SendStreamingMessage` — the streaming counterpart of
    /// `SendMessage`, server-sent events on a Streamable HTTP
    /// POST.
    SendStreamingMessage,
    /// `GetTask` — fetch a task by id.
    GetTask,
    /// `ListTasks` — page through the tasks the agent owns.
    ListTasks,
    /// `CancelTask` — request cancellation of an in-flight task.
    CancelTask,
    /// `SubscribeToTask` — long-poll or SSE-style subscription to
    /// task updates.
    SubscribeToTask,
    /// Push-notification config CRUD — the four `Create*`,
    /// `Get*`, `List*`, `Delete*` methods.
    PushNotificationConfig,
    /// `GetExtendedAgentCard` — fetch the authenticated
    /// `AgentCard` once the peer has proven its identity.
    GetExtendedAgentCard,
}

impl Feature {
    /// Every variant, in declaration order. The matrix's
    /// column order is this order; a variant appended here
    /// becomes a new column without a separate edit.
    pub const ALL: [Self; 8] = [
        Self::SendMessage,
        Self::SendStreamingMessage,
        Self::GetTask,
        Self::ListTasks,
        Self::CancelTask,
        Self::SubscribeToTask,
        Self::PushNotificationConfig,
        Self::GetExtendedAgentCard,
    ];

    /// The human-readable label used in the matrix snapshot and
    /// in the `Display` form. Kept here rather than derived so a
    /// future variant that needs a different label (e.g. for
    /// `camelCase` ↔ `snake_case`) can hold one without changing
    /// the matrix's column header.
    pub const fn label(&self) -> &'static str {
        match self {
            Self::SendMessage => "SendMessage",
            Self::SendStreamingMessage => "SendStreamingMessage",
            Self::GetTask => "GetTask",
            Self::ListTasks => "ListTasks",
            Self::CancelTask => "CancelTask",
            Self::SubscribeToTask => "SubscribeToTask",
            Self::PushNotificationConfig => "PushNotificationConfig",
            Self::GetExtendedAgentCard => "GetExtendedAgentCard",
        }
    }
}

/// The per-revision capability bitset for an A2A peer.
///
/// The matrix is small (one row per known version, one column
/// per `Feature::ALL`); the type holds it as a fixed-size
/// `[bool; N_FEATURES]` so the matrix lives in the type
/// system rather than in a `HashMap` that the feature set could
/// silently diverge from.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct A2aFeatureProfile {
    /// The version this profile was derived for.
    version: A2aVersion,
    /// One bit per `Feature::ALL`, in the same order.
    bitset: [bool; 8],
}

impl A2aFeatureProfile {
    /// Build the profile for a known version. The matrix is the
    /// authoritative source; a per-version row is a literal
    /// here. A future revision adds a row, not a column.
    pub const fn for_known_version(version: A2aVersion) -> Self {
        match version {
            A2aVersion::V1_0 => Self {
                version,
                // Every v1.0 method is supported. The streaming
                // shape is gated by transport, not by version
                // — the profile here is the *capability*, the
                // transport is consulted separately.
                bitset: [true; 8],
            },
            A2aVersion::Unknown { major: _, minor: _ } => Self {
                version,
                // An unknown version is a peer the matrix has
                // not been updated for. The conservative default
                // is "no features" so a feature-specific code
                // path consults the profile and falls through to
                // the typed error, not to a silent
                // success-on-typo. The peer that names a
                // `VersionNotSupportedError` upgrade is the
                // upstream-correct path.
                bitset: [false; 8],
            },
        }
    }

    /// `true` if the named feature is enabled. The match is on
    /// a fixed-size array indexed by the feature's position in
    /// `Feature::ALL`; a new variant appended to `ALL` is the
    /// only edit the index needs to pick up.
    pub const fn is_enabled(&self, feature: Feature) -> bool {
        // `feature_index` returns a value in `0..Feature::ALL.len()`,
        // and `self.bitset` is sized to that constant. The match in
        // `feature_index` is exhaustive on `Feature`, so a future
        // variant added to the enum without an index entry is a
        // compile error rather than a runtime out-of-bounds.
        match feature {
            Feature::SendMessage => self.bitset[0],
            Feature::SendStreamingMessage => self.bitset[1],
            Feature::GetTask => self.bitset[2],
            Feature::ListTasks => self.bitset[3],
            Feature::CancelTask => self.bitset[4],
            Feature::SubscribeToTask => self.bitset[5],
            Feature::PushNotificationConfig => self.bitset[6],
            Feature::GetExtendedAgentCard => self.bitset[7],
        }
    }

    /// The version this profile was derived for. The
    /// dispatcher's `VersionNotSupportedError` includes this so
    /// a peer that did not negotiate sees which version it
    /// landed on.
    pub const fn version(&self) -> A2aVersion {
        self.version
    }
}

/// Compile-time feature match. The match in
/// `A2aFeatureProfile::is_enabled` is exhaustive on every
/// variant of `Feature`, so a future variant added to the
/// enum without an entry is a compile error rather than a
/// runtime out-of-bounds.
const fn _ensure_feature_match_is_exhaustive(feature: Feature) -> usize {
    match feature {
        Feature::SendMessage => 0,
        Feature::SendStreamingMessage => 1,
        Feature::GetTask => 2,
        Feature::ListTasks => 3,
        Feature::CancelTask => 4,
        Feature::SubscribeToTask => 5,
        Feature::PushNotificationConfig => 6,
        Feature::GetExtendedAgentCard => 7,
    }
}

#[cfg(test)]
mod tests {
    use super::{A2aFeatureProfile, Feature};
    use crate::domain::a2a::version::A2aVersion;

    /// `Feature::ALL` is the source of
    /// truth for the matrix column
    /// count and ordering. A new
    /// variant appended to the enum
    /// must also be appended to `ALL`
    /// for the matrix to pick it up;
    /// the test pins the current
    /// shape (eight features, in the
    /// order the spec introduces
    /// them).
    #[test]
    fn feature_all_lists_eight_features_in_order() {
        assert_eq!(Feature::ALL.len(), 8);
        assert_eq!(Feature::ALL[0], Feature::SendMessage);
        assert_eq!(Feature::ALL[1], Feature::SendStreamingMessage);
        assert_eq!(Feature::ALL[2], Feature::GetTask);
        assert_eq!(Feature::ALL[3], Feature::ListTasks);
        assert_eq!(Feature::ALL[4], Feature::CancelTask);
        assert_eq!(Feature::ALL[5], Feature::SubscribeToTask);
        assert_eq!(Feature::ALL[6], Feature::PushNotificationConfig);
        assert_eq!(Feature::ALL[7], Feature::GetExtendedAgentCard);
    }

    /// `label()` returns a non-empty
    /// string per variant. The label
    /// is the column header in the
    /// matrix snapshot and the
    /// `Display` form for diagnostics;
    /// a regression that returned the
    /// empty string would render an
    /// empty cell.
    #[test]
    fn feature_label_is_non_empty_for_every_variant() {
        for feature in Feature::ALL {
            assert!(
                !feature.label().is_empty(),
                "{feature:?} has an empty label"
            );
        }
    }

    /// `A2aFeatureProfile::for_known_version(V1_0)`
    /// enables every feature. The
    /// v1.0 row in the matrix is the
    /// all-`true` row, and a future
    /// revision that adds a feature
    /// will add a column with the
    /// right value for v1.0.
    #[test]
    fn v1_0_profile_enables_every_feature() {
        let profile = A2aFeatureProfile::for_known_version(A2aVersion::V1_0);
        for feature in Feature::ALL {
            assert!(
                profile.is_enabled(feature),
                "{feature:?} should be enabled in V1_0"
            );
        }
    }

    /// `A2aFeatureProfile::for_known_version(Unknown)`
    /// disables every feature. An
    /// unknown version is a peer the
    /// matrix has not been updated
    /// for; the conservative default
    /// is "no features" so a
    /// feature-specific code path is
    /// never silently enabled for a
    /// version the consumer has not
    /// audited. A regression that
    /// enabled features for an
    /// `Unknown` would ship a wire
    /// shape the build cannot encode.
    #[test]
    fn unknown_version_profile_disables_every_feature() {
        let profile =
            A2aFeatureProfile::for_known_version(A2aVersion::Unknown { major: 2, minor: 5 });
        for feature in Feature::ALL {
            assert!(
                !profile.is_enabled(feature),
                "{feature:?} should be disabled for an Unknown version"
            );
        }
    }

    /// `is_enabled` returns the
    /// bitset value at the feature's
    /// index in `Feature::ALL`. A
    /// regression that returned the
    /// wrong bit (e.g. `SendMessage`
    /// reporting the value of
    /// `GetTask`) would silently
    /// route requests the build
    /// cannot serve.
    #[test]
    fn is_enabled_returns_the_bit_at_the_feature_index() {
        let profile = A2aFeatureProfile::for_known_version(A2aVersion::V1_0);
        // The V1_0 row is all `true`,
        // so a spot check that every
        // feature reports enabled
        // catches an index mix-up.
        assert!(profile.is_enabled(Feature::SendMessage));
        assert!(profile.is_enabled(Feature::GetExtendedAgentCard));
        // An unknown version is all
        // `false`; the same check
        // catches a constant-return
        // regression.
        let unknown =
            A2aFeatureProfile::for_known_version(A2aVersion::Unknown { major: 2, minor: 0 });
        assert!(!unknown.is_enabled(Feature::SendMessage));
        assert!(!unknown.is_enabled(Feature::GetExtendedAgentCard));
    }

    /// `version()` returns the
    /// version the profile was
    /// derived for. The
    /// dispatcher's
    /// `VersionNotSupportedError`
    /// includes this, so a peer that
    /// did not negotiate sees which
    /// version it landed on. A
    /// regression that returned
    /// `V1_0` for an `Unknown` would
    /// lie to the peer about what the
    /// server actually speaks.
    #[test]
    fn version_returns_the_profile_version() {
        let v1 = A2aFeatureProfile::for_known_version(A2aVersion::V1_0);
        assert_eq!(v1.version(), A2aVersion::V1_0);

        let unknown =
            A2aFeatureProfile::for_known_version(A2aVersion::Unknown { major: 2, minor: 5 });
        assert_eq!(
            unknown.version(),
            A2aVersion::Unknown { major: 2, minor: 5 }
        );
    }
}