start of hooks engine (#13276)

(Experimental)

This PR adds a first MVP for hooks, with SessionStart and Stop

The core design is:

- hooks live in a dedicated engine under codex-rs/hooks
- each hook type has its own event-specific file
- hook execution is synchronous and blocks normal turn progression while
running
- matching hooks run in parallel, then their results are aggregated into
a normalized HookRunSummary

On the AppServer side, hooks are exposed as operational metadata rather
than transcript-native items:

- new live notifications: hook/started, hook/completed
- persisted/replayed hook results live on Turn.hookRuns
- we intentionally did not add hook-specific ThreadItem variants

Hooks messages are not persisted, they remain ephemeral. The context
changes they add are (they get appended to the user's prompt)
This commit is contained in:
Andrei Eternal
2026-03-10 04:11:31 +00:00
committed by GitHub
parent da616136cc
commit 244b2d53f4
73 changed files with 4791 additions and 483 deletions
@@ -43,6 +43,8 @@ use codex_app_server_protocol::FileChangeRequestApprovalParams;
use codex_app_server_protocol::FileChangeRequestApprovalResponse;
use codex_app_server_protocol::FileUpdateChange;
use codex_app_server_protocol::GrantedPermissionProfile as V2GrantedPermissionProfile;
use codex_app_server_protocol::HookCompletedNotification;
use codex_app_server_protocol::HookStartedNotification;
use codex_app_server_protocol::InterruptConversationResponse;
use codex_app_server_protocol::ItemCompletedNotification;
use codex_app_server_protocol::ItemStartedNotification;
@@ -1308,6 +1310,30 @@ pub(crate) async fn apply_bespoke_event_handling(
.send_server_notification(ServerNotification::ItemCompleted(notification))
.await;
}
EventMsg::HookStarted(event) => {
if let ApiVersion::V2 = api_version {
let notification = HookStartedNotification {
thread_id: conversation_id.to_string(),
turn_id: event.turn_id,
run: event.run.into(),
};
outgoing
.send_server_notification(ServerNotification::HookStarted(notification))
.await;
}
}
EventMsg::HookCompleted(event) => {
if let ApiVersion::V2 = api_version {
let notification = HookCompletedNotification {
thread_id: conversation_id.to_string(),
turn_id: event.turn_id,
run: event.run.into(),
};
outgoing
.send_server_notification(ServerNotification::HookCompleted(notification))
.await;
}
}
EventMsg::ExitedReviewMode(review_event) => {
let review = match review_event.review_output {
Some(output) => render_review_output_text(&output),