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