use akribes_types::event as core_event;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ValidationErrorWire {
pub stage: String,
pub message: String,
pub path: Option<String>,
}
impl From<core_event::ValidationErrorWire> for ValidationErrorWire {
fn from(v: core_event::ValidationErrorWire) -> Self {
Self {
stage: v.stage,
message: v.message,
path: v.path,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct UnableRecord {
pub reason: String,
#[serde(default)]
pub missing: Vec<String>,
pub category: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind")]
pub enum SuspendTrigger {
DagPosition,
ValidationExhausted {
task_name: String,
retry_count: u32,
last_attempt: String,
validation_errors: Vec<ValidationErrorWire>,
},
AgentUnable {
task_name: String,
unable: UnableRecord,
},
AgentVariant {
task_name: String,
variant: String,
payload: serde_json::Value,
},
#[serde(other)]
Unknown,
}
impl Default for SuspendTrigger {
fn default() -> Self {
SuspendTrigger::DagPosition
}
}
impl From<core_event::SuspendTrigger> for SuspendTrigger {
fn from(t: core_event::SuspendTrigger) -> Self {
match t {
core_event::SuspendTrigger::DagPosition => SuspendTrigger::DagPosition,
core_event::SuspendTrigger::ValidationExhausted {
task_name,
retry_count,
last_attempt,
validation_errors,
} => SuspendTrigger::ValidationExhausted {
task_name,
retry_count,
last_attempt,
validation_errors: validation_errors.into_iter().map(Into::into).collect(),
},
core_event::SuspendTrigger::AgentUnable { task_name, unable } => {
SuspendTrigger::AgentUnable {
task_name,
unable: UnableRecord {
reason: unable.reason,
missing: unable.missing,
category: unable.category.as_wire_str().to_string(),
},
}
}
core_event::SuspendTrigger::AgentVariant {
task_name,
variant,
payload,
} => SuspendTrigger::AgentVariant {
task_name,
variant,
payload,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn dag_position_roundtrips_byte_identical() {
let wire = r#"{"kind":"DagPosition"}"#;
let parsed: SuspendTrigger = serde_json::from_str(wire).unwrap();
assert!(matches!(parsed, SuspendTrigger::DagPosition));
let reserialized = serde_json::to_string(&parsed).unwrap();
assert_eq!(reserialized, wire);
}
#[test]
fn validation_exhausted_roundtrips_byte_identical() {
let wire = r#"{"kind":"ValidationExhausted","task_name":"decompose_claims","retry_count":3,"last_attempt":"{\"bad\":true}","validation_errors":[{"stage":"schema","message":"required property \"number\" missing","path":"/0"}]}"#;
let parsed: SuspendTrigger = serde_json::from_str(wire).unwrap();
match &parsed {
SuspendTrigger::ValidationExhausted {
task_name,
retry_count,
last_attempt,
validation_errors,
} => {
assert_eq!(task_name, "decompose_claims");
assert_eq!(*retry_count, 3);
assert_eq!(last_attempt, r#"{"bad":true}"#);
assert_eq!(validation_errors.len(), 1);
assert_eq!(validation_errors[0].stage, "schema");
assert_eq!(validation_errors[0].path.as_deref(), Some("/0"));
}
other => panic!("expected ValidationExhausted, got {other:?}"),
}
let reserialized = serde_json::to_string(&parsed).unwrap();
assert_eq!(reserialized, wire);
}
#[test]
fn agent_unable_roundtrips_byte_identical() {
let wire = r#"{"kind":"AgentUnable","task_name":"escalate","unable":{"reason":"image too blurry to OCR","missing":["claim_text"],"category":"input_ambiguous"}}"#;
let parsed: SuspendTrigger = serde_json::from_str(wire).unwrap();
match &parsed {
SuspendTrigger::AgentUnable { task_name, unable } => {
assert_eq!(task_name, "escalate");
assert_eq!(unable.reason, "image too blurry to OCR");
assert_eq!(unable.missing, vec!["claim_text".to_string()]);
assert_eq!(unable.category, "input_ambiguous");
}
other => panic!("expected AgentUnable, got {other:?}"),
}
let reserialized = serde_json::to_string(&parsed).unwrap();
assert_eq!(reserialized, wire);
}
#[test]
fn agent_unable_accepts_missing_field_default() {
let wire = json!({
"kind": "AgentUnable",
"task_name": "t",
"unable": { "reason": "x", "category": "other" },
});
let parsed: SuspendTrigger = serde_json::from_value(wire).unwrap();
match parsed {
SuspendTrigger::AgentUnable { unable, .. } => {
assert!(unable.missing.is_empty());
}
other => panic!("expected AgentUnable, got {other:?}"),
}
}
#[test]
fn unknown_kind_deserializes_to_unknown_variant() {
let wire = json!({
"kind": "SomeFutureVariant",
"extra_field": 42,
});
let parsed: SuspendTrigger = serde_json::from_value(wire).unwrap();
assert!(matches!(parsed, SuspendTrigger::Unknown));
}
#[test]
fn unknown_kind_with_no_extra_fields_still_parses() {
let parsed: SuspendTrigger = serde_json::from_str(r#"{"kind":"Nope"}"#).unwrap();
assert!(matches!(parsed, SuspendTrigger::Unknown));
}
#[test]
fn converts_from_core_dag_position() {
let core = core_event::SuspendTrigger::DagPosition;
let sdk: SuspendTrigger = core.into();
assert!(matches!(sdk, SuspendTrigger::DagPosition));
}
#[test]
fn converts_from_core_validation_exhausted() {
let core = core_event::SuspendTrigger::ValidationExhausted {
task_name: "t".into(),
retry_count: 2,
last_attempt: "{}".into(),
validation_errors: vec![core_event::ValidationErrorWire {
stage: "parse".into(),
message: "bad json".into(),
path: None,
}],
};
let sdk: SuspendTrigger = core.into();
match sdk {
SuspendTrigger::ValidationExhausted {
task_name,
retry_count,
validation_errors,
..
} => {
assert_eq!(task_name, "t");
assert_eq!(retry_count, 2);
assert_eq!(validation_errors[0].stage, "parse");
}
other => panic!("expected ValidationExhausted, got {other:?}"),
}
}
#[test]
fn converts_from_core_agent_unable() {
let core = core_event::SuspendTrigger::AgentUnable {
task_name: "escalate".into(),
unable: akribes_types::value::UnableRecord {
reason: "blurry".into(),
missing: vec!["claim_text".into()],
category: akribes_types::value::UnableCategory::InputAmbiguous,
},
};
let sdk: SuspendTrigger = core.into();
match sdk {
SuspendTrigger::AgentUnable { task_name, unable } => {
assert_eq!(task_name, "escalate");
assert_eq!(unable.reason, "blurry");
assert_eq!(unable.category, "input_ambiguous");
assert_eq!(unable.missing, vec!["claim_text".to_string()]);
}
other => panic!("expected AgentUnable, got {other:?}"),
}
}
#[test]
fn default_is_dag_position() {
assert!(matches!(
SuspendTrigger::default(),
SuspendTrigger::DagPosition
));
}
}