Skip to content

Commit b518be2

Browse files
committed
feat(hooks): integrate SessionStart/SessionEnd hooks
1 parent 951e159 commit b518be2

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

src/agent.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ use crate::build;
2828
use crate::bus::{Bus, BusEvent, SubagentStatus, ToolEvent, ToolStatus};
2929
use crate::cache_tracker::CacheTracker;
3030
use crate::compaction::CompactionEvent;
31+
use crate::hooks::config::{HookEvent, load_hooks_config};
32+
use crate::hooks::execute::{execute_hook, HookResult};
33+
use crate::hooks::registry::{HookContext, HookRegistry};
34+
use crate::hooks::types::HookInput;
3135
use crate::id;
3236
use crate::logging;
3337
use crate::message::{
@@ -47,6 +51,7 @@ use std::path::PathBuf;
4751
use std::sync::{Arc, LazyLock, Mutex as StdMutex};
4852
use std::time::{Duration, Instant};
4953
use tokio::sync::{broadcast, mpsc};
54+
use tracing::debug;
5055

5156
use interrupts::{NoToolCallOutcome, PostToolInterruptOutcome};
5257
pub use jcode_agent_runtime::{
@@ -324,9 +329,37 @@ impl Agent {
324329
agent.session.parent_id.clone(),
325330
false,
326331
);
332+
let session_id = agent.session.id.clone();
333+
let cwd = agent.session.working_dir.clone().unwrap_or_default();
334+
tokio::spawn(async move {
335+
if let Some(hook_result) = Self::execute_session_start_hook(&session_id, &cwd).await {
336+
debug!("SessionStart hook executed: {:?}", hook_result);
337+
}
338+
});
327339
agent
328340
}
329341

342+
async fn execute_session_start_hook(session_id: &str, cwd: &str) -> Option<HookResult> {
343+
let config = load_hooks_config();
344+
let registry = HookRegistry::from_config(config);
345+
if registry.is_empty() {
346+
return None;
347+
}
348+
let hook_ctx = HookContext::for_session_start(session_id.to_string(), cwd.to_string());
349+
let hook_input = HookInput::for_session_start(session_id.to_string(), cwd.to_string());
350+
let matching = registry.get_matching(&HookEvent::SessionStart, &hook_ctx);
351+
if matching.is_empty() {
352+
return None;
353+
}
354+
match execute_hook(matching[0], &hook_input).await {
355+
Ok(result) => Some(result),
356+
Err(e) => {
357+
debug!("SessionStart hook error: {}", e);
358+
None
359+
}
360+
}
361+
}
362+
330363
pub fn new_with_session(
331364
provider: Arc<dyn Provider>,
332365
registry: Registry,
@@ -813,13 +846,42 @@ impl Agent {
813846
&self.provider.model(),
814847
crate::telemetry::SessionEndReason::NormalExit,
815848
);
849+
let session_id = self.session.id.clone();
850+
Self::execute_session_end_hook(&session_id);
816851
self.persist_soft_interrupt_snapshot();
817852
self.session.mark_closed();
818853
if !self.session.messages.is_empty() {
819854
self.persist_session_best_effort("session close state");
820855
}
821856
}
822857

858+
fn execute_session_end_hook(session_id: &str) {
859+
let config = load_hooks_config();
860+
let registry = HookRegistry::from_config(config);
861+
if registry.is_empty() {
862+
return;
863+
}
864+
let hook_ctx = HookContext::for_session_end(session_id.to_string());
865+
let hook_input = HookInput::for_session_end(session_id.to_string());
866+
let matching = registry.get_matching(&HookEvent::SessionEnd, &hook_ctx);
867+
for handler in matching {
868+
let runtime = tokio::runtime::Handle::current();
869+
let result = runtime.block_on(execute_hook(handler, &hook_input));
870+
match result {
871+
Ok(HookResult::Continue(_)) => {
872+
debug!("SessionEnd hook completed for {}", session_id);
873+
}
874+
Ok(HookResult::Failed { error }) => {
875+
debug!("SessionEnd hook failed for {}: {}", session_id, error);
876+
}
877+
Err(e) => {
878+
debug!("SessionEnd hook error for {}: {}", session_id, e);
879+
}
880+
_ => {}
881+
}
882+
}
883+
}
884+
823885
pub fn mark_crashed(&mut self, message: Option<String>) {
824886
crate::telemetry::record_crash(
825887
self.provider.name(),

0 commit comments

Comments
 (0)