Skip to content

Commit fa5b5a4

Browse files
tusharmathautofix-ci[bot]laststylebender14
authored
refactor(hook): add lifecycle hook system (#2284)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: laststylebender14 <ran.mahadik14@gmail.com> Co-authored-by: laststylebender <43403528+laststylebender14@users.noreply.github.com>
1 parent 228f77c commit fa5b5a4

8 files changed

Lines changed: 1148 additions & 30 deletions

File tree

crates/forge_app/src/orch.rs

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub struct Orchestrator<S> {
2727
agent: Agent,
2828
event: Event,
2929
error_tracker: ToolErrorTracker,
30+
hook: Arc<Hook>,
3031
}
3132

3233
impl<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?;

crates/forge_domain/src/agent.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{
99

1010
/// Runtime agent representation with required model and provider
1111
/// Created by converting AgentDefinition with resolved defaults
12-
#[derive(Debug, Clone, Setters)]
12+
#[derive(Debug, Clone, PartialEq, Setters)]
1313
#[setters(strip_option, into)]
1414
pub struct Agent {
1515
/// Flag to enable/disable tool support for this agent.

crates/forge_domain/src/chat_response.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ impl ChatResponse {
7979
}
8080
}
8181

82-
#[derive(Debug, Clone)]
82+
#[derive(Debug, Clone, PartialEq, Eq)]
8383
pub enum InterruptionReason {
8484
MaxToolFailurePerTurnLimitReached {
8585
limit: u64,

crates/forge_domain/src/env.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const VERSION: &str = match option_env!("APP_VERSION") {
1313
None => env!("CARGO_PKG_VERSION"),
1414
};
1515

16-
#[derive(Debug, Setters, Clone, Serialize, Deserialize, fake::Dummy)]
16+
#[derive(Debug, Setters, Clone, PartialEq, Serialize, Deserialize, fake::Dummy)]
1717
#[serde(rename_all = "camelCase")]
1818
#[setters(strip_option)]
1919
/// Represents the environment in which the application is running.

crates/forge_domain/src/event.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ impl EventValue {
8484
#[serde(transparent)]
8585
pub struct UserPrompt(String);
8686

87-
#[derive(Clone, Serialize, Deserialize, Debug, Setters)]
87+
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Setters)]
8888
pub struct EventContext {
8989
event: EventContextValue,
9090
suggestions: Vec<String>,
9191
variables: HashMap<String, Value>,
9292
current_date: String,
9393
}
9494

95-
#[derive(Clone, Serialize, Deserialize, Debug, Setters)]
95+
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Setters)]
9696
pub struct EventContextValue {
9797
pub name: String,
9898
pub value: String,

0 commit comments

Comments
 (0)