use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum HookPayload {
#[default]
None,
SessionSetup {
initial_message: String,
source: SessionStartSource,
},
Round { round: u32 },
Prompt { prompt: String },
ToolExecution {
tool_name: String,
tool_call_id: String,
parsed_args: serde_json::Value,
},
ToolResult {
tool_name: String,
tool_call_id: String,
outcome: HookToolOutcome,
},
Compression {
estimated_tokens: u32,
usage_percent: f64,
max_context_tokens: u32,
trigger_context_tokens: u32,
trigger: String,
phase: String,
},
Notification {
#[serde(default, skip_serializing_if = "Option::is_none")]
id: Option<String>,
category: String,
priority: String,
title: String,
body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
dedup_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
click_url: Option<String>,
},
Finalize { stop_hook_active: bool },
SessionEnd {
status: SessionEndStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
completion_reason: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SessionStartSource {
#[default]
Startup,
Resume,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionEndStatus {
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HookToolOutcome {
pub success: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default)]
pub needs_human: bool,
#[serde(default)]
pub duration_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentHookPoint {
BeforeSessionSetup,
AfterSessionSetup,
BeforeFinalize,
AfterSessionEnd,
BeforeRound,
AfterRound,
BeforePromptAssembly,
AfterPromptAssembly,
BeforeLlmCall,
AfterLlmCall,
BeforeToolExecution,
AfterToolExecution,
BeforeMemoryRecall,
AfterMemoryRecall,
BeforeCompression,
AfterCompression,
AfterNotification,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum HookResult {
#[default]
Continue,
Mutated,
Allow,
Deny { reason: String },
Ask,
InjectContext { text: String },
WithContext {
result: Box<HookResult>,
text: String,
},
Suspend { reason: String },
Abort { reason: String },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hook_point_serialization_round_trip() {
let points = [
AgentHookPoint::BeforeSessionSetup,
AgentHookPoint::AfterSessionSetup,
AgentHookPoint::BeforeFinalize,
AgentHookPoint::AfterSessionEnd,
AgentHookPoint::BeforeRound,
AgentHookPoint::AfterRound,
AgentHookPoint::BeforePromptAssembly,
AgentHookPoint::AfterPromptAssembly,
AgentHookPoint::BeforeLlmCall,
AgentHookPoint::AfterLlmCall,
AgentHookPoint::BeforeToolExecution,
AgentHookPoint::AfterToolExecution,
AgentHookPoint::BeforeMemoryRecall,
AgentHookPoint::AfterMemoryRecall,
AgentHookPoint::BeforeCompression,
AgentHookPoint::AfterCompression,
AgentHookPoint::AfterNotification,
];
for point in &points {
let json = serde_json::to_string(point).unwrap();
let restored: AgentHookPoint = serde_json::from_str(&json).unwrap();
assert_eq!(point, &restored);
}
}
#[test]
fn hook_result_default_is_continue() {
assert_eq!(HookResult::default(), HookResult::Continue);
}
#[test]
fn hook_result_variants_serialize() {
let variants = [
HookResult::Continue,
HookResult::Mutated,
HookResult::Allow,
HookResult::Deny {
reason: "blocked".to_string(),
},
HookResult::Ask,
HookResult::InjectContext {
text: "extra context".to_string(),
},
HookResult::WithContext {
result: Box::new(HookResult::Allow),
text: "allowed context".to_string(),
},
HookResult::Suspend {
reason: "waiting".to_string(),
},
HookResult::Abort {
reason: "error".to_string(),
},
];
for variant in &variants {
let json = serde_json::to_string(variant).unwrap();
let restored: HookResult = serde_json::from_str(&json).unwrap();
assert_eq!(variant, &restored);
}
}
#[test]
fn hook_payload_round_trips_structured_tool_data() {
let payload = HookPayload::ToolExecution {
tool_name: "Bash".to_string(),
tool_call_id: "call-1".to_string(),
parsed_args: serde_json::json!({"command": "pwd"}),
};
let json = serde_json::to_string(&payload).unwrap();
let restored: HookPayload = serde_json::from_str(&json).unwrap();
assert_eq!(restored, payload);
}
}