mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add SubagentStop hook (#22873)
# What <img width="1792" height="1024" alt="image" src="https://github.com/user-attachments/assets/8f81d232-5813-4994-a61d-e42a05a93a3e" /> `SubagentStop` runs when a thread-spawned subagent turn is about to finish. Thread-spawned subagents use `SubagentStop` instead of the normal root-agent `Stop` hook. Configured handlers match on `agent_type`. Hook input includes the normal stop fields plus: - `agent_id`: the child thread id. - `agent_type`: the resolved subagent type. - `agent_transcript_path`: the child subagent transcript path. - `transcript_path`: the parent thread transcript path. - `last_assistant_message`: the final assistant message from the child turn, when available. - `stop_hook_active`: `true` when the child is already continuing because an earlier stop-like hook blocked completion. `SubagentStop` shares the same completion-control semantics as `Stop`, scoped to the child turn: - No decision allows the child turn to finish. - `decision: "block"` with a non-empty `reason` records that reason as hook feedback and continues the child with that prompt. - `continue: false` stops the child turn. If `stopReason` is present, Codex surfaces it as the stop reason. # Lifecycle Scope Only thread-spawned subagents run `SubagentStop`. Internal/system subagents such as Review, Compact, MemoryConsolidation, and Other do not run normal `Stop` hooks and do not run `SubagentStop`. This avoids exposing synthetic matcher labels for internal implementation paths. # Stack 1. #22782: add `SubagentStart`. 2. This PR: add `SubagentStop`. 3. #22882: add subagent identity to normal hook inputs.
This commit is contained in:
committed by
GitHub
Unverified
parent
40ad7be2b5
commit
eee3e60db3
@@ -50,6 +50,7 @@ pub(crate) fn select_handlers_for_matcher_inputs(
|
||||
| HookEventName::PostToolUse
|
||||
| HookEventName::SessionStart
|
||||
| HookEventName::SubagentStart
|
||||
| HookEventName::SubagentStop
|
||||
| HookEventName::PreCompact
|
||||
| HookEventName::PostCompact => {
|
||||
if matcher_inputs.is_empty() {
|
||||
@@ -147,6 +148,7 @@ fn scope_for_event(event_name: HookEventName) -> HookScope {
|
||||
| HookEventName::PreCompact
|
||||
| HookEventName::PostCompact
|
||||
| HookEventName::UserPromptSubmit
|
||||
| HookEventName::SubagentStop
|
||||
| HookEventName::Stop => HookScope::Turn,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ impl ConfiguredHandler {
|
||||
codex_protocol::protocol::HookEventName::SessionStart => "session-start",
|
||||
codex_protocol::protocol::HookEventName::UserPromptSubmit => "user-prompt-submit",
|
||||
codex_protocol::protocol::HookEventName::SubagentStart => "subagent-start",
|
||||
codex_protocol::protocol::HookEventName::SubagentStop => "subagent-stop",
|
||||
codex_protocol::protocol::HookEventName::Stop => "stop",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ use crate::schema::PreToolUsePermissionDecisionWire;
|
||||
use crate::schema::SessionStartCommandOutputWire;
|
||||
use crate::schema::StopCommandOutputWire;
|
||||
use crate::schema::SubagentStartCommandOutputWire;
|
||||
use crate::schema::SubagentStopCommandOutputWire;
|
||||
use crate::schema::UserPromptSubmitCommandOutputWire;
|
||||
|
||||
pub(crate) fn parse_session_start(stdout: &str) -> Option<SessionStartOutput> {
|
||||
@@ -280,22 +281,46 @@ pub(crate) fn parse_user_prompt_submit(stdout: &str) -> Option<UserPromptSubmitO
|
||||
|
||||
pub(crate) fn parse_stop(stdout: &str) -> Option<StopOutput> {
|
||||
let wire: StopCommandOutputWire = parse_json(stdout)?;
|
||||
let should_block = matches!(wire.decision, Some(BlockDecisionWire::Block));
|
||||
Some(stop_output(
|
||||
wire.universal,
|
||||
wire.decision,
|
||||
wire.reason,
|
||||
"Stop",
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_subagent_stop(stdout: &str) -> Option<StopOutput> {
|
||||
let wire: SubagentStopCommandOutputWire = parse_json(stdout)?;
|
||||
Some(stop_output(
|
||||
wire.universal,
|
||||
wire.decision,
|
||||
wire.reason,
|
||||
"SubagentStop",
|
||||
))
|
||||
}
|
||||
|
||||
fn stop_output(
|
||||
universal: HookUniversalOutputWire,
|
||||
decision: Option<BlockDecisionWire>,
|
||||
reason: Option<String>,
|
||||
event_name: &str,
|
||||
) -> StopOutput {
|
||||
let should_block = matches!(decision, Some(BlockDecisionWire::Block));
|
||||
let invalid_block_reason = if should_block
|
||||
&& match wire.reason.as_deref() {
|
||||
&& match reason.as_deref() {
|
||||
Some(reason) => reason.trim().is_empty(),
|
||||
None => true,
|
||||
} {
|
||||
Some(invalid_block_message("Stop"))
|
||||
Some(invalid_block_message(event_name))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(StopOutput {
|
||||
universal: UniversalOutput::from(wire.universal),
|
||||
StopOutput {
|
||||
universal: UniversalOutput::from(universal),
|
||||
should_block: should_block && invalid_block_reason.is_none(),
|
||||
reason: wire.reason,
|
||||
reason,
|
||||
invalid_block_reason,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HookUniversalOutputWire> for UniversalOutput {
|
||||
|
||||
@@ -18,6 +18,8 @@ pub(crate) struct GeneratedHookSchemas {
|
||||
pub session_start_command_output: Value,
|
||||
pub subagent_start_command_input: Value,
|
||||
pub subagent_start_command_output: Value,
|
||||
pub subagent_stop_command_input: Value,
|
||||
pub subagent_stop_command_output: Value,
|
||||
pub user_prompt_submit_command_input: Value,
|
||||
pub user_prompt_submit_command_output: Value,
|
||||
pub stop_command_input: Value,
|
||||
@@ -83,6 +85,14 @@ pub(crate) fn generated_hook_schemas() -> &'static GeneratedHookSchemas {
|
||||
"subagent-start.command.output",
|
||||
include_str!("../../schema/generated/subagent-start.command.output.schema.json"),
|
||||
),
|
||||
subagent_stop_command_input: parse_json_schema(
|
||||
"subagent-stop.command.input",
|
||||
include_str!("../../schema/generated/subagent-stop.command.input.schema.json"),
|
||||
),
|
||||
subagent_stop_command_output: parse_json_schema(
|
||||
"subagent-stop.command.output",
|
||||
include_str!("../../schema/generated/subagent-stop.command.output.schema.json"),
|
||||
),
|
||||
user_prompt_submit_command_input: parse_json_schema(
|
||||
"user-prompt-submit.command.input",
|
||||
include_str!("../../schema/generated/user-prompt-submit.command.input.schema.json"),
|
||||
@@ -130,6 +140,8 @@ mod tests {
|
||||
assert_eq!(schemas.session_start_command_output["type"], "object");
|
||||
assert_eq!(schemas.subagent_start_command_input["type"], "object");
|
||||
assert_eq!(schemas.subagent_start_command_output["type"], "object");
|
||||
assert_eq!(schemas.subagent_stop_command_input["type"], "object");
|
||||
assert_eq!(schemas.subagent_stop_command_output["type"], "object");
|
||||
assert_eq!(schemas.user_prompt_submit_command_input["type"], "object");
|
||||
assert_eq!(schemas.user_prompt_submit_command_output["type"], "object");
|
||||
assert_eq!(schemas.stop_command_input["type"], "object");
|
||||
|
||||
Reference in New Issue
Block a user