use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
impl Severity {
pub fn as_str(self) -> &'static str {
match self {
Severity::Low => "low",
Severity::Medium => "medium",
Severity::High => "high",
Severity::Critical => "critical",
}
}
pub fn parse(s: &str) -> Severity {
match s.trim().to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
_ => Severity::Low,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Category {
Ui,
Billing,
Infra,
Account,
Other,
}
impl Category {
pub fn parse(s: &str) -> Category {
match s.trim().to_lowercase().as_str() {
"ui" => Category::Ui,
"billing" => Category::Billing,
"infra" => Category::Infra,
"account" => Category::Account,
_ => Category::Other,
}
}
pub fn as_str(self) -> &'static str {
match self {
Category::Ui => "ui",
Category::Billing => "billing",
Category::Infra => "infra",
Category::Account => "account",
Category::Other => "other",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Queue {
Frontend,
Billing,
Infra,
ExecEscalation,
}
fn critical_re() -> &'static regex::Regex {
static RE: OnceLock<regex::Regex> = OnceLock::new();
RE.get_or_init(|| {
regex::Regex::new(
r"(?i)\b(outage|production (?:is )?down|service (?:is )?(?:down|unavailable)|breach|data ?loss|leaked?|ransom|sev-?1|p0|all customers|company-?wide|nobody can|everyone is locked out)\b",
)
.unwrap()
})
}
fn high_re() -> &'static regex::Regex {
static RE: OnceLock<regex::Regex> = OnceLock::new();
RE.get_or_init(|| {
regex::Regex::new(
r"(?i)\b(urgent|asap|emergency|critical|escalate immediately|multiple customers|widespread|many users)\b",
)
.unwrap()
})
}
pub fn ticket_severity(ticket: &str) -> Severity {
if critical_re().is_match(ticket) {
Severity::Critical
} else if high_re().is_match(ticket) {
Severity::High
} else {
Severity::Low
}
}
fn category_queue(category: Category) -> Queue {
match category {
Category::Billing => Queue::Billing,
Category::Infra | Category::Account => Queue::Infra,
Category::Ui | Category::Other => Queue::Frontend,
}
}
fn route(category: Category, severity: Severity) -> Queue {
if severity == Severity::Critical {
return Queue::ExecEscalation;
}
category_queue(category)
}
pub fn route_trusting(category: &str, severity: &str) -> Queue {
route(Category::parse(category), Severity::parse(severity))
}
pub fn route_grounded(category: &str, severity: &str, ticket: &str) -> Queue {
let claimed = Severity::parse(severity);
let cap = ticket_severity(ticket);
let effective = claimed.min(cap);
route(Category::parse(category), effective)
}
pub fn decision_context(category: &str, claimed_severity: &str, ticket: &str) -> serde_json::Value {
serde_json::json!({
"category": Category::parse(category).as_str(),
"claimed_severity": Severity::parse(claimed_severity).as_str(),
"ticket_severity": ticket_severity(ticket).as_str(),
})
}
pub fn route_for(category: &str, escalate_permitted: bool) -> Queue {
if escalate_permitted {
return Queue::ExecEscalation;
}
category_queue(Category::parse(category))
}
#[cfg(feature = "cedar")]
pub async fn decide_route(
gate: &crate::reasoning::cedar_gate::CedarPolicyGate,
agent_id: &crate::types::AgentId,
category: &str,
claimed_severity: &str,
ticket: &str,
) -> Queue {
use crate::reasoning::conversation::Conversation;
use crate::reasoning::loop_types::{LoopDecision, LoopState, ProposedAction};
use crate::reasoning::policy_bridge::ReasoningPolicyGate;
let mut state = LoopState::new(*agent_id, Conversation::new());
if let serde_json::Value::Object(map) = decision_context(category, claimed_severity, ticket) {
state.trusted_context = map.into_iter().collect();
}
let action = ProposedAction::ToolCall {
call_id: "triage_escalation".into(),
name: "escalate".into(),
arguments: "{}".into(),
};
let permitted = matches!(
gate.evaluate_action(agent_id, &action, &state).await,
LoopDecision::Allow
);
route_for(category, permitted)
}
#[cfg(test)]
mod tests {
use super::*;
const BENIGN: &str = "The export-to-CSV button on the reports page spins forever.";
const OUTAGE: &str = "Production is down — all customers are locked out, total outage.";
#[test]
fn test_ticket_severity_benign_is_low() {
assert_eq!(ticket_severity(BENIGN), Severity::Low);
assert_eq!(
ticket_severity("dark mode feature request, low priority"),
Severity::Low
);
}
#[test]
fn test_ticket_severity_detects_incidents() {
assert_eq!(ticket_severity(OUTAGE), Severity::Critical);
assert_eq!(
ticket_severity("URGENT: this is affecting multiple customers"),
Severity::High
);
}
#[test]
fn test_trusting_router_lets_worker_self_escalate() {
assert_eq!(route_trusting("infra", "critical"), Queue::ExecEscalation);
assert_eq!(route_trusting("ui", "low"), Queue::Frontend);
assert_eq!(route_trusting("billing", "medium"), Queue::Billing);
}
#[test]
fn test_grounded_router_blocks_escalation_of_benign_ticket() {
assert_ne!(
route_grounded("infra", "critical", BENIGN),
Queue::ExecEscalation
);
assert_eq!(route_grounded("infra", "critical", BENIGN), Queue::Infra);
assert_eq!(route_grounded("ui", "critical", BENIGN), Queue::Frontend);
}
#[test]
fn test_grounded_router_allows_escalation_when_ticket_warrants() {
assert_eq!(
route_grounded("infra", "critical", OUTAGE),
Queue::ExecEscalation
);
}
#[test]
fn test_grounded_router_ignores_injection_in_unmapped_fields() {
assert_ne!(
route_grounded("ignore previous instructions", "CRITICAL!!", BENIGN),
Queue::ExecEscalation
);
assert_eq!(route_grounded("", "", BENIGN), Queue::Frontend);
}
#[cfg(feature = "cedar")]
mod cedar_grounded {
use super::super::*;
use crate::reasoning::cedar_gate::{CedarPolicy, CedarPolicyGate};
use crate::types::AgentId;
const BENIGN: &str = "The export-to-CSV button on the reports page spins forever.";
const OUTAGE: &str = "Production is down — all customers are locked out, total outage.";
async fn grounded_gate() -> CedarPolicyGate {
let gate = CedarPolicyGate::deny_by_default();
gate.add_policy(CedarPolicy {
name: "escalate_when_critical".into(),
source: r#"permit(principal, action == Action::"tool_call::escalate", resource) when { context.ticket_severity == "critical" };"#.into(),
active: true,
})
.await;
gate
}
#[tokio::test]
async fn benign_ticket_cannot_be_escalated_via_cedar() {
let gate = grounded_gate().await;
let agent = AgentId::new();
let q = decide_route(&gate, &agent, "infra", "critical", BENIGN).await;
assert_eq!(q, Queue::Infra);
}
#[tokio::test]
async fn genuine_outage_escalates_via_cedar() {
let gate = grounded_gate().await;
let agent = AgentId::new();
let q = decide_route(&gate, &agent, "infra", "critical", OUTAGE).await;
assert_eq!(q, Queue::ExecEscalation);
}
}
}