protocol: add hook lifecycle types and turn hookRuns

This commit is contained in:
Andrei Eternal
2026-03-03 14:05:28 -08:00
parent 69fe65f4c7
commit 37a973ecf9
47 changed files with 3461 additions and 4 deletions
@@ -752,7 +752,9 @@ server_notification_definitions! {
ThreadNameUpdated => "thread/name/updated" (v2::ThreadNameUpdatedNotification),
ThreadTokenUsageUpdated => "thread/tokenUsage/updated" (v2::ThreadTokenUsageUpdatedNotification),
TurnStarted => "turn/started" (v2::TurnStartedNotification),
HookStarted => "hook/started" (v2::HookStartedNotification),
TurnCompleted => "turn/completed" (v2::TurnCompletedNotification),
HookCompleted => "hook/completed" (v2::HookCompletedNotification),
TurnDiffUpdated => "turn/diff/updated" (v2::TurnDiffUpdatedNotification),
TurnPlanUpdated => "turn/plan/updated" (v2::TurnPlanUpdatedNotification),
ItemStarted => "item/started" (v2::ItemStartedNotification),
@@ -6,6 +6,7 @@ use crate::protocol::v2::CommandExecutionStatus;
use crate::protocol::v2::DynamicToolCallOutputContentItem;
use crate::protocol::v2::DynamicToolCallStatus;
use crate::protocol::v2::FileUpdateChange;
use crate::protocol::v2::HookRunSummary;
use crate::protocol::v2::McpToolCallError;
use crate::protocol::v2::McpToolCallResult;
use crate::protocol::v2::McpToolCallStatus;
@@ -162,6 +163,8 @@ impl ThreadHistoryBuilder {
EventMsg::ExitedReviewMode(payload) => self.handle_exited_review_mode(payload),
EventMsg::ItemStarted(payload) => self.handle_item_started(payload),
EventMsg::ItemCompleted(payload) => self.handle_item_completed(payload),
EventMsg::HookStarted(_) => {}
EventMsg::HookCompleted(payload) => self.handle_hook_completed(payload),
EventMsg::Error(payload) => self.handle_error(payload),
EventMsg::TokenCount(_) => {}
EventMsg::ThreadRolledBack(payload) => self.handle_thread_rollback(payload),
@@ -868,9 +871,35 @@ impl ThreadHistoryBuilder {
self.next_item_index = i64::try_from(item_count.saturating_add(1)).unwrap_or(i64::MAX);
}
fn handle_hook_completed(&mut self, payload: &codex_protocol::protocol::HookCompletedEvent) {
let run = HookRunSummary::from(payload.run.clone());
if let Some(turn_id) = payload.turn_id.as_deref() {
if let Some(turn) = self.current_turn.as_mut()
&& turn.id == turn_id
{
turn.hook_runs.push(run);
return;
}
if let Some(turn) = self.turns.iter_mut().find(|turn| turn.id == turn_id) {
turn.hook_runs.push(run);
return;
}
warn!("dropping hook run for unknown turn id `{turn_id}`");
return;
}
self.ensure_turn().hook_runs.push(run);
}
fn finish_current_turn(&mut self) {
if let Some(turn) = self.current_turn.take() {
if turn.items.is_empty() && !turn.opened_explicitly && !turn.saw_compaction {
if turn.items.is_empty()
&& turn.hook_runs.is_empty()
&& !turn.opened_explicitly
&& !turn.saw_compaction
{
return;
}
self.turns.push(turn.into());
@@ -881,6 +910,7 @@ impl ThreadHistoryBuilder {
PendingTurn {
id: id.unwrap_or_else(|| Uuid::now_v7().to_string()),
items: Vec::new(),
hook_runs: Vec::new(),
error: None,
status: TurnStatus::Completed,
opened_explicitly: false,
@@ -1040,6 +1070,7 @@ fn upsert_turn_item(items: &mut Vec<ThreadItem>, item: ThreadItem) {
struct PendingTurn {
id: String,
items: Vec<ThreadItem>,
hook_runs: Vec<HookRunSummary>,
error: Option<TurnError>,
status: TurnStatus,
/// True when this turn originated from an explicit `turn_started`/`turn_complete`
@@ -1067,6 +1098,7 @@ impl From<PendingTurn> for Turn {
Self {
id: value.id,
items: value.items,
hook_runs: value.hook_runs,
error: value.error,
status: value.status,
}
@@ -1078,6 +1110,7 @@ impl From<&PendingTurn> for Turn {
Self {
id: value.id.clone(),
items: value.items.clone(),
hook_runs: value.hook_runs.clone(),
error: value.error.clone(),
status: value.status.clone(),
}
@@ -2257,6 +2290,7 @@ mod tests {
status: TurnStatus::Completed,
error: None,
items: Vec::new(),
hook_runs: Vec::new(),
}]
);
}
@@ -2383,6 +2417,7 @@ mod tests {
text_elements: Vec::new(),
}],
}],
hook_runs: Vec::new(),
}
);
}
@@ -44,6 +44,14 @@ use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus;
use codex_protocol::protocol::HookEventName as CoreHookEventName;
use codex_protocol::protocol::HookExecutionMode as CoreHookExecutionMode;
use codex_protocol::protocol::HookHandlerType as CoreHookHandlerType;
use codex_protocol::protocol::HookOutputEntry as CoreHookOutputEntry;
use codex_protocol::protocol::HookOutputEntryKind as CoreHookOutputEntryKind;
use codex_protocol::protocol::HookRunStatus as CoreHookRunStatus;
use codex_protocol::protocol::HookRunSummary as CoreHookRunSummary;
use codex_protocol::protocol::HookScope as CoreHookScope;
use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason;
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
@@ -272,6 +280,98 @@ v2_enum_from_core!(
}
);
v2_enum_from_core!(
pub enum HookEventName from CoreHookEventName {
SessionStart, Stop
}
);
v2_enum_from_core!(
pub enum HookHandlerType from CoreHookHandlerType {
Command, Prompt, Agent
}
);
v2_enum_from_core!(
pub enum HookExecutionMode from CoreHookExecutionMode {
Sync, Async
}
);
v2_enum_from_core!(
pub enum HookScope from CoreHookScope {
Thread, Turn
}
);
v2_enum_from_core!(
pub enum HookRunStatus from CoreHookRunStatus {
Running, Completed, Failed, Blocked, Stopped
}
);
v2_enum_from_core!(
pub enum HookOutputEntryKind from CoreHookOutputEntryKind {
Warning, Stop, Feedback, Context, Error
}
);
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct HookOutputEntry {
pub kind: HookOutputEntryKind,
pub text: String,
}
impl From<CoreHookOutputEntry> for HookOutputEntry {
fn from(value: CoreHookOutputEntry) -> Self {
Self {
kind: value.kind.into(),
text: value.text,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct HookRunSummary {
pub id: String,
pub event_name: HookEventName,
pub handler_type: HookHandlerType,
pub execution_mode: HookExecutionMode,
pub scope: HookScope,
pub source_path: PathBuf,
pub display_order: i64,
pub status: HookRunStatus,
pub status_message: Option<String>,
pub started_at: i64,
pub completed_at: Option<i64>,
pub duration_ms: Option<i64>,
pub entries: Vec<HookOutputEntry>,
}
impl From<CoreHookRunSummary> for HookRunSummary {
fn from(value: CoreHookRunSummary) -> Self {
Self {
id: value.id,
event_name: value.event_name.into(),
handler_type: value.handler_type.into(),
execution_mode: value.execution_mode.into(),
scope: value.scope.into(),
source_path: value.source_path,
display_order: value.display_order,
status: value.status.into(),
status_message: value.status_message,
started_at: value.started_at,
completed_at: value.completed_at,
duration_ms: value.duration_ms,
entries: value.entries.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
@@ -2657,6 +2757,7 @@ pub struct Turn {
/// For all other responses and notifications returning a Turn,
/// the items field will be an empty list.
pub items: Vec<ThreadItem>,
pub hook_runs: Vec<HookRunSummary>,
pub status: TurnStatus,
/// Only populated when the Turn's status is failed.
pub error: Option<TurnError>,
@@ -3598,6 +3699,15 @@ pub struct TurnStartedNotification {
pub turn: Turn,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct HookStartedNotification {
pub thread_id: String,
pub turn_id: Option<String>,
pub run: HookRunSummary,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -3615,6 +3725,15 @@ pub struct TurnCompletedNotification {
pub turn: Turn,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct HookCompletedNotification {
pub thread_id: String,
pub turn_id: Option<String>,
pub run: HookRunSummary,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]