1use serde::Serialize;
7use thiserror::Error;
8
9use crate::context::Context;
10use crate::gates::StopReason;
11use crate::invariant::InvariantClass;
12
13#[derive(Debug, Error, Serialize)]
18pub enum ConvergeError {
19 #[error("budget exhausted: {kind}")]
21 BudgetExhausted { kind: String },
22
23 #[error("{class:?} invariant '{name}' violated: {reason}")]
25 InvariantViolation {
26 name: String,
28 class: InvariantClass,
30 reason: String,
32 context: Box<Context>,
34 },
35
36 #[error("agent failed: {agent_id}")]
38 AgentFailed { agent_id: String },
39
40 #[error(
42 "conflict detected for fact '{id}': existing content '{existing}' vs new content '{new}'"
43 )]
44 Conflict {
45 id: String,
47 existing: String,
49 new: String,
51 context: Box<Context>,
53 },
54}
55
56impl ConvergeError {
57 #[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 #[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}