turul-a2a-patterns 0.2.3

Reusable A2A skill-authoring patterns: SKILL.md manifest, SkillHandler / SkillRegistry / SkillProgressSink / TerminalHook.
Documentation
//! Progress sink trait used by handlers to emit non-terminal status updates
//! and streaming artifacts. Terminal task states remain framework-owned and
//! are deliberately not constructible through this surface.

use async_trait::async_trait;

use turul_a2a_types::{Artifact, Message};

use crate::error::SinkError;

/// Non-terminal task states a skill may emit during execution.
/// Terminal states are framework-owned; not constructible here.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgressState {
    Working,
    InputRequired,
    AuthRequired,
}

/// Object-safe progress emission trait used by `SkillHandler::run`.
///
/// Implemented with `#[async_trait]` so adopters write standard
/// `async fn` rather than hand-rolling `Pin<Box<dyn Future>>`. The
/// macro produces an object-safe trait, verified by the const-fn
/// assertion at the bottom of this module.
#[async_trait]
pub trait SkillProgressSink: Send + Sync {
    async fn set_status(
        &self,
        state: ProgressState,
        message: Option<Message>,
    ) -> Result<(), SinkError>;

    async fn emit_artifact(
        &self,
        artifact: Artifact,
        append: bool,
        last_chunk: bool,
    ) -> Result<(), SinkError>;

    fn is_closed(&self) -> bool {
        false
    }
}

// Compile-time object-safety assertion: `SkillProgressSink` must remain
// usable behind `dyn`.
const _: fn() = || {
    fn assert<T: ?Sized + SkillProgressSink>() {}
    assert::<dyn SkillProgressSink>();
};

#[cfg(test)]
mod tests {
    use super::ProgressState;

    /// `ProgressState` carries exactly the non-terminal states. Inside the
    /// defining crate `#[non_exhaustive]` does not force a wildcard arm,
    /// so this match has none: adding any variant — in particular a
    /// terminal state (Completed / Failed / Canceled / Rejected) — fails
    /// to compile here until the binding contract is deliberately
    /// amended.
    #[test]
    fn progress_state_variant_set_is_fixed_and_non_terminal() {
        fn label(state: ProgressState) -> &'static str {
            match state {
                ProgressState::Working => "working",
                ProgressState::InputRequired => "input-required",
                ProgressState::AuthRequired => "auth-required",
            }
        }
        assert_eq!(
            [
                ProgressState::Working,
                ProgressState::InputRequired,
                ProgressState::AuthRequired,
            ]
            .map(label),
            ["working", "input-required", "auth-required"]
        );
    }
}