Merge subagent hooks base into stop stack

This commit is contained in:
Abhinav Vedmala
2026-05-15 15:49:11 -07:00
5 changed files with 154 additions and 127 deletions

View File

@@ -13,7 +13,7 @@ use codex_hooks::PostToolUseRequest;
use codex_hooks::PreToolUseOutcome;
use codex_hooks::PreToolUseRequest;
use codex_hooks::SessionStartOutcome;
use codex_hooks::SessionStartTarget;
use codex_hooks::StartHookTarget;
use codex_hooks::StopHookTarget;
use codex_hooks::StopOutcome;
use codex_hooks::UserPromptSubmitOutcome;
@@ -120,6 +120,9 @@ pub(crate) async fn run_pending_session_start_hooks(
return false;
};
// Pending session-start hooks are reused to dispatch thread-spawn subagent
// starts. Other subagent sessions are internal/system work and do not run
// start hooks.
let target = match &turn_context.session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_role, .. })
if matches!(
@@ -127,15 +130,17 @@ pub(crate) async fn run_pending_session_start_hooks(
codex_hooks::SessionStartSource::Startup
) =>
{
let metadata = subagent_hook_metadata(sess, agent_role);
SessionStartTarget::SubagentStart {
let agent_type = agent_role
.clone()
.unwrap_or_else(|| crate::agent::role::DEFAULT_ROLE_NAME.to_string());
StartHookTarget::SubagentStart {
turn_id: turn_context.sub_id.clone(),
agent_id: metadata.agent_id,
agent_type: metadata.agent_type,
agent_id: sess.thread_id().to_string(),
agent_type,
}
}
SessionSource::SubAgent(_) => return false,
_ => SessionStartTarget::SessionStart {
_ => StartHookTarget::SessionStart {
source: session_start_source,
},
};
@@ -309,7 +314,9 @@ pub(crate) async fn run_turn_stop_hooks(
parent_thread_id,
..
}) => {
let metadata = subagent_hook_metadata(sess, agent_role);
let agent_type = agent_role
.clone()
.unwrap_or_else(|| crate::agent::role::DEFAULT_ROLE_NAME.to_string());
let agent_transcript_path = sess.hook_transcript_path().await;
let parent_transcript_path = match sess
.services
@@ -333,8 +340,8 @@ pub(crate) async fn run_turn_stop_hooks(
};
(
StopHookTarget::SubagentStop {
agent_id: metadata.agent_id,
agent_type: metadata.agent_type,
agent_id: sess.thread_id().to_string(),
agent_type,
agent_transcript_path,
},
parent_transcript_path,
@@ -688,23 +695,6 @@ fn hook_permission_mode(turn_context: &TurnContext) -> String {
.to_string()
}
struct SubagentHookMetadata {
agent_id: String,
agent_type: String,
}
fn subagent_hook_metadata(
sess: &Arc<Session>,
agent_role: &Option<String>,
) -> SubagentHookMetadata {
SubagentHookMetadata {
agent_id: sess.thread_id().to_string(),
agent_type: agent_role
.clone()
.unwrap_or_else(|| crate::agent::role::DEFAULT_ROLE_NAME.to_string()),
}
}
fn compaction_trigger_label(value: CompactionTrigger) -> &'static str {
match value {
CompactionTrigger::Manual => "manual",

View File

@@ -532,7 +532,7 @@ async fn preview_session_start_hooks(
transcript_path: None,
model: "gpt-5.2".to_string(),
permission_mode: "default".to_string(),
target: codex_hooks::SessionStartTarget::SessionStart {
target: codex_hooks::StartHookTarget::SessionStart {
source: codex_hooks::SessionStartSource::Startup,
},
}),
@@ -1306,7 +1306,7 @@ async fn reload_user_config_layer_refreshes_hooks() -> anyhow::Result<()> {
transcript_path: None,
model: "gpt-5.2".to_string(),
permission_mode: "default".to_string(),
target: codex_hooks::SessionStartTarget::SessionStart {
target: codex_hooks::StartHookTarget::SessionStart {
source: codex_hooks::SessionStartSource::Startup,
},
};
@@ -1413,7 +1413,7 @@ async fn refresh_runtime_config_refreshes_hooks() -> anyhow::Result<()> {
transcript_path: None,
model: "gpt-5.2".to_string(),
permission_mode: "default".to_string(),
target: codex_hooks::SessionStartTarget::SessionStart {
target: codex_hooks::StartHookTarget::SessionStart {
source: codex_hooks::SessionStartSource::Startup,
},
};

View File

@@ -258,21 +258,21 @@ fn read_hook_log(home: &Path, filename: &str) -> Result<Vec<serde_json::Value>>
.collect()
}
async fn wait_for_hook_log_entries(
async fn wait_for_hook_log(
home: &Path,
filename: &str,
expected_len: usize,
) -> Result<Vec<serde_json::Value>> {
let deadline = Instant::now() + Duration::from_secs(2);
loop {
let entries = read_hook_log(home, filename)?;
if entries.len() >= expected_len {
return Ok(entries);
let inputs = read_hook_log(home, filename)?;
if inputs.len() >= expected_len {
return Ok(inputs);
}
if Instant::now() >= deadline {
anyhow::bail!(
"expected at least {expected_len} entries in {filename}, got {}",
entries.len()
inputs.len()
);
}
sleep(Duration::from_millis(10)).await;
@@ -517,7 +517,12 @@ async fn subagent_start_replaces_session_start_and_injects_context() -> Result<(
let child_requests = wait_for_requests(&child_request_log).await?;
assert_eq!(child_requests.len(), 1);
let start_inputs = read_hook_log(test.codex_home_path(), "subagent_start_hook_log.jsonl")?;
let start_inputs = wait_for_hook_log(
test.codex_home_path(),
"subagent_start_hook_log.jsonl",
/*expected_len*/ 1,
)
.await?;
assert_eq!(start_inputs.len(), 1);
assert_eq!(start_inputs[0]["agent_type"].as_str(), Some("worker"));
let spawned_id = wait_for_spawned_thread_id(&test).await?;
@@ -526,8 +531,12 @@ async fn subagent_start_replaces_session_start_and_injects_context() -> Result<(
Some(spawned_id.as_str())
);
let session_start_inputs =
read_hook_log(test.codex_home_path(), "session_start_hook_log.jsonl")?;
let session_start_inputs = wait_for_hook_log(
test.codex_home_path(),
"session_start_hook_log.jsonl",
/*expected_len*/ 1,
)
.await?;
assert_eq!(session_start_inputs.len(), 1);
assert_eq!(session_start_inputs[0]["source"].as_str(), Some("startup"));
assert_ne!(
@@ -628,7 +637,7 @@ async fn subagent_stop_replaces_stop_and_skips_internal_subagents() -> Result<()
let _ = wait_for_requests(&first_child_request).await?;
let _ = wait_for_requests(&second_child_request).await?;
let subagent_stop_inputs = wait_for_hook_log_entries(
let subagent_stop_inputs = wait_for_hook_log(
test.codex_home_path(),
"subagent_stop_hook_log.jsonl",
/*expected_len*/ 2,