Add agents.interrupt_message for interruption markers (#19351)

## Why

Agent interruptions currently always persist a model-visible
interrupted-turn marker before emitting `TurnAborted`. That marker is
useful by default because it gives the next model turn context about a
deliberately interrupted task, but some deployments need to suppress
that history injection entirely while still keeping the client-visible
interruption event.

## What changed

- Add `[agents] interrupt_message = false` to disable the model-visible
interrupted-turn marker.
- Resolve the setting into `Config::agent_interrupt_message_enabled`,
defaulting to `true` so existing behavior is unchanged.
- Apply the setting to both live interrupted turns and interrupted fork
snapshots.
- Keep emitting `TurnAborted` even when the history marker is disabled.
- Regenerate `core/config.schema.json` for the new
`agents.interrupt_message` field.

## Testing

- `cargo test -p codex-core load_config_resolves_agent_interrupt_message
-- --nocapture`
- `cargo test -p codex-core
disabled_interrupted_fork_snapshot_appends_only_interrupt_event --
--nocapture`
- `cargo test -p codex-core
multi_agent_v2_interrupted_marker_uses_developer_input_message --
--nocapture`
- `cargo test -p codex-core
multi_agent_v2_followup_task_can_disable_interrupted_marker --
--nocapture`
- `cargo test -p codex-core
multi_agent_v2_followup_task_interrupts_busy_child_without_losing_message
-- --nocapture`
- `cargo check -p codex-core`
This commit is contained in:
jif-oai
2026-04-24 16:02:45 +02:00
committed by GitHub
parent deb4509302
commit 28742866c7
8 changed files with 287 additions and 57 deletions
+56 -27
View File
@@ -19,6 +19,7 @@ use tracing::info_span;
use tracing::trace;
use tracing::warn;
use crate::config::Config;
use crate::context::ContextualUserFragment;
use crate::hook_runtime::PendingInputHookDisposition;
use crate::hook_runtime::inspect_pending_input;
@@ -62,27 +63,50 @@ pub(crate) use user_shell::execute_user_shell_command;
const GRACEFULL_INTERRUPTION_TIMEOUT_MS: u64 = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InterruptedTurnHistoryMarker {
Disabled,
ContextualUser,
Developer,
}
impl InterruptedTurnHistoryMarker {
pub(crate) fn from_config(config: &Config) -> Self {
if !config.agent_interrupt_message_enabled {
return Self::Disabled;
}
if config.features.enabled(Feature::MultiAgentV2) {
Self::Developer
} else {
Self::ContextualUser
}
}
}
/// Shared model-visible marker used by both the real interrupt path and
/// interrupted fork snapshots.
pub(crate) fn interrupted_turn_history_marker(multi_agent_v2_enabled: bool) -> ResponseItem {
let guidance = if multi_agent_v2_enabled {
crate::context::TurnAborted::INTERRUPTED_DEVELOPER_GUIDANCE
} else {
crate::context::TurnAborted::INTERRUPTED_GUIDANCE
};
let marker = crate::context::TurnAborted::new(guidance);
if multi_agent_v2_enabled {
ResponseItem::Message {
id: None,
role: "developer".to_string(),
content: vec![ContentItem::InputText {
text: marker.render(),
}],
end_turn: None,
phase: None,
pub(crate) fn interrupted_turn_history_marker(
marker: InterruptedTurnHistoryMarker,
) -> Option<ResponseItem> {
match marker {
InterruptedTurnHistoryMarker::Disabled => None,
InterruptedTurnHistoryMarker::ContextualUser => Some(ContextualUserFragment::into(
crate::context::TurnAborted::new(crate::context::TurnAborted::INTERRUPTED_GUIDANCE),
)),
InterruptedTurnHistoryMarker::Developer => {
let marker = crate::context::TurnAborted::new(
crate::context::TurnAborted::INTERRUPTED_DEVELOPER_GUIDANCE,
);
Some(ResponseItem::Message {
id: None,
role: "developer".to_string(),
content: vec![ContentItem::InputText {
text: marker.render(),
}],
end_turn: None,
phase: None,
})
}
} else {
ContextualUserFragment::into(marker)
}
}
@@ -692,15 +716,20 @@ impl Session {
if reason == TurnAbortReason::Interrupted {
self.cleanup_after_interrupt(&task.turn_context).await;
let marker = interrupted_turn_history_marker(self.enabled(Feature::MultiAgentV2));
self.record_into_history(std::slice::from_ref(&marker), task.turn_context.as_ref())
.await;
self.persist_rollout_items(&[RolloutItem::ResponseItem(marker)])
.await;
// Ensure the marker is durably visible before emitting TurnAborted: some clients
// synchronously re-read the rollout on receipt of the abort event.
if let Err(err) = self.flush_rollout().await {
warn!("failed to flush interrupted-turn marker before emitting TurnAborted: {err}");
if let Some(marker) = interrupted_turn_history_marker(
InterruptedTurnHistoryMarker::from_config(task.turn_context.config.as_ref()),
) {
self.record_into_history(std::slice::from_ref(&marker), task.turn_context.as_ref())
.await;
self.persist_rollout_items(&[RolloutItem::ResponseItem(marker)])
.await;
// Ensure the marker is durably visible before emitting TurnAborted: some clients
// synchronously re-read the rollout on receipt of the abort event.
if let Err(err) = self.flush_rollout().await {
warn!(
"failed to flush interrupted-turn marker before emitting TurnAborted: {err}"
);
}
}
}