mirror of
https://github.com/openai/codex.git
synced 2026-05-23 12:34:25 +00:00
Add subagent identity to hook inputs
This commit is contained in:
@@ -16,6 +16,7 @@ use codex_hooks::SessionStartOutcome;
|
||||
use codex_hooks::SessionStartTarget;
|
||||
use codex_hooks::StopHookTarget;
|
||||
use codex_hooks::StopOutcome;
|
||||
use codex_hooks::SubagentHookContext;
|
||||
use codex_hooks::UserPromptSubmitOutcome;
|
||||
use codex_hooks::UserPromptSubmitRequest;
|
||||
use codex_otel::HOOK_RUN_DURATION_METRIC;
|
||||
@@ -127,11 +128,11 @@ pub(crate) async fn run_pending_session_start_hooks(
|
||||
codex_hooks::SessionStartSource::Startup
|
||||
) =>
|
||||
{
|
||||
let metadata = subagent_hook_metadata(sess, agent_role);
|
||||
let context = subagent_hook_context(sess, agent_role);
|
||||
SessionStartTarget::SubagentStart {
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
agent_id: metadata.agent_id,
|
||||
agent_type: metadata.agent_type,
|
||||
agent_id: context.agent_id,
|
||||
agent_type: context.agent_type,
|
||||
}
|
||||
}
|
||||
SessionSource::SubAgent(_) => return false,
|
||||
@@ -176,6 +177,7 @@ pub(crate) async fn run_pre_tool_use_hooks(
|
||||
let request = PreToolUseRequest {
|
||||
session_id: sess.session_id().into(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
subagent: thread_spawn_subagent_hook_context(sess, turn_context),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
@@ -236,6 +238,7 @@ pub(crate) async fn run_permission_request_hooks(
|
||||
let request = PermissionRequestRequest {
|
||||
session_id: sess.session_id().into(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
subagent: thread_spawn_subagent_hook_context(sess, turn_context),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
@@ -277,6 +280,7 @@ pub(crate) async fn run_post_tool_use_hooks(
|
||||
let request = PostToolUseRequest {
|
||||
session_id: sess.session_id().into(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
subagent: thread_spawn_subagent_hook_context(sess, turn_context),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
@@ -309,7 +313,7 @@ pub(crate) async fn run_turn_stop_hooks(
|
||||
parent_thread_id,
|
||||
..
|
||||
}) => {
|
||||
let metadata = subagent_hook_metadata(sess, agent_role);
|
||||
let context = subagent_hook_context(sess, agent_role);
|
||||
let agent_transcript_path = sess.hook_transcript_path().await;
|
||||
let parent_transcript_path = match sess
|
||||
.services
|
||||
@@ -333,8 +337,8 @@ pub(crate) async fn run_turn_stop_hooks(
|
||||
};
|
||||
(
|
||||
StopHookTarget::SubagentStop {
|
||||
agent_id: metadata.agent_id,
|
||||
agent_type: metadata.agent_type,
|
||||
agent_id: context.agent_id,
|
||||
agent_type: context.agent_type,
|
||||
agent_transcript_path,
|
||||
},
|
||||
parent_transcript_path,
|
||||
@@ -371,6 +375,7 @@ pub(crate) async fn run_pre_compact_hooks(
|
||||
let request = codex_hooks::PreCompactRequest {
|
||||
session_id: sess.session_id().into(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
subagent: thread_spawn_subagent_hook_context(sess, turn_context),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
@@ -409,6 +414,7 @@ pub(crate) async fn run_post_compact_hooks(
|
||||
let request = codex_hooks::PostCompactRequest {
|
||||
session_id: sess.session_id().into(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
subagent: thread_spawn_subagent_hook_context(sess, turn_context),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
@@ -435,6 +441,7 @@ pub(crate) async fn run_user_prompt_submit_hooks(
|
||||
let request = UserPromptSubmitRequest {
|
||||
session_id: sess.session_id().into(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
subagent: thread_spawn_subagent_hook_context(sess, turn_context),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
@@ -688,16 +695,20 @@ fn hook_permission_mode(turn_context: &TurnContext) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
struct SubagentHookMetadata {
|
||||
agent_id: String,
|
||||
agent_type: String,
|
||||
fn thread_spawn_subagent_hook_context(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &TurnContext,
|
||||
) -> Option<SubagentHookContext> {
|
||||
match &turn_context.session_source {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_role, .. }) => {
|
||||
Some(subagent_hook_context(sess, agent_role))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn subagent_hook_metadata(
|
||||
sess: &Arc<Session>,
|
||||
agent_role: &Option<String>,
|
||||
) -> SubagentHookMetadata {
|
||||
SubagentHookMetadata {
|
||||
fn subagent_hook_context(sess: &Arc<Session>, agent_role: &Option<String>) -> SubagentHookContext {
|
||||
SubagentHookContext {
|
||||
agent_id: sess.thread_id().to_string(),
|
||||
agent_type: agent_role
|
||||
.clone()
|
||||
|
||||
@@ -161,6 +161,21 @@ print(json.dumps({{"hookSpecificOutput": {{"hookEventName": "SubagentStart", "ad
|
||||
start_log_path = start_log_path.display(),
|
||||
);
|
||||
|
||||
let user_prompt_submit_script_path = home.join("user_prompt_submit_hook.py");
|
||||
let user_prompt_submit_log_path = home.join("user_prompt_submit_hook_log.jsonl");
|
||||
let user_prompt_submit_script = format!(
|
||||
r#"import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
log_path = Path(r"{user_prompt_submit_log_path}")
|
||||
payload = json.load(sys.stdin)
|
||||
with log_path.open("a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(payload) + "\n")
|
||||
"#,
|
||||
user_prompt_submit_log_path = user_prompt_submit_log_path.display(),
|
||||
);
|
||||
|
||||
let subagent_stop_script_path = home.join("subagent_stop_hook.py");
|
||||
let subagent_stop_log_path = home.join("subagent_stop_hook_log.jsonl");
|
||||
let prompts_json = serde_json::to_string(stop_prompts)?;
|
||||
@@ -222,6 +237,12 @@ print(json.dumps({{"systemMessage": "root stop complete"}}))
|
||||
"command": format!("python3 {}", start_script_path.display()),
|
||||
}]
|
||||
}],
|
||||
"UserPromptSubmit": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": format!("python3 {}", user_prompt_submit_script_path.display()),
|
||||
}]
|
||||
}],
|
||||
"SubagentStop": [{
|
||||
"matcher": subagent_stop_matcher,
|
||||
"hooks": [{
|
||||
@@ -240,6 +261,7 @@ print(json.dumps({{"systemMessage": "root stop complete"}}))
|
||||
|
||||
fs::write(&session_start_script_path, session_start_script)?;
|
||||
fs::write(&start_script_path, start_script)?;
|
||||
fs::write(&user_prompt_submit_script_path, user_prompt_submit_script)?;
|
||||
fs::write(&subagent_stop_script_path, subagent_stop_script)?;
|
||||
fs::write(&stop_script_path, stop_script)?;
|
||||
fs::write(home.join("hooks.json"), hooks.to_string())?;
|
||||
@@ -526,6 +548,25 @@ async fn subagent_start_replaces_session_start_and_injects_context() -> Result<(
|
||||
Some(spawned_id.as_str())
|
||||
);
|
||||
|
||||
let user_prompt_submit_inputs =
|
||||
read_hook_log(test.codex_home_path(), "user_prompt_submit_hook_log.jsonl")?;
|
||||
let parent_prompt_input = user_prompt_submit_inputs
|
||||
.iter()
|
||||
.find(|input| input["prompt"].as_str() == Some(TURN_1_PROMPT))
|
||||
.expect("parent prompt submit hook input should be logged");
|
||||
assert_eq!(parent_prompt_input.get("agent_id"), None);
|
||||
assert_eq!(parent_prompt_input.get("agent_type"), None);
|
||||
|
||||
let child_prompt_input = user_prompt_submit_inputs
|
||||
.iter()
|
||||
.find(|input| input["prompt"].as_str() == Some(CHILD_PROMPT))
|
||||
.expect("child prompt submit hook input should be logged");
|
||||
assert_eq!(
|
||||
child_prompt_input["agent_id"].as_str(),
|
||||
Some(spawned_id.as_str())
|
||||
);
|
||||
assert_eq!(child_prompt_input["agent_type"].as_str(), Some("worker"));
|
||||
|
||||
let session_start_inputs =
|
||||
read_hook_log(test.codex_home_path(), "session_start_hook_log.jsonl")?;
|
||||
assert_eq!(session_start_inputs.len(), 1);
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"agent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"agent_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"agent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"agent_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"agent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"agent_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"agent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"agent_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"agent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"agent_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"agent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"agent_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -224,6 +224,7 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle:
|
||||
let preview = engine.preview_pre_tool_use(&PreToolUseRequest {
|
||||
session_id: ThreadId::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
subagent: None,
|
||||
cwd: cwd.clone(),
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
@@ -240,6 +241,7 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle:
|
||||
.run_pre_tool_use(PreToolUseRequest {
|
||||
session_id: ThreadId::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
subagent: None,
|
||||
cwd,
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
@@ -311,6 +313,7 @@ async fn requirements_managed_hooks_execute_windows_command_override() {
|
||||
.run_pre_tool_use(PreToolUseRequest {
|
||||
session_id: ThreadId::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
subagent: None,
|
||||
cwd: cwd(),
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
@@ -696,6 +699,7 @@ fn requirements_managed_hooks_load_when_managed_dir_is_missing() {
|
||||
let preview = engine.preview_pre_tool_use(&PreToolUseRequest {
|
||||
session_id: ThreadId::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
subagent: None,
|
||||
cwd,
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
@@ -1101,6 +1105,7 @@ fn discovers_hooks_from_json_and_toml_in_the_same_layer() {
|
||||
let preview = engine.preview_pre_tool_use(&PreToolUseRequest {
|
||||
session_id: ThreadId::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
subagent: None,
|
||||
cwd,
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
@@ -1186,6 +1191,7 @@ print(json.dumps({
|
||||
let preview = engine.preview_pre_tool_use(&PreToolUseRequest {
|
||||
session_id: ThreadId::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
subagent: None,
|
||||
cwd: cwd(),
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
@@ -1217,6 +1223,7 @@ print(json.dumps({
|
||||
.run_pre_tool_use(PreToolUseRequest {
|
||||
session_id: ThreadId::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
subagent: None,
|
||||
cwd: cwd(),
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
|
||||
@@ -8,6 +8,13 @@ use codex_protocol::protocol::HookRunSummary;
|
||||
use crate::engine::ConfiguredHandler;
|
||||
use crate::engine::dispatcher;
|
||||
|
||||
/// Identifies a thread-spawned subagent when a normal hook runs inside it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SubagentHookContext {
|
||||
pub agent_id: String,
|
||||
pub agent_type: String,
|
||||
}
|
||||
|
||||
pub(crate) fn join_text_chunks(chunks: Vec<String>) -> Option<String> {
|
||||
if chunks.is_empty() {
|
||||
None
|
||||
|
||||
@@ -17,11 +17,13 @@ use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
use crate::schema::PostCompactCommandInput;
|
||||
use crate::schema::PreCompactCommandInput;
|
||||
use crate::schema::SubagentCommandInputFields;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PreCompactRequest {
|
||||
pub session_id: ThreadId,
|
||||
pub turn_id: String,
|
||||
pub subagent: Option<common::SubagentHookContext>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub transcript_path: Option<PathBuf>,
|
||||
pub model: String,
|
||||
@@ -32,6 +34,7 @@ pub struct PreCompactRequest {
|
||||
pub struct PostCompactRequest {
|
||||
pub session_id: ThreadId,
|
||||
pub turn_id: String,
|
||||
pub subagent: Option<common::SubagentHookContext>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub transcript_path: Option<PathBuf>,
|
||||
pub model: String,
|
||||
@@ -120,9 +123,12 @@ pub(crate) async fn run_pre(
|
||||
}
|
||||
|
||||
fn pre_command_input_json(request: &PreCompactRequest) -> Result<String, serde_json::Error> {
|
||||
let subagent = SubagentCommandInputFields::from(request.subagent.as_ref());
|
||||
serde_json::to_string(&PreCompactCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
agent_id: subagent.agent_id,
|
||||
agent_type: subagent.agent_type,
|
||||
transcript_path: crate::schema::NullableString::from_path(request.transcript_path.clone()),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "PreCompact".to_string(),
|
||||
@@ -199,9 +205,12 @@ pub(crate) async fn run_post(
|
||||
}
|
||||
|
||||
fn post_command_input_json(request: &PostCompactRequest) -> Result<String, serde_json::Error> {
|
||||
let subagent = SubagentCommandInputFields::from(request.subagent.as_ref());
|
||||
serde_json::to_string(&PostCompactCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
agent_id: subagent.agent_id,
|
||||
agent_type: subagent.agent_type,
|
||||
transcript_path: crate::schema::NullableString::from_path(request.transcript_path.clone()),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "PostCompact".to_string(),
|
||||
@@ -563,6 +572,7 @@ mod tests {
|
||||
session_id: ThreadId::from_string("00000000-0000-4000-8000-000000000001")
|
||||
.expect("valid thread id"),
|
||||
turn_id: "turn-1".to_string(),
|
||||
subagent: None,
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
@@ -575,6 +585,7 @@ mod tests {
|
||||
session_id: ThreadId::from_string("00000000-0000-4000-8000-000000000002")
|
||||
.expect("valid thread id"),
|
||||
turn_id: "turn-1".to_string(),
|
||||
subagent: None,
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
use crate::schema::PermissionRequestCommandInput;
|
||||
use crate::schema::SubagentCommandInputFields;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::HookCompletedEvent;
|
||||
use codex_protocol::protocol::HookEventName;
|
||||
@@ -35,6 +36,7 @@ use serde_json::Value;
|
||||
pub struct PermissionRequestRequest {
|
||||
pub session_id: ThreadId,
|
||||
pub turn_id: String,
|
||||
pub subagent: Option<common::SubagentHookContext>,
|
||||
pub cwd: PathBuf,
|
||||
pub transcript_path: Option<PathBuf>,
|
||||
pub model: String,
|
||||
@@ -168,9 +170,12 @@ fn resolve_permission_request_decision<'a>(
|
||||
}
|
||||
|
||||
fn build_command_input(request: &PermissionRequestRequest) -> PermissionRequestCommandInput {
|
||||
let subagent = SubagentCommandInputFields::from(request.subagent.as_ref());
|
||||
PermissionRequestCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
agent_id: subagent.agent_id,
|
||||
agent_type: subagent.agent_type,
|
||||
transcript_path: crate::schema::NullableString::from_path(request.transcript_path.clone()),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "PermissionRequest".to_string(),
|
||||
|
||||
@@ -17,11 +17,13 @@ use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
use crate::schema::PostToolUseCommandInput;
|
||||
use crate::schema::SubagentCommandInputFields;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PostToolUseRequest {
|
||||
pub session_id: ThreadId,
|
||||
pub turn_id: String,
|
||||
pub subagent: Option<common::SubagentHookContext>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub transcript_path: Option<PathBuf>,
|
||||
pub model: String,
|
||||
@@ -148,9 +150,12 @@ pub(crate) async fn run(
|
||||
/// events across processes. Shell-like tools pass `{ "command": ... }` as
|
||||
/// `tool_input`; MCP tools pass their resolved JSON arguments.
|
||||
fn command_input_json(request: &PostToolUseRequest) -> Result<String, serde_json::Error> {
|
||||
let subagent = SubagentCommandInputFields::from(request.subagent.as_ref());
|
||||
serde_json::to_string(&PostToolUseCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
agent_id: subagent.agent_id,
|
||||
agent_type: subagent.agent_type,
|
||||
transcript_path: crate::schema::NullableString::from_path(request.transcript_path.clone()),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "PostToolUse".to_string(),
|
||||
@@ -571,6 +576,7 @@ mod tests {
|
||||
super::PostToolUseRequest {
|
||||
session_id: ThreadId::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
subagent: None,
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
|
||||
@@ -17,11 +17,13 @@ use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
use crate::schema::PreToolUseCommandInput;
|
||||
use crate::schema::SubagentCommandInputFields;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PreToolUseRequest {
|
||||
pub session_id: ThreadId,
|
||||
pub turn_id: String,
|
||||
pub subagent: Option<common::SubagentHookContext>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub transcript_path: Option<PathBuf>,
|
||||
pub model: String,
|
||||
@@ -166,9 +168,12 @@ fn latest_updated_input(
|
||||
/// stable. Shell-like tools pass `{ "command": ... }` as `tool_input`; MCP
|
||||
/// tools pass their resolved JSON arguments.
|
||||
fn command_input_json(request: &PreToolUseRequest) -> Result<String, serde_json::Error> {
|
||||
let subagent = SubagentCommandInputFields::from(request.subagent.as_ref());
|
||||
serde_json::to_string(&PreToolUseCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
agent_id: subagent.agent_id,
|
||||
agent_type: subagent.agent_type,
|
||||
transcript_path: crate::schema::NullableString::from_path(request.transcript_path.clone()),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "PreToolUse".to_string(),
|
||||
@@ -763,6 +768,7 @@ mod tests {
|
||||
super::PreToolUseRequest {
|
||||
session_id: ThreadId::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
subagent: None,
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
|
||||
@@ -16,12 +16,14 @@ use crate::engine::command_runner::CommandRunResult;
|
||||
use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
use crate::schema::NullableString;
|
||||
use crate::schema::SubagentCommandInputFields;
|
||||
use crate::schema::UserPromptSubmitCommandInput;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserPromptSubmitRequest {
|
||||
pub session_id: ThreadId,
|
||||
pub turn_id: String,
|
||||
pub subagent: Option<common::SubagentHookContext>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub transcript_path: Option<PathBuf>,
|
||||
pub model: String,
|
||||
@@ -77,9 +79,12 @@ pub(crate) async fn run(
|
||||
};
|
||||
}
|
||||
|
||||
let subagent = SubagentCommandInputFields::from(request.subagent.as_ref());
|
||||
let input_json = match serde_json::to_string(&UserPromptSubmitCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
agent_id: subagent.agent_id,
|
||||
agent_type: subagent.agent_type,
|
||||
transcript_path: NullableString::from_path(request.transcript_path.clone()),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "UserPromptSubmit".to_string(),
|
||||
|
||||
@@ -14,6 +14,7 @@ pub use config_rules::hook_states_from_stack;
|
||||
pub use declarations::PluginHookDeclaration;
|
||||
pub use declarations::plugin_hook_declarations;
|
||||
pub use engine::HookListEntry;
|
||||
pub use events::common::SubagentHookContext;
|
||||
/// Hook event names as they appear in hooks JSON and config files.
|
||||
pub const HOOK_EVENT_NAMES: [&str; 10] = [
|
||||
"PreToolUse",
|
||||
|
||||
@@ -12,6 +12,8 @@ use serde_json::Value;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::events::common::SubagentHookContext;
|
||||
|
||||
const GENERATED_DIR: &str = "generated";
|
||||
const POST_TOOL_USE_INPUT_FIXTURE: &str = "post-tool-use.command.input.schema.json";
|
||||
const POST_TOOL_USE_OUTPUT_FIXTURE: &str = "post-tool-use.command.output.schema.json";
|
||||
@@ -61,6 +63,24 @@ impl JsonSchema for NullableString {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct SubagentCommandInputFields {
|
||||
pub agent_id: Option<String>,
|
||||
pub agent_type: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Option<&SubagentHookContext>> for SubagentCommandInputFields {
|
||||
fn from(value: Option<&SubagentHookContext>) -> Self {
|
||||
match value {
|
||||
Some(context) => Self {
|
||||
agent_id: Some(context.agent_id.clone()),
|
||||
agent_type: Some(context.agent_type.clone()),
|
||||
},
|
||||
None => Self::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -251,6 +271,10 @@ pub(crate) struct PreToolUseCommandInput {
|
||||
pub session_id: String,
|
||||
/// Codex extension: expose the active turn id to internal turn-scoped hooks.
|
||||
pub turn_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_type: Option<String>,
|
||||
pub transcript_path: NullableString,
|
||||
pub cwd: String,
|
||||
#[schemars(schema_with = "pre_tool_use_hook_event_name_schema")]
|
||||
@@ -270,6 +294,10 @@ pub(crate) struct PermissionRequestCommandInput {
|
||||
pub session_id: String,
|
||||
/// Codex extension: expose the active turn id to internal turn-scoped hooks.
|
||||
pub turn_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_type: Option<String>,
|
||||
pub transcript_path: NullableString,
|
||||
pub cwd: String,
|
||||
#[schemars(schema_with = "permission_request_hook_event_name_schema")]
|
||||
@@ -288,6 +316,10 @@ pub(crate) struct PostToolUseCommandInput {
|
||||
pub session_id: String,
|
||||
/// Codex extension: expose the active turn id to internal turn-scoped hooks.
|
||||
pub turn_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_type: Option<String>,
|
||||
pub transcript_path: NullableString,
|
||||
pub cwd: String,
|
||||
#[schemars(schema_with = "post_tool_use_hook_event_name_schema")]
|
||||
@@ -308,6 +340,10 @@ pub(crate) struct PreCompactCommandInput {
|
||||
pub session_id: String,
|
||||
/// Codex extension: expose the active turn id to internal turn-scoped hooks.
|
||||
pub turn_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_type: Option<String>,
|
||||
pub transcript_path: NullableString,
|
||||
pub cwd: String,
|
||||
#[schemars(schema_with = "pre_compact_hook_event_name_schema")]
|
||||
@@ -324,6 +360,10 @@ pub(crate) struct PostCompactCommandInput {
|
||||
pub session_id: String,
|
||||
/// Codex extension: expose the active turn id to internal turn-scoped hooks.
|
||||
pub turn_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_type: Option<String>,
|
||||
pub transcript_path: NullableString,
|
||||
pub cwd: String,
|
||||
#[schemars(schema_with = "post_compact_hook_event_name_schema")]
|
||||
@@ -486,6 +526,10 @@ pub(crate) struct UserPromptSubmitCommandInput {
|
||||
pub session_id: String,
|
||||
/// Codex extension: expose the active turn id to internal turn-scoped hooks.
|
||||
pub turn_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_type: Option<String>,
|
||||
pub transcript_path: NullableString,
|
||||
pub cwd: String,
|
||||
#[schemars(schema_with = "user_prompt_submit_hook_event_name_schema")]
|
||||
@@ -761,6 +805,7 @@ fn default_continue() -> bool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::NullableString;
|
||||
use super::PERMISSION_REQUEST_INPUT_FIXTURE;
|
||||
use super::PERMISSION_REQUEST_OUTPUT_FIXTURE;
|
||||
use super::POST_COMPACT_INPUT_FIXTURE;
|
||||
@@ -785,6 +830,7 @@ mod tests {
|
||||
use super::SUBAGENT_STOP_INPUT_FIXTURE;
|
||||
use super::SUBAGENT_STOP_OUTPUT_FIXTURE;
|
||||
use super::StopCommandInput;
|
||||
use super::SubagentCommandInputFields;
|
||||
use super::SubagentStartCommandInput;
|
||||
use super::SubagentStopCommandInput;
|
||||
use super::USER_PROMPT_SUBMIT_INPUT_FIXTURE;
|
||||
@@ -792,8 +838,10 @@ mod tests {
|
||||
use super::UserPromptSubmitCommandInput;
|
||||
use super::schema_json;
|
||||
use super::write_schema_fixtures;
|
||||
use crate::events::common::SubagentHookContext;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn expected_fixture(name: &str) -> &'static str {
|
||||
@@ -968,4 +1016,87 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subagent_context_fields_are_optional_for_hooks_that_run_inside_subagents() {
|
||||
let schemas = [
|
||||
schema_json::<PreToolUseCommandInput>().expect("serialize pre tool use input schema"),
|
||||
schema_json::<PermissionRequestCommandInput>()
|
||||
.expect("serialize permission request input schema"),
|
||||
schema_json::<PostToolUseCommandInput>().expect("serialize post tool use input schema"),
|
||||
schema_json::<PreCompactCommandInput>().expect("serialize pre compact input schema"),
|
||||
schema_json::<PostCompactCommandInput>().expect("serialize post compact input schema"),
|
||||
schema_json::<UserPromptSubmitCommandInput>()
|
||||
.expect("serialize user prompt submit input schema"),
|
||||
];
|
||||
|
||||
for schema in schemas {
|
||||
let schema: Value = serde_json::from_slice(&schema).expect("parse hook input schema");
|
||||
assert_eq!(schema["properties"]["agent_id"]["type"], "string");
|
||||
assert_eq!(schema["properties"]["agent_type"]["type"], "string");
|
||||
let required = schema["required"]
|
||||
.as_array()
|
||||
.expect("schema required fields");
|
||||
assert!(!required.contains(&Value::String("agent_id".to_string())));
|
||||
assert!(!required.contains(&Value::String("agent_type".to_string())));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subagent_context_fields_serialize_flat_and_omit_when_absent() {
|
||||
let subagent = SubagentCommandInputFields::from(Some(&SubagentHookContext {
|
||||
agent_id: "agent-1".to_string(),
|
||||
agent_type: "worker".to_string(),
|
||||
}));
|
||||
let input = PreToolUseCommandInput {
|
||||
session_id: "session-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
agent_id: subagent.agent_id,
|
||||
agent_type: subagent.agent_type,
|
||||
transcript_path: NullableString::from_path(None),
|
||||
cwd: "/tmp".to_string(),
|
||||
hook_event_name: "PreToolUse".to_string(),
|
||||
model: "gpt-test".to_string(),
|
||||
permission_mode: "default".to_string(),
|
||||
tool_name: "Bash".to_string(),
|
||||
tool_input: json!({ "command": "echo hello" }),
|
||||
tool_use_id: "tool-1".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(input).expect("serialize subagent hook input"),
|
||||
json!({
|
||||
"session_id": "session-1",
|
||||
"turn_id": "turn-1",
|
||||
"agent_id": "agent-1",
|
||||
"agent_type": "worker",
|
||||
"transcript_path": null,
|
||||
"cwd": "/tmp",
|
||||
"hook_event_name": "PreToolUse",
|
||||
"model": "gpt-test",
|
||||
"permission_mode": "default",
|
||||
"tool_name": "Bash",
|
||||
"tool_input": { "command": "echo hello" },
|
||||
"tool_use_id": "tool-1",
|
||||
})
|
||||
);
|
||||
|
||||
let root_input = PreToolUseCommandInput {
|
||||
session_id: "session-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
agent_id: None,
|
||||
agent_type: None,
|
||||
transcript_path: NullableString::from_path(None),
|
||||
cwd: "/tmp".to_string(),
|
||||
hook_event_name: "PreToolUse".to_string(),
|
||||
model: "gpt-test".to_string(),
|
||||
permission_mode: "default".to_string(),
|
||||
tool_name: "Bash".to_string(),
|
||||
tool_input: json!({ "command": "echo hello" }),
|
||||
tool_use_id: "tool-1".to_string(),
|
||||
};
|
||||
let root_input = serde_json::to_value(root_input).expect("serialize root hook input");
|
||||
assert_eq!(root_input.get("agent_id"), None);
|
||||
assert_eq!(root_input.get("agent_type"), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user