mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
244b2d53f4
(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)
122 lines
3.4 KiB
Rust
122 lines
3.4 KiB
Rust
use codex_config::ConfigLayerStack;
|
|
use tokio::process::Command;
|
|
|
|
use crate::engine::ClaudeHooksEngine;
|
|
use crate::engine::CommandShell;
|
|
use crate::events::session_start::SessionStartOutcome;
|
|
use crate::events::session_start::SessionStartRequest;
|
|
use crate::events::stop::StopOutcome;
|
|
use crate::events::stop::StopRequest;
|
|
use crate::types::Hook;
|
|
use crate::types::HookEvent;
|
|
use crate::types::HookPayload;
|
|
use crate::types::HookResponse;
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct HooksConfig {
|
|
pub legacy_notify_argv: Option<Vec<String>>,
|
|
pub feature_enabled: bool,
|
|
pub config_layer_stack: Option<ConfigLayerStack>,
|
|
pub shell_program: Option<String>,
|
|
pub shell_args: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Hooks {
|
|
after_agent: Vec<Hook>,
|
|
after_tool_use: Vec<Hook>,
|
|
engine: ClaudeHooksEngine,
|
|
}
|
|
|
|
impl Default for Hooks {
|
|
fn default() -> Self {
|
|
Self::new(HooksConfig::default())
|
|
}
|
|
}
|
|
|
|
impl Hooks {
|
|
pub fn new(config: HooksConfig) -> Self {
|
|
let after_agent = config
|
|
.legacy_notify_argv
|
|
.filter(|argv| !argv.is_empty() && !argv[0].is_empty())
|
|
.map(crate::notify_hook)
|
|
.into_iter()
|
|
.collect();
|
|
let engine = ClaudeHooksEngine::new(
|
|
config.feature_enabled,
|
|
config.config_layer_stack.as_ref(),
|
|
CommandShell {
|
|
program: config.shell_program.unwrap_or_default(),
|
|
args: config.shell_args,
|
|
},
|
|
);
|
|
Self {
|
|
after_agent,
|
|
after_tool_use: Vec::new(),
|
|
engine,
|
|
}
|
|
}
|
|
|
|
pub fn startup_warnings(&self) -> &[String] {
|
|
self.engine.warnings()
|
|
}
|
|
|
|
fn hooks_for_event(&self, hook_event: &HookEvent) -> &[Hook] {
|
|
match hook_event {
|
|
HookEvent::AfterAgent { .. } => &self.after_agent,
|
|
HookEvent::AfterToolUse { .. } => &self.after_tool_use,
|
|
}
|
|
}
|
|
|
|
pub async fn dispatch(&self, hook_payload: HookPayload) -> Vec<HookResponse> {
|
|
let hooks = self.hooks_for_event(&hook_payload.hook_event);
|
|
let mut outcomes = Vec::with_capacity(hooks.len());
|
|
for hook in hooks {
|
|
let outcome = hook.execute(&hook_payload).await;
|
|
let should_abort_operation = outcome.result.should_abort_operation();
|
|
outcomes.push(outcome);
|
|
if should_abort_operation {
|
|
break;
|
|
}
|
|
}
|
|
|
|
outcomes
|
|
}
|
|
|
|
pub fn preview_session_start(
|
|
&self,
|
|
request: &SessionStartRequest,
|
|
) -> Vec<codex_protocol::protocol::HookRunSummary> {
|
|
self.engine.preview_session_start(request)
|
|
}
|
|
|
|
pub async fn run_session_start(
|
|
&self,
|
|
request: SessionStartRequest,
|
|
turn_id: Option<String>,
|
|
) -> SessionStartOutcome {
|
|
self.engine.run_session_start(request, turn_id).await
|
|
}
|
|
|
|
pub fn preview_stop(
|
|
&self,
|
|
request: &StopRequest,
|
|
) -> Vec<codex_protocol::protocol::HookRunSummary> {
|
|
self.engine.preview_stop(request)
|
|
}
|
|
|
|
pub async fn run_stop(&self, request: StopRequest) -> StopOutcome {
|
|
self.engine.run_stop(request).await
|
|
}
|
|
}
|
|
|
|
pub fn command_from_argv(argv: &[String]) -> Option<Command> {
|
|
let (program, args) = argv.split_first()?;
|
|
if program.is_empty() {
|
|
return None;
|
|
}
|
|
let mut command = Command::new(program);
|
|
command.args(args);
|
|
Some(command)
|
|
}
|