Skip to main content

akribes_sdk/
task_end.rs

1//! SDK-facing mirror of `akribes_types::event::TaskEndVariant`.
2//!
3//! `akribes-core` is the source of truth for the `TaskEnd` event wire shape. It
4//! defines [`akribes_types::event::TaskEndVariant`] with two concrete variants
5//! today (`Success`, `Unable`) — #205 is slated to add `Partial`. We re-mirror
6//! the shape at the SDK layer so the SDK stays forward-compatible without a
7//! new release when a future akribes-core adds a variant:
8//!
9//! * The SDK mirror carries an [`Unknown`] catch-all via `#[serde(other)]`.
10//!   A future discriminant deserializes to `Unknown` rather than erroring,
11//!   so the `RunStream` keeps yielding instead of dying on the new event.
12//! * `#[serde(default)]`-friendly [`Default`] returns `Success`, matching
13//!   the akribes-core default. This handles old server streams that emit
14//!   `TaskEnd` without the `variant` field at all (pre-#206 shape).
15//!
16//! The conversion from [`akribes_types::event::TaskEndVariant`] is a total
17//! mapping today (every core variant has an SDK variant). As core adds new
18//! arms, new SDK arms should be added in lock-step, with the `Unknown`
19//! catch-all absorbing anything the SDK release hasn't caught up to on
20//! streams from a newer server.
21//!
22//! [`Unknown`]: TaskEndVariant::Unknown
23
24use akribes_types::event as core_event;
25use serde::{Deserialize, Serialize};
26
27/// How a task finished. Wire-compatible with [`akribes_types::event::TaskEndVariant`]
28/// — a plain `snake_case` string on the wire (`"success"`, `"unable"`, ...).
29/// The `#[serde(other)]` arm is the forward-compat contract at the SDK
30/// boundary: future akribes-core arms (e.g. `Partial` from #205) surface as
31/// [`Unknown`](Self::Unknown) until the SDK is updated, and the stream
32/// keeps flowing.
33#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
34#[serde(rename_all = "snake_case")]
35#[derive(Default)]
36pub enum TaskEndVariant {
37    /// Task produced a well-typed value that passed the full validation
38    /// pipeline. Wire default when `variant` is absent (pre-#206).
39    #[default]
40    Success,
41    /// Task had a `T | Unable` return type and the agent emitted a canonical
42    /// `{"unable": ...}` envelope. The owning `TaskEnd.value` carries the
43    /// full `Value::Unable` payload.
44    Unable,
45    /// Task ended with a dispatch-level failure (provider error, sandbox
46    /// timeout, OOM kill, exhausted validation budget). The owning
47    /// `TaskEnd.value` is a `Value::FatalError`. Surfaced from the
48    /// runtime dispatch path. (PR #672.)
49    Failed,
50    /// Catch-all for discriminants the SDK doesn't recognize (a variant
51    /// added by a newer akribes-core). The raw `TaskEnd.value` is still
52    /// available — consumers that need strict handling should read the raw
53    /// [`akribes_types::event::EngineEvent::TaskEnd`] directly.
54    #[serde(other)]
55    Unknown,
56}
57
58impl From<core_event::TaskEndVariant> for TaskEndVariant {
59    fn from(v: core_event::TaskEndVariant) -> Self {
60        match v {
61            core_event::TaskEndVariant::Success => TaskEndVariant::Success,
62            core_event::TaskEndVariant::Unable => TaskEndVariant::Unable,
63            core_event::TaskEndVariant::Failed => TaskEndVariant::Failed,
64            // Mirror the Unknown-passthrough: a caller constructing a core
65            // `Unknown` (e.g. from upstream deserialisation) should surface
66            // as SDK-Unknown too.
67            core_event::TaskEndVariant::Unknown => TaskEndVariant::Unknown,
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use serde_json::json;
76
77    #[test]
78    fn success_roundtrips_byte_identical() {
79        let wire = r#""success""#;
80        let parsed: TaskEndVariant = serde_json::from_str(wire).unwrap();
81        assert_eq!(parsed, TaskEndVariant::Success);
82        let reserialized = serde_json::to_string(&parsed).unwrap();
83        assert_eq!(reserialized, wire);
84    }
85
86    #[test]
87    fn unable_roundtrips_byte_identical() {
88        let wire = r#""unable""#;
89        let parsed: TaskEndVariant = serde_json::from_str(wire).unwrap();
90        assert_eq!(parsed, TaskEndVariant::Unable);
91        let reserialized = serde_json::to_string(&parsed).unwrap();
92        assert_eq!(reserialized, wire);
93    }
94
95    #[test]
96    fn unknown_discriminant_deserializes_to_unknown() {
97        // Forward-compat: a newer akribes-core adds `"partial"` and the SDK
98        // must not crash — it forwards as `Unknown`.
99        let wire = json!("partial");
100        let parsed: TaskEndVariant = serde_json::from_value(wire).unwrap();
101        assert_eq!(parsed, TaskEndVariant::Unknown);
102    }
103
104    #[test]
105    fn default_is_success() {
106        assert_eq!(TaskEndVariant::default(), TaskEndVariant::Success);
107    }
108
109    #[test]
110    fn converts_from_core_success() {
111        let core = core_event::TaskEndVariant::Success;
112        let sdk: TaskEndVariant = core.into();
113        assert_eq!(sdk, TaskEndVariant::Success);
114    }
115
116    #[test]
117    fn converts_from_core_unable() {
118        let core = core_event::TaskEndVariant::Unable;
119        let sdk: TaskEndVariant = core.into();
120        assert_eq!(sdk, TaskEndVariant::Unable);
121    }
122
123    #[test]
124    fn converts_from_core_unknown() {
125        let core = core_event::TaskEndVariant::Unknown;
126        let sdk: TaskEndVariant = core.into();
127        assert_eq!(sdk, TaskEndVariant::Unknown);
128    }
129
130    #[test]
131    fn variants_are_exhaustive_known_set() {
132        // A safety net: every snake_case tag we advertise as "known" must
133        // parse to a non-Unknown arm. If a new variant is added to
134        // akribes-core and the SDK is updated (adding a new arm), extend this
135        // list and the `From<core>` match above in the same commit — this
136        // test is the mechanical reminder.
137        for known in ["success", "unable"] {
138            let wire = json!(known);
139            let parsed: TaskEndVariant = serde_json::from_value(wire).unwrap();
140            assert_ne!(
141                parsed,
142                TaskEndVariant::Unknown,
143                "known tag {known} surfaced as Unknown"
144            );
145        }
146    }
147}