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");
|
||||
|
||||
@@ -105,6 +105,7 @@ pub(crate) fn matcher_pattern_for_event(
|
||||
| HookEventName::PostToolUse
|
||||
| HookEventName::SessionStart
|
||||
| HookEventName::SubagentStart
|
||||
| HookEventName::SubagentStop
|
||||
| HookEventName::PreCompact
|
||||
| HookEventName::PostCompact => matcher,
|
||||
HookEventName::UserPromptSubmit | HookEventName::Stop => None,
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::engine::dispatcher;
|
||||
use crate::engine::output_parser;
|
||||
use crate::schema::NullableString;
|
||||
use crate::schema::StopCommandInput;
|
||||
use crate::schema::SubagentStopCommandInput;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StopRequest {
|
||||
@@ -29,9 +30,36 @@ pub struct StopRequest {
|
||||
pub permission_mode: String,
|
||||
pub stop_hook_active: bool,
|
||||
pub last_assistant_message: Option<String>,
|
||||
pub target: StopHookTarget,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StopHookTarget {
|
||||
Stop,
|
||||
SubagentStop {
|
||||
agent_id: String,
|
||||
agent_type: String,
|
||||
agent_transcript_path: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
impl StopHookTarget {
|
||||
fn event_name(&self) -> HookEventName {
|
||||
match self {
|
||||
Self::Stop => HookEventName::Stop,
|
||||
Self::SubagentStop { .. } => HookEventName::SubagentStop,
|
||||
}
|
||||
}
|
||||
|
||||
fn matcher_input(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Stop => None,
|
||||
Self::SubagentStop { agent_type, .. } => Some(agent_type.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StopOutcome {
|
||||
pub hook_events: Vec<HookCompletedEvent>,
|
||||
pub should_stop: bool,
|
||||
@@ -52,12 +80,16 @@ struct StopHandlerData {
|
||||
|
||||
pub(crate) fn preview(
|
||||
handlers: &[ConfiguredHandler],
|
||||
_request: &StopRequest,
|
||||
request: &StopRequest,
|
||||
) -> Vec<HookRunSummary> {
|
||||
dispatcher::select_handlers(handlers, HookEventName::Stop, /*matcher_input*/ None)
|
||||
.into_iter()
|
||||
.map(|handler| dispatcher::running_summary(&handler))
|
||||
.collect()
|
||||
dispatcher::select_handlers(
|
||||
handlers,
|
||||
request.target.event_name(),
|
||||
request.target.matcher_input(),
|
||||
)
|
||||
.into_iter()
|
||||
.map(|handler| dispatcher::running_summary(&handler))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn run(
|
||||
@@ -65,8 +97,11 @@ pub(crate) async fn run(
|
||||
shell: &CommandShell,
|
||||
request: StopRequest,
|
||||
) -> StopOutcome {
|
||||
let matched =
|
||||
dispatcher::select_handlers(handlers, HookEventName::Stop, /*matcher_input*/ None);
|
||||
let matched = dispatcher::select_handlers(
|
||||
handlers,
|
||||
request.target.event_name(),
|
||||
request.target.matcher_input(),
|
||||
);
|
||||
if matched.is_empty() {
|
||||
return StopOutcome {
|
||||
hook_events: Vec::new(),
|
||||
@@ -78,24 +113,67 @@ pub(crate) async fn run(
|
||||
};
|
||||
}
|
||||
|
||||
let input_json = match serde_json::to_string(&StopCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
transcript_path: NullableString::from_path(request.transcript_path.clone()),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "Stop".to_string(),
|
||||
model: request.model.clone(),
|
||||
permission_mode: request.permission_mode.clone(),
|
||||
stop_hook_active: request.stop_hook_active,
|
||||
last_assistant_message: NullableString::from_string(request.last_assistant_message.clone()),
|
||||
}) {
|
||||
Ok(input_json) => input_json,
|
||||
Err(error) => {
|
||||
return serialization_failure_outcome(common::serialization_failure_hook_events(
|
||||
matched,
|
||||
Some(request.turn_id),
|
||||
format!("failed to serialize stop hook input: {error}"),
|
||||
));
|
||||
let input_json = match request.target {
|
||||
StopHookTarget::Stop => {
|
||||
let input = StopCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
transcript_path: NullableString::from_path(request.transcript_path.clone()),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "Stop".to_string(),
|
||||
model: request.model.clone(),
|
||||
permission_mode: request.permission_mode.clone(),
|
||||
stop_hook_active: request.stop_hook_active,
|
||||
last_assistant_message: NullableString::from_string(
|
||||
request.last_assistant_message.clone(),
|
||||
),
|
||||
};
|
||||
match serde_json::to_string(&input) {
|
||||
Ok(input_json) => input_json,
|
||||
Err(error) => {
|
||||
return serialization_failure_outcome(
|
||||
common::serialization_failure_hook_events(
|
||||
matched,
|
||||
Some(request.turn_id),
|
||||
format!("failed to serialize stop hook input: {error}"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
StopHookTarget::SubagentStop {
|
||||
agent_id,
|
||||
agent_type,
|
||||
agent_transcript_path,
|
||||
} => {
|
||||
let input = SubagentStopCommandInput {
|
||||
session_id: request.session_id.to_string(),
|
||||
turn_id: request.turn_id.clone(),
|
||||
transcript_path: NullableString::from_path(request.transcript_path.clone()),
|
||||
agent_transcript_path: NullableString::from_path(agent_transcript_path),
|
||||
cwd: request.cwd.display().to_string(),
|
||||
hook_event_name: "SubagentStop".to_string(),
|
||||
model: request.model.clone(),
|
||||
permission_mode: request.permission_mode.clone(),
|
||||
stop_hook_active: request.stop_hook_active,
|
||||
agent_id,
|
||||
agent_type,
|
||||
last_assistant_message: NullableString::from_string(
|
||||
request.last_assistant_message.clone(),
|
||||
),
|
||||
};
|
||||
match serde_json::to_string(&input) {
|
||||
Ok(input_json) => input_json,
|
||||
Err(error) => {
|
||||
return serialization_failure_outcome(
|
||||
common::serialization_failure_hook_events(
|
||||
matched,
|
||||
Some(request.turn_id),
|
||||
format!("failed to serialize subagent stop hook input: {error}"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -133,6 +211,12 @@ fn parse_completed(
|
||||
let mut should_block = false;
|
||||
let mut block_reason = None;
|
||||
let mut continuation_prompt = None;
|
||||
let hook_event_name = match handler.event_name {
|
||||
HookEventName::Stop | HookEventName::SubagentStop => handler.event_name,
|
||||
event_name => {
|
||||
panic!("expected stop hook event, got {event_name:?}");
|
||||
}
|
||||
};
|
||||
|
||||
match run_result.error.as_deref() {
|
||||
Some(error) => {
|
||||
@@ -146,7 +230,13 @@ fn parse_completed(
|
||||
Some(0) => {
|
||||
let trimmed_stdout = run_result.stdout.trim();
|
||||
if trimmed_stdout.is_empty() {
|
||||
} else if let Some(parsed) = output_parser::parse_stop(&run_result.stdout) {
|
||||
} else if let Some(parsed) = match hook_event_name {
|
||||
HookEventName::Stop => output_parser::parse_stop(&run_result.stdout),
|
||||
HookEventName::SubagentStop => {
|
||||
output_parser::parse_subagent_stop(&run_result.stdout)
|
||||
}
|
||||
_ => unreachable!("validated stop hook event"),
|
||||
} {
|
||||
if let Some(system_message) = parsed.universal.system_message {
|
||||
entries.push(HookOutputEntry {
|
||||
kind: HookOutputEntryKind::Warning,
|
||||
@@ -186,9 +276,12 @@ fn parse_completed(
|
||||
status = HookRunStatus::Failed;
|
||||
entries.push(HookOutputEntry {
|
||||
kind: HookOutputEntryKind::Error,
|
||||
text:
|
||||
"Stop hook returned decision:block without a non-empty reason"
|
||||
.to_string(),
|
||||
text: match hook_event_name {
|
||||
HookEventName::Stop => "Stop hook returned decision:block without a non-empty reason",
|
||||
HookEventName::SubagentStop => "SubagentStop hook returned decision:block without a non-empty reason",
|
||||
_ => unreachable!("validated stop hook event"),
|
||||
}
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -196,7 +289,14 @@ fn parse_completed(
|
||||
status = HookRunStatus::Failed;
|
||||
entries.push(HookOutputEntry {
|
||||
kind: HookOutputEntryKind::Error,
|
||||
text: "hook returned invalid stop hook JSON output".to_string(),
|
||||
text: match hook_event_name {
|
||||
HookEventName::Stop => "hook returned invalid stop hook JSON output",
|
||||
HookEventName::SubagentStop => {
|
||||
"hook returned invalid subagent stop hook JSON output"
|
||||
}
|
||||
_ => unreachable!("validated stop hook event"),
|
||||
}
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -214,9 +314,16 @@ fn parse_completed(
|
||||
status = HookRunStatus::Failed;
|
||||
entries.push(HookOutputEntry {
|
||||
kind: HookOutputEntryKind::Error,
|
||||
text:
|
||||
"Stop hook exited with code 2 but did not write a continuation prompt to stderr"
|
||||
.to_string(),
|
||||
text: match hook_event_name {
|
||||
HookEventName::Stop => {
|
||||
"Stop hook exited with code 2 but did not write a continuation prompt to stderr"
|
||||
}
|
||||
HookEventName::SubagentStop => {
|
||||
"SubagentStop hook exited with code 2 but did not write a continuation prompt to stderr"
|
||||
}
|
||||
_ => unreachable!("validated stop hook event"),
|
||||
}
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ pub use declarations::PluginHookDeclaration;
|
||||
pub use declarations::plugin_hook_declarations;
|
||||
pub use engine::HookListEntry;
|
||||
/// Hook event names as they appear in hooks JSON and config files.
|
||||
pub const HOOK_EVENT_NAMES: [&str; 9] = [
|
||||
pub const HOOK_EVENT_NAMES: [&str; 10] = [
|
||||
"PreToolUse",
|
||||
"PermissionRequest",
|
||||
"PostToolUse",
|
||||
@@ -24,6 +24,7 @@ pub const HOOK_EVENT_NAMES: [&str; 9] = [
|
||||
"SessionStart",
|
||||
"UserPromptSubmit",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"Stop",
|
||||
];
|
||||
|
||||
@@ -32,7 +33,7 @@ pub const HOOK_EVENT_NAMES: [&str; 9] = [
|
||||
/// Other events can appear in hooks JSON, but Codex ignores their matcher
|
||||
/// fields because those events do not dispatch against a tool, compaction
|
||||
/// trigger, or session-start source.
|
||||
pub const HOOK_EVENT_NAMES_WITH_MATCHERS: [&str; 7] = [
|
||||
pub const HOOK_EVENT_NAMES_WITH_MATCHERS: [&str; 8] = [
|
||||
"PreToolUse",
|
||||
"PermissionRequest",
|
||||
"PostToolUse",
|
||||
@@ -40,6 +41,7 @@ pub const HOOK_EVENT_NAMES_WITH_MATCHERS: [&str; 7] = [
|
||||
"PostCompact",
|
||||
"SessionStart",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
];
|
||||
|
||||
pub use events::compact::PostCompactRequest;
|
||||
@@ -57,6 +59,7 @@ pub use events::session_start::SessionStartOutcome;
|
||||
pub use events::session_start::SessionStartRequest;
|
||||
pub use events::session_start::SessionStartSource;
|
||||
pub use events::session_start::StartHookTarget;
|
||||
pub use events::stop::StopHookTarget;
|
||||
pub use events::stop::StopOutcome;
|
||||
pub use events::stop::StopRequest;
|
||||
pub use events::user_prompt_submit::UserPromptSubmitOutcome;
|
||||
@@ -87,6 +90,7 @@ pub fn hook_event_key_label(event_name: HookEventName) -> &'static str {
|
||||
HookEventName::SessionStart => "session_start",
|
||||
HookEventName::UserPromptSubmit => "user_prompt_submit",
|
||||
HookEventName::SubagentStart => "subagent_start",
|
||||
HookEventName::SubagentStop => "subagent_stop",
|
||||
HookEventName::Stop => "stop",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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