Skip to main content

a3s_code_core/hooks/
events.rs

1//! Hook Event Types
2//!
3//! Defines all event types that can trigger hooks.
4
5use serde::{Deserialize, Serialize};
6
7/// Hook event types
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum HookEventType {
11    /// Before tool execution
12    PreToolUse,
13    /// After tool execution
14    PostToolUse,
15    /// Before interactive permission is requested
16    PermissionRequest,
17    /// Before context compaction
18    PreCompact,
19    /// After context compaction
20    PostCompact,
21    /// Before LLM generation
22    GenerateStart,
23    /// After LLM generation
24    GenerateEnd,
25    /// When session is created
26    SessionStart,
27    /// When session is destroyed
28    SessionEnd,
29    /// When a skill is loaded
30    SkillLoad,
31    /// When a skill is unloaded
32    SkillUnload,
33    /// Before prompt augmentation (can modify prompt)
34    PrePrompt,
35    /// After LLM response is processed, before returning to user
36    PostResponse,
37    /// When an error occurs (tool failure, LLM error, etc.)
38    OnError,
39    // === New harness points ===
40    /// Before context perception (model needs workspace knowledge)
41    PreContextPerception,
42    /// After context perception
43    PostContextPerception,
44
45    /// When an operation succeeds (mirrors OnError for success case)
46    OnSuccess,
47
48    /// Before memory recall (model needs to retrieve from memory)
49    PreMemoryRecall,
50    /// After memory recall completes
51    PostMemoryRecall,
52
53    /// Before task planning/decomposition
54    PrePlanning,
55    /// After planning completes
56    PostPlanning,
57
58    /// Before reasoning (CoT/ToT start)
59    PreReasoning,
60    /// After reasoning completes
61    PostReasoning,
62
63    /// When rate limit is triggered
64    OnRateLimit,
65
66    /// When user confirmation is needed
67    OnConfirmation,
68
69    /// Intent detection - detect user intent from prompt (blocking)
70    IntentDetection,
71    /// Before a host steer/interrupt request is admitted to a Run.
72    PreRunControl,
73    /// After a host run-control request receives an accepted/applied/
74    /// settled/rejected receipt.
75    PostRunControl,
76}
77
78impl std::fmt::Display for HookEventType {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            HookEventType::PreToolUse => write!(f, "pre_tool_use"),
82            HookEventType::PostToolUse => write!(f, "post_tool_use"),
83            HookEventType::PermissionRequest => write!(f, "permission_request"),
84            HookEventType::PreCompact => write!(f, "pre_compact"),
85            HookEventType::PostCompact => write!(f, "post_compact"),
86            HookEventType::GenerateStart => write!(f, "generate_start"),
87            HookEventType::GenerateEnd => write!(f, "generate_end"),
88            HookEventType::SessionStart => write!(f, "session_start"),
89            HookEventType::SessionEnd => write!(f, "session_end"),
90            HookEventType::SkillLoad => write!(f, "skill_load"),
91            HookEventType::SkillUnload => write!(f, "skill_unload"),
92            HookEventType::PrePrompt => write!(f, "pre_prompt"),
93            HookEventType::PostResponse => write!(f, "post_response"),
94            HookEventType::OnError => write!(f, "on_error"),
95            // New harness points
96            HookEventType::PreContextPerception => write!(f, "pre_context_perception"),
97            HookEventType::PostContextPerception => write!(f, "post_context_perception"),
98            HookEventType::OnSuccess => write!(f, "on_success"),
99            HookEventType::PreMemoryRecall => write!(f, "pre_memory_recall"),
100            HookEventType::PostMemoryRecall => write!(f, "post_memory_recall"),
101            HookEventType::PrePlanning => write!(f, "pre_planning"),
102            HookEventType::PostPlanning => write!(f, "post_planning"),
103            HookEventType::PreReasoning => write!(f, "pre_reasoning"),
104            HookEventType::PostReasoning => write!(f, "post_reasoning"),
105            HookEventType::OnRateLimit => write!(f, "on_rate_limit"),
106            HookEventType::OnConfirmation => write!(f, "on_confirmation"),
107            HookEventType::IntentDetection => write!(f, "intent_detection"),
108            HookEventType::PreRunControl => write!(f, "pre_run_control"),
109            HookEventType::PostRunControl => write!(f, "post_run_control"),
110        }
111    }
112}
113
114/// Tool execution result data
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ToolResultData {
117    /// Whether execution succeeded
118    pub success: bool,
119    /// Tool output
120    pub output: String,
121    /// Exit code (for shell commands)
122    pub exit_code: Option<i32>,
123    /// Execution duration in milliseconds
124    pub duration_ms: u64,
125}
126
127/// Pre-tool-use event payload
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct PreToolUseEvent {
130    /// Session ID
131    pub session_id: String,
132    /// Tool name
133    pub tool: String,
134    /// Tool arguments
135    pub args: serde_json::Value,
136    /// Working directory
137    pub working_directory: String,
138    /// Recent tools executed (for context)
139    pub recent_tools: Vec<String>,
140}
141
142/// Post-tool-use event payload
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct PostToolUseEvent {
145    /// Session ID
146    pub session_id: String,
147    /// Tool name
148    pub tool: String,
149    /// Tool arguments
150    pub args: serde_json::Value,
151    /// Execution result
152    pub result: ToolResultData,
153}
154
155/// Permission-request event payload, fired before the host confirmation UI.
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct PermissionRequestEvent {
158    pub session_id: String,
159    pub tool_id: String,
160    pub tool: String,
161    pub args: serde_json::Value,
162}
163
164/// Pre-compaction event payload.
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct PreCompactEvent {
167    pub session_id: String,
168    pub message_count: usize,
169    pub used_tokens: usize,
170    pub max_tokens: usize,
171}
172
173/// Post-compaction event payload.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct PostCompactEvent {
176    pub session_id: String,
177    pub message_count_before: usize,
178    pub message_count_after: usize,
179    pub summary_generated: bool,
180}
181
182/// Generate start event payload
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct GenerateStartEvent {
185    /// Session ID
186    pub session_id: String,
187    /// User prompt
188    pub prompt: String,
189    /// System prompt (if any)
190    pub system_prompt: Option<String>,
191    /// Model provider
192    pub model_provider: String,
193    /// Model name
194    pub model_name: String,
195    /// Available tools
196    pub available_tools: Vec<String>,
197}
198
199/// Generate end event payload
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct GenerateEndEvent {
202    /// Session ID
203    pub session_id: String,
204    /// User prompt
205    pub prompt: String,
206    /// Response text
207    pub response_text: String,
208    /// Tool calls made
209    pub tool_calls: Vec<ToolCallInfo>,
210    /// Token usage
211    pub usage: TokenUsageInfo,
212    /// Duration in milliseconds
213    pub duration_ms: u64,
214}
215
216/// Tool call information
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ToolCallInfo {
219    /// Tool name
220    pub name: String,
221    /// Tool arguments
222    pub args: serde_json::Value,
223}
224
225/// Token usage information
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct TokenUsageInfo {
228    /// Prompt tokens
229    pub prompt_tokens: i32,
230    /// Completion tokens
231    pub completion_tokens: i32,
232    /// Total tokens
233    pub total_tokens: i32,
234}
235
236/// Session start event payload
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct SessionStartEvent {
239    /// Session ID
240    pub session_id: String,
241    /// System prompt (if any)
242    pub system_prompt: Option<String>,
243    /// Model configuration
244    pub model_provider: String,
245    pub model_name: String,
246}
247
248/// Session end event payload
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct SessionEndEvent {
251    /// Session ID
252    pub session_id: String,
253    /// Total token usage
254    pub total_tokens: i32,
255    /// Total tool calls
256    pub total_tool_calls: i32,
257    /// Session duration in milliseconds
258    pub duration_ms: u64,
259}
260
261/// Skill load event payload
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct SkillLoadEvent {
264    /// Skill name
265    pub skill_name: String,
266    /// Tool names loaded from the skill
267    pub tool_names: Vec<String>,
268    /// Skill version (if available)
269    pub version: Option<String>,
270    /// Skill description (if available)
271    pub description: Option<String>,
272    /// Timestamp when skill was loaded (Unix milliseconds)
273    pub loaded_at: i64,
274}
275
276/// Skill unload event payload
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct SkillUnloadEvent {
279    /// Skill name
280    pub skill_name: String,
281    /// Tool names that were unloaded
282    pub tool_names: Vec<String>,
283    /// How long the skill was loaded (milliseconds)
284    pub duration_ms: u64,
285}
286
287/// Pre-prompt event payload (fired before prompt augmentation)
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct PrePromptEvent {
290    /// Session ID
291    pub session_id: String,
292    /// User prompt text
293    pub prompt: String,
294    /// Current system prompt (if any)
295    pub system_prompt: Option<String>,
296    /// Number of messages in conversation history
297    pub message_count: usize,
298}
299
300/// Post-response event payload (fired after LLM response is processed)
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct PostResponseEvent {
303    /// Session ID
304    pub session_id: String,
305    /// Final response text
306    pub response_text: String,
307    /// Number of tool calls made during this turn
308    pub tool_calls_count: usize,
309    /// Token usage
310    pub usage: TokenUsageInfo,
311    /// Total duration in milliseconds
312    pub duration_ms: u64,
313}
314
315/// Error type classification for OnError events
316#[derive(Debug, Clone, Serialize, Deserialize)]
317#[serde(rename_all = "snake_case")]
318pub enum ErrorType {
319    /// Tool execution failed
320    ToolFailure,
321    /// LLM API call failed
322    LlmFailure,
323    /// Permission denied
324    PermissionDenied,
325    /// Timeout
326    Timeout,
327    /// Other error
328    Other,
329}
330
331impl std::fmt::Display for ErrorType {
332    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333        match self {
334            ErrorType::ToolFailure => write!(f, "tool_failure"),
335            ErrorType::LlmFailure => write!(f, "llm_failure"),
336            ErrorType::PermissionDenied => write!(f, "permission_denied"),
337            ErrorType::Timeout => write!(f, "timeout"),
338            ErrorType::Other => write!(f, "other"),
339        }
340    }
341}
342
343/// On-error event payload
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct OnErrorEvent {
346    /// Session ID
347    pub session_id: String,
348    /// Error classification
349    pub error_type: ErrorType,
350    /// Error message
351    pub error_message: String,
352    /// Additional context (e.g., tool name, model name)
353    pub context: serde_json::Value,
354}
355
356// ============================================================================
357// New Driving Point Payloads
358// ============================================================================
359
360/// Pre-context-perception event payload
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct PreContextPerceptionEvent {
363    pub session_id: String,
364    pub intent: String,
365    pub target_type: String,
366    pub target_name: String,
367    pub domain: String,
368    pub query: Option<String>,
369    pub working_directory: String,
370    pub urgency: String,
371}
372
373/// Post-context-perception event payload
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct PostContextPerceptionEvent {
376    pub session_id: String,
377    pub intent: String,
378    pub target_type: String,
379    pub success: bool,
380    pub facts_retrieved: usize,
381    pub files_retrieved: usize,
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub error: Option<String>,
384}
385
386/// On-success event payload (mirrors OnError for success)
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct OnSuccessEvent {
389    /// Session ID
390    pub session_id: String,
391    /// Action type that succeeded
392    pub action_type: String,
393    /// Summary of the successful action
394    pub action_summary: String,
395    /// Duration in milliseconds
396    pub duration_ms: u64,
397}
398
399/// Pre-memory-recall event payload
400#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct PreMemoryRecallEvent {
402    pub session_id: String,
403    /// Query or intent for recall
404    pub query: String,
405    /// Memory type (e.g., "semantic", "episodic", "working")
406    pub memory_type: String,
407    /// Maximum results to retrieve
408    pub max_results: usize,
409    /// Current working directory
410    pub working_directory: String,
411}
412
413/// Post-memory-recall event payload
414#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct PostMemoryRecallEvent {
416    pub session_id: String,
417    pub query: String,
418    pub memory_type: String,
419    pub facts_retrieved: usize,
420    pub success: bool,
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub error: Option<String>,
423}
424
425/// Planning strategy type
426#[derive(Debug, Clone, Serialize, Deserialize)]
427#[serde(rename_all = "snake_case")]
428pub enum PlanningStrategy {
429    None,
430    StepByStep,
431    TreeOfThoughts,
432    GraphPlanning,
433    Custom(String),
434}
435
436/// Pre-planning event payload
437#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct PrePlanningEvent {
439    pub session_id: String,
440    /// Task description to plan
441    pub task_description: String,
442    /// Available planning strategies
443    pub available_strategies: Vec<PlanningStrategy>,
444    /// Constraints or requirements
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub constraints: Option<serde_json::Value>,
447}
448
449/// Post-planning event payload
450#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct PostPlanningEvent {
452    pub session_id: String,
453    pub task_description: String,
454    pub strategy_used: PlanningStrategy,
455    /// Generated subtasks or plan steps
456    pub subtasks: Vec<String>,
457    pub success: bool,
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub error: Option<String>,
460}
461
462/// Reasoning type
463#[derive(Debug, Clone, Serialize, Deserialize)]
464#[serde(rename_all = "snake_case")]
465pub enum ReasoningType {
466    ChainOfThought,
467    TreeOfThoughts,
468    ReAct,
469    Reflexion,
470    Other(String),
471}
472
473/// Pre-reasoning event payload
474#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct PreReasoningEvent {
476    pub session_id: String,
477    /// Type of reasoning being performed
478    pub reasoning_type: ReasoningType,
479    /// Problem or question to reason about
480    pub problem_statement: String,
481    /// Available hints or context
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub hints: Option<Vec<String>>,
484}
485
486/// Post-reasoning event payload
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct PostReasoningEvent {
489    pub session_id: String,
490    pub reasoning_type: ReasoningType,
491    pub conclusion: String,
492    pub steps_count: usize,
493    pub success: bool,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub error: Option<String>,
496}
497
498/// Rate limit type
499#[derive(Debug, Clone, Serialize, Deserialize)]
500#[serde(rename_all = "snake_case")]
501pub enum RateLimitType {
502    LlmTokenLimit,
503    LlmRequestLimit,
504    ApiRequestLimit,
505    ToolExecutionLimit,
506    Custom(String),
507}
508
509/// On-rate-limit event payload
510#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct OnRateLimitEvent {
512    pub session_id: String,
513    /// Type of rate limit
514    pub limit_type: RateLimitType,
515    /// Retry after milliseconds (suggested)
516    pub retry_after_ms: u64,
517    /// Current usage information
518    pub current_usage: String,
519}
520
521/// Confirmation type
522#[derive(Debug, Clone, Serialize, Deserialize)]
523#[serde(rename_all = "snake_case")]
524pub enum ConfirmationType {
525    SafetyConfirm,
526    UserConfirm,
527    CostConfirm,
528    Custom(String),
529}
530
531/// On-confirmation event payload
532#[derive(Debug, Clone, Serialize, Deserialize)]
533pub struct OnConfirmationEvent {
534    pub session_id: String,
535    /// Type of confirmation needed
536    pub confirmation_type: ConfirmationType,
537    /// Message to show to user
538    pub message: String,
539    /// Options to present (if any)
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub options: Option<Vec<String>>,
542}
543
544/// Intent detection event payload
545#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct IntentDetectionEvent {
547    pub session_id: String,
548    pub prompt: String,
549    pub workspace: String,
550    /// Optional language hint auto-detected from input
551    #[serde(skip_serializing_if = "Option::is_none")]
552    pub language_hint: Option<String>,
553}
554
555/// Run-control admission event. This is a gating hook: a Block/Retry/
556/// Escalate outcome prevents the request from entering the run inbox.
557#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct PreRunControlEvent {
559    pub session_id: String,
560    pub run_id: String,
561    pub request_id: String,
562    pub operation: crate::run_control::RunControlOperation,
563    pub command: crate::run_control::RunControlCommand,
564    #[serde(skip_serializing_if = "Option::is_none")]
565    pub expected_turn_id: Option<String>,
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub expected_turn_revision: Option<u64>,
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub deadline_ms: Option<u64>,
570}
571
572/// Run-control receipt event. This is observational and is emitted for every
573/// durable state transition visible to the host.
574#[derive(Debug, Clone, Serialize, Deserialize)]
575pub struct PostRunControlEvent {
576    pub session_id: String,
577    pub run_id: String,
578    pub request_id: String,
579    pub operation: crate::run_control::RunControlOperation,
580    pub state: crate::run_control::RunControlReceiptState,
581    pub sequence: u64,
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub turn_id: Option<String>,
584    pub turn_revision: u64,
585    pub accepted_at_ms: u64,
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub applied_at_ms: Option<u64>,
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub error: Option<crate::run_control::RunControlErrorInfo>,
590}
591
592/// Unified hook event enum
593#[derive(Debug, Clone, Serialize, Deserialize)]
594#[serde(tag = "event_type", content = "payload")]
595pub enum HookEvent {
596    #[serde(rename = "pre_tool_use")]
597    PreToolUse(PreToolUseEvent),
598    #[serde(rename = "post_tool_use")]
599    PostToolUse(PostToolUseEvent),
600    #[serde(rename = "permission_request")]
601    PermissionRequest(PermissionRequestEvent),
602    #[serde(rename = "pre_compact")]
603    PreCompact(PreCompactEvent),
604    #[serde(rename = "post_compact")]
605    PostCompact(PostCompactEvent),
606    #[serde(rename = "generate_start")]
607    GenerateStart(GenerateStartEvent),
608    #[serde(rename = "generate_end")]
609    GenerateEnd(GenerateEndEvent),
610    #[serde(rename = "session_start")]
611    SessionStart(SessionStartEvent),
612    #[serde(rename = "session_end")]
613    SessionEnd(SessionEndEvent),
614    #[serde(rename = "skill_load")]
615    SkillLoad(SkillLoadEvent),
616    #[serde(rename = "skill_unload")]
617    SkillUnload(SkillUnloadEvent),
618    #[serde(rename = "pre_prompt")]
619    PrePrompt(PrePromptEvent),
620    #[serde(rename = "post_response")]
621    PostResponse(PostResponseEvent),
622    #[serde(rename = "on_error")]
623    OnError(OnErrorEvent),
624    // New harness points
625    #[serde(rename = "pre_context_perception")]
626    PreContextPerception(PreContextPerceptionEvent),
627    #[serde(rename = "post_context_perception")]
628    PostContextPerception(PostContextPerceptionEvent),
629    #[serde(rename = "on_success")]
630    OnSuccess(OnSuccessEvent),
631    #[serde(rename = "pre_memory_recall")]
632    PreMemoryRecall(PreMemoryRecallEvent),
633    #[serde(rename = "post_memory_recall")]
634    PostMemoryRecall(PostMemoryRecallEvent),
635    #[serde(rename = "pre_planning")]
636    PrePlanning(PrePlanningEvent),
637    #[serde(rename = "post_planning")]
638    PostPlanning(PostPlanningEvent),
639    #[serde(rename = "pre_reasoning")]
640    PreReasoning(PreReasoningEvent),
641    #[serde(rename = "post_reasoning")]
642    PostReasoning(PostReasoningEvent),
643    #[serde(rename = "on_rate_limit")]
644    OnRateLimit(OnRateLimitEvent),
645    #[serde(rename = "on_confirmation")]
646    OnConfirmation(OnConfirmationEvent),
647    #[serde(rename = "intent_detection")]
648    IntentDetection(IntentDetectionEvent),
649    #[serde(rename = "pre_run_control")]
650    PreRunControl(PreRunControlEvent),
651    #[serde(rename = "post_run_control")]
652    PostRunControl(PostRunControlEvent),
653}
654
655impl HookEvent {
656    /// Get the event type
657    pub fn event_type(&self) -> HookEventType {
658        match self {
659            HookEvent::PreToolUse(_) => HookEventType::PreToolUse,
660            HookEvent::PostToolUse(_) => HookEventType::PostToolUse,
661            HookEvent::PermissionRequest(_) => HookEventType::PermissionRequest,
662            HookEvent::PreCompact(_) => HookEventType::PreCompact,
663            HookEvent::PostCompact(_) => HookEventType::PostCompact,
664            HookEvent::GenerateStart(_) => HookEventType::GenerateStart,
665            HookEvent::GenerateEnd(_) => HookEventType::GenerateEnd,
666            HookEvent::SessionStart(_) => HookEventType::SessionStart,
667            HookEvent::SessionEnd(_) => HookEventType::SessionEnd,
668            HookEvent::SkillLoad(_) => HookEventType::SkillLoad,
669            HookEvent::SkillUnload(_) => HookEventType::SkillUnload,
670            HookEvent::PrePrompt(_) => HookEventType::PrePrompt,
671            HookEvent::PostResponse(_) => HookEventType::PostResponse,
672            HookEvent::OnError(_) => HookEventType::OnError,
673            // New harness points
674            HookEvent::PreContextPerception(_) => HookEventType::PreContextPerception,
675            HookEvent::PostContextPerception(_) => HookEventType::PostContextPerception,
676            HookEvent::OnSuccess(_) => HookEventType::OnSuccess,
677            HookEvent::PreMemoryRecall(_) => HookEventType::PreMemoryRecall,
678            HookEvent::PostMemoryRecall(_) => HookEventType::PostMemoryRecall,
679            HookEvent::PrePlanning(_) => HookEventType::PrePlanning,
680            HookEvent::PostPlanning(_) => HookEventType::PostPlanning,
681            HookEvent::PreReasoning(_) => HookEventType::PreReasoning,
682            HookEvent::PostReasoning(_) => HookEventType::PostReasoning,
683            HookEvent::OnRateLimit(_) => HookEventType::OnRateLimit,
684            HookEvent::OnConfirmation(_) => HookEventType::OnConfirmation,
685            HookEvent::IntentDetection(_) => HookEventType::IntentDetection,
686            HookEvent::PreRunControl(_) => HookEventType::PreRunControl,
687            HookEvent::PostRunControl(_) => HookEventType::PostRunControl,
688        }
689    }
690
691    /// Get the session ID (returns empty string for skill events which are global)
692    pub fn session_id(&self) -> &str {
693        match self {
694            HookEvent::PreToolUse(e) => &e.session_id,
695            HookEvent::PostToolUse(e) => &e.session_id,
696            HookEvent::PermissionRequest(e) => &e.session_id,
697            HookEvent::PreCompact(e) => &e.session_id,
698            HookEvent::PostCompact(e) => &e.session_id,
699            HookEvent::GenerateStart(e) => &e.session_id,
700            HookEvent::GenerateEnd(e) => &e.session_id,
701            HookEvent::SessionStart(e) => &e.session_id,
702            HookEvent::SessionEnd(e) => &e.session_id,
703            HookEvent::PrePrompt(e) => &e.session_id,
704            HookEvent::PostResponse(e) => &e.session_id,
705            HookEvent::OnError(e) => &e.session_id,
706            // New harness points
707            HookEvent::PreContextPerception(e) => &e.session_id,
708            HookEvent::PostContextPerception(e) => &e.session_id,
709            HookEvent::OnSuccess(e) => &e.session_id,
710            HookEvent::PreMemoryRecall(e) => &e.session_id,
711            HookEvent::PostMemoryRecall(e) => &e.session_id,
712            HookEvent::PrePlanning(e) => &e.session_id,
713            HookEvent::PostPlanning(e) => &e.session_id,
714            HookEvent::PreReasoning(e) => &e.session_id,
715            HookEvent::PostReasoning(e) => &e.session_id,
716            HookEvent::OnRateLimit(e) => &e.session_id,
717            HookEvent::OnConfirmation(e) => &e.session_id,
718            HookEvent::IntentDetection(e) => &e.session_id,
719            HookEvent::PreRunControl(e) => &e.session_id,
720            HookEvent::PostRunControl(e) => &e.session_id,
721            // Skill events are global (not session-specific)
722            HookEvent::SkillLoad(_) => "",
723            HookEvent::SkillUnload(_) => "",
724        }
725    }
726
727    /// Get the tool name (for tool events)
728    pub fn tool_name(&self) -> Option<&str> {
729        match self {
730            HookEvent::PreToolUse(e) => Some(&e.tool),
731            HookEvent::PostToolUse(e) => Some(&e.tool),
732            _ => None,
733        }
734    }
735
736    /// Get the tool args (for tool events)
737    pub fn tool_args(&self) -> Option<&serde_json::Value> {
738        match self {
739            HookEvent::PreToolUse(e) => Some(&e.args),
740            HookEvent::PostToolUse(e) => Some(&e.args),
741            _ => None,
742        }
743    }
744
745    /// Get the skill name (for skill events)
746    pub fn skill_name(&self) -> Option<&str> {
747        match self {
748            HookEvent::SkillLoad(e) => Some(&e.skill_name),
749            HookEvent::SkillUnload(e) => Some(&e.skill_name),
750            _ => None,
751        }
752    }
753}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758
759    #[test]
760    fn test_hook_event_type_display() {
761        assert_eq!(HookEventType::PreToolUse.to_string(), "pre_tool_use");
762        assert_eq!(HookEventType::PostToolUse.to_string(), "post_tool_use");
763        assert_eq!(HookEventType::GenerateStart.to_string(), "generate_start");
764        assert_eq!(HookEventType::GenerateEnd.to_string(), "generate_end");
765        assert_eq!(HookEventType::SessionStart.to_string(), "session_start");
766        assert_eq!(HookEventType::SessionEnd.to_string(), "session_end");
767        assert_eq!(HookEventType::SkillLoad.to_string(), "skill_load");
768        assert_eq!(HookEventType::SkillUnload.to_string(), "skill_unload");
769    }
770
771    #[test]
772    fn test_pre_tool_use_event() {
773        let event = PreToolUseEvent {
774            session_id: "session-1".to_string(),
775            tool: "Bash".to_string(),
776            args: serde_json::json!({"command": "echo hello"}),
777            working_directory: "/workspace".to_string(),
778            recent_tools: vec!["Read".to_string()],
779        };
780
781        assert_eq!(event.session_id, "session-1");
782        assert_eq!(event.tool, "Bash");
783    }
784
785    #[test]
786    fn test_post_tool_use_event() {
787        let event = PostToolUseEvent {
788            session_id: "session-1".to_string(),
789            tool: "Bash".to_string(),
790            args: serde_json::json!({"command": "echo hello"}),
791            result: ToolResultData {
792                success: true,
793                output: "hello\n".to_string(),
794                exit_code: Some(0),
795                duration_ms: 50,
796            },
797        };
798
799        assert!(event.result.success);
800        assert_eq!(event.result.exit_code, Some(0));
801    }
802
803    #[test]
804    fn test_hook_event_type() {
805        let pre_tool = HookEvent::PreToolUse(PreToolUseEvent {
806            session_id: "s1".to_string(),
807            tool: "Bash".to_string(),
808            args: serde_json::json!({}),
809            working_directory: "/".to_string(),
810            recent_tools: vec![],
811        });
812
813        assert_eq!(pre_tool.event_type(), HookEventType::PreToolUse);
814        assert_eq!(pre_tool.session_id(), "s1");
815        assert_eq!(pre_tool.tool_name(), Some("Bash"));
816    }
817
818    #[test]
819    fn test_hook_event_serialization() {
820        let event = HookEvent::PreToolUse(PreToolUseEvent {
821            session_id: "s1".to_string(),
822            tool: "Bash".to_string(),
823            args: serde_json::json!({"command": "ls"}),
824            working_directory: "/workspace".to_string(),
825            recent_tools: vec![],
826        });
827
828        let json = serde_json::to_string(&event).unwrap();
829        assert!(json.contains("pre_tool_use"));
830        assert!(json.contains("Bash"));
831
832        // Deserialize back
833        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
834        assert_eq!(parsed.event_type(), HookEventType::PreToolUse);
835    }
836
837    #[test]
838    fn test_generate_events() {
839        let start = GenerateStartEvent {
840            session_id: "s1".to_string(),
841            prompt: "Hello".to_string(),
842            system_prompt: Some("You are helpful".to_string()),
843            model_provider: "anthropic".to_string(),
844            model_name: "claude-3".to_string(),
845            available_tools: vec!["Bash".to_string(), "Read".to_string()],
846        };
847
848        let end = GenerateEndEvent {
849            session_id: "s1".to_string(),
850            prompt: "Hello".to_string(),
851            response_text: "Hi there!".to_string(),
852            tool_calls: vec![],
853            usage: TokenUsageInfo {
854                prompt_tokens: 10,
855                completion_tokens: 5,
856                total_tokens: 15,
857            },
858            duration_ms: 500,
859        };
860
861        assert_eq!(start.prompt, "Hello");
862        assert_eq!(end.response_text, "Hi there!");
863        assert_eq!(end.usage.total_tokens, 15);
864    }
865
866    #[test]
867    fn test_session_events() {
868        let start = SessionStartEvent {
869            session_id: "s1".to_string(),
870            system_prompt: Some("System".to_string()),
871            model_provider: "anthropic".to_string(),
872            model_name: "claude-3".to_string(),
873        };
874
875        let end = SessionEndEvent {
876            session_id: "s1".to_string(),
877            total_tokens: 1000,
878            total_tool_calls: 5,
879            duration_ms: 60000,
880        };
881
882        let start_event = HookEvent::SessionStart(start);
883        let end_event = HookEvent::SessionEnd(end);
884
885        assert_eq!(start_event.event_type(), HookEventType::SessionStart);
886        assert_eq!(end_event.event_type(), HookEventType::SessionEnd);
887        assert!(start_event.tool_name().is_none());
888    }
889
890    #[test]
891    fn test_skill_load_event() {
892        let event = SkillLoadEvent {
893            skill_name: "test-skill".to_string(),
894            tool_names: vec!["tool1".to_string(), "tool2".to_string()],
895            version: Some("1.0.0".to_string()),
896            description: Some("A test skill".to_string()),
897            loaded_at: 1234567890,
898        };
899
900        assert_eq!(event.skill_name, "test-skill");
901        assert_eq!(event.tool_names.len(), 2);
902        assert_eq!(event.version, Some("1.0.0".to_string()));
903        assert_eq!(event.loaded_at, 1234567890);
904    }
905
906    #[test]
907    fn test_skill_unload_event() {
908        let event = SkillUnloadEvent {
909            skill_name: "test-skill".to_string(),
910            tool_names: vec!["tool1".to_string(), "tool2".to_string()],
911            duration_ms: 60000,
912        };
913
914        assert_eq!(event.skill_name, "test-skill");
915        assert_eq!(event.tool_names.len(), 2);
916        assert_eq!(event.duration_ms, 60000);
917    }
918
919    #[test]
920    fn test_hook_event_skill_name() {
921        let load_event = HookEvent::SkillLoad(SkillLoadEvent {
922            skill_name: "my-skill".to_string(),
923            tool_names: vec!["tool1".to_string()],
924            version: None,
925            description: None,
926            loaded_at: 0,
927        });
928
929        let unload_event = HookEvent::SkillUnload(SkillUnloadEvent {
930            skill_name: "my-skill".to_string(),
931            tool_names: vec!["tool1".to_string()],
932            duration_ms: 1000,
933        });
934
935        assert_eq!(load_event.event_type(), HookEventType::SkillLoad);
936        assert_eq!(load_event.skill_name(), Some("my-skill"));
937        assert_eq!(load_event.session_id(), ""); // Skills are global
938
939        assert_eq!(unload_event.event_type(), HookEventType::SkillUnload);
940        assert_eq!(unload_event.skill_name(), Some("my-skill"));
941        assert_eq!(unload_event.session_id(), ""); // Skills are global
942
943        // Non-skill events return None for skill_name
944        let pre_tool = HookEvent::PreToolUse(PreToolUseEvent {
945            session_id: "s1".to_string(),
946            tool: "Bash".to_string(),
947            args: serde_json::json!({}),
948            working_directory: "/".to_string(),
949            recent_tools: vec![],
950        });
951        assert!(pre_tool.skill_name().is_none());
952    }
953
954    #[test]
955    fn test_skill_event_serialization() {
956        let event = HookEvent::SkillLoad(SkillLoadEvent {
957            skill_name: "test-skill".to_string(),
958            tool_names: vec!["tool1".to_string()],
959            version: Some("1.0.0".to_string()),
960            description: None,
961            loaded_at: 1234567890,
962        });
963
964        let json = serde_json::to_string(&event).unwrap();
965        assert!(json.contains("skill_load"));
966        assert!(json.contains("test-skill"));
967        assert!(json.contains("1.0.0"));
968
969        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
970        assert_eq!(parsed.event_type(), HookEventType::SkillLoad);
971        assert_eq!(parsed.skill_name(), Some("test-skill"));
972    }
973
974    #[test]
975    fn test_hook_event_type_display_new_variants() {
976        assert_eq!(HookEventType::PrePrompt.to_string(), "pre_prompt");
977        assert_eq!(HookEventType::PostResponse.to_string(), "post_response");
978        assert_eq!(HookEventType::OnError.to_string(), "on_error");
979    }
980
981    #[test]
982    fn test_pre_prompt_event() {
983        let event = PrePromptEvent {
984            session_id: "s1".to_string(),
985            prompt: "Fix the bug".to_string(),
986            system_prompt: Some("You are helpful".to_string()),
987            message_count: 5,
988        };
989
990        assert_eq!(event.session_id, "s1");
991        assert_eq!(event.prompt, "Fix the bug");
992        assert_eq!(event.message_count, 5);
993
994        let hook_event = HookEvent::PrePrompt(event);
995        assert_eq!(hook_event.event_type(), HookEventType::PrePrompt);
996        assert_eq!(hook_event.session_id(), "s1");
997        assert!(hook_event.tool_name().is_none());
998        assert!(hook_event.skill_name().is_none());
999    }
1000
1001    #[test]
1002    fn test_post_response_event() {
1003        let event = PostResponseEvent {
1004            session_id: "s1".to_string(),
1005            response_text: "Done!".to_string(),
1006            tool_calls_count: 3,
1007            usage: TokenUsageInfo {
1008                prompt_tokens: 100,
1009                completion_tokens: 50,
1010                total_tokens: 150,
1011            },
1012            duration_ms: 2000,
1013        };
1014
1015        assert_eq!(event.response_text, "Done!");
1016        assert_eq!(event.tool_calls_count, 3);
1017        assert_eq!(event.usage.total_tokens, 150);
1018
1019        let hook_event = HookEvent::PostResponse(event);
1020        assert_eq!(hook_event.event_type(), HookEventType::PostResponse);
1021        assert_eq!(hook_event.session_id(), "s1");
1022    }
1023
1024    #[test]
1025    fn test_on_error_event() {
1026        let event = OnErrorEvent {
1027            session_id: "s1".to_string(),
1028            error_type: ErrorType::ToolFailure,
1029            error_message: "Command failed with exit code 1".to_string(),
1030            context: serde_json::json!({"tool": "Bash", "command": "false"}),
1031        };
1032
1033        assert_eq!(event.error_type.to_string(), "tool_failure");
1034        assert_eq!(event.error_message, "Command failed with exit code 1");
1035
1036        let hook_event = HookEvent::OnError(event);
1037        assert_eq!(hook_event.event_type(), HookEventType::OnError);
1038        assert_eq!(hook_event.session_id(), "s1");
1039    }
1040
1041    #[test]
1042    fn test_error_type_display() {
1043        assert_eq!(ErrorType::ToolFailure.to_string(), "tool_failure");
1044        assert_eq!(ErrorType::LlmFailure.to_string(), "llm_failure");
1045        assert_eq!(ErrorType::PermissionDenied.to_string(), "permission_denied");
1046        assert_eq!(ErrorType::Timeout.to_string(), "timeout");
1047        assert_eq!(ErrorType::Other.to_string(), "other");
1048    }
1049
1050    #[test]
1051    fn test_new_event_serialization() {
1052        // PrePrompt
1053        let event = HookEvent::PrePrompt(PrePromptEvent {
1054            session_id: "s1".to_string(),
1055            prompt: "Hello".to_string(),
1056            system_prompt: None,
1057            message_count: 0,
1058        });
1059        let json = serde_json::to_string(&event).unwrap();
1060        assert!(json.contains("pre_prompt"));
1061        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
1062        assert_eq!(parsed.event_type(), HookEventType::PrePrompt);
1063
1064        // PostResponse
1065        let event = HookEvent::PostResponse(PostResponseEvent {
1066            session_id: "s1".to_string(),
1067            response_text: "Hi".to_string(),
1068            tool_calls_count: 0,
1069            usage: TokenUsageInfo {
1070                prompt_tokens: 10,
1071                completion_tokens: 5,
1072                total_tokens: 15,
1073            },
1074            duration_ms: 100,
1075        });
1076        let json = serde_json::to_string(&event).unwrap();
1077        assert!(json.contains("post_response"));
1078        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
1079        assert_eq!(parsed.event_type(), HookEventType::PostResponse);
1080
1081        // OnError
1082        let event = HookEvent::OnError(OnErrorEvent {
1083            session_id: "s1".to_string(),
1084            error_type: ErrorType::LlmFailure,
1085            error_message: "API timeout".to_string(),
1086            context: serde_json::json!({}),
1087        });
1088        let json = serde_json::to_string(&event).unwrap();
1089        assert!(json.contains("on_error"));
1090        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
1091        assert_eq!(parsed.event_type(), HookEventType::OnError);
1092    }
1093}