@@ -27,6 +27,7 @@ pub struct Orchestrator<S> {
2727 agent : Agent ,
2828 event : Event ,
2929 error_tracker : ToolErrorTracker ,
30+ hook : Arc < Hook > ,
3031}
3132
3233impl < S : AgentService > Orchestrator < S > {
@@ -47,6 +48,7 @@ impl<S: AgentService> Orchestrator<S> {
4748 tool_definitions : Default :: default ( ) ,
4849 models : Default :: default ( ) ,
4950 error_tracker : Default :: default ( ) ,
51+ hook : Arc :: new ( Hook :: default ( ) ) ,
5052 }
5153 }
5254
@@ -58,11 +60,10 @@ impl<S: AgentService> Orchestrator<S> {
5860 // Helper function to get all tool results from a vector of tool calls
5961 #[ async_recursion]
6062 async fn execute_tool_calls < ' a > (
61- & self ,
63+ & mut self ,
6264 tool_calls : & [ ToolCallFull ] ,
6365 tool_context : & ToolCallContext ,
6466 ) -> anyhow:: Result < Vec < ( ToolCallFull , ToolResult ) > > {
65- let agent = & self . agent ;
6667 // Always process tool calls sequentially
6768 let mut tool_call_records = Vec :: with_capacity ( tool_calls. len ( ) ) ;
6869
@@ -80,22 +81,42 @@ impl<S: AgentService> Orchestrator<S> {
8081 . await ?;
8182 }
8283
84+ // Fire the ToolcallStart lifecycle event
85+ let toolcall_start_event = LifecycleEvent :: ToolcallStart ( EventData :: new (
86+ self . agent . clone ( ) ,
87+ self . agent . model . clone ( ) ,
88+ ToolcallStartPayload :: new ( tool_call. clone ( ) ) ,
89+ ) ) ;
90+ self . hook
91+ . handle ( & toolcall_start_event, & mut self . conversation )
92+ . await ?;
93+
8394 // Execute the tool
8495 let tool_result = self
8596 . services
86- . call ( agent, tool_context, tool_call. clone ( ) )
97+ . call ( & self . agent , tool_context, tool_call. clone ( ) )
8798 . await ;
8899
89100 if tool_result. is_error ( ) {
90101 warn ! (
91- agent_id = %agent. id,
102+ agent_id = %self . agent. id,
92103 name = %tool_call. name,
93104 arguments = %tool_call. arguments. to_owned( ) . into_string( ) ,
94105 output = ?tool_result. output,
95106 "Tool call failed" ,
96107 ) ;
97108 }
98109
110+ // Fire the ToolcallEnd lifecycle event
111+ let toolcall_end_event = LifecycleEvent :: ToolcallEnd ( EventData :: new (
112+ self . agent . clone ( ) ,
113+ self . agent . model . clone ( ) ,
114+ ToolcallEndPayload :: new ( tool_result. clone ( ) ) ,
115+ ) ) ;
116+ self . hook
117+ . handle ( & toolcall_end_event, & mut self . conversation )
118+ . await ?;
119+
99120 // Send the end notification for system tools and not agent as a tool
100121 if is_system_tool {
101122 self . send ( ChatResponse :: ToolCallEnd ( tool_result. clone ( ) ) )
@@ -118,11 +139,10 @@ impl<S: AgentService> Orchestrator<S> {
118139
119140 // Returns if agent supports tool or not.
120141 fn is_tool_supported ( & self ) -> anyhow:: Result < bool > {
121- let agent = & self . agent ;
122- let model_id = & agent. model ;
142+ let model_id = & self . agent . model ;
123143
124144 // Check if at agent level tool support is defined
125- let tool_supported = match agent. tool_supported {
145+ let tool_supported = match self . agent . tool_supported {
126146 Some ( tool_supported) => tool_supported,
127147 None => {
128148 // If not defined at agent level, check model level
@@ -135,7 +155,7 @@ impl<S: AgentService> Orchestrator<S> {
135155 } ;
136156
137157 debug ! (
138- agent_id = %agent. id,
158+ agent_id = %self . agent. id,
139159 model_id = %model_id,
140160 tool_supported,
141161 "Tool support check"
@@ -172,16 +192,15 @@ impl<S: AgentService> Orchestrator<S> {
172192 }
173193 /// Checks if compaction is needed and performs it if necessary
174194 fn check_and_compact ( & self , context : & Context ) -> anyhow:: Result < Option < Context > > {
175- let agent = & self . agent ;
176195 // Estimate token count for compaction decision
177196 let token_count = context. token_count ( ) ;
178- if agent. compact . should_compact ( context, * token_count) {
179- info ! ( agent_id = %agent. id, "Compaction needed" ) ;
180- Compactor :: new ( agent. compact . clone ( ) , self . environment . clone ( ) )
197+ if self . agent . compact . should_compact ( context, * token_count) {
198+ info ! ( agent_id = %self . agent. id, "Compaction needed" ) ;
199+ Compactor :: new ( self . agent . compact . clone ( ) , self . environment . clone ( ) )
181200 . compact ( context. clone ( ) , false )
182201 . map ( Some )
183202 } else {
184- debug ! ( agent_id = %agent. id, "Compaction not needed" ) ;
203+ debug ! ( agent_id = %self . agent. id, "Compaction not needed" ) ;
185204 Ok ( None )
186205 }
187206 }
@@ -207,8 +226,15 @@ impl<S: AgentService> Orchestrator<S> {
207226
208227 let mut context = self . conversation . context . clone ( ) . unwrap_or_default ( ) ;
209228
210- // Create agent reference for the rest of the method
211- let agent = & self . agent ;
229+ // Fire the Start lifecycle event
230+ let start_event = LifecycleEvent :: Start ( EventData :: new (
231+ self . agent . clone ( ) ,
232+ model_id. clone ( ) ,
233+ StartPayload ,
234+ ) ) ;
235+ self . hook
236+ . handle ( & start_event, & mut self . conversation )
237+ . await ?;
212238
213239 // Signals that the loop should suspend (task may or may not be completed)
214240 let mut should_yield = false ;
@@ -219,8 +245,7 @@ impl<S: AgentService> Orchestrator<S> {
219245 let mut request_count = 0 ;
220246
221247 // Retrieve the number of requests allowed per tick.
222- let max_requests_per_turn = agent. max_requests_per_turn ;
223-
248+ let max_requests_per_turn = self . agent . max_requests_per_turn ;
224249 let tool_context =
225250 ToolCallContext :: new ( self . conversation . metrics . clone ( ) ) . sender ( self . sender . clone ( ) ) ;
226251
@@ -233,12 +258,22 @@ impl<S: AgentService> Orchestrator<S> {
233258 self . conversation . context = Some ( context. clone ( ) ) ;
234259 self . services . update ( self . conversation . clone ( ) ) . await ?;
235260
261+ // Fire the Request lifecycle event
262+ let request_event = LifecycleEvent :: Request ( EventData :: new (
263+ self . agent . clone ( ) ,
264+ model_id. clone ( ) ,
265+ RequestPayload :: new ( request_count) ,
266+ ) ) ;
267+ self . hook
268+ . handle ( & request_event, & mut self . conversation )
269+ . await ?;
270+
236271 let message = crate :: retry:: retry_with_config (
237272 & self . environment . retry_config ,
238273 || self . execute_chat_turn ( & model_id, context. clone ( ) , context. is_reasoning_supported ( ) ) ,
239274 self . sender . as_ref ( ) . map ( |sender| {
240275 let sender = sender. clone ( ) ;
241- let agent_id = agent. id . clone ( ) ;
276+ let agent_id = self . agent . id . clone ( ) ;
242277 let model_id = model_id. clone ( ) ;
243278 move |error : & anyhow:: Error , duration : Duration | {
244279 let root_cause = error. root_cause ( ) ;
@@ -252,15 +287,25 @@ impl<S: AgentService> Orchestrator<S> {
252287 } ) ,
253288 ) . await ?;
254289
290+ // Fire the Response lifecycle event
291+ let response_event = LifecycleEvent :: Response ( EventData :: new (
292+ self . agent . clone ( ) ,
293+ model_id. clone ( ) ,
294+ ResponsePayload :: new ( message. clone ( ) ) ,
295+ ) ) ;
296+ self . hook
297+ . handle ( & response_event, & mut self . conversation )
298+ . await ?;
299+
255300 // TODO: Add a unit test in orch spec, to guarantee that compaction is
256- // triggered after receiving the response Trigger compaction after
301+ // triggered after receiving the response
257302 // making a request NOTE: Ideally compaction should be implemented
258303 // as a transformer
259304 if let Some ( c_context) = self . check_and_compact ( & context) ? {
260- info ! ( agent_id = %agent. id, "Using compacted context from execution" ) ;
305+ info ! ( agent_id = %self . agent. id, "Using compacted context from execution" ) ;
261306 context = c_context;
262307 } else {
263- debug ! ( agent_id = %agent. id, "No compaction was needed" ) ;
308+ debug ! ( agent_id = %self . agent. id, "No compaction was needed" ) ;
264309 }
265310
266311 info ! (
@@ -274,8 +319,7 @@ impl<S: AgentService> Orchestrator<S> {
274319 "Processing usage information"
275320 ) ;
276321
277- debug ! ( agent_id = %agent. id, tool_call_count = message. tool_calls. len( ) , "Tool call count" ) ;
278-
322+ debug ! ( agent_id = %self . agent. id, tool_call_count = message. tool_calls. len( ) , "Tool call count" ) ;
279323 // Turn is completed, if finish_reason is 'stop'. Gemini models return stop as
280324 // finish reason with tool calls.
281325 is_complete =
@@ -340,7 +384,7 @@ impl<S: AgentService> Orchestrator<S> {
340384 // Check if agent has reached the maximum request per turn limit
341385 if request_count >= max_request_allowed {
342386 warn ! (
343- agent_id = %agent. id,
387+ agent_id = %self . agent. id,
344388 model_id = %model_id,
345389 request_count,
346390 max_request_allowed,
@@ -372,6 +416,18 @@ impl<S: AgentService> Orchestrator<S> {
372416
373417 self . services . update ( self . conversation . clone ( ) ) . await ?;
374418
419+ // Fire the End lifecycle event
420+ self . hook
421+ . handle (
422+ & LifecycleEvent :: End ( EventData :: new (
423+ self . agent . clone ( ) ,
424+ model_id. clone ( ) ,
425+ EndPayload ,
426+ ) ) ,
427+ & mut self . conversation ,
428+ )
429+ . await ?;
430+
375431 // Signal Task Completion
376432 if is_complete {
377433 self . send ( ChatResponse :: TaskComplete ) . await ?;
0 commit comments