use serde::{Deserialize, Serialize};
use crate::core::session::SessionId;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "decision", content = "detail")]
pub enum OverseerDecision {
Allow,
Block {
reason: String,
},
Respond {
text: String,
},
FlagForHuman {
summary: String,
},
}
impl OverseerDecision {
pub fn tag(&self) -> &'static str {
match self {
Self::Allow => "allow",
Self::Block { .. } => "block",
Self::Respond { .. } => "respond",
Self::FlagForHuman { .. } => "flag",
}
}
pub fn reason(&self) -> &str {
match self {
Self::Allow => "",
Self::Block { reason } => reason,
Self::Respond { text } => text,
Self::FlagForHuman { summary } => summary,
}
}
}
#[derive(Debug, Clone)]
pub struct OverseerContext {
pub session_id: SessionId,
pub tmux_name: String,
pub tool_name: Option<String>,
pub tool_input: Option<String>,
}
impl OverseerContext {
pub fn new(
session_id: SessionId,
tmux_name: impl Into<String>,
tool_name: Option<String>,
tool_input: Option<String>,
) -> Self {
Self {
session_id,
tmux_name: tmux_name.into(),
tool_name,
tool_input,
}
}
}
pub trait Overseer: std::fmt::Debug + Send + Sync {
fn pre_tool_use(&self, ctx: &OverseerContext) -> OverseerDecision;
fn post_tool_use(&self, ctx: &OverseerContext, output: &str) -> OverseerDecision;
fn session_question(&self, ctx: &OverseerContext, question: &str) -> OverseerDecision;
fn is_enabled(&self) -> bool;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decision_json_roundtrip() {
for decision in [
OverseerDecision::Allow,
OverseerDecision::Block {
reason: "blocked".into(),
},
OverseerDecision::Respond { text: "yes".into() },
OverseerDecision::FlagForHuman {
summary: "needs review".into(),
},
] {
let json = serde_json::to_string(&decision).unwrap();
let back: OverseerDecision = serde_json::from_str(&json).unwrap();
assert_eq!(back, decision);
}
}
#[test]
fn decision_tag_is_stable() {
assert_eq!(OverseerDecision::Allow.tag(), "allow");
assert_eq!(
OverseerDecision::Block { reason: "x".into() }.tag(),
"block"
);
assert_eq!(
OverseerDecision::Respond { text: "x".into() }.tag(),
"respond"
);
assert_eq!(
OverseerDecision::FlagForHuman {
summary: "x".into()
}
.tag(),
"flag"
);
}
#[test]
fn decision_reason_extracts_text() {
assert_eq!(OverseerDecision::Allow.reason(), "");
assert_eq!(
OverseerDecision::Block {
reason: "danger".into()
}
.reason(),
"danger"
);
assert_eq!(
OverseerDecision::Respond {
text: "proceed".into()
}
.reason(),
"proceed"
);
}
}