kaizen/collect/hooks/
normalize.rs1use crate::collect::hooks::{EventKind as HookKind, HookEvent};
5use crate::core::event::{Event, EventKind, EventSource, SessionStatus};
6
7pub fn hook_to_event(h: &HookEvent, seq: u64) -> Event {
9 let cost_usd_e6 = h
10 .payload
11 .get("total_cost_usd")
12 .and_then(|v| v.as_f64())
13 .map(|f| (f * 1_000_000.0) as i64);
14 Event {
15 session_id: h.session_id.clone(),
16 seq,
17 ts_ms: h.ts_ms,
18 ts_exact: true,
19 kind: EventKind::Hook,
20 source: EventSource::Hook,
21 tool: None,
22 tool_call_id: hook_tool_id(&h.payload),
23 tokens_in: None,
24 tokens_out: None,
25 reasoning_tokens: None,
26 cost_usd_e6,
27 stop_reason: None,
28 latency_ms: None,
29 ttft_ms: None,
30 retry_count: None,
31 context_used_tokens: None,
32 context_max_tokens: None,
33 cache_creation_tokens: None,
34 cache_read_tokens: None,
35 system_prompt_tokens: None,
36 payload: h.payload.clone(),
37 }
38}
39
40fn hook_tool_id(payload: &serde_json::Value) -> Option<String> {
41 ["tool_call_id", "tool_use_id", "call_id", "id"]
42 .iter()
43 .find_map(|k| payload.get(k).and_then(|v| v.as_str()))
44 .map(ToOwned::to_owned)
45}
46
47pub fn hook_to_status(kind: &HookKind) -> Option<SessionStatus> {
51 match kind {
52 HookKind::PreToolUse => Some(SessionStatus::Waiting),
53 HookKind::PostToolUse => Some(SessionStatus::Running),
54 HookKind::Stop => Some(SessionStatus::Done),
55 HookKind::SessionStart => Some(SessionStatus::Running),
56 HookKind::Unknown(_) => None,
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use crate::collect::hooks::HookEvent;
64 use serde_json::json;
65
66 fn make_event(kind: HookKind) -> HookEvent {
67 HookEvent {
68 kind,
69 session_id: "s1".to_string(),
70 ts_ms: 1000,
71 payload: json!({}),
72 }
73 }
74
75 #[test]
76 fn session_start_maps_running() {
77 assert_eq!(
78 hook_to_status(&HookKind::SessionStart),
79 Some(SessionStatus::Running)
80 );
81 }
82
83 #[test]
84 fn pre_tool_use_maps_waiting() {
85 assert_eq!(
86 hook_to_status(&HookKind::PreToolUse),
87 Some(SessionStatus::Waiting)
88 );
89 }
90
91 #[test]
92 fn post_tool_use_maps_running() {
93 assert_eq!(
94 hook_to_status(&HookKind::PostToolUse),
95 Some(SessionStatus::Running)
96 );
97 }
98
99 #[test]
100 fn stop_maps_done() {
101 assert_eq!(hook_to_status(&HookKind::Stop), Some(SessionStatus::Done));
102 }
103
104 #[test]
105 fn unknown_maps_none() {
106 assert_eq!(hook_to_status(&HookKind::Unknown("x".to_string())), None);
107 }
108
109 #[test]
110 fn hook_to_event_kind_is_hook() {
111 let h = make_event(HookKind::Stop);
112 let ev = hook_to_event(&h, 5);
113 assert_eq!(ev.kind, EventKind::Hook);
114 assert_eq!(ev.seq, 5);
115 assert_eq!(ev.session_id, "s1");
116 }
117
118 #[test]
119 fn hook_to_event_maps_total_cost_usd_to_microdollars() {
120 let h = HookEvent {
121 kind: HookKind::Stop,
122 session_id: "s1".to_string(),
123 ts_ms: 1000,
124 payload: json!({ "total_cost_usd": 0.042 }),
125 };
126 let ev = hook_to_event(&h, 0);
127 assert_eq!(ev.cost_usd_e6, Some(42_000));
128 }
129}