mirror of
https://github.com/openai/codex.git
synced 2026-09-15 12:08:01 +00:00
Add session start hook
This commit is contained in:
@@ -44,6 +44,7 @@ use async_channel::Receiver;
|
||||
use async_channel::Sender;
|
||||
use codex_hooks::HookEvent;
|
||||
use codex_hooks::HookEventAfterAgent;
|
||||
use codex_hooks::HookEventSessionStart;
|
||||
use codex_hooks::HookPayload;
|
||||
use codex_hooks::Hooks;
|
||||
use codex_hooks::HooksConfig;
|
||||
@@ -1327,6 +1328,16 @@ impl Session {
|
||||
Arc::clone(&config),
|
||||
&session_configuration.session_source,
|
||||
);
|
||||
sess.hooks()
|
||||
.dispatch(HookPayload {
|
||||
session_id: sess.conversation_id,
|
||||
cwd: session_configuration.cwd.clone(),
|
||||
triggered_at: chrono::Utc::now(),
|
||||
hook_event: HookEvent::SessionStart {
|
||||
event: HookEventSessionStart {},
|
||||
},
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(sess)
|
||||
}
|
||||
|
||||
22
codex-rs/hooks/README.md
Normal file
22
codex-rs/hooks/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Hooks
|
||||
|
||||
Hooks are arbitrary programs which run at various deterministic, pre-defined points in the Codex lifecycle.
|
||||
|
||||
Hooks implementation is in progress (as of 2025-02-10).
|
||||
|
||||
## TODO
|
||||
|
||||
- Allow hooks to return errors which halt execution of subsequent hooks.
|
||||
- Add a /hooks slash command to list and debug hooks.
|
||||
- Implement the following hooks:
|
||||
- SessionStart
|
||||
- SessionEnd
|
||||
- BeforeAgent
|
||||
- BeforeTool
|
||||
- Add Hooks to config.toml
|
||||
|
||||
## Done
|
||||
|
||||
- Hooks for:
|
||||
- AfterAgent
|
||||
- AfterTool
|
||||
@@ -9,6 +9,7 @@ pub use types::Hook;
|
||||
pub use types::HookEvent;
|
||||
pub use types::HookEventAfterAgent;
|
||||
pub use types::HookEventAfterToolUse;
|
||||
pub use types::HookEventSessionStart;
|
||||
pub use types::HookOutcome;
|
||||
pub use types::HookPayload;
|
||||
pub use types::HookToolInput;
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct HooksConfig {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Hooks {
|
||||
session_start: Vec<Hook>,
|
||||
after_agent: Vec<Hook>,
|
||||
after_tool_use: Vec<Hook>,
|
||||
}
|
||||
@@ -33,6 +34,7 @@ impl Hooks {
|
||||
.into_iter()
|
||||
.collect();
|
||||
Self {
|
||||
session_start: Vec::new(),
|
||||
after_agent,
|
||||
after_tool_use: Vec::new(),
|
||||
}
|
||||
@@ -40,6 +42,7 @@ impl Hooks {
|
||||
|
||||
fn hooks_for_event(&self, hook_event: &HookEvent) -> &[Hook] {
|
||||
match hook_event {
|
||||
HookEvent::SessionStart { .. } => &self.session_start,
|
||||
HookEvent::AfterAgent { .. } => &self.after_agent,
|
||||
HookEvent::AfterToolUse { .. } => &self.after_tool_use,
|
||||
}
|
||||
@@ -88,6 +91,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::types::HookEventAfterAgent;
|
||||
use crate::types::HookEventAfterToolUse;
|
||||
use crate::types::HookEventSessionStart;
|
||||
use crate::types::HookToolInput;
|
||||
use crate::types::HookToolKind;
|
||||
|
||||
@@ -155,6 +159,20 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn session_start_payload() -> HookPayload {
|
||||
HookPayload {
|
||||
session_id: ThreadId::new(),
|
||||
cwd: PathBuf::from(CWD),
|
||||
triggered_at: Utc
|
||||
.with_ymd_and_hms(2025, 1, 1, 0, 0, 0)
|
||||
.single()
|
||||
.expect("valid timestamp"),
|
||||
hook_event: HookEvent::SessionStart {
|
||||
event: HookEventSessionStart {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_from_argv_returns_none_for_empty_args() {
|
||||
assert!(command_from_argv(&[]).is_none());
|
||||
@@ -269,6 +287,18 @@ mod tests {
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_executes_session_start_hooks() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let hooks = Hooks {
|
||||
session_start: vec![counting_hook(&calls, HookOutcome::Continue)],
|
||||
..Hooks::default()
|
||||
};
|
||||
|
||||
hooks.dispatch(session_start_payload()).await;
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[tokio::test]
|
||||
async fn hook_executes_program_with_payload_argument_unix() -> Result<()> {
|
||||
|
||||
@@ -50,6 +50,10 @@ pub struct HookEventAfterAgent {
|
||||
pub last_assistant_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct HookEventSessionStart {}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HookToolKind {
|
||||
@@ -116,6 +120,10 @@ where
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "event_type", rename_all = "snake_case")]
|
||||
pub enum HookEvent {
|
||||
SessionStart {
|
||||
#[serde(flatten)]
|
||||
event: HookEventSessionStart,
|
||||
},
|
||||
AfterAgent {
|
||||
#[serde(flatten)]
|
||||
event: HookEventAfterAgent,
|
||||
@@ -147,6 +155,7 @@ mod tests {
|
||||
use super::HookEvent;
|
||||
use super::HookEventAfterAgent;
|
||||
use super::HookEventAfterToolUse;
|
||||
use super::HookEventSessionStart;
|
||||
use super::HookPayload;
|
||||
use super::HookToolInput;
|
||||
use super::HookToolInputLocalShell;
|
||||
@@ -190,6 +199,34 @@ mod tests {
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_start_payload_serializes_stable_wire_shape() {
|
||||
let session_id = ThreadId::new();
|
||||
let payload = HookPayload {
|
||||
session_id,
|
||||
cwd: PathBuf::from("tmp"),
|
||||
triggered_at: Utc
|
||||
.with_ymd_and_hms(2025, 1, 1, 0, 0, 0)
|
||||
.single()
|
||||
.expect("valid timestamp"),
|
||||
hook_event: HookEvent::SessionStart {
|
||||
event: HookEventSessionStart {},
|
||||
},
|
||||
};
|
||||
|
||||
let actual = serde_json::to_value(payload).expect("serialize hook payload");
|
||||
let expected = json!({
|
||||
"session_id": session_id.to_string(),
|
||||
"cwd": "tmp",
|
||||
"triggered_at": "2025-01-01T00:00:00Z",
|
||||
"hook_event": {
|
||||
"event_type": "session_start",
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn after_tool_use_payload_serializes_stable_wire_shape() {
|
||||
let session_id = ThreadId::new();
|
||||
|
||||
Reference in New Issue
Block a user