lifeloop/protocol/
codex.rs1use crate::LifecycleEventKind;
15use serde_json::{Value, json};
16
17use super::{
18 FrameAdmissionDirective, ProtocolAdapter, RenderError, RenderRequest, RenderedHookPayload,
19 render_additional_context,
20};
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
24pub enum CodexHookEvent {
25 SessionStart,
26 UserPromptSubmit,
27 PreCompact,
28 PostCompact,
29 Stop,
30}
31
32impl CodexHookEvent {
33 pub const ALL: &'static [Self] = &[
34 Self::SessionStart,
35 Self::UserPromptSubmit,
36 Self::PreCompact,
37 Self::PostCompact,
38 Self::Stop,
39 ];
40
41 pub fn as_str(self) -> &'static str {
42 match self {
43 Self::SessionStart => "SessionStart",
44 Self::UserPromptSubmit => "UserPromptSubmit",
45 Self::PreCompact => "PreCompact",
46 Self::PostCompact => "PostCompact",
47 Self::Stop => "Stop",
48 }
49 }
50}
51
52pub fn codex_hook_event_for(event: LifecycleEventKind) -> Option<CodexHookEvent> {
56 match event {
57 LifecycleEventKind::SessionStarting | LifecycleEventKind::SessionStarted => {
58 Some(CodexHookEvent::SessionStart)
59 }
60 LifecycleEventKind::FrameOpening => Some(CodexHookEvent::UserPromptSubmit),
61 LifecycleEventKind::ContextPressureObserved => Some(CodexHookEvent::PreCompact),
62 LifecycleEventKind::ContextCompacted => Some(CodexHookEvent::PostCompact),
63 LifecycleEventKind::FrameEnding | LifecycleEventKind::FrameEnded => {
64 Some(CodexHookEvent::Stop)
65 }
66 LifecycleEventKind::FrameOpened
67 | LifecycleEventKind::SessionEnding
68 | LifecycleEventKind::SessionEnded
69 | LifecycleEventKind::SupervisorTick
70 | LifecycleEventKind::CapabilityDegraded
71 | LifecycleEventKind::ReceiptEmitted
72 | LifecycleEventKind::ReceiptGapDetected => None,
73 }
74}
75
76pub(crate) fn render(req: &RenderRequest<'_>) -> Result<RenderedHookPayload, RenderError> {
77 let Some(event) = codex_hook_event_for(req.event) else {
78 return Err(RenderError::UnsupportedEvent {
79 adapter: ProtocolAdapter::Codex,
80 event: req.event,
81 });
82 };
83 let body = match event {
84 CodexHookEvent::SessionStart => session_start_payload(req),
85 CodexHookEvent::UserPromptSubmit => user_prompt_submit_payload(req),
86 CodexHookEvent::PreCompact | CodexHookEvent::PostCompact => json!({}),
87 CodexHookEvent::Stop => stop_payload(req),
88 };
89 Ok(RenderedHookPayload {
90 hook_event_name: Some(event.as_str()),
91 body,
92 })
93}
94
95fn session_start_payload(req: &RenderRequest<'_>) -> Value {
96 json!({
97 "hookSpecificOutput": {
98 "hookEventName": CodexHookEvent::SessionStart.as_str(),
99 "additionalContext": render_additional_context(req.payloads),
100 }
101 })
102}
103
104fn user_prompt_submit_payload(req: &RenderRequest<'_>) -> Value {
105 let mut payload = json!({
106 "hookSpecificOutput": {
107 "hookEventName": CodexHookEvent::UserPromptSubmit.as_str(),
108 "additionalContext": render_additional_context(req.payloads),
109 }
110 });
111
112 if let Some(FrameAdmissionDirective::Block { reason }) = req.directive {
113 let obj = payload
114 .as_object_mut()
115 .expect("user_prompt_submit payload is a json object");
116 obj.insert("decision".to_string(), Value::String("block".to_string()));
117 obj.insert("reason".to_string(), Value::String(reason.clone()));
118 }
119
120 payload
121}
122
123fn stop_payload(req: &RenderRequest<'_>) -> Value {
124 if let Some(FrameAdmissionDirective::Block { reason }) = req.directive {
125 return json!({
126 "decision": "block",
127 "reason": reason,
128 });
129 }
130 json!({})
131}