weavegraph 0.7.0

Graph-driven, concurrent agent workflow framework with versioned state, deterministic barrier merges, and rich diagnostics.
Documentation
//! Error event types for structured error capture and propagation through the workflow.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::telemetry::{FormatterMode, PlainFormatter, TelemetryFormatter};

/// A workflow error event capturing scope, error payload, tags, and context.
///
/// Serializes to JSON with a tagged-union `scope` field:
///
/// ```json
/// {
///   "when": "2025-11-02T10:30:00Z",
///   "scope": { "scope": "node", "kind": "Parser", "step": 1 },
///   "error": {
///     "message": "Failed to parse input",
///     "cause": { "message": "Invalid JSON syntax", "details": {"line": 3} }
///   },
///   "tags": ["validation"],
///   "context": {"file": "/tmp/input.json"}
/// }
/// ```
///
/// Scope variants: `"node"` (kind, step), `"scheduler"` (step),
/// `"runner"` (session, step), `"app"`.
///
/// See `docs/schemas/error_event.json` for the full JSON Schema.
///
/// # Example
///
/// ```
/// use weavegraph::channels::errors::{ErrorEvent, WeaveError};
/// use serde_json::json;
///
/// let event = ErrorEvent::node("Parser", 1, WeaveError::msg("Parse error"))
///     .with_tag("validation")
///     .with_context(json!({"line": 42}));
///
/// let json_str = serde_json::to_string(&event).unwrap();
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct ErrorEvent {
    /// Timestamp at which the error occurred.
    #[serde(default = "chrono::Utc::now")]
    pub when: DateTime<Utc>,
    /// Where in the workflow the error originated.
    #[serde(default)]
    pub scope: ErrorScope,
    /// Structured error payload.
    #[serde(default)]
    pub error: WeaveError,
    /// String tags for filtering and categorization.
    #[serde(default)]
    pub tags: Vec<String>,
    /// Optional structured context metadata.
    #[serde(default)]
    pub context: serde_json::Value,
}

impl ErrorEvent {
    fn with_scope(scope: ErrorScope, error: WeaveError) -> Self {
        Self {
            when: Utc::now(),
            scope,
            error,
            tags: Vec::new(),
            context: serde_json::Value::Null,
        }
    }

    /// Creates a node-scoped error event.
    pub fn node<S: Into<String>>(kind: S, step: u64, error: WeaveError) -> Self {
        Self::with_scope(
            ErrorScope::Node {
                kind: kind.into(),
                step,
            },
            error,
        )
    }

    /// Creates a scheduler-scoped error event.
    pub fn scheduler(step: u64, error: WeaveError) -> Self {
        Self::with_scope(ErrorScope::Scheduler { step }, error)
    }

    /// Creates a runner-scoped error event.
    pub fn runner<S: Into<String>>(session: S, step: u64, error: WeaveError) -> Self {
        Self::with_scope(
            ErrorScope::Runner {
                session: session.into(),
                step,
            },
            error,
        )
    }

    /// Creates an app-scoped error event.
    pub fn app(error: WeaveError) -> Self {
        Self::with_scope(ErrorScope::App, error)
    }

    /// Replaces the tag list.
    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
        self.tags = tags;
        self
    }

    /// Appends a single tag.
    pub fn with_tag<S: Into<String>>(mut self, tag: S) -> Self {
        self.tags.push(tag.into());
        self
    }

    /// Attaches context metadata.
    pub fn with_context(mut self, context: serde_json::Value) -> Self {
        self.context = context;
        self
    }
}

/// Where an [`ErrorEvent`] originated in the workflow.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(tag = "scope", rename_all = "snake_case")]
pub enum ErrorScope {
    /// Error occurred in a node execution.
    Node {
        /// Node kind identifier.
        kind: String,
        /// Step number at which the error occurred.
        step: u64,
    },
    /// Error occurred in the scheduler.
    Scheduler {
        /// Step number at which the error occurred.
        step: u64,
    },
    /// Error occurred in the runner.
    Runner {
        /// Session identifier.
        session: String,
        /// Step number at which the error occurred.
        step: u64,
    },
    /// Error occurred at the application level (default).
    #[default]
    App,
}

/// Structured error payload for an [`ErrorEvent`].
///
/// Supports nested cause chains and optional machine-readable details.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct WeaveError {
    /// Human-readable error message.
    pub message: String,
    /// Nested cause for error chaining.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cause: Option<Box<WeaveError>>,
    /// Optional structured metadata.
    #[serde(default)]
    pub details: serde_json::Value,
}

impl std::fmt::Display for WeaveError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.message)
    }
}

impl std::error::Error for WeaveError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.cause.as_ref().map(|c| c as &dyn std::error::Error)
    }
}

impl WeaveError {
    /// Constructs an error from a message.
    pub fn msg<M: Into<String>>(m: M) -> Self {
        Self {
            message: m.into(),
            ..Default::default()
        }
    }

    /// Attaches structured details.
    pub fn with_details(mut self, details: serde_json::Value) -> Self {
        self.details = details;
        self
    }

    /// Attaches a nested cause.
    pub fn with_cause(mut self, cause: WeaveError) -> Self {
        self.cause = Some(Box::new(cause));
        self
    }
}

/// Formats error events with explicit color mode control.
///
/// - [`FormatterMode::Auto`]: auto-detects TTY capability on stderr
/// - [`FormatterMode::Colored`]: always emits ANSI color codes
/// - [`FormatterMode::Plain`]: never emits ANSI color codes
///
/// # Example
///
/// ```
/// use weavegraph::channels::errors::{ErrorEvent, WeaveError, pretty_print_with_mode};
/// use weavegraph::telemetry::FormatterMode;
///
/// let events = vec![ErrorEvent::node("parser", 1, WeaveError::msg("Parse failed"))];
///
/// let plain = pretty_print_with_mode(&events, FormatterMode::Plain);
/// assert!(!plain.contains("\x1b["));
/// ```
pub fn pretty_print_with_mode(events: &[ErrorEvent], mode: FormatterMode) -> String {
    PlainFormatter::with_mode(mode)
        .render_errors(events)
        .into_iter()
        .map(|r| r.join_lines())
        .collect::<Vec<_>>()
        .join("\n")
}

/// Formats error events with auto-detected color support.
///
/// Colors are enabled when stderr is a TTY. For explicit control, use [`pretty_print_with_mode`].
///
/// # Example
///
/// ```
/// use weavegraph::channels::errors::{ErrorEvent, WeaveError, pretty_print};
///
/// let events = vec![ErrorEvent::node("parser", 1, WeaveError::msg("Parse failed"))];
/// let output = pretty_print(&events);
/// ```
pub fn pretty_print(events: &[ErrorEvent]) -> String {
    pretty_print_with_mode(events, FormatterMode::Auto)
}