Add buffered code-mode exec yields (#34441)

## What changed

- Add the experimental `code_mode_buffered_exec` feature.
- When enabled, default code-mode `exec` calls without an explicit
  `yield_time_ms` to 30 seconds instead of 10 seconds.
- Report the effective default in the model-visible `exec` tool description.

## Testing

- Verify that the generated tool description advertises the 30-second default
  when the feature is enabled.

GitOrigin-RevId: 0eea4d0105137f09050ce31aad18c25c79df6b2a
This commit is contained in:
pakrym-oai authored and copyberry committed 2026-07-21 02:07:13 +00:00
1 parent c0cd337766
commit 99efeef650
9 files changed
+92 -18

No files matched your search

+18 -3
View File
@@ -253,10 +253,14 @@ pub fn build_exec_tool_description(
enabled_tools: &[ToolDefinition], enabled_tools: &[ToolDefinition],
deferred_tools: &[ToolDefinition], deferred_tools: &[ToolDefinition],
namespace_descriptions: &BTreeMap<String, ToolNamespaceDescription>, namespace_descriptions: &BTreeMap<String, ToolNamespaceDescription>,
default_exec_yield_time_ms: u64,
code_mode_only: bool, code_mode_only: bool,
) -> String { ) -> String {
let mut sections = Vec::new(); let mut sections = Vec::new();
sections.push(EXEC_DESCRIPTION_TEMPLATE.to_string()); sections.push(EXEC_DESCRIPTION_TEMPLATE.replace(
"Defaults to 10000 ms.",
&format!("Defaults to {default_exec_yield_time_ms} ms."),
));
if !deferred_tools.is_empty() { if !deferred_tools.is_empty() {
sections.push(DEFERRED_NESTED_TOOLS_GUIDANCE.to_string()); sections.push(DEFERRED_NESTED_TOOLS_GUIDANCE.to_string());
} }
@@ -868,6 +872,7 @@ mod tests {
}], }],
&[], &[],
&BTreeMap::new(), &BTreeMap::new(),
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true, /*code_mode_only*/ true,
); );
assert!(description.contains( assert!(description.contains(
@@ -879,8 +884,13 @@ bar"
#[test] #[test]
fn exec_description_mentions_timeout_helpers() { fn exec_description_mentions_timeout_helpers() {
let description = let description = build_exec_tool_description(
build_exec_tool_description(&[], &[], &BTreeMap::new(), /*code_mode_only*/ false); &[],
&[],
&BTreeMap::new(),
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ false,
);
assert!(description.contains("`audio(audioUrlOrItem:")); assert!(description.contains("`audio(audioUrlOrItem:"));
assert!(description.contains("`setTimeout(callback: () => void, delayMs?: number)`")); assert!(description.contains("`setTimeout(callback: () => void, delayMs?: number)`"));
assert!(description.contains("`clearTimeout(timeoutId?: number)`")); assert!(description.contains("`clearTimeout(timeoutId?: number)`"));
@@ -932,6 +942,7 @@ bar"
], ],
&[], &[],
&namespace_descriptions, &namespace_descriptions,
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true, /*code_mode_only*/ true,
); );
assert_eq!(description.matches("## mcp__sample").count(), 1); assert_eq!(description.matches("## mcp__sample").count(), 1);
@@ -972,6 +983,7 @@ bar"
}], }],
&[], &[],
&namespace_descriptions, &namespace_descriptions,
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true, /*code_mode_only*/ true,
); );
@@ -1071,6 +1083,7 @@ bar"
], ],
&[], &[],
&BTreeMap::new(), &BTreeMap::new(),
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true, /*code_mode_only*/ true,
); );
@@ -1106,6 +1119,7 @@ bar"
&[], &[],
&[deferred_tool], &[deferred_tool],
&BTreeMap::new(), &BTreeMap::new(),
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true, /*code_mode_only*/ true,
); );
@@ -1127,6 +1141,7 @@ bar"
output_schema: None, output_schema: None,
}], }],
&BTreeMap::new(), &BTreeMap::new(),
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ false, /*code_mode_only*/ false,
); );
+6
View File
@@ -458,6 +458,9 @@
"code_mode": { "code_mode": {
"$ref": "#/definitions/FeatureToml_for_CodeModeConfigToml" "$ref": "#/definitions/FeatureToml_for_CodeModeConfigToml"
}, },
"code_mode_buffered_exec": {
"type": "boolean"
},
"code_mode_host": { "code_mode_host": {
"type": "boolean" "type": "boolean"
}, },
@@ -4896,6 +4899,9 @@
"code_mode": { "code_mode": {
"$ref": "#/definitions/FeatureToml_for_CodeModeConfigToml" "$ref": "#/definitions/FeatureToml_for_CodeModeConfigToml"
}, },
"code_mode_buffered_exec": {
"type": "boolean"
},
"code_mode_host": { "code_mode_host": {
"type": "boolean" "type": "boolean"
}, },
+4 -3
View File
@@ -1137,9 +1137,10 @@ impl Session {
session_configuration.parent_thread_id, session_configuration.parent_thread_id,
), ),
), ),
code_mode_service: crate::tools::code_mode::CodeModeService::new(Arc::clone( code_mode_service: crate::tools::code_mode::CodeModeService::new(
&code_mode_session_provider, Arc::clone(&code_mode_session_provider),
)), &config.features,
),
tool_search_handler_cache: Default::default(), tool_search_handler_cache: Default::default(),
turn_environments: Arc::clone(&turn_environments), turn_environments: Arc::clone(&turn_environments),
}; };
+8 -6
View File
@@ -5385,9 +5385,10 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
/*attestation_provider*/ None, /*attestation_provider*/ None,
config.http_client_factory(), config.http_client_factory(),
), ),
code_mode_service: crate::tools::code_mode::CodeModeService::new(Arc::new( code_mode_service: crate::tools::code_mode::CodeModeService::new(
codex_code_mode::InProcessCodeModeSessionProvider, Arc::new(codex_code_mode::InProcessCodeModeSessionProvider),
)), &config.features,
),
tool_search_handler_cache: Default::default(), tool_search_handler_cache: Default::default(),
turn_environments: Arc::clone(&turn_environments), turn_environments: Arc::clone(&turn_environments),
}; };
@@ -7542,9 +7543,10 @@ where
/*attestation_provider*/ None, /*attestation_provider*/ None,
config.http_client_factory(), config.http_client_factory(),
), ),
code_mode_service: crate::tools::code_mode::CodeModeService::new(Arc::new( code_mode_service: crate::tools::code_mode::CodeModeService::new(
codex_code_mode::InProcessCodeModeSessionProvider, Arc::new(codex_code_mode::InProcessCodeModeSessionProvider),
)), &config.features,
),
tool_search_handler_cache: Default::default(), tool_search_handler_cache: Default::default(),
turn_environments: Arc::clone(&turn_environments), turn_environments: Arc::clone(&turn_environments),
}; };
@@ -8,6 +8,7 @@ pub(crate) fn create_code_mode_tool(
enabled_tools: &[CodeModeToolDefinition], enabled_tools: &[CodeModeToolDefinition],
deferred_tools: &[CodeModeToolDefinition], deferred_tools: &[CodeModeToolDefinition],
namespace_descriptions: &BTreeMap<String, codex_code_mode::ToolNamespaceDescription>, namespace_descriptions: &BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
default_exec_yield_time_ms: u64,
code_mode_only: bool, code_mode_only: bool,
) -> ToolSpec { ) -> ToolSpec {
const CODE_MODE_FREEFORM_GRAMMAR: &str = r#" const CODE_MODE_FREEFORM_GRAMMAR: &str = r#"
@@ -26,6 +27,7 @@ SOURCE: /[\s\S]+/
enabled_tools, enabled_tools,
deferred_tools, deferred_tools,
namespace_descriptions, namespace_descriptions,
default_exec_yield_time_ms,
code_mode_only, code_mode_only,
), ),
format: FreeformToolFormat { format: FreeformToolFormat {
@@ -58,6 +60,7 @@ mod tests {
&enabled_tools, &enabled_tools,
&[], &[],
&BTreeMap::new(), &BTreeMap::new(),
codex_code_mode::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true, /*code_mode_only*/ true,
), ),
ToolSpec::Freeform(FreeformTool { ToolSpec::Freeform(FreeformTool {
@@ -66,6 +69,7 @@ mod tests {
&enabled_tools, &enabled_tools,
&[], &[],
&BTreeMap::new(), &BTreeMap::new(),
codex_code_mode::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true, /*code_mode_only*/ true,
), ),
format: FreeformToolFormat { format: FreeformToolFormat {
+25 -6
View File
@@ -16,6 +16,8 @@ use codex_code_mode::CodeModeSession;
use codex_code_mode::CodeModeSessionProvider; use codex_code_mode::CodeModeSessionProvider;
use codex_code_mode::CodeModeToolKind; use codex_code_mode::CodeModeToolKind;
use codex_code_mode::RuntimeResponse; use codex_code_mode::RuntimeResponse;
use codex_features::Feature;
use codex_features::Features;
use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputContentItem;
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
@@ -52,6 +54,13 @@ pub(crate) use wait_handler::CodeModeWaitHandler;
pub(crate) const PUBLIC_TOOL_NAME: &str = codex_code_mode::PUBLIC_TOOL_NAME; pub(crate) const PUBLIC_TOOL_NAME: &str = codex_code_mode::PUBLIC_TOOL_NAME;
pub(crate) const WAIT_TOOL_NAME: &str = codex_code_mode::WAIT_TOOL_NAME; pub(crate) const WAIT_TOOL_NAME: &str = codex_code_mode::WAIT_TOOL_NAME;
pub(crate) const DEFAULT_WAIT_YIELD_TIME_MS: u64 = codex_code_mode::DEFAULT_WAIT_YIELD_TIME_MS; pub(crate) const DEFAULT_WAIT_YIELD_TIME_MS: u64 = codex_code_mode::DEFAULT_WAIT_YIELD_TIME_MS;
const BUFFERED_EXEC_YIELD_TIME_MS: u64 = 30_000;
pub(crate) fn default_exec_yield_time_override_ms(features: &Features) -> Option<u64> {
features
.enabled(Feature::CodeModeBufferedExec)
.then_some(BUFFERED_EXEC_YIELD_TIME_MS)
}
/// Returns true for the un-namespaced code-mode `exec` tool. /// Returns true for the un-namespaced code-mode `exec` tool.
pub(crate) fn is_exec_tool_name(tool_name: &ToolName) -> bool { pub(crate) fn is_exec_tool_name(tool_name: &ToolName) -> bool {
@@ -68,16 +77,21 @@ pub(crate) struct CodeModeService {
session: OnceCell<Arc<dyn CodeModeSession>>, session: OnceCell<Arc<dyn CodeModeSession>>,
session_provider: Arc<dyn CodeModeSessionProvider>, session_provider: Arc<dyn CodeModeSessionProvider>,
dispatch_broker: Arc<CodeModeDispatchBroker>, dispatch_broker: Arc<CodeModeDispatchBroker>,
default_exec_yield_time_override_ms: Option<u64>,
shutting_down: AtomicBool, shutting_down: AtomicBool,
} }
impl CodeModeService { impl CodeModeService {
pub(crate) fn new(session_provider: Arc<dyn CodeModeSessionProvider>) -> Self { pub(crate) fn new(
session_provider: Arc<dyn CodeModeSessionProvider>,
features: &Features,
) -> Self {
let dispatch_broker = Arc::new(CodeModeDispatchBroker::new()); let dispatch_broker = Arc::new(CodeModeDispatchBroker::new());
Self { Self {
session: OnceCell::new(), session: OnceCell::new(),
session_provider, session_provider,
dispatch_broker, dispatch_broker,
default_exec_yield_time_override_ms: default_exec_yield_time_override_ms(features),
shutting_down: AtomicBool::new(false), shutting_down: AtomicBool::new(false),
} }
} }
@@ -88,8 +102,11 @@ impl CodeModeService {
pub(crate) async fn execute( pub(crate) async fn execute(
&self, &self,
request: codex_code_mode::ExecuteRequest, mut request: codex_code_mode::ExecuteRequest,
) -> Result<codex_code_mode::StartedCell, String> { ) -> Result<codex_code_mode::StartedCell, String> {
if request.yield_time_ms.is_none() {
request.yield_time_ms = self.default_exec_yield_time_override_ms;
}
self.session().await?.execute(request).await self.session().await?.execute(request).await
} }
@@ -371,6 +388,7 @@ mod tests {
use codex_code_mode::FunctionCallOutputContentItem as CodeModeOutputContentItem; use codex_code_mode::FunctionCallOutputContentItem as CodeModeOutputContentItem;
use codex_code_mode::ProcessOwnedCodeModeSessionProvider; use codex_code_mode::ProcessOwnedCodeModeSessionProvider;
use codex_code_mode::RuntimeResponse; use codex_code_mode::RuntimeResponse;
use codex_features::Features;
use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputContentItem;
use codex_tools::ToolName; use codex_tools::ToolName;
use serde_json::json; use serde_json::json;
@@ -444,11 +462,12 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn missing_process_host_falls_back_to_in_process_session() { async fn missing_process_host_falls_back_to_in_process_session() {
let service = CodeModeService::new(Arc::new( let service = CodeModeService::new(
ProcessOwnedCodeModeSessionProvider::with_host_program( Arc::new(ProcessOwnedCodeModeSessionProvider::with_host_program(
"codex-code-mode-host-does-not-exist".into(), "codex-code-mode-host-does-not-exist".into(),
), )),
)); &Features::with_defaults(),
);
let response = service let response = service
.execute(ExecuteRequest { .execute(ExecuteRequest {
+5
View File
@@ -2,6 +2,7 @@ use crate::agent::exceeds_thread_spawn_depth_limit;
use crate::agent::next_thread_spawn_depth; use crate::agent::next_thread_spawn_depth;
use crate::session::step_context::StepContext; use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnContext; use crate::session::turn_context::TurnContext;
use crate::tools::code_mode::default_exec_yield_time_override_ms;
use crate::tools::code_mode::execute_spec::create_code_mode_tool; use crate::tools::code_mode::execute_spec::create_code_mode_tool;
use crate::tools::context::ToolInvocation; use crate::tools::context::ToolInvocation;
use crate::tools::effective_tool_mode; use crate::tools::effective_tool_mode;
@@ -472,6 +473,9 @@ fn build_code_mode_executors(
.sort_by(|left, right| compare_code_mode_tools(left, right, &namespace_descriptions)); .sort_by(|left, right| compare_code_mode_tools(left, right, &namespace_descriptions));
let deferred_tools = let deferred_tools =
collect_code_mode_exec_prompt_tool_definitions(deferred_exec_prompt_tool_specs.iter()); collect_code_mode_exec_prompt_tool_definitions(deferred_exec_prompt_tool_specs.iter());
let default_exec_yield_time_ms =
default_exec_yield_time_override_ms(&turn_context.config.features)
.unwrap_or(codex_code_mode::DEFAULT_EXEC_YIELD_TIME_MS);
vec![ vec![
Arc::new(CodeModeExecuteHandler::new( Arc::new(CodeModeExecuteHandler::new(
@@ -479,6 +483,7 @@ fn build_code_mode_executors(
&enabled_tools, &enabled_tools,
&deferred_tools, &deferred_tools,
&namespace_descriptions, &namespace_descriptions,
default_exec_yield_time_ms,
tool_mode == ToolMode::CodeModeOnly, tool_mode == ToolMode::CodeModeOnly,
), ),
code_mode_nested_tool_specs, code_mode_nested_tool_specs,
@@ -1071,6 +1071,20 @@ async fn code_mode_only_exposes_code_executor_and_hides_nested_tools() {
); );
} }
#[tokio::test]
async fn code_mode_buffered_exec_updates_exec_description() {
let plan = probe(|turn| {
set_features(turn, &[Feature::CodeMode, Feature::CodeModeBufferedExec]);
})
.await;
let ToolSpec::Freeform(exec) = plan.visible_spec(codex_code_mode::PUBLIC_TOOL_NAME) else {
panic!("expected code mode exec tool");
};
assert!(exec.description.contains("Defaults to 30000 ms."));
assert!(!exec.description.contains("Defaults to 10000 ms."));
}
#[tokio::test] #[tokio::test]
async fn code_mode_only_exposes_configured_dynamic_namespace_directly() { async fn code_mode_only_exposes_configured_dynamic_namespace_directly() {
let plan = probe_with( let plan = probe_with(
+8
View File
@@ -92,6 +92,8 @@ pub enum Feature {
// Experimental // Experimental
/// Enable JavaScript code mode backed by the in-process V8 runtime. /// Enable JavaScript code mode backed by the in-process V8 runtime.
CodeMode, CodeMode,
/// Use a 30-second default yield timeout for code mode exec calls.
CodeModeBufferedExec,
/// Run JavaScript code mode in the standalone host process. /// Run JavaScript code mode in the standalone host process.
CodeModeHost, CodeModeHost,
/// Restrict model-visible tools to code mode entrypoints (`exec`, `wait`). /// Restrict model-visible tools to code mode entrypoints (`exec`, `wait`).
@@ -859,6 +861,12 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::UnderDevelopment, stage: Stage::UnderDevelopment,
default_enabled: false, default_enabled: false,
}, },
FeatureSpec {
id: Feature::CodeModeBufferedExec,
key: "code_mode_buffered_exec",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec { FeatureSpec {
id: Feature::CodeModeHost, id: Feature::CodeModeHost,
key: "code_mode_host", key: "code_mode_host",