use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PermissionDecision {
Allow,
Deny(String),
Transform(serde_json::Value),
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum PlanningMode {
#[default]
Immediate,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum FinishReason {
NoMoreToolCalls,
BudgetExceeded,
ProviderStop(String),
Stuck {
repeated_call: String,
repeats: usize,
},
TranscriptLimit { chars: usize, limit: usize },
Cancelled,
PermissionDenialLimit,
WallClockExceeded { secs: u64 },
}
impl std::fmt::Display for FinishReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FinishReason::NoMoreToolCalls => write!(f, "no_more_tool_calls"),
FinishReason::BudgetExceeded => write!(f, "budget_exceeded"),
FinishReason::ProviderStop(reason) => write!(f, "provider_stop:{reason}"),
FinishReason::Stuck {
repeated_call,
repeats,
} => write!(f, "stuck:{repeated_call}:{repeats}"),
FinishReason::TranscriptLimit { chars, limit } => {
write!(f, "transcript_limit:{chars}/{limit}")
}
FinishReason::Cancelled => write!(f, "cancelled"),
FinishReason::PermissionDenialLimit => write!(f, "permission_denial_limit"),
FinishReason::WallClockExceeded { secs } => {
write!(f, "wall_clock_exceeded:{secs}")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finish_reason_display_all_variants() {
assert_eq!(
FinishReason::NoMoreToolCalls.to_string(),
"no_more_tool_calls"
);
assert_eq!(FinishReason::BudgetExceeded.to_string(), "budget_exceeded");
assert_eq!(
FinishReason::ProviderStop("length".into()).to_string(),
"provider_stop:length"
);
assert_eq!(
FinishReason::Stuck {
repeated_call: "Read".into(),
repeats: 3
}
.to_string(),
"stuck:Read:3"
);
assert_eq!(
FinishReason::TranscriptLimit {
chars: 10_000,
limit: 8_000
}
.to_string(),
"transcript_limit:10000/8000"
);
assert_eq!(FinishReason::Cancelled.to_string(), "cancelled");
assert_eq!(
FinishReason::PermissionDenialLimit.to_string(),
"permission_denial_limit"
);
assert_eq!(
FinishReason::WallClockExceeded { secs: 120 }.to_string(),
"wall_clock_exceeded:120"
);
}
#[test]
fn planning_mode_default_is_immediate() {
assert_eq!(PlanningMode::default(), PlanningMode::Immediate);
}
#[test]
fn finish_reason_struct_variants_serialize_with_kind_tag() {
let json = serde_json::to_value(&FinishReason::NoMoreToolCalls).unwrap();
assert_eq!(json["kind"], "no_more_tool_calls");
let json = serde_json::to_value(&FinishReason::BudgetExceeded).unwrap();
assert_eq!(json["kind"], "budget_exceeded");
let json = serde_json::to_value(&FinishReason::Stuck {
repeated_call: "Bash".into(),
repeats: 5,
})
.unwrap();
assert_eq!(json["kind"], "stuck");
assert_eq!(json["repeated_call"], "Bash");
assert_eq!(json["repeats"], 5);
let json = serde_json::to_value(&FinishReason::TranscriptLimit {
chars: 1000,
limit: 500,
})
.unwrap();
assert_eq!(json["kind"], "transcript_limit");
assert_eq!(json["chars"], 1000);
assert_eq!(json["limit"], 500);
let json = serde_json::to_value(&FinishReason::Cancelled).unwrap();
assert_eq!(json["kind"], "cancelled");
let json = serde_json::to_value(&FinishReason::PermissionDenialLimit).unwrap();
assert_eq!(json["kind"], "permission_denial_limit");
let json = serde_json::to_value(&FinishReason::WallClockExceeded { secs: 300 }).unwrap();
assert_eq!(json["kind"], "wall_clock_exceeded");
assert_eq!(json["secs"], 300);
}
#[test]
fn finish_reason_deserializes_from_kind_tag() {
let r: FinishReason = serde_json::from_str(r#"{"kind":"no_more_tool_calls"}"#).unwrap();
assert!(matches!(r, FinishReason::NoMoreToolCalls));
let r: FinishReason =
serde_json::from_str(r#"{"kind":"stuck","repeated_call":"Read","repeats":3}"#).unwrap();
match r {
FinishReason::Stuck {
repeated_call,
repeats,
} => {
assert_eq!(repeated_call, "Read");
assert_eq!(repeats, 3);
}
other => panic!("expected Stuck, got {other:?}"),
}
let r: FinishReason = serde_json::from_str(r#"{"kind":"cancelled"}"#).unwrap();
assert!(matches!(r, FinishReason::Cancelled));
let r: FinishReason =
serde_json::from_str(r#"{"kind":"wall_clock_exceeded","secs":99}"#).unwrap();
assert!(matches!(r, FinishReason::WallClockExceeded { secs: 99 }));
}
#[test]
fn permission_decision_serializes_deny_and_allow() {
let deny = serde_json::to_value(PermissionDecision::Deny("blocked".into())).unwrap();
assert!(
deny.get("deny").is_some(),
"Deny must have 'deny' key, got {deny}"
);
let allow = serde_json::to_value(&PermissionDecision::Allow).unwrap();
assert_eq!(
allow,
serde_json::json!("allow"),
"Allow must serialize to string 'allow'"
);
}
}