Add the async user message tool (#39319)

## What changed

- Add `send_user_message_async` for root agents when the async-message feature is enabled and the model advertises tool support.
- Emit the supplied text as an asynchronous agent message and immediately return an accepted result so the turn can continue.
- Keep the user-visible update out of the model's input context.

## Testing

- Add an integration test covering tool registration, item events, continued turn execution, and model context.

GitOrigin-RevId: 13d5168cb8cd6742db17bb74a15c910994e9c98b
This commit is contained in:
rka-oai
2026-08-18 23:59:19 +00:00
committed by copyberry
parent 657bd889ae
commit 71dbf72b05
5 changed files with 268 additions and 0 deletions
+2
View File
@@ -23,6 +23,7 @@ mod request_plugin_install;
pub(crate) mod request_plugin_install_spec;
mod request_user_input;
pub(crate) mod request_user_input_spec;
mod send_user_message_async;
mod shell;
pub(crate) mod shell_spec;
mod sleep;
@@ -68,6 +69,7 @@ pub use plan::PlanHandler;
pub use request_permissions::RequestPermissionsHandler;
pub use request_plugin_install::RequestPluginInstallHandler;
pub use request_user_input::RequestUserInputHandler;
pub use send_user_message_async::SendUserMessageAsyncHandler;
pub use shell::ShellCommandHandler;
pub(crate) use shell::ShellCommandHandlerOptions;
pub use sleep::SleepHandler;
@@ -0,0 +1,101 @@
use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::context::boxed_tool_output;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
use codex_protocol::items::AgentMessageContent;
use codex_protocol::items::AgentMessageDelivery;
use codex_protocol::items::AgentMessageItem;
use codex_protocol::items::TurnItem;
use codex_protocol::models::MessagePhase;
use codex_tools::JsonSchema;
use codex_tools::ResponsesApiTool;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use serde::Deserialize;
use std::collections::BTreeMap;
const TOOL_NAME: &str = "send_user_message_async";
pub struct SendUserMessageAsyncHandler;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct SendUserMessageAsyncArgs {
message: String,
}
impl ToolExecutor<ToolInvocation> for SendUserMessageAsyncHandler {
fn tool_name(&self) -> ToolName {
ToolName::plain(TOOL_NAME)
}
fn spec(&self) -> ToolSpec {
let properties = BTreeMap::from([(
"message".to_string(),
JsonSchema::string(Some(
"The concise question or update to send to the user.".to_string(),
)),
)]);
ToolSpec::Function(ResponsesApiTool {
name: TOOL_NAME.to_string(),
description: "Send a concise, user-visible acknowledgment, important update, or blocking question. Returns immediately; any reply arrives asynchronously as a new user message."
.to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(
properties,
Some(vec!["message".to_string()]),
/*additional_properties*/ Some(false.into()),
),
output_schema: None,
})
}
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> {
Box::pin(async move {
let ToolInvocation {
session,
turn,
call_id,
payload,
..
} = invocation;
let ToolPayload::Function { arguments } = payload else {
return Err(FunctionCallError::RespondToModel(format!(
"{TOOL_NAME} handler received unsupported payload"
)));
};
let args: SendUserMessageAsyncArgs = parse_arguments(&arguments)?;
let message = args.message.trim();
if message.is_empty() {
return Err(FunctionCallError::RespondToModel(
"message must not be empty".to_string(),
));
}
let item = TurnItem::AgentMessage(AgentMessageItem {
id: call_id,
content: vec![AgentMessageContent::Text {
text: message.to_string(),
}],
phase: Some(MessagePhase::FinalAnswer),
memory_citation: None,
delivery: Some(AgentMessageDelivery::Async),
});
session.emit_turn_item_started(turn.as_ref(), &item).await;
session.emit_turn_item_completed(turn.as_ref(), item).await;
Ok(boxed_tool_output(FunctionToolOutput::from_text(
r#"{"accepted":true}"#.to_string(),
/*success*/ Some(true),
)))
})
}
}
impl CoreToolRuntime for SendUserMessageAsyncHandler {}
+12
View File
@@ -24,6 +24,7 @@ use crate::tools::handlers::ReadMcpResourceHandler;
use crate::tools::handlers::RequestPermissionsHandler;
use crate::tools::handlers::RequestPluginInstallHandler;
use crate::tools::handlers::RequestUserInputHandler;
use crate::tools::handlers::SendUserMessageAsyncHandler;
use crate::tools::handlers::ShellCommandHandler;
use crate::tools::handlers::ShellCommandHandlerOptions;
use crate::tools::handlers::SleepHandler;
@@ -1061,6 +1062,17 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
);
}
if !turn_context.session_source.is_non_root_agent()
&& features.enabled(Feature::SendAsyncMessage)
&& turn_context
.model_info
.experimental_supported_tools
.iter()
.any(|tool| tool == "send_user_message_async")
{
registry.add(SendUserMessageAsyncHandler);
}
if environment_mode.has_environment() && features.enabled(Feature::RequestPermissionsTool) {
registry.add(RequestPermissionsHandler);
}