1use async_trait::async_trait;
2use serde_json::Value;
3use std::sync::Arc;
4use std::time::Instant;
5use tracing::{debug, error, info, warn};
6
7use ai_agents_core::{AgentError, AgentResponse, ToolExecutionRecord};
8use ai_agents_hitl::{ApprovalRequest, ApprovalResolvedOutcome, ApprovalResult};
9use ai_agents_llm::{ChatMessage, LLMResponse};
10use ai_agents_memory::{MemoryBudgetEvent, MemoryCompressEvent, MemoryEvictEvent};
11use ai_agents_tools::ToolResult;
12
13fn preview_text(text: &str, max_chars: usize) -> String {
14 if max_chars == 0 {
15 return String::new();
16 }
17
18 let mut chars = text.chars();
19 let preview: String = chars.by_ref().take(max_chars).collect();
20 if chars.next().is_some() {
21 format!("{}...", preview)
22 } else {
23 text.to_string()
24 }
25}
26
27#[async_trait]
33pub trait AgentHooks: Send + Sync {
34 async fn on_message_received(&self, _message: &str) {}
35
36 async fn on_llm_start(&self, _messages: &[ChatMessage]) {}
37
38 async fn on_llm_complete(&self, _response: &LLMResponse, _duration_ms: u64) {}
39
40 async fn on_tool_start(&self, _tool: &str, _args: &Value) {}
44
45 async fn on_tool_complete(&self, _tool: &str, _result: &ToolResult, _duration_ms: u64) {}
49
50 async fn on_tool_execution_record(&self, _record: &ToolExecutionRecord) {}
52
53 async fn on_state_transition(&self, _from: Option<&str>, _to: &str, _reason: &str) {}
54
55 async fn on_error(&self, _error: &AgentError) {}
56
57 async fn on_response(&self, _response: &AgentResponse) {}
61
62 async fn on_approval_requested(&self, _request: &ApprovalRequest) {}
63
64 async fn on_approval_result(&self, _request_id: &str, _result: &ApprovalResult) {}
65
66 async fn on_approval_resolved(
67 &self,
68 _request: &ApprovalRequest,
69 _raw_result: &ApprovalResult,
70 _outcome: &ApprovalResolvedOutcome,
71 ) {
72 }
73
74 async fn on_memory_compress(&self, _event: &MemoryCompressEvent) {}
75
76 async fn on_memory_evict(&self, _event: &MemoryEvictEvent) {}
77
78 async fn on_memory_budget_warning(&self, _event: &MemoryBudgetEvent) {}
79
80 async fn on_delegate_start(&self, _agent_id: &str, _state: &str) {}
82
83 async fn on_delegate_complete(&self, _agent_id: &str, _state: &str, _duration_ms: u64) {}
85
86 async fn on_concurrent_complete(
88 &self,
89 _agent_ids: &[String],
90 _strategy: &str,
91 _duration_ms: u64,
92 ) {
93 }
94
95 async fn on_group_chat_round(&self, _round: u32, _speaker: &str, _content: &str) {}
97
98 async fn on_pipeline_stage(&self, _stage: usize, _agent_id: &str, _duration_ms: u64) {}
100
101 async fn on_pipeline_complete(&self, _stages: usize, _duration_ms: u64) {}
103
104 async fn on_handoff_start(&self, _initial_agent: &str) {}
106
107 async fn on_handoff(&self, _from: &str, _to: &str, _reason: &str) {}
109
110 async fn on_persona_evolve(
112 &self,
113 _field: &str,
114 _old_value: &Value,
115 _new_value: &Value,
116 _reason: Option<&str>,
117 ) {
118 }
119
120 async fn on_secret_revealed(&self, _content: &str) {}
122
123 async fn on_facts_extracted(&self, _actor_id: &str, _facts: &[ai_agents_core::KeyFact]) {}
125
126 async fn on_actor_memory_loaded(&self, _actor_id: &str, _fact_count: usize) {}
128
129 async fn on_session_created(&self, _session_id: &str) {}
131
132 async fn on_sessions_expired(&self, _count: usize) {}
134
135 async fn on_relationship_loaded(
137 &self,
138 _actor_id: &str,
139 _relationship: &ai_agents_relationships::Relationship,
140 ) {
141 }
142
143 async fn on_relationship_change(
145 &self,
146 _actor_id: &str,
147 _changes: &[ai_agents_relationships::DimensionChange],
148 ) {
149 }
150
151 async fn on_notable_event(
153 &self,
154 _actor_id: &str,
155 _event: &ai_agents_relationships::RelationshipEvent,
156 ) {
157 }
158}
159
160pub struct NoopHooks;
161
162#[async_trait]
163impl AgentHooks for NoopHooks {}
164
165pub struct LoggingHooks {
166 prefix: String,
167}
168
169impl LoggingHooks {
170 pub fn new() -> Self {
171 Self {
172 prefix: "[Agent]".to_string(),
173 }
174 }
175
176 pub fn with_prefix(prefix: impl Into<String>) -> Self {
177 Self {
178 prefix: prefix.into(),
179 }
180 }
181}
182
183impl Default for LoggingHooks {
184 fn default() -> Self {
185 Self::new()
186 }
187}
188
189#[async_trait]
190impl AgentHooks for LoggingHooks {
191 async fn on_message_received(&self, message: &str) {
192 let preview = preview_text(message, 100);
193 info!("{} Message received: {}", self.prefix, preview);
194 }
195
196 async fn on_llm_start(&self, messages: &[ChatMessage]) {
197 debug!(
198 "{} LLM starting with {} messages",
199 self.prefix,
200 messages.len()
201 );
202 }
203
204 async fn on_llm_complete(&self, response: &LLMResponse, duration_ms: u64) {
205 info!(
206 "{} LLM complete in {}ms, tokens: {:?}",
207 self.prefix, duration_ms, response.usage
208 );
209 }
210
211 async fn on_tool_start(&self, tool: &str, args: &Value) {
212 debug!("{} Tool {} starting with args: {}", self.prefix, tool, args);
213 }
214
215 async fn on_tool_complete(&self, tool: &str, result: &ToolResult, duration_ms: u64) {
216 if result.success {
217 info!(
218 "{} Tool {} completed in {}ms",
219 self.prefix, tool, duration_ms
220 );
221 } else {
222 warn!(
223 "{} Tool {} failed in {}ms: {}",
224 self.prefix, tool, duration_ms, result.output
225 );
226 }
227 }
228
229 async fn on_state_transition(&self, from: Option<&str>, to: &str, reason: &str) {
230 info!(
231 "{} State transition: {:?} -> {} ({})",
232 self.prefix, from, to, reason
233 );
234 }
235
236 async fn on_error(&self, err: &AgentError) {
237 error!("{} Error: {}", self.prefix, err);
238 }
239
240 async fn on_response(&self, response: &AgentResponse) {
241 let preview = preview_text(&response.content, 100);
242 debug!("{} Response: {}", self.prefix, preview);
243 }
244
245 async fn on_approval_requested(&self, request: &ApprovalRequest) {
246 info!(
247 "{} Approval requested [{}]: {}",
248 self.prefix, request.id, request.message
249 );
250 }
251
252 async fn on_approval_result(&self, request_id: &str, result: &ApprovalResult) {
253 match result {
254 ApprovalResult::Approved => {
255 info!("{} Approval [{}]: approved", self.prefix, request_id);
256 }
257 ApprovalResult::Rejected { reason } => {
258 warn!(
259 "{} Approval [{}]: rejected ({:?})",
260 self.prefix, request_id, reason
261 );
262 }
263 ApprovalResult::Modified { .. } => {
264 info!(
265 "{} Approval [{}]: approved with modifications",
266 self.prefix, request_id
267 );
268 }
269 ApprovalResult::Timeout => {
270 warn!("{} Approval [{}]: timeout", self.prefix, request_id);
271 }
272 }
273 }
274
275 async fn on_memory_compress(&self, event: &MemoryCompressEvent) {
276 info!(
277 "{} Memory compressed: {} messages, ratio: {:.2}",
278 self.prefix, event.messages_compressed, event.compression_ratio
279 );
280 }
281
282 async fn on_memory_evict(&self, event: &MemoryEvictEvent) {
283 warn!(
284 "{} Memory evicted: {} messages, reason: {:?}",
285 self.prefix, event.messages_evicted, event.reason
286 );
287 }
288
289 async fn on_memory_budget_warning(&self, event: &MemoryBudgetEvent) {
290 warn!(
291 "{} Memory budget warning: {} at {:.1}% ({}/{} tokens)",
292 self.prefix,
293 event.component,
294 event.usage_percent,
295 event.used_tokens,
296 event.budget_tokens
297 );
298 }
299
300 async fn on_delegate_start(&self, agent_id: &str, state: &str) {
301 info!(
302 "{} Delegation started: agent={}, state={}",
303 self.prefix, agent_id, state
304 );
305 }
306
307 async fn on_delegate_complete(&self, agent_id: &str, state: &str, duration_ms: u64) {
308 info!(
309 "{} Delegation complete: agent={}, state={}, duration={}ms",
310 self.prefix, agent_id, state, duration_ms
311 );
312 }
313
314 async fn on_concurrent_complete(&self, agent_ids: &[String], strategy: &str, duration_ms: u64) {
315 info!(
316 "{} Concurrent complete: agents={:?}, strategy={}, duration={}ms",
317 self.prefix, agent_ids, strategy, duration_ms
318 );
319 }
320
321 async fn on_group_chat_round(&self, round: u32, speaker: &str, content: &str) {
322 let preview = preview_text(content, 80);
323 debug!(
324 "{} Group chat round {}: {} said: {}",
325 self.prefix, round, speaker, preview
326 );
327 }
328
329 async fn on_pipeline_stage(&self, stage: usize, agent_id: &str, duration_ms: u64) {
330 info!(
331 "{} Pipeline stage {}: agent={}, duration={}ms",
332 self.prefix, stage, agent_id, duration_ms
333 );
334 }
335
336 async fn on_pipeline_complete(&self, stages: usize, duration_ms: u64) {
337 info!(
338 "{} Pipeline complete: {} stages, duration={}ms",
339 self.prefix, stages, duration_ms
340 );
341 }
342
343 async fn on_handoff_start(&self, initial_agent: &str) {
344 info!(
345 "{} Handoff chain started: initial_agent={}",
346 self.prefix, initial_agent
347 );
348 }
349
350 async fn on_handoff(&self, from: &str, to: &str, reason: &str) {
351 info!("{} Handoff: {} -> {} ({})", self.prefix, from, to, reason);
352 }
353
354 async fn on_persona_evolve(
355 &self,
356 field: &str,
357 _old_value: &Value,
358 new_value: &Value,
359 reason: Option<&str>,
360 ) {
361 info!(
362 "{} Persona evolved: field={}, new_value={}, reason={}",
363 self.prefix,
364 field,
365 new_value,
366 reason.unwrap_or("(none)")
367 );
368 }
369
370 async fn on_secret_revealed(&self, content: &str) {
371 debug!("{}[secret_revealed] {}", self.prefix, content);
372 }
373
374 async fn on_facts_extracted(&self, actor_id: &str, facts: &[ai_agents_core::KeyFact]) {
375 debug!(
376 "{}[facts_extracted] actor={} count={}",
377 self.prefix,
378 actor_id,
379 facts.len()
380 );
381 }
382
383 async fn on_actor_memory_loaded(&self, actor_id: &str, fact_count: usize) {
384 debug!(
385 "{}[actor_memory_loaded] actor={} facts={}",
386 self.prefix, actor_id, fact_count
387 );
388 }
389
390 async fn on_session_created(&self, session_id: &str) {
391 debug!("{}[session_created] session={}", self.prefix, session_id);
392 }
393
394 async fn on_sessions_expired(&self, count: usize) {
395 debug!("{}[sessions_expired] count={}", self.prefix, count);
396 }
397
398 async fn on_relationship_loaded(
399 &self,
400 actor_id: &str,
401 relationship: &ai_agents_relationships::Relationship,
402 ) {
403 debug!(
404 "{}[relationship_loaded] actor={} dimensions={}",
405 self.prefix,
406 actor_id,
407 relationship.dimensions.len()
408 );
409 }
410
411 async fn on_relationship_change(
412 &self,
413 actor_id: &str,
414 changes: &[ai_agents_relationships::DimensionChange],
415 ) {
416 debug!(
417 "{}[relationship_change] actor={} changes={}",
418 self.prefix,
419 actor_id,
420 changes.len()
421 );
422 }
423
424 async fn on_notable_event(
425 &self,
426 actor_id: &str,
427 event: &ai_agents_relationships::RelationshipEvent,
428 ) {
429 debug!(
430 "{}[notable_event] actor={} significance={:.2} description={}",
431 self.prefix, actor_id, event.significance, event.description
432 );
433 }
434}
435
436pub struct CompositeHooks {
437 hooks: Vec<Arc<dyn AgentHooks>>,
438}
439
440impl CompositeHooks {
441 pub fn new() -> Self {
442 Self { hooks: Vec::new() }
443 }
444
445 #[allow(clippy::should_implement_trait)]
449 pub fn add(mut self, hooks: Arc<dyn AgentHooks>) -> Self {
450 self.hooks.push(hooks);
451 self
452 }
453
454 pub fn with_hooks(hooks: Vec<Arc<dyn AgentHooks>>) -> Self {
455 Self { hooks }
456 }
457}
458
459impl Default for CompositeHooks {
460 fn default() -> Self {
461 Self::new()
462 }
463}
464
465#[async_trait]
466impl AgentHooks for CompositeHooks {
467 async fn on_message_received(&self, message: &str) {
468 for hook in &self.hooks {
469 hook.on_message_received(message).await;
470 }
471 }
472
473 async fn on_llm_start(&self, messages: &[ChatMessage]) {
474 for hook in &self.hooks {
475 hook.on_llm_start(messages).await;
476 }
477 }
478
479 async fn on_llm_complete(&self, response: &LLMResponse, duration_ms: u64) {
480 for hook in &self.hooks {
481 hook.on_llm_complete(response, duration_ms).await;
482 }
483 }
484
485 async fn on_tool_start(&self, tool: &str, args: &Value) {
486 for hook in &self.hooks {
487 hook.on_tool_start(tool, args).await;
488 }
489 }
490
491 async fn on_tool_complete(&self, tool: &str, result: &ToolResult, duration_ms: u64) {
492 for hook in &self.hooks {
493 hook.on_tool_complete(tool, result, duration_ms).await;
494 }
495 }
496
497 async fn on_tool_execution_record(&self, record: &ToolExecutionRecord) {
498 for hook in &self.hooks {
499 hook.on_tool_execution_record(record).await;
500 }
501 }
502
503 async fn on_state_transition(&self, from: Option<&str>, to: &str, reason: &str) {
504 for hook in &self.hooks {
505 hook.on_state_transition(from, to, reason).await;
506 }
507 }
508
509 async fn on_error(&self, error: &AgentError) {
510 for hook in &self.hooks {
511 hook.on_error(error).await;
512 }
513 }
514
515 async fn on_response(&self, response: &AgentResponse) {
516 for hook in &self.hooks {
517 hook.on_response(response).await;
518 }
519 }
520
521 async fn on_approval_requested(&self, request: &ApprovalRequest) {
522 for hook in &self.hooks {
523 hook.on_approval_requested(request).await;
524 }
525 }
526
527 async fn on_approval_result(&self, request_id: &str, result: &ApprovalResult) {
528 for hook in &self.hooks {
529 hook.on_approval_result(request_id, result).await;
530 }
531 }
532
533 async fn on_approval_resolved(
534 &self,
535 request: &ApprovalRequest,
536 raw_result: &ApprovalResult,
537 outcome: &ApprovalResolvedOutcome,
538 ) {
539 for hook in &self.hooks {
540 hook.on_approval_resolved(request, raw_result, outcome)
541 .await;
542 }
543 }
544
545 async fn on_memory_compress(&self, event: &MemoryCompressEvent) {
546 for hook in &self.hooks {
547 hook.on_memory_compress(event).await;
548 }
549 }
550
551 async fn on_memory_evict(&self, event: &MemoryEvictEvent) {
552 for hook in &self.hooks {
553 hook.on_memory_evict(event).await;
554 }
555 }
556
557 async fn on_memory_budget_warning(&self, event: &MemoryBudgetEvent) {
558 for hook in &self.hooks {
559 hook.on_memory_budget_warning(event).await;
560 }
561 }
562
563 async fn on_delegate_start(&self, agent_id: &str, state: &str) {
564 for hook in &self.hooks {
565 hook.on_delegate_start(agent_id, state).await;
566 }
567 }
568
569 async fn on_delegate_complete(&self, agent_id: &str, state: &str, duration_ms: u64) {
570 for hook in &self.hooks {
571 hook.on_delegate_complete(agent_id, state, duration_ms)
572 .await;
573 }
574 }
575
576 async fn on_concurrent_complete(&self, agent_ids: &[String], strategy: &str, duration_ms: u64) {
577 for hook in &self.hooks {
578 hook.on_concurrent_complete(agent_ids, strategy, duration_ms)
579 .await;
580 }
581 }
582
583 async fn on_group_chat_round(&self, round: u32, speaker: &str, content: &str) {
584 for hook in &self.hooks {
585 hook.on_group_chat_round(round, speaker, content).await;
586 }
587 }
588
589 async fn on_pipeline_stage(&self, stage: usize, agent_id: &str, duration_ms: u64) {
590 for hook in &self.hooks {
591 hook.on_pipeline_stage(stage, agent_id, duration_ms).await;
592 }
593 }
594
595 async fn on_pipeline_complete(&self, stages: usize, duration_ms: u64) {
596 for hook in &self.hooks {
597 hook.on_pipeline_complete(stages, duration_ms).await;
598 }
599 }
600
601 async fn on_handoff_start(&self, initial_agent: &str) {
602 for hook in &self.hooks {
603 hook.on_handoff_start(initial_agent).await;
604 }
605 }
606
607 async fn on_handoff(&self, _from: &str, _to: &str, _reason: &str) {
608 for hook in &self.hooks {
609 hook.on_handoff(_from, _to, _reason).await;
610 }
611 }
612
613 async fn on_persona_evolve(
614 &self,
615 _field: &str,
616 _old_value: &Value,
617 _new_value: &Value,
618 _reason: Option<&str>,
619 ) {
620 for hook in &self.hooks {
621 hook.on_persona_evolve(_field, _old_value, _new_value, _reason)
622 .await;
623 }
624 }
625
626 async fn on_secret_revealed(&self, content: &str) {
627 for hook in &self.hooks {
628 hook.on_secret_revealed(content).await;
629 }
630 }
631
632 async fn on_facts_extracted(&self, actor_id: &str, facts: &[ai_agents_core::KeyFact]) {
633 for hook in &self.hooks {
634 hook.on_facts_extracted(actor_id, facts).await;
635 }
636 }
637
638 async fn on_actor_memory_loaded(&self, actor_id: &str, fact_count: usize) {
639 for hook in &self.hooks {
640 hook.on_actor_memory_loaded(actor_id, fact_count).await;
641 }
642 }
643
644 async fn on_session_created(&self, session_id: &str) {
645 for hook in &self.hooks {
646 hook.on_session_created(session_id).await;
647 }
648 }
649
650 async fn on_sessions_expired(&self, count: usize) {
651 for hook in &self.hooks {
652 hook.on_sessions_expired(count).await;
653 }
654 }
655
656 async fn on_relationship_loaded(
657 &self,
658 actor_id: &str,
659 relationship: &ai_agents_relationships::Relationship,
660 ) {
661 for hook in &self.hooks {
662 hook.on_relationship_loaded(actor_id, relationship).await;
663 }
664 }
665
666 async fn on_relationship_change(
667 &self,
668 actor_id: &str,
669 changes: &[ai_agents_relationships::DimensionChange],
670 ) {
671 for hook in &self.hooks {
672 hook.on_relationship_change(actor_id, changes).await;
673 }
674 }
675
676 async fn on_notable_event(
677 &self,
678 actor_id: &str,
679 event: &ai_agents_relationships::RelationshipEvent,
680 ) {
681 for hook in &self.hooks {
682 hook.on_notable_event(actor_id, event).await;
683 }
684 }
685}
686
687pub struct HookTimer {
688 start: Instant,
689}
690
691impl HookTimer {
692 pub fn start() -> Self {
693 Self {
694 start: Instant::now(),
695 }
696 }
697
698 pub fn elapsed_ms(&self) -> u64 {
699 self.start.elapsed().as_millis() as u64
700 }
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706 use parking_lot::Mutex;
707
708 struct RecordingHooks {
709 events: Arc<Mutex<Vec<String>>>,
710 }
711
712 impl RecordingHooks {
713 fn new() -> Self {
714 Self {
715 events: Arc::new(Mutex::new(Vec::new())),
716 }
717 }
718
719 fn events(&self) -> Vec<String> {
720 self.events.lock().clone()
721 }
722 }
723
724 #[async_trait]
725 impl AgentHooks for RecordingHooks {
726 async fn on_message_received(&self, message: &str) {
727 self.events
728 .lock()
729 .push(format!("message_received:{}", message));
730 }
731
732 async fn on_llm_start(&self, messages: &[ChatMessage]) {
733 self.events
734 .lock()
735 .push(format!("llm_start:{}", messages.len()));
736 }
737
738 async fn on_llm_complete(&self, _response: &LLMResponse, duration_ms: u64) {
739 self.events
740 .lock()
741 .push(format!("llm_complete:{}", duration_ms));
742 }
743
744 async fn on_tool_start(&self, tool: &str, _args: &Value) {
745 self.events.lock().push(format!("tool_start:{}", tool));
746 }
747
748 async fn on_tool_complete(&self, tool: &str, result: &ToolResult, duration_ms: u64) {
749 self.events.lock().push(format!(
750 "tool_complete:{}:{}:{}",
751 tool, result.success, duration_ms
752 ));
753 }
754
755 async fn on_state_transition(&self, from: Option<&str>, to: &str, reason: &str) {
756 self.events
757 .lock()
758 .push(format!("state_transition:{:?}:{}:{}", from, to, reason));
759 }
760
761 async fn on_error(&self, error: &AgentError) {
762 self.events.lock().push(format!("error:{}", error));
763 }
764
765 async fn on_response(&self, response: &AgentResponse) {
766 self.events
767 .lock()
768 .push(format!("response:{}", response.content.len()));
769 }
770
771 async fn on_approval_requested(&self, request: &ApprovalRequest) {
772 self.events
773 .lock()
774 .push(format!("approval_requested:{}", request.id));
775 }
776
777 async fn on_approval_result(&self, request_id: &str, result: &ApprovalResult) {
778 let status = approval_status(result);
779 self.events
780 .lock()
781 .push(format!("approval_result:{}:{}", request_id, status));
782 }
783
784 async fn on_approval_resolved(
785 &self,
786 request: &ApprovalRequest,
787 raw_result: &ApprovalResult,
788 outcome: &ApprovalResolvedOutcome,
789 ) {
790 self.events.lock().push(format!(
791 "approval_resolved:{}:{}:{}",
792 request.id,
793 approval_status(raw_result),
794 resolved_status(outcome)
795 ));
796 }
797 }
798
799 fn approval_status(result: &ApprovalResult) -> &'static str {
800 match result {
801 ApprovalResult::Approved => "approved",
802 ApprovalResult::Rejected { .. } => "rejected",
803 ApprovalResult::Modified { .. } => "modified",
804 ApprovalResult::Timeout => "timeout",
805 }
806 }
807
808 fn resolved_status(outcome: &ApprovalResolvedOutcome) -> &'static str {
809 match outcome {
810 ApprovalResolvedOutcome::Approved => "approved",
811 ApprovalResolvedOutcome::Rejected { .. } => "rejected",
812 ApprovalResolvedOutcome::Modified { .. } => "modified",
813 ApprovalResolvedOutcome::Error { .. } => "error",
814 }
815 }
816
817 #[tokio::test]
818 async fn test_noop_hooks() {
819 let hooks = NoopHooks;
820 hooks.on_message_received("test").await;
821 hooks.on_llm_start(&[]).await;
822 }
823
824 #[tokio::test]
825 async fn test_logging_hooks() {
826 let hooks = LoggingHooks::new();
827 hooks.on_message_received("test message").await;
828 hooks.on_llm_start(&[ChatMessage::user("hello")]).await;
829 }
830
831 #[test]
832 fn test_preview_text_handles_unicode_boundaries() {
833 let text = "제 이름은 Jay이고 가족관계 관련해서 계약서 내용을 확인하고 싶어서";
834 let preview = preview_text(text, 34);
835 assert!(preview.ends_with("..."));
836 assert!(preview.starts_with("제 이름은 Jay"));
837 }
838
839 #[tokio::test]
840 async fn test_recording_hooks() {
841 let hooks = RecordingHooks::new();
842
843 hooks.on_message_received("hello").await;
844 hooks.on_llm_start(&[ChatMessage::user("test")]).await;
845
846 let events = hooks.events();
847 assert_eq!(events.len(), 2);
848 assert!(events[0].contains("message_received"));
849 assert!(events[1].contains("llm_start"));
850 }
851
852 #[tokio::test]
853 async fn test_composite_hooks_with_vec() {
854 let hooks1 = Arc::new(RecordingHooks::new());
855 let hooks2 = Arc::new(RecordingHooks::new());
856
857 let composite = CompositeHooks::with_hooks(vec![
858 hooks1.clone() as Arc<dyn AgentHooks>,
859 hooks2.clone() as Arc<dyn AgentHooks>,
860 ]);
861
862 composite
863 .on_tool_start("calculator", &serde_json::json!({}))
864 .await;
865 let request = ApprovalRequest::new(
866 ai_agents_hitl::ApprovalTrigger::tool("calculator", serde_json::json!({})),
867 "Approve?",
868 );
869 composite
870 .on_approval_resolved(
871 &request,
872 &ApprovalResult::Timeout,
873 &ApprovalResolvedOutcome::Approved,
874 )
875 .await;
876
877 assert_eq!(
878 hooks1.events(),
879 vec![
880 "tool_start:calculator".to_string(),
881 format!("approval_resolved:{}:timeout:approved", request.id)
882 ]
883 );
884 assert_eq!(hooks1.events(), hooks2.events());
885 }
886
887 #[tokio::test]
888 async fn test_hook_timer() {
889 let timer = HookTimer::start();
890 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
891 let elapsed = timer.elapsed_ms();
892 assert!(elapsed >= 10);
893 }
894
895 #[test]
896 fn test_composite_hooks_default() {
897 let hooks = CompositeHooks::default();
898 assert!(hooks.hooks.is_empty());
899 }
900}