use std::time::SystemTime;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HookResult {
pub allow: bool,
pub message: String,
}
impl HookResult {
#[must_use]
pub const fn allow() -> Self {
Self {
allow: true,
message: String::new(),
}
}
#[must_use]
pub fn allow_with_message(message: impl Into<String>) -> Self {
Self {
allow: true,
message: message.into(),
}
}
#[must_use]
pub fn deny(reason: impl Into<String>) -> Self {
Self {
allow: false,
message: reason.into(),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SessionContext {
pub session_id: String,
pub agent_id: u64,
#[serde(default = "SessionContext::default_started_at")]
pub started_at: SystemTime,
}
impl SessionContext {
fn default_started_at() -> SystemTime {
SystemTime::UNIX_EPOCH
}
}
#[non_exhaustive]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OnSessionStartContext {
pub session: SessionContext,
}
#[non_exhaustive]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OnSessionEndContext {
pub session: SessionContext,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OnCompactionContext {}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OnInteractionContext {
pub message: String,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreTurnContext {
pub prompt: String,
pub turn_number: u32,
}
impl PreTurnContext {
#[must_use]
pub fn new(prompt: impl Into<String>, turn_number: u32) -> Self {
Self {
prompt: prompt.into(),
turn_number,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostTurnContext {
pub response_text: String,
pub turn_number: u32,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreToolCallDecideContext {
#[serde(alias = "name")]
pub tool_name: String,
#[serde(alias = "args", default)]
pub tool_args: serde_json::Value,
}
impl PreToolCallDecideContext {
#[must_use]
pub fn new(tool_name: impl Into<String>, tool_args: serde_json::Value) -> Self {
Self {
tool_name: tool_name.into(),
tool_args,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostToolCallContext {
#[serde(alias = "name")]
pub tool_name: String,
#[serde(alias = "args", default)]
pub tool_args: serde_json::Value,
pub result: String,
#[serde(default)]
pub metadata: serde_json::Value,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OnToolErrorContext {
#[serde(alias = "name")]
pub tool_name: String,
#[serde(alias = "args", default)]
pub tool_args: serde_json::Value,
pub error: String,
#[serde(default)]
pub metadata: serde_json::Value,
}
impl OnToolErrorContext {
#[must_use]
pub fn is_not_found(&self) -> bool {
self.metadata
.get(llm_tool::ToolError::ERROR_KIND_KEY)
.and_then(serde_json::Value::as_str)
== Some(llm_tool::ToolError::KIND_NOT_REGISTERED)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum HookPoint {
PreTurn,
PostTurn,
PreToolCallDecide,
PostToolCall,
OnCompaction,
OnSessionStart,
OnSessionEnd,
OnToolError,
OnInteraction,
Stop,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum StopDecision {
#[default]
#[serde(rename = "ALLOW_STOP")]
AllowStop,
#[serde(rename = "CONTINUE")]
Continue,
}
impl StopDecision {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::AllowStop => "ALLOW_STOP",
Self::Continue => "CONTINUE",
}
}
#[must_use]
pub const fn to_proto_i32(self) -> i32 {
match self {
Self::AllowStop => 1,
Self::Continue => 2,
}
}
#[must_use]
pub const fn from_proto_i32(val: i32) -> Option<Self> {
match val {
1 => Some(Self::AllowStop),
2 => Some(Self::Continue),
_ => None,
}
}
}
impl std::fmt::Display for StopDecision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for StopDecision {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ALLOW_STOP" | "allow_stop" => Ok(Self::AllowStop),
"CONTINUE" | "continue" => Ok(Self::Continue),
other => Err(format!("Unrecognized StopDecision: {other}")),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct StopArgs {
#[serde(default)]
pub response_text: String,
#[serde(default)]
pub trajectory_id: String,
#[serde(default)]
pub continuation_count: u32,
#[serde(default)]
pub stop_reason: crate::types::StopReason,
#[serde(default)]
pub error_message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct StopHookResult {
#[serde(default)]
pub decision: StopDecision,
#[serde(default)]
pub reason: String,
}
impl StopHookResult {
#[must_use]
pub const fn allow() -> Self {
Self {
decision: StopDecision::AllowStop,
reason: String::new(),
}
}
#[must_use]
pub fn continue_with(reason: impl Into<String>) -> Self {
Self {
decision: StopDecision::Continue,
reason: reason.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookEntry {
pub name: String,
pub point: HookPoint,
pub callback_id: String,
}
impl HookEntry {
pub fn new(
name: impl Into<String>,
point: HookPoint,
callback_id: impl Into<String>,
) -> Result<Self, crate::error::Error> {
let entry = Self {
name: name.into(),
point,
callback_id: callback_id.into(),
};
entry.validate()?;
Ok(entry)
}
pub fn validate(&self) -> Result<(), crate::error::Error> {
if self.name.trim().is_empty() {
return Err(crate::error::Error::InvalidConfig {
message: "HookEntry name must not be empty".to_owned(),
});
}
if self.callback_id.trim().is_empty() {
return Err(crate::error::Error::InvalidConfig {
message: format!("HookEntry '{}' has an empty callback_id", self.name),
});
}
Ok(())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HookSet {
entries: Vec<HookEntry>,
}
impl HookSet {
#[must_use]
pub const fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn push(&mut self, entry: HookEntry) -> Result<(), crate::error::Error> {
entry.validate()?;
if let Some(pos) = self
.entries
.iter()
.position(|e| e.name == entry.name && e.point == entry.point)
{
tracing::warn!(
hook = %entry.name,
point = %entry.point.label(),
"duplicate hook name+point in HookSet — replacing previous entry"
);
self.entries[pos] = entry;
} else {
self.entries.push(entry);
}
Ok(())
}
pub fn at_point(&self, point: HookPoint) -> impl Iterator<Item = &HookEntry> {
self.entries.iter().filter(move |e| e.point == point)
}
pub fn iter(&self) -> impl Iterator<Item = &HookEntry> {
self.entries.iter()
}
#[must_use]
pub const fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl From<HookSet> for Vec<HookEntry> {
fn from(set: HookSet) -> Self {
set.entries
}
}
impl From<&HookSet> for Vec<HookEntry> {
fn from(set: &HookSet) -> Self {
set.entries.clone()
}
}
impl IntoIterator for HookSet {
type Item = HookEntry;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
}
}
impl FromIterator<HookEntry> for HookSet {
fn from_iter<T: IntoIterator<Item = HookEntry>>(iter: T) -> Self {
let mut set = Self::new();
for entry in iter {
let name = entry.name.clone();
if let Err(e) = set.push(entry) {
tracing::error!(
error = %e,
hook = %name,
"Failed to push hook entry during from_iter"
);
}
}
set
}
}
impl From<Vec<HookEntry>> for HookSet {
fn from(entries: Vec<HookEntry>) -> Self {
Self::from_iter(entries)
}
}
impl<const N: usize> From<[HookEntry; N]> for HookSet {
fn from(entries: [HookEntry; N]) -> Self {
Self::from_iter(entries)
}
}
type TransformToolInputFn =
dyn Fn(&PreToolCallDecideContext) -> Option<serde_json::Value> + Send + Sync;
type OnToolErrorFn = dyn Fn(&OnToolErrorContext) -> Option<String> + Send + Sync;
#[non_exhaustive]
pub enum HookCallback {
PreTurn(Box<dyn Fn(&PreTurnContext) -> HookResult + Send + Sync>),
PostTurn(Box<dyn Fn(&PostTurnContext) + Send + Sync>),
PreToolCallDecide(Box<dyn Fn(&PreToolCallDecideContext) -> HookResult + Send + Sync>),
PostToolCall(Box<dyn Fn(&PostToolCallContext) + Send + Sync>),
OnToolError(Box<OnToolErrorFn>),
OnSessionStart(Box<dyn Fn(&OnSessionStartContext) + Send + Sync>),
OnSessionEnd(Box<dyn Fn(&OnSessionEndContext) + Send + Sync>),
OnCompaction(Box<dyn Fn(&OnCompactionContext) + Send + Sync>),
OnInteraction(Box<dyn Fn(&OnInteractionContext) -> HookResult + Send + Sync>),
Stop(Box<dyn Fn(&StopArgs) -> StopHookResult + Send + Sync>),
TransformToolInput(Box<TransformToolInputFn>),
}
impl HookCallback {
#[must_use]
pub(crate) const fn hook_point(&self) -> HookPoint {
match self {
Self::PreTurn(_) => HookPoint::PreTurn,
Self::PostTurn(_) => HookPoint::PostTurn,
Self::PreToolCallDecide(_) | Self::TransformToolInput(_) => {
HookPoint::PreToolCallDecide
}
Self::PostToolCall(_) => HookPoint::PostToolCall,
Self::OnToolError(_) => HookPoint::OnToolError,
Self::OnSessionStart(_) => HookPoint::OnSessionStart,
Self::OnSessionEnd(_) => HookPoint::OnSessionEnd,
Self::OnCompaction(_) => HookPoint::OnCompaction,
Self::OnInteraction(_) => HookPoint::OnInteraction,
Self::Stop(_) => HookPoint::Stop,
}
}
}
impl HookPoint {
pub const PRE_TURN: &str = "pre_turn";
pub const POST_TURN: &str = "post_turn";
pub const PRE_TOOL_CALL_DECIDE: &str = "pre_tool_call_decide";
pub const POST_TOOL_CALL: &str = "post_tool_call";
pub const ON_COMPACTION: &str = "on_compaction";
pub const ON_SESSION_START: &str = "on_session_start";
pub const ON_SESSION_END: &str = "on_session_end";
pub const ON_TOOL_ERROR: &str = "on_tool_error";
pub const ON_INTERACTION: &str = "on_interaction";
pub const STOP: &str = "stop";
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::PreTurn => Self::PRE_TURN,
Self::PostTurn => Self::POST_TURN,
Self::PreToolCallDecide => Self::PRE_TOOL_CALL_DECIDE,
Self::PostToolCall => Self::POST_TOOL_CALL,
Self::OnCompaction => Self::ON_COMPACTION,
Self::OnSessionStart => Self::ON_SESSION_START,
Self::OnSessionEnd => Self::ON_SESSION_END,
Self::OnToolError => Self::ON_TOOL_ERROR,
Self::OnInteraction => Self::ON_INTERACTION,
Self::Stop => Self::STOP,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.label()
}
}
impl core::str::FromStr for HookPoint {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
Self::PRE_TURN => Ok(Self::PreTurn),
Self::POST_TURN => Ok(Self::PostTurn),
Self::PRE_TOOL_CALL_DECIDE => Ok(Self::PreToolCallDecide),
Self::POST_TOOL_CALL => Ok(Self::PostToolCall),
Self::ON_COMPACTION => Ok(Self::OnCompaction),
Self::ON_SESSION_START => Ok(Self::OnSessionStart),
Self::ON_SESSION_END => Ok(Self::OnSessionEnd),
Self::ON_TOOL_ERROR => Ok(Self::OnToolError),
Self::ON_INTERACTION => Ok(Self::OnInteraction),
Self::STOP => Ok(Self::Stop),
other => Err(format!("Unknown hook point: {other}")),
}
}
}
impl std::fmt::Debug for HookCallback {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("HookCallback::")?;
match self {
Self::TransformToolInput(_) => f.write_str("transform_tool_input"),
other => f.write_str(other.hook_point().label()),
}
}
}