lifeloop/protocol/
codex.rs1use crate::LifecycleEventKind;
15use serde_json::{Value, json};
16
17use super::{
18 FrameAdmissionDirective, ProtocolAdapter, RenderError, RenderRequest, RenderedHookPayload,
19 render_additional_context, render_side_channel_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 => json!({}),
87 CodexHookEvent::PostCompact => post_compact_payload(req),
88 CodexHookEvent::Stop => stop_payload(req),
89 };
90 Ok(RenderedHookPayload {
91 hook_event_name: Some(event.as_str()),
92 body,
93 })
94}
95
96fn session_start_payload(req: &RenderRequest<'_>) -> Value {
97 json!({
98 "hookSpecificOutput": {
99 "hookEventName": CodexHookEvent::SessionStart.as_str(),
100 "additionalContext": render_additional_context(req.payloads),
101 }
102 })
103}
104
105fn user_prompt_submit_payload(req: &RenderRequest<'_>) -> Value {
106 let mut payload = json!({
107 "hookSpecificOutput": {
108 "hookEventName": CodexHookEvent::UserPromptSubmit.as_str(),
109 "additionalContext": render_additional_context(req.payloads),
110 }
111 });
112
113 if let Some(FrameAdmissionDirective::Block { reason }) = req.directive {
114 let obj = payload
115 .as_object_mut()
116 .expect("user_prompt_submit payload is a json object");
117 obj.insert("decision".to_string(), Value::String("block".to_string()));
118 obj.insert("reason".to_string(), Value::String(reason.clone()));
119 }
120
121 payload
122}
123
124fn post_compact_payload(req: &RenderRequest<'_>) -> Value {
125 let additional_context = render_side_channel_context(req.payloads);
126 if additional_context == "{}" {
127 return json!({});
128 }
129
130 json!({
131 "hookSpecificOutput": {
132 "hookEventName": CodexHookEvent::PostCompact.as_str(),
133 "additionalContext": additional_context,
134 }
135 })
136}
137
138fn stop_payload(req: &RenderRequest<'_>) -> Value {
139 if let Some(FrameAdmissionDirective::Block { reason }) = req.directive {
140 return json!({
141 "decision": "block",
142 "reason": reason,
143 });
144 }
145 json!({})
146}