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
@@ -29,6 +29,8 @@ const USER_PROMPT_SUBMIT_INPUT_FIXTURE: &str = "user-prompt-submit.command.input
|
||||
const USER_PROMPT_SUBMIT_OUTPUT_FIXTURE: &str = "user-prompt-submit.command.output.schema.json";
|
||||
const SUBAGENT_START_INPUT_FIXTURE: &str = "subagent-start.command.input.schema.json";
|
||||
const SUBAGENT_START_OUTPUT_FIXTURE: &str = "subagent-start.command.output.schema.json";
|
||||
const SUBAGENT_STOP_INPUT_FIXTURE: &str = "subagent-stop.command.input.schema.json";
|
||||
const SUBAGENT_STOP_OUTPUT_FIXTURE: &str = "subagent-stop.command.output.schema.json";
|
||||
const STOP_INPUT_FIXTURE: &str = "stop.command.input.schema.json";
|
||||
const STOP_OUTPUT_FIXTURE: &str = "stop.command.output.schema.json";
|
||||
|
||||
@@ -91,6 +93,8 @@ pub(crate) enum HookEventNameWire {
|
||||
UserPromptSubmit,
|
||||
#[serde(rename = "SubagentStart")]
|
||||
SubagentStart,
|
||||
#[serde(rename = "SubagentStop")]
|
||||
SubagentStop,
|
||||
#[serde(rename = "Stop")]
|
||||
Stop,
|
||||
}
|
||||
@@ -399,6 +403,21 @@ pub(crate) struct StopCommandOutputWire {
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[schemars(rename = "subagent-stop.command.output")]
|
||||
pub(crate) struct SubagentStopCommandOutputWire {
|
||||
#[serde(flatten)]
|
||||
pub universal: HookUniversalOutputWire,
|
||||
#[serde(default)]
|
||||
pub decision: Option<BlockDecisionWire>,
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub(crate) enum BlockDecisionWire {
|
||||
#[serde(rename = "block")]
|
||||
@@ -495,6 +514,27 @@ pub(crate) struct StopCommandInput {
|
||||
pub last_assistant_message: NullableString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[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 agent_transcript_path: NullableString,
|
||||
pub cwd: String,
|
||||
#[schemars(schema_with = "subagent_stop_hook_event_name_schema")]
|
||||
pub hook_event_name: String,
|
||||
pub model: String,
|
||||
#[schemars(schema_with = "permission_mode_schema")]
|
||||
pub permission_mode: String,
|
||||
pub stop_hook_active: bool,
|
||||
pub agent_id: String,
|
||||
pub agent_type: String,
|
||||
pub last_assistant_message: NullableString,
|
||||
}
|
||||
|
||||
pub fn write_schema_fixtures(schema_root: &Path) -> anyhow::Result<()> {
|
||||
let generated_dir = schema_root.join(GENERATED_DIR);
|
||||
ensure_empty_dir(&generated_dir)?;
|
||||
@@ -563,6 +603,14 @@ pub fn write_schema_fixtures(schema_root: &Path) -> anyhow::Result<()> {
|
||||
&generated_dir.join(SUBAGENT_START_OUTPUT_FIXTURE),
|
||||
schema_json::<SubagentStartCommandOutputWire>()?,
|
||||
)?;
|
||||
write_schema(
|
||||
&generated_dir.join(SUBAGENT_STOP_INPUT_FIXTURE),
|
||||
schema_json::<SubagentStopCommandInput>()?,
|
||||
)?;
|
||||
write_schema(
|
||||
&generated_dir.join(SUBAGENT_STOP_OUTPUT_FIXTURE),
|
||||
schema_json::<SubagentStopCommandOutputWire>()?,
|
||||
)?;
|
||||
write_schema(
|
||||
&generated_dir.join(STOP_INPUT_FIXTURE),
|
||||
schema_json::<StopCommandInput>()?,
|
||||
@@ -658,6 +706,10 @@ fn subagent_start_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("SubagentStart")
|
||||
}
|
||||
|
||||
fn subagent_stop_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("SubagentStop")
|
||||
}
|
||||
|
||||
fn stop_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("Stop")
|
||||
}
|
||||
@@ -730,8 +782,11 @@ mod tests {
|
||||
use super::STOP_OUTPUT_FIXTURE;
|
||||
use super::SUBAGENT_START_INPUT_FIXTURE;
|
||||
use super::SUBAGENT_START_OUTPUT_FIXTURE;
|
||||
use super::SUBAGENT_STOP_INPUT_FIXTURE;
|
||||
use super::SUBAGENT_STOP_OUTPUT_FIXTURE;
|
||||
use super::StopCommandInput;
|
||||
use super::SubagentStartCommandInput;
|
||||
use super::SubagentStopCommandInput;
|
||||
use super::USER_PROMPT_SUBMIT_INPUT_FIXTURE;
|
||||
use super::USER_PROMPT_SUBMIT_OUTPUT_FIXTURE;
|
||||
use super::UserPromptSubmitCommandInput;
|
||||
@@ -791,6 +846,12 @@ mod tests {
|
||||
SUBAGENT_START_OUTPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/subagent-start.command.output.schema.json")
|
||||
}
|
||||
SUBAGENT_STOP_INPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/subagent-stop.command.input.schema.json")
|
||||
}
|
||||
SUBAGENT_STOP_OUTPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/subagent-stop.command.output.schema.json")
|
||||
}
|
||||
STOP_INPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/stop.command.input.schema.json")
|
||||
}
|
||||
@@ -828,6 +889,8 @@ mod tests {
|
||||
USER_PROMPT_SUBMIT_OUTPUT_FIXTURE,
|
||||
SUBAGENT_START_INPUT_FIXTURE,
|
||||
SUBAGENT_START_OUTPUT_FIXTURE,
|
||||
SUBAGENT_STOP_INPUT_FIXTURE,
|
||||
SUBAGENT_STOP_OUTPUT_FIXTURE,
|
||||
STOP_INPUT_FIXTURE,
|
||||
STOP_OUTPUT_FIXTURE,
|
||||
] {
|
||||
@@ -875,6 +938,11 @@ mod tests {
|
||||
.expect("serialize subagent start input schema"),
|
||||
)
|
||||
.expect("parse subagent start input schema");
|
||||
let subagent_stop: Value = serde_json::from_slice(
|
||||
&schema_json::<SubagentStopCommandInput>()
|
||||
.expect("serialize subagent stop input schema"),
|
||||
)
|
||||
.expect("parse subagent stop input schema");
|
||||
let stop: Value = serde_json::from_slice(
|
||||
&schema_json::<StopCommandInput>().expect("serialize stop input schema"),
|
||||
)
|
||||
@@ -888,6 +956,7 @@ mod tests {
|
||||
&post_compact,
|
||||
&user_prompt_submit,
|
||||
&subagent_start,
|
||||
&subagent_stop,
|
||||
&stop,
|
||||
] {
|
||||
assert_eq!(schema["properties"]["turn_id"]["type"], "string");
|
||||
|
||||
Reference in New Issue
Block a user