use serde::{Deserialize, Serialize};
use crate::tool_annotations::SideEffectLevel;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCallStatus {
Pending,
InProgress,
Completed,
Failed,
}
impl ToolCallStatus {
pub const ALL: [Self; 4] = [
Self::Pending,
Self::InProgress,
Self::Completed,
Self::Failed,
];
pub fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::InProgress => "in_progress",
Self::Completed => "completed",
Self::Failed => "failed",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolMutationStatus {
Applied,
NotApplied,
Unknown,
}
impl ToolMutationStatus {
pub const ALL: [Self; 3] = [Self::Applied, Self::NotApplied, Self::Unknown];
pub fn as_str(self) -> &'static str {
match self {
Self::Applied => "applied",
Self::NotApplied => "not_applied",
Self::Unknown => "unknown",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCallErrorCategory {
SchemaValidation,
ToolError,
McpServerError,
HostBridgeError,
PermissionDenied,
RejectedLoop,
ParseAborted,
Timeout,
Network,
ResourceBusy,
Cancelled,
AbandonedAtLoopExit,
Environment,
Unknown,
}
impl ToolCallErrorCategory {
pub const ALL: [Self; 14] = [
Self::SchemaValidation,
Self::ToolError,
Self::McpServerError,
Self::HostBridgeError,
Self::PermissionDenied,
Self::RejectedLoop,
Self::ParseAborted,
Self::Timeout,
Self::Network,
Self::ResourceBusy,
Self::Cancelled,
Self::AbandonedAtLoopExit,
Self::Environment,
Self::Unknown,
];
pub fn is_recoverable(self) -> bool {
matches!(self, Self::SchemaValidation)
}
pub fn as_str(self) -> &'static str {
match self {
Self::SchemaValidation => "schema_validation",
Self::ToolError => "tool_error",
Self::McpServerError => "mcp_server_error",
Self::HostBridgeError => "host_bridge_error",
Self::PermissionDenied => "permission_denied",
Self::RejectedLoop => "rejected_loop",
Self::ParseAborted => "parse_aborted",
Self::Timeout => "timeout",
Self::Network => "network",
Self::ResourceBusy => "resource_busy",
Self::Cancelled => "cancelled",
Self::AbandonedAtLoopExit => "abandoned_at_loop_exit",
Self::Environment => "environment",
Self::Unknown => "unknown",
}
}
pub fn from_internal(category: &crate::value::ErrorCategory) -> Self {
use crate::value::ErrorCategory as Internal;
match category {
Internal::Timeout => Self::Timeout,
Internal::RateLimit
| Internal::Overloaded
| Internal::ServerError
| Internal::TransientNetwork => Self::Network,
Internal::ResourceBusy => Self::ResourceBusy,
Internal::SchemaValidation | Internal::SchemaStreamAborted => Self::SchemaValidation,
Internal::ToolError => Self::ToolError,
Internal::ToolRejected => Self::PermissionDenied,
Internal::Cancelled => Self::Cancelled,
Internal::Environment => Self::Environment,
Internal::EgressBlocked => Self::Environment,
Internal::Auth
| Internal::ChannelClosed
| Internal::NotFound
| Internal::CircuitOpen
| Internal::BudgetExceeded
| Internal::Internal
| Internal::Generic => Self::HostBridgeError,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DenialGate {
ToolCeiling,
MalformedToolWrapper,
CapabilityCeiling,
SideEffectCeiling,
ArgConstraint,
DynamicPermission,
ApprovalPolicy,
ApprovalUnavailable,
HostRejected,
HookDeny,
DeterministicPrecheck,
#[default]
Unknown,
}
impl DenialGate {
pub const ALL: [Self; 12] = [
Self::ToolCeiling,
Self::MalformedToolWrapper,
Self::CapabilityCeiling,
Self::SideEffectCeiling,
Self::ArgConstraint,
Self::DynamicPermission,
Self::ApprovalPolicy,
Self::ApprovalUnavailable,
Self::HostRejected,
Self::HookDeny,
Self::DeterministicPrecheck,
Self::Unknown,
];
pub fn as_str(self) -> &'static str {
match self {
Self::ToolCeiling => "tool_ceiling",
Self::MalformedToolWrapper => "malformed_tool_wrapper",
Self::CapabilityCeiling => "capability_ceiling",
Self::SideEffectCeiling => "side_effect_ceiling",
Self::ArgConstraint => "arg_constraint",
Self::DynamicPermission => "dynamic_permission",
Self::ApprovalPolicy => "approval_policy",
Self::ApprovalUnavailable => "approval_unavailable",
Self::HostRejected => "host_rejected",
Self::HookDeny => "hook_deny",
Self::DeterministicPrecheck => "deterministic_precheck",
Self::Unknown => "unknown",
}
}
fn reason_prefix(self) -> &'static str {
match self {
Self::ToolCeiling => "Tool ceiling denial",
Self::MalformedToolWrapper => "Malformed tool wrapper denial",
Self::CapabilityCeiling => "Capability ceiling denial",
Self::SideEffectCeiling => "Side-effect ceiling denial",
Self::ArgConstraint => "Tool argument constraint denial",
Self::DynamicPermission => "Dynamic permission denial",
Self::ApprovalPolicy => "Approval policy denial",
Self::ApprovalUnavailable => "Approval unavailable denial",
Self::HostRejected => "Host rejection denial",
Self::HookDeny => "Pre-tool hook denial",
Self::DeterministicPrecheck => "Deterministic precheck denial",
Self::Unknown => "Unclassified tool denial",
}
}
pub fn render_reason(self, particulars: impl Into<String>) -> String {
let particulars = particulars.into();
if self.owns_reason(&particulars) {
return particulars;
}
debug_assert!(
!Self::ALL
.iter()
.copied()
.any(|gate| gate.has_signature(&particulars)),
"{} denial particulars carry another gate signature: {particulars}",
self.as_str(),
);
let particulars = particulars.trim();
if particulars.is_empty() {
format!("{}: no further details were provided", self.reason_prefix())
} else {
format!("{}: {particulars}", self.reason_prefix())
}
}
fn has_signature(self, reason: &str) -> bool {
reason.contains(&format!("{}:", self.reason_prefix()))
}
pub(crate) fn owns_reason(self, reason: &str) -> bool {
reason.starts_with(&format!("{}:", self.reason_prefix()))
&& !Self::ALL
.iter()
.copied()
.any(|gate| gate != self && gate.has_signature(reason))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SideEffectCeilingRemedy {
RequestPermission,
RaiseSideEffectCeiling,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SideEffectCeilingDetails {
pub ceiling: SideEffectLevel,
pub required_level: SideEffectLevel,
pub tool: String,
pub remedy: SideEffectCeilingRemedy,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToolDenial {
pub gate: DenialGate,
pub capability: Option<String>,
pub denied_paths: Vec<String>,
pub retryable: bool,
pub reason: String,
pub denial_class: Option<String>,
pub class_repeat_count: Option<u64>,
pub side_effect_ceiling: Option<SideEffectCeilingDetails>,
pub machine_reason: Option<String>,
pub human_summary: Option<String>,
}
impl ToolDenial {
pub fn terminal(
gate: DenialGate,
capability: Option<String>,
particulars: impl Into<String>,
) -> Self {
Self {
gate,
capability,
denied_paths: Vec::new(),
retryable: false,
reason: gate.render_reason(particulars),
denial_class: None,
class_repeat_count: None,
side_effect_ceiling: None,
machine_reason: None,
human_summary: None,
}
}
pub fn retryable(
gate: DenialGate,
capability: Option<String>,
particulars: impl Into<String>,
) -> Self {
Self {
gate,
capability,
denied_paths: Vec::new(),
retryable: true,
reason: gate.render_reason(particulars),
denial_class: None,
class_repeat_count: None,
side_effect_ceiling: None,
machine_reason: None,
human_summary: None,
}
}
pub(crate) fn reclassify(
mut self,
gate: DenialGate,
capability: Option<String>,
retryable: bool,
particulars: impl Into<String>,
) -> Self {
self.gate = gate;
self.capability = capability;
self.retryable = retryable;
self.reason = gate.render_reason(particulars);
self.denial_class = None;
self.class_repeat_count = None;
self.side_effect_ceiling = None;
self.machine_reason = None;
self.human_summary = None;
self
}
pub fn with_denial_class(mut self, denial_class: impl Into<String>, repeat_count: u64) -> Self {
self.denial_class = Some(denial_class.into());
self.class_repeat_count = Some(repeat_count);
self
}
pub fn with_side_effect_ceiling(mut self, details: SideEffectCeilingDetails) -> Self {
self.side_effect_ceiling = Some(details);
self
}
pub fn with_audiences(
mut self,
machine_reason: Option<String>,
human_summary: Option<String>,
) -> Self {
self.machine_reason = machine_reason;
self.human_summary = human_summary;
self
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
}
}
impl Default for ToolDenial {
fn default() -> Self {
Self::terminal(DenialGate::Unknown, None, "")
}
}
impl Serialize for ToolDenial {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::{Error, SerializeStruct};
if !self.gate.owns_reason(&self.reason) {
return Err(S::Error::custom(format!(
"{} denial carries unattributable reason text: {}",
self.gate.as_str(),
self.reason,
)));
}
let mut field_count = 3;
field_count += usize::from(self.capability.is_some());
field_count += usize::from(!self.denied_paths.is_empty());
field_count += usize::from(self.denial_class.is_some());
field_count += usize::from(self.class_repeat_count.is_some());
field_count += usize::from(self.side_effect_ceiling.is_some());
field_count += usize::from(self.machine_reason.is_some());
field_count += usize::from(self.human_summary.is_some());
let mut record = serializer.serialize_struct("ToolDenial", field_count)?;
record.serialize_field("gate", &self.gate)?;
if let Some(capability) = &self.capability {
record.serialize_field("capability", capability)?;
}
if !self.denied_paths.is_empty() {
record.serialize_field("denied_paths", &self.denied_paths)?;
}
record.serialize_field("retryable", &self.retryable)?;
record.serialize_field("reason", &self.reason)?;
if let Some(denial_class) = &self.denial_class {
record.serialize_field("denial_class", denial_class)?;
}
if let Some(class_repeat_count) = self.class_repeat_count {
record.serialize_field("class_repeat_count", &class_repeat_count)?;
}
if let Some(details) = &self.side_effect_ceiling {
record.serialize_field("side_effect_ceiling", details)?;
}
if let Some(machine_reason) = &self.machine_reason {
record.serialize_field("machine_reason", machine_reason)?;
}
if let Some(human_summary) = &self.human_summary {
record.serialize_field("human_summary", human_summary)?;
}
record.end()
}
}
#[derive(Deserialize)]
struct ToolDenialRecord {
gate: DenialGate,
#[serde(default)]
capability: Option<String>,
#[serde(default)]
denied_paths: Vec<String>,
retryable: bool,
reason: String,
#[serde(default)]
denial_class: Option<String>,
#[serde(default)]
class_repeat_count: Option<u64>,
#[serde(default)]
side_effect_ceiling: Option<SideEffectCeilingDetails>,
#[serde(default)]
machine_reason: Option<String>,
#[serde(default)]
human_summary: Option<String>,
}
impl<'de> Deserialize<'de> for ToolDenial {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let record = ToolDenialRecord::deserialize(deserializer)?;
let reason = if record.gate.owns_reason(&record.reason) {
record.reason
} else {
if DenialGate::ALL
.iter()
.copied()
.any(|gate| gate.has_signature(&record.reason))
{
return Err(D::Error::custom(format!(
"{} denial carries another gate's reason signature",
record.gate.as_str(),
)));
}
record.gate.render_reason(record.reason)
};
Ok(Self {
gate: record.gate,
capability: record.capability,
denied_paths: record.denied_paths,
retryable: record.retryable,
reason,
denial_class: record.denial_class,
class_repeat_count: record.class_repeat_count,
side_effect_ceiling: record.side_effect_ceiling,
machine_reason: record.machine_reason,
human_summary: record.human_summary,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ToolExecutor {
HarnBuiltin,
HostBridge,
McpServer { server_name: String },
ProviderNative,
}