mirror of
https://github.com/openai/codex.git
synced 2026-10-21 05:22:21 +00:00
Add a feature flag for asynchronous user messages (#45124)
## What changed Add the disabled-by-default `send_message_to_user_async` feature flag so root agents can use the tool without model catalog support. Preserve catalog-based opt-in and keep the tool unavailable to subagents. Register the flag as under development and expose it in the configuration schema. ## Testing Extend integration tests to cover feature and catalog opt-ins, deduplication when both are enabled, subagent exclusion, and the retired `send_async_message` flag. Check that either opt-in allows messages to be emitted without ending the turn. GitOrigin-RevId: 3f53181ab074b0432c795550de73a9b1da7caf15
This commit is contained in:
committed by
copyberry
parent
70eb36203d
commit
b979d4f1f0
@@ -971,6 +971,9 @@
|
||||
"send_async_message": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"send_message_to_user_async": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"shell_snapshot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -6586,6 +6589,9 @@
|
||||
"send_async_message": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"send_message_to_user_async": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"shell_snapshot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -1190,11 +1190,12 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
|
||||
}
|
||||
|
||||
if !turn_context.session_source.is_non_root_agent()
|
||||
&& context
|
||||
.model_info
|
||||
.experimental_supported_tools
|
||||
.iter()
|
||||
.any(|tool| tool == "send_message_to_user_async")
|
||||
&& (features.enabled(Feature::SendMessageToUserAsync)
|
||||
|| context
|
||||
.model_info
|
||||
.experimental_supported_tools
|
||||
.iter()
|
||||
.any(|tool| tool == "send_message_to_user_async"))
|
||||
{
|
||||
registry.add_with_exposure(SendMessageToUserAsyncHandler, ToolExposure::DirectModelOnly);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use codex_core::StartThreadOptions;
|
||||
use codex_core::TurnInputRequest;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use codex_protocol::items::AgentMessageDelivery;
|
||||
use codex_protocol::items::AgentMessageItem;
|
||||
@@ -120,15 +121,21 @@ async fn persistent_async_message_guidance_follows_tool_availability(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(SessionSource::Exec, None, false; "root_without_opt_in")]
|
||||
#[test_case(SessionSource::Exec, Some("send_user_message_async"), false; "root_with_legacy_question_tool")]
|
||||
#[test_case(SessionSource::Exec, Some("request_user_input_async"), false; "root_with_question_tool")]
|
||||
#[test_case(SessionSource::Exec, Some("send_message_to_user_async"), true; "root_with_freeform_tool")]
|
||||
#[test_case(SessionSource::SubAgent(SubAgentSource::Other("test".to_string())), Some("send_message_to_user_async"), false; "subagent_with_freeform_tool")]
|
||||
#[test_case(SessionSource::Exec, None, &[], false; "root_without_opt_in")]
|
||||
#[test_case(SessionSource::Exec, None, &[Feature::SendAsyncMessage], false; "root_with_retired_feature")]
|
||||
#[test_case(SessionSource::Exec, Some("send_user_message_async"), &[], false; "root_with_legacy_question_tool")]
|
||||
#[test_case(SessionSource::Exec, Some("request_user_input_async"), &[], false; "root_with_question_tool")]
|
||||
#[test_case(SessionSource::Exec, Some("send_message_to_user_async"), &[], true; "root_with_catalog_opt_in")]
|
||||
#[test_case(SessionSource::Exec, None, &[Feature::SendMessageToUserAsync], true; "root_with_feature_opt_in")]
|
||||
#[test_case(SessionSource::Exec, Some("send_message_to_user_async"), &[Feature::SendMessageToUserAsync], true; "root_with_both_opt_ins")]
|
||||
#[test_case(SessionSource::SubAgent(SubAgentSource::Other("test".to_string())), Some("send_message_to_user_async"), &[], false; "subagent_with_catalog_opt_in")]
|
||||
#[test_case(SessionSource::SubAgent(SubAgentSource::Other("test".to_string())), None, &[Feature::SendMessageToUserAsync], false; "subagent_with_feature_opt_in")]
|
||||
#[test_case(SessionSource::SubAgent(SubAgentSource::Other("test".to_string())), Some("send_message_to_user_async"), &[Feature::SendMessageToUserAsync], false; "subagent_with_both_opt_ins")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn freeform_async_message_requires_root_and_exact_catalog_opt_in(
|
||||
async fn freeform_async_message_requires_root_and_catalog_or_feature_opt_in(
|
||||
session_source: SessionSource,
|
||||
catalog_tool: Option<&'static str>,
|
||||
enabled_features: &'static [Feature],
|
||||
expected: bool,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -145,6 +152,14 @@ async fn freeform_async_message_requires_root_and_exact_catalog_opt_in(
|
||||
model.experimental_supported_tools =
|
||||
catalog_tool.map(str::to_string).into_iter().collect();
|
||||
})
|
||||
.with_config(move |config| {
|
||||
for &feature in enabled_features {
|
||||
config
|
||||
.features
|
||||
.enable(feature)
|
||||
.expect("enable async messaging test feature");
|
||||
}
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
let thread = test
|
||||
@@ -168,14 +183,20 @@ async fn freeform_async_message_requires_root_and_exact_catalog_opt_in(
|
||||
assert_eq!(
|
||||
tools
|
||||
.iter()
|
||||
.any(|tool| tool["name"] == "send_message_to_user_async"),
|
||||
expected,
|
||||
.filter(|tool| tool["name"] == "send_message_to_user_async")
|
||||
.count(),
|
||||
usize::from(expected),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Some("send_message_to_user_async"), &[]; "catalog_opt_in")]
|
||||
#[test_case(None, &[Feature::SendMessageToUserAsync]; "feature_opt_in")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn freeform_async_message_emits_an_item_without_ending_the_turn() -> Result<()> {
|
||||
async fn freeform_async_message_emits_an_item_without_ending_the_turn(
|
||||
catalog_tool: Option<&'static str>,
|
||||
enabled_features: &'static [Feature],
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
const CALL_ID: &str = "freeform-message";
|
||||
@@ -213,12 +234,12 @@ async fn freeform_async_message_emits_an_item_without_ending_the_turn() -> Resul
|
||||
)
|
||||
.await;
|
||||
let test = test_codex()
|
||||
.with_model_info_override("gpt-5.2", |model| {
|
||||
.with_model_info_override("gpt-5.2", move |model| {
|
||||
model.tool_mode = Some(ToolMode::CodeModeOnly);
|
||||
model.experimental_supported_tools.extend([
|
||||
"send_message_to_user_async".to_string(),
|
||||
"request_user_input_async".to_string(),
|
||||
]);
|
||||
model.experimental_supported_tools = vec!["request_user_input_async".to_string()];
|
||||
model
|
||||
.experimental_supported_tools
|
||||
.extend(catalog_tool.map(str::to_string));
|
||||
model
|
||||
.model_messages
|
||||
.as_mut()
|
||||
@@ -229,6 +250,14 @@ async fn freeform_async_message_emits_an_item_without_ending_the_turn() -> Resul
|
||||
}),
|
||||
});
|
||||
})
|
||||
.with_config(move |config| {
|
||||
for &feature in enabled_features {
|
||||
config
|
||||
.features
|
||||
.enable(feature)
|
||||
.expect("enable async messaging test feature");
|
||||
}
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
test.codex
|
||||
|
||||
@@ -307,6 +307,8 @@ pub enum Feature {
|
||||
DefaultModeRequestUserInput,
|
||||
/// Removed compatibility flag for model-enabled async user messaging.
|
||||
SendAsyncMessage,
|
||||
/// Allow root agents to send async user messages without model catalog support.
|
||||
SendMessageToUserAsync,
|
||||
/// Enable automatic review for approval prompts.
|
||||
GuardianApproval,
|
||||
/// Select thread-owned context for both Guardian reviewers.
|
||||
@@ -1557,6 +1559,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::Removed,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::SendMessageToUserAsync,
|
||||
key: "send_message_to_user_async",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::TerminalVisualizationInstructions,
|
||||
key: "terminal_visualization_instructions",
|
||||
|
||||
Reference in New Issue
Block a user