Skip to main content

turul_a2a_patterns/
sink.rs

1//! Progress sink trait used by handlers to emit non-terminal status updates
2//! and streaming artifacts. Terminal task states remain framework-owned and
3//! are deliberately not constructible through this surface.
4
5use async_trait::async_trait;
6
7use turul_a2a_types::{Artifact, Message};
8
9use crate::error::SinkError;
10
11/// Non-terminal task states a skill may emit during execution.
12/// Terminal states are framework-owned; not constructible here.
13#[non_exhaustive]
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ProgressState {
16    Working,
17    InputRequired,
18    AuthRequired,
19}
20
21/// Object-safe progress emission trait used by `SkillHandler::run`.
22///
23/// Implemented with `#[async_trait]` so adopters write standard
24/// `async fn` rather than hand-rolling `Pin<Box<dyn Future>>`. The
25/// macro produces an object-safe trait, verified by the const-fn
26/// assertion at the bottom of this module.
27#[async_trait]
28pub trait SkillProgressSink: Send + Sync {
29    async fn set_status(
30        &self,
31        state: ProgressState,
32        message: Option<Message>,
33    ) -> Result<(), SinkError>;
34
35    async fn emit_artifact(
36        &self,
37        artifact: Artifact,
38        append: bool,
39        last_chunk: bool,
40    ) -> Result<(), SinkError>;
41
42    fn is_closed(&self) -> bool {
43        false
44    }
45
46    /// Records that the skill is alive without emitting an event, so a
47    /// long silent step is not mistaken for a stalled executor. The
48    /// default does nothing; a sink backed by the server's event sink
49    /// delegates to its heartbeat.
50    fn heartbeat(&self) {}
51}
52
53// Compile-time object-safety assertion: `SkillProgressSink` must remain
54// usable behind `dyn`.
55const _: fn() = || {
56    fn assert<T: ?Sized + SkillProgressSink>() {}
57    assert::<dyn SkillProgressSink>();
58};
59
60#[cfg(test)]
61mod tests {
62    use super::ProgressState;
63
64    /// `ProgressState` carries exactly the non-terminal states. Inside the
65    /// defining crate `#[non_exhaustive]` does not force a wildcard arm,
66    /// so this match has none: adding any variant — in particular a
67    /// terminal state (Completed / Failed / Canceled / Rejected) — fails
68    /// to compile here until the binding contract is deliberately
69    /// amended.
70    #[test]
71    fn progress_state_variant_set_is_fixed_and_non_terminal() {
72        fn label(state: ProgressState) -> &'static str {
73            match state {
74                ProgressState::Working => "working",
75                ProgressState::InputRequired => "input-required",
76                ProgressState::AuthRequired => "auth-required",
77            }
78        }
79        assert_eq!(
80            [
81                ProgressState::Working,
82                ProgressState::InputRequired,
83                ProgressState::AuthRequired,
84            ]
85            .map(label),
86            ["working", "input-required", "auth-required"]
87        );
88    }
89}