Skip to main content

starweaver_runtime/output/
validation.rs

1use async_trait::async_trait;
2use thiserror::Error;
3
4use crate::run::AgentRunState;
5
6use super::{OutputSchema, OutputValue};
7
8/// Structured output validation error.
9#[derive(Debug, Error)]
10pub enum OutputValidationError {
11    /// Output could not be parsed as JSON.
12    #[error("output is not valid JSON: {0}")]
13    InvalidJson(String),
14    /// Output did not match the schema foundation checks.
15    #[error("output schema validation failed: {0}")]
16    Schema(String),
17    /// Custom validator rejected the output and requested a model retry.
18    #[error("output retry requested: {0}")]
19    Retry(String),
20    /// Custom validator failed the run.
21    #[error("output validation failed: {0}")]
22    Failed(String),
23}
24
25impl OutputValidationError {
26    /// Create a retry validation error.
27    #[must_use]
28    pub fn retry(message: impl Into<String>) -> Self {
29        Self::Retry(message.into())
30    }
31
32    /// Create a failed validation error.
33    #[must_use]
34    pub fn failed(message: impl Into<String>) -> Self {
35        Self::Failed(message.into())
36    }
37}
38
39/// Output validator result.
40pub type OutputValidationResult<T> = Result<T, OutputValidationError>;
41
42/// Validator for parsed structured output values.
43#[async_trait]
44pub trait OutputValidator: Send + Sync {
45    /// Validate a parsed output value.
46    ///
47    /// # Errors
48    ///
49    /// Returns an error when validation fails or should trigger a model retry.
50    async fn validate(
51        &self,
52        state: &mut AgentRunState,
53        output: &OutputValue,
54    ) -> OutputValidationResult<()>;
55}
56
57/// Function-backed output validator.
58pub struct FunctionOutputValidator<F> {
59    function: F,
60}
61
62impl<F> FunctionOutputValidator<F> {
63    /// Build a validator from a function.
64    #[must_use]
65    pub const fn new(function: F) -> Self {
66        Self { function }
67    }
68}
69
70#[async_trait]
71impl<F, Fut> OutputValidator for FunctionOutputValidator<F>
72where
73    F: Send + Sync + Fn(&mut AgentRunState, &OutputValue) -> Fut,
74    Fut: Send + std::future::Future<Output = OutputValidationResult<()>>,
75{
76    async fn validate(
77        &self,
78        state: &mut AgentRunState,
79        output: &OutputValue,
80    ) -> OutputValidationResult<()> {
81        (self.function)(state, output).await
82    }
83}
84
85/// Parse and validate raw text according to an optional output schema.
86///
87/// # Errors
88///
89/// Returns an error when parsing or foundation schema checks fail.
90pub fn parse_output(
91    raw_output: &str,
92    schema: Option<&OutputSchema>,
93) -> OutputValidationResult<OutputValue> {
94    match schema {
95        Some(schema) => {
96            let value = serde_json::from_str(raw_output)
97                .map_err(|error| OutputValidationError::InvalidJson(error.to_string()))?;
98            validate_json_value(&value, schema)?;
99            Ok(OutputValue::Json(value))
100        }
101        None => Ok(OutputValue::Text(raw_output.to_string())),
102    }
103}
104
105fn validate_json_value(
106    value: &serde_json::Value,
107    schema: &OutputSchema,
108) -> OutputValidationResult<()> {
109    let validator = jsonschema::validator_for(&schema.schema).map_err(|error| {
110        OutputValidationError::Schema(format!("invalid output schema {}: {error}", schema.name))
111    })?;
112    validator.validate(value).map_err(|error| {
113        let path = error.instance_path.as_str();
114        let detail = error.masked().to_string();
115        if path.is_empty() {
116            OutputValidationError::Schema(detail)
117        } else {
118            OutputValidationError::Schema(format!("{path}: {detail}"))
119        }
120    })
121}