bamboo_hooks/
dispatcher.rs1use std::path::PathBuf;
2use std::sync::Arc;
3use std::time::Instant;
4
5use bamboo_agent_core::{AgentHook, Session};
6use bamboo_config::LifecycleHooksConfig;
7use bamboo_domain::{AgentHookPoint, HookPayload, HookResult};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct HookRunOutcome {
12 pub decision: HookResult,
13 pub injected_contexts: Vec<String>,
14 pub plugin_contexts: Vec<crate::portable::PluginContext>,
15}
16
17impl Default for HookRunOutcome {
18 fn default() -> Self {
19 Self {
20 decision: HookResult::Continue,
21 injected_contexts: Vec::new(),
22 plugin_contexts: Vec::new(),
23 }
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct HookExecution {
30 pub hook_name: String,
31 pub point: AgentHookPoint,
32 pub duration_ms: u64,
33 pub result: HookResult,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Default)]
38pub struct HookDispatchReport {
39 pub outcome: HookRunOutcome,
40 pub executions: Vec<HookExecution>,
41}
42
43#[derive(Clone, Default)]
45pub struct HookDispatcher {
46 hooks: Vec<Arc<dyn AgentHook>>,
47}
48
49impl HookDispatcher {
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn register(&mut self, hook: Arc<dyn AgentHook>) {
56 self.hooks.push(hook);
57 self.hooks.sort_by_key(|hook| hook.priority());
58 }
59
60 pub fn with_lifecycle_config(
63 &self,
64 config: &LifecycleHooksConfig,
65 fallback_cwd: Option<PathBuf>,
66 ) -> Self {
67 let mut dispatcher = self.clone();
68 crate::configured::register_configured_hooks(&mut dispatcher, config, fallback_cwd);
69 dispatcher
70 }
71
72 pub async fn run_hooks(
73 &self,
74 point: AgentHookPoint,
75 payload: &HookPayload,
76 session: &Session,
77 ) -> HookDispatchReport {
78 self.run_hooks_with_control(point, payload, session, true)
79 .await
80 }
81
82 pub async fn run_observer_hooks(
85 &self,
86 point: AgentHookPoint,
87 payload: &HookPayload,
88 session: &Session,
89 ) -> HookDispatchReport {
90 self.run_hooks_with_control(point, payload, session, false)
91 .await
92 }
93
94 async fn run_hooks_with_control(
95 &self,
96 point: AgentHookPoint,
97 payload: &HookPayload,
98 session: &Session,
99 honor_control_decisions: bool,
100 ) -> HookDispatchReport {
101 let mut report = HookDispatchReport::default();
102
103 for hook in &self.hooks {
104 if hook.point() != point || !hook.matches(payload) {
105 continue;
106 }
107
108 let started = Instant::now();
109 let result = hook.run(point, payload, session).await;
110 report.executions.push(HookExecution {
111 hook_name: hook.name().to_string(),
112 point,
113 duration_ms: started.elapsed().as_millis() as u64,
114 result: result.clone(),
115 });
116
117 let (result, mut contexts) = unwrap_context_result(result);
118 report.outcome.injected_contexts.append(&mut contexts);
119
120 match &result {
121 HookResult::Abort { .. }
122 | HookResult::Suspend { .. }
123 | HookResult::Deny { .. }
124 | HookResult::Ask => {
125 if honor_control_decisions {
126 report.outcome.decision = result;
127 return report;
128 }
129 }
130 HookResult::InjectContext { text } => {
131 report.outcome.injected_contexts.push(text.clone());
132 }
133 HookResult::Mutated => {
134 if matches!(report.outcome.decision, HookResult::Continue) {
135 report.outcome.decision = HookResult::Mutated;
136 }
137 }
138 HookResult::Allow => report.outcome.decision = HookResult::Allow,
139 HookResult::Continue => {}
140 HookResult::WithContext { .. } => unreachable!("context results are unwrapped"),
141 }
142 }
143
144 report
145 }
146
147 pub fn has_hooks_for(&self, point: AgentHookPoint) -> bool {
148 self.hooks.iter().any(|hook| hook.point() == point)
149 }
150
151 pub fn len(&self) -> usize {
152 self.hooks.len()
153 }
154
155 pub fn is_empty(&self) -> bool {
156 self.hooks.is_empty()
157 }
158}
159
160fn unwrap_context_result(mut result: HookResult) -> (HookResult, Vec<String>) {
161 let mut contexts = Vec::new();
162 while let HookResult::WithContext {
163 result: inner,
164 text,
165 } = result
166 {
167 if !text.trim().is_empty() {
168 contexts.push(text);
169 }
170 result = *inner;
171 }
172 (result, contexts)
173}