Skip to main content

converge_core/
error.rs

1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! Error types for Converge.
5
6use serde::Serialize;
7use thiserror::Error;
8
9use crate::context::Context;
10use crate::gates::StopReason;
11use crate::invariant::InvariantClass;
12
13/// Top-level error type for Converge operations.
14///
15/// Note: Context is boxed in error variants to keep the error type small,
16/// as recommended by clippy. Access via `error.context()` method.
17#[derive(Debug, Error, Serialize)]
18pub enum ConvergeError {
19    /// Budget limit exceeded (cycles, facts, or time).
20    #[error("budget exhausted: {kind}")]
21    BudgetExhausted { kind: String },
22
23    /// An invariant was violated during execution.
24    #[error("{class:?} invariant '{name}' violated: {reason}")]
25    InvariantViolation {
26        /// Name of the violated invariant.
27        name: String,
28        /// Class of the invariant (Structural, Semantic, Acceptance).
29        class: InvariantClass,
30        /// Reason for the violation.
31        reason: String,
32        /// Final context state (including diagnostic facts). Boxed to reduce error size.
33        context: Box<Context>,
34    },
35
36    /// Suggestor execution failed.
37    #[error("agent failed: {agent_id}")]
38    AgentFailed { agent_id: String },
39
40    /// Conflicting facts detected for the same ID.
41    #[error(
42        "conflict detected for fact '{id}': existing content '{existing}' vs new content '{new}'"
43    )]
44    Conflict {
45        /// ID of the conflicting fact.
46        id: String,
47        /// Existing content.
48        existing: String,
49        /// New conflicting content.
50        new: String,
51        /// Final context state. Boxed to reduce error size.
52        context: Box<Context>,
53    },
54}
55
56impl ConvergeError {
57    /// Returns a reference to the context if this error variant carries one.
58    #[must_use]
59    pub fn context(&self) -> Option<&Context> {
60        match self {
61            Self::InvariantViolation { context, .. } | Self::Conflict { context, .. } => {
62                Some(context)
63            }
64            Self::BudgetExhausted { .. } | Self::AgentFailed { .. } => None,
65        }
66    }
67
68    /// Convert this error into a platform-level stop reason.
69    #[must_use]
70    pub fn stop_reason(&self) -> StopReason {
71        match self {
72            Self::BudgetExhausted { kind } => StopReason::Error {
73                message: format!("budget exhausted: {kind}"),
74                category: crate::gates::ErrorCategory::Resource,
75            },
76            Self::InvariantViolation {
77                name,
78                class,
79                reason,
80                ..
81            } => StopReason::invariant_violated(*class, name.clone(), reason.clone()),
82            Self::AgentFailed { agent_id } => StopReason::AgentRefused {
83                agent_id: agent_id.clone(),
84                reason: "agent execution failed".to_string(),
85            },
86            Self::Conflict {
87                id, existing, new, ..
88            } => StopReason::Error {
89                message: format!("conflict for fact '{id}': existing '{existing}' vs new '{new}'"),
90                category: crate::gates::ErrorCategory::Internal,
91            },
92        }
93    }
94}