mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user