diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index a8198f051b..335cd7d753 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -163,7 +163,7 @@ impl AgentControl { self.session_id } - pub(crate) async fn thread_rollout_path(&self, thread_id: ThreadId) -> Option { + pub(crate) async fn rollout_path_for_thread(&self, thread_id: ThreadId) -> Option { let state = self.upgrade().ok()?; state.get_thread(thread_id).await.ok()?.rollout_path() } diff --git a/codex-rs/core/src/hook_runtime.rs b/codex-rs/core/src/hook_runtime.rs index 4d572a4fe7..8660835fdb 100644 --- a/codex-rs/core/src/hook_runtime.rs +++ b/codex-rs/core/src/hook_runtime.rs @@ -69,7 +69,7 @@ pub(crate) enum PendingInputRecord { }, } -pub(crate) struct StopHookOutcome { +pub(crate) struct TurnStopHookOutcome { pub should_stop: bool, pub should_block: bool, pub continuation_fragments: Vec, @@ -345,7 +345,7 @@ pub(crate) async fn run_turn_stop_hooks( turn_context: &Arc, stop_hook_active: bool, last_assistant_message: Option, -) -> StopHookOutcome { +) -> TurnStopHookOutcome { let hooks = sess.hooks(); let outcome = if let Some(metadata) = subagent_hook_metadata(sess, turn_context).await { let request = codex_hooks::SubagentStopRequest { @@ -381,7 +381,7 @@ pub(crate) async fn run_turn_stop_hooks( }; emit_hook_completed_events(sess, turn_context, outcome.hook_events).await; - StopHookOutcome { + TurnStopHookOutcome { should_stop: outcome.should_stop, should_block: outcome.should_block, continuation_fragments: outcome.continuation_fragments, @@ -733,7 +733,7 @@ async fn subagent_hook_metadata( let parent_transcript_path = if let Some(parent_thread_id) = parent_thread_id { sess.services .agent_control - .thread_rollout_path(parent_thread_id) + .rollout_path_for_thread(parent_thread_id) .await } else { None @@ -747,6 +747,9 @@ async fn subagent_hook_metadata( }) } +// Hook `agent_type` mirrors the spawn_agent `agent_type` argument. Internally, +// thread-spawned agents store that value as `agent_role`; omitted values use +// the default role, while system subagents expose fixed type labels. fn subagent_hook_agent_type(subagent_source: &SubAgentSource) -> String { match subagent_source { SubAgentSource::ThreadSpawn { agent_role, .. } => agent_role diff --git a/codex-rs/hooks/schema/generated/permission-request.command.input.schema.json b/codex-rs/hooks/schema/generated/permission-request.command.input.schema.json index 55b3843c0b..01bb470542 100644 --- a/codex-rs/hooks/schema/generated/permission-request.command.input.schema.json +++ b/codex-rs/hooks/schema/generated/permission-request.command.input.schema.json @@ -41,7 +41,6 @@ "$ref": "#/definitions/NullableString" }, "turn_id": { - "description": "Codex extension: expose the active turn id to internal turn-scoped hooks.", "type": "string" } }, diff --git a/codex-rs/hooks/schema/generated/post-compact.command.input.schema.json b/codex-rs/hooks/schema/generated/post-compact.command.input.schema.json index e80ed092b7..480c18d7b0 100644 --- a/codex-rs/hooks/schema/generated/post-compact.command.input.schema.json +++ b/codex-rs/hooks/schema/generated/post-compact.command.input.schema.json @@ -34,7 +34,6 @@ "type": "string" }, "turn_id": { - "description": "Codex extension: expose the active turn id to internal turn-scoped hooks.", "type": "string" } }, diff --git a/codex-rs/hooks/schema/generated/post-tool-use.command.input.schema.json b/codex-rs/hooks/schema/generated/post-tool-use.command.input.schema.json index 1ec5fb3082..1ff356244a 100644 --- a/codex-rs/hooks/schema/generated/post-tool-use.command.input.schema.json +++ b/codex-rs/hooks/schema/generated/post-tool-use.command.input.schema.json @@ -45,7 +45,6 @@ "$ref": "#/definitions/NullableString" }, "turn_id": { - "description": "Codex extension: expose the active turn id to internal turn-scoped hooks.", "type": "string" } }, diff --git a/codex-rs/hooks/schema/generated/pre-compact.command.input.schema.json b/codex-rs/hooks/schema/generated/pre-compact.command.input.schema.json index 816fae23c8..fda6380777 100644 --- a/codex-rs/hooks/schema/generated/pre-compact.command.input.schema.json +++ b/codex-rs/hooks/schema/generated/pre-compact.command.input.schema.json @@ -34,7 +34,6 @@ "type": "string" }, "turn_id": { - "description": "Codex extension: expose the active turn id to internal turn-scoped hooks.", "type": "string" } }, diff --git a/codex-rs/hooks/schema/generated/pre-tool-use.command.input.schema.json b/codex-rs/hooks/schema/generated/pre-tool-use.command.input.schema.json index f9cde01020..ae6245f4fc 100644 --- a/codex-rs/hooks/schema/generated/pre-tool-use.command.input.schema.json +++ b/codex-rs/hooks/schema/generated/pre-tool-use.command.input.schema.json @@ -44,7 +44,6 @@ "$ref": "#/definitions/NullableString" }, "turn_id": { - "description": "Codex extension: expose the active turn id to internal turn-scoped hooks.", "type": "string" } }, diff --git a/codex-rs/hooks/schema/generated/stop.command.input.schema.json b/codex-rs/hooks/schema/generated/stop.command.input.schema.json index dbd4a3f648..0a5bc5ee7d 100644 --- a/codex-rs/hooks/schema/generated/stop.command.input.schema.json +++ b/codex-rs/hooks/schema/generated/stop.command.input.schema.json @@ -43,7 +43,6 @@ "$ref": "#/definitions/NullableString" }, "turn_id": { - "description": "Codex extension: expose the active turn id to internal turn-scoped hooks.", "type": "string" } }, diff --git a/codex-rs/hooks/schema/generated/stop.command.output.schema.json b/codex-rs/hooks/schema/generated/stop.command.output.schema.json index a2bac59cd1..12ef49d3c0 100644 --- a/codex-rs/hooks/schema/generated/stop.command.output.schema.json +++ b/codex-rs/hooks/schema/generated/stop.command.output.schema.json @@ -24,7 +24,6 @@ }, "reason": { "default": null, - "description": "Claude requires `reason` when `decision` is `block`; we enforce that semantic rule during output parsing rather than in the JSON schema.", "type": "string" }, "stopReason": { diff --git a/codex-rs/hooks/schema/generated/subagent-start.command.input.schema.json b/codex-rs/hooks/schema/generated/subagent-start.command.input.schema.json index 2f6edd6038..62e73fcf55 100644 --- a/codex-rs/hooks/schema/generated/subagent-start.command.input.schema.json +++ b/codex-rs/hooks/schema/generated/subagent-start.command.input.schema.json @@ -43,7 +43,6 @@ "$ref": "#/definitions/NullableString" }, "turn_id": { - "description": "Codex extension: expose the active turn id to internal turn-scoped hooks.", "type": "string" } }, diff --git a/codex-rs/hooks/schema/generated/subagent-stop.command.input.schema.json b/codex-rs/hooks/schema/generated/subagent-stop.command.input.schema.json index 19646db1d2..2e824f4986 100644 --- a/codex-rs/hooks/schema/generated/subagent-stop.command.input.schema.json +++ b/codex-rs/hooks/schema/generated/subagent-stop.command.input.schema.json @@ -52,7 +52,6 @@ "$ref": "#/definitions/NullableString" }, "turn_id": { - "description": "Codex extension: expose the active turn id to internal turn-scoped hooks.", "type": "string" } }, diff --git a/codex-rs/hooks/schema/generated/subagent-stop.command.output.schema.json b/codex-rs/hooks/schema/generated/subagent-stop.command.output.schema.json index 0cb1dd2a57..2f4d0f3783 100644 --- a/codex-rs/hooks/schema/generated/subagent-stop.command.output.schema.json +++ b/codex-rs/hooks/schema/generated/subagent-stop.command.output.schema.json @@ -24,7 +24,6 @@ }, "reason": { "default": null, - "description": "Claude requires `reason` when `decision` is `block`; we enforce that semantic rule during output parsing rather than in the JSON schema.", "type": "string" }, "stopReason": { diff --git a/codex-rs/hooks/schema/generated/user-prompt-submit.command.input.schema.json b/codex-rs/hooks/schema/generated/user-prompt-submit.command.input.schema.json index be5e16fc50..71345cc08e 100644 --- a/codex-rs/hooks/schema/generated/user-prompt-submit.command.input.schema.json +++ b/codex-rs/hooks/schema/generated/user-prompt-submit.command.input.schema.json @@ -40,7 +40,6 @@ "$ref": "#/definitions/NullableString" }, "turn_id": { - "description": "Codex extension: expose the active turn id to internal turn-scoped hooks.", "type": "string" } }, diff --git a/codex-rs/hooks/src/engine/output_parser.rs b/codex-rs/hooks/src/engine/output_parser.rs index a759d1b88f..38ad1c5c1d 100644 --- a/codex-rs/hooks/src/engine/output_parser.rs +++ b/codex-rs/hooks/src/engine/output_parser.rs @@ -10,7 +10,6 @@ pub(crate) struct UniversalOutput { pub(crate) struct SessionStartOutput { pub universal: UniversalOutput, pub additional_context: Option, - pub invalid_reason: Option, } #[derive(Debug, Clone)] @@ -99,24 +98,17 @@ pub(crate) fn parse_session_start(stdout: &str) -> Option { Some(SessionStartOutput { universal: UniversalOutput::from(wire.universal), additional_context, - invalid_reason: None, }) } pub(crate) fn parse_subagent_start(stdout: &str) -> Option { let wire: SubagentStartCommandOutputWire = parse_json(stdout)?; - let universal = UniversalOutput::from(wire.universal); - let invalid_reason = unsupported_subagent_start_universal(&universal); - let additional_context = if invalid_reason.is_none() { - wire.hook_specific_output - .and_then(|output| output.additional_context) - } else { - None - }; + let additional_context = wire + .hook_specific_output + .and_then(|output| output.additional_context); Some(SessionStartOutput { - universal, + universal: UniversalOutput::from(wire.universal), additional_context, - invalid_reason, }) } @@ -393,18 +385,6 @@ fn unsupported_post_tool_use_universal(universal: &UniversalOutput) -> Option Option { - if !universal.continue_processing { - Some("SubagentStart hook returned unsupported continue:false".to_string()) - } else if universal.stop_reason.is_some() { - Some("SubagentStart hook returned unsupported stopReason".to_string()) - } else if universal.suppress_output { - Some("SubagentStart hook returned unsupported suppressOutput".to_string()) - } else { - None - } -} - fn unsupported_permission_request_hook_specific_output( decision: Option<&PermissionRequestDecisionWire>, ) -> Option { diff --git a/codex-rs/hooks/src/events/subagent_start.rs b/codex-rs/hooks/src/events/subagent_start.rs index 0edda8394e..5e70fea693 100644 --- a/codex-rs/hooks/src/events/subagent_start.rs +++ b/codex-rs/hooks/src/events/subagent_start.rs @@ -144,13 +144,7 @@ fn parse_completed( text: system_message, }); } - if let Some(invalid_reason) = parsed.invalid_reason { - status = HookRunStatus::Failed; - entries.push(HookOutputEntry { - kind: HookOutputEntryKind::Error, - text: invalid_reason, - }); - } else if let Some(additional_context) = parsed.additional_context { + if let Some(additional_context) = parsed.additional_context { common::append_additional_context( &mut entries, &mut additional_contexts_for_model, diff --git a/codex-rs/hooks/src/schema.rs b/codex-rs/hooks/src/schema.rs index 96386b481e..2a63704e0a 100644 --- a/codex-rs/hooks/src/schema.rs +++ b/codex-rs/hooks/src/schema.rs @@ -249,7 +249,6 @@ pub(crate) enum PreToolUseDecisionWire { #[schemars(rename = "pre-tool-use.command.input")] pub(crate) struct PreToolUseCommandInput { pub session_id: String, - /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, pub transcript_path: NullableString, pub cwd: String, @@ -268,7 +267,6 @@ pub(crate) struct PreToolUseCommandInput { #[schemars(rename = "permission-request.command.input")] pub(crate) struct PermissionRequestCommandInput { pub session_id: String, - /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, pub transcript_path: NullableString, pub cwd: String, @@ -286,7 +284,6 @@ pub(crate) struct PermissionRequestCommandInput { #[schemars(rename = "post-tool-use.command.input")] pub(crate) struct PostToolUseCommandInput { pub session_id: String, - /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, pub transcript_path: NullableString, pub cwd: String, @@ -306,7 +303,6 @@ pub(crate) struct PostToolUseCommandInput { #[schemars(rename = "pre-compact.command.input")] pub(crate) struct PreCompactCommandInput { pub session_id: String, - /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, pub transcript_path: NullableString, pub cwd: String, @@ -322,7 +318,6 @@ pub(crate) struct PreCompactCommandInput { #[schemars(rename = "post-compact.command.input")] pub(crate) struct PostCompactCommandInput { pub session_id: String, - /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, pub transcript_path: NullableString, pub cwd: String, @@ -406,8 +401,6 @@ pub(crate) struct StopCommandOutputWire { pub universal: HookUniversalOutputWire, #[serde(default)] pub decision: Option, - /// Claude requires `reason` when `decision` is `block`; we enforce that - /// semantic rule during output parsing rather than in the JSON schema. #[serde(default)] pub reason: Option, } @@ -421,8 +414,6 @@ pub(crate) struct SubagentStopCommandOutputWire { pub universal: HookUniversalOutputWire, #[serde(default)] pub decision: Option, - /// Claude requires `reason` when `decision` is `block`; we enforce that - /// semantic rule during output parsing rather than in the JSON schema. #[serde(default)] pub reason: Option, } @@ -475,7 +466,6 @@ impl SessionStartCommandInput { #[schemars(rename = "user-prompt-submit.command.input")] pub(crate) struct UserPromptSubmitCommandInput { pub session_id: String, - /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, pub transcript_path: NullableString, pub cwd: String, @@ -492,7 +482,6 @@ pub(crate) struct UserPromptSubmitCommandInput { #[schemars(rename = "subagent-start.command.input")] pub(crate) struct SubagentStartCommandInput { pub session_id: String, - /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, pub transcript_path: NullableString, pub cwd: String, @@ -510,7 +499,6 @@ pub(crate) struct SubagentStartCommandInput { #[schemars(rename = "stop.command.input")] pub(crate) struct StopCommandInput { pub session_id: String, - /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, pub transcript_path: NullableString, pub cwd: String, @@ -528,7 +516,6 @@ pub(crate) struct StopCommandInput { #[schemars(rename = "subagent-stop.command.input")] pub(crate) struct SubagentStopCommandInput { pub session_id: String, - /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, pub transcript_path: NullableString, pub cwd: String, diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hooks_popup_shows_list_diagnostics.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hooks_popup_shows_list_diagnostics.snap index 5224197d2f..99922ac630 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hooks_popup_shows_list_diagnostics.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hooks_popup_shows_list_diagnostics.snap @@ -17,6 +17,8 @@ expression: popup PostCompact 0 0 After context compaction SessionStart 0 0 When a new session starts UserPromptSubmit 0 0 When the user submits a prompt + SubagentStart 0 0 When a subagent is created + SubagentStop 0 0 Right before a subagent ends its turn Stop 0 0 Right before Codex ends its turn Press enter to view hooks; esc to close