mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Append MCP instructions to deferred code-mode tools
This commit is contained in:
@@ -85,6 +85,7 @@ use codex_tools::request_user_input_available_modes;
|
||||
use codex_tools::shell_command_backend_for_features;
|
||||
use codex_tools::shell_type_for_model_and_features;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
@@ -426,6 +427,7 @@ fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName)
|
||||
fn build_code_mode_executors(
|
||||
turn_context: &TurnContext,
|
||||
executors: &[Arc<dyn CoreToolRuntime>],
|
||||
deferred_mcp_tools: Option<&[ToolInfo]>,
|
||||
) -> Vec<Arc<dyn CoreToolRuntime>> {
|
||||
if !matches!(
|
||||
turn_context.tool_mode,
|
||||
@@ -438,6 +440,17 @@ fn build_code_mode_executors(
|
||||
let mut exec_prompt_tool_specs = Vec::new();
|
||||
let mut deferred_tools_available = false;
|
||||
let deferred_tools_guidance_enabled = search_tool_enabled(turn_context);
|
||||
let deferred_mcp_instructions = deferred_mcp_tools
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter_map(|tool| {
|
||||
tool.namespace_description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|instructions| !instructions.is_empty())
|
||||
.map(|instructions| (tool.canonical_tool_name(), instructions))
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
for executor in executors {
|
||||
let exposure = executor.exposure();
|
||||
if exposure == ToolExposure::DirectModelOnly {
|
||||
@@ -452,9 +465,22 @@ fn build_code_mode_executors(
|
||||
continue;
|
||||
}
|
||||
|
||||
let spec = executor.spec();
|
||||
let mut spec = executor.spec();
|
||||
|
||||
if exposure == ToolExposure::Deferred {
|
||||
if let Some(instructions) = deferred_mcp_instructions.get(&executor.tool_name())
|
||||
&& let ToolSpec::Namespace(namespace) = &mut spec
|
||||
{
|
||||
for tool in &mut namespace.tools {
|
||||
let ResponsesApiNamespaceTool::Function(tool) = tool;
|
||||
if tool.description.trim().is_empty() {
|
||||
tool.description = (*instructions).to_string();
|
||||
} else {
|
||||
tool.description =
|
||||
format!("{}\n\n{instructions}", tool.description.trim_end());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only show deferred-tool guidance when supported and an included spec is usable by code mode.
|
||||
deferred_tools_available |= deferred_tools_guidance_enabled
|
||||
&& !collect_code_mode_exec_prompt_tool_definitions(std::iter::once(&spec))
|
||||
@@ -864,7 +890,11 @@ fn prepend_code_mode_executors(
|
||||
planned_tools: &mut PlannedTools,
|
||||
) {
|
||||
let turn_context = context.turn_context;
|
||||
let code_mode_executors = build_code_mode_executors(turn_context, planned_tools.runtimes());
|
||||
let code_mode_executors = build_code_mode_executors(
|
||||
turn_context,
|
||||
planned_tools.runtimes(),
|
||||
context.deferred_mcp_tools,
|
||||
);
|
||||
planned_tools.runtimes.splice(0..0, code_mode_executors);
|
||||
}
|
||||
|
||||
|
||||
@@ -330,8 +330,13 @@ async fn run_code_mode_turn_with_rmcp_model(
|
||||
model: &'static str,
|
||||
) -> Result<(TestCodex, ResponseMock)> {
|
||||
run_code_mode_turn_with_rmcp_config(
|
||||
server, prompt, code, model, /*code_mode_only*/ false,
|
||||
server,
|
||||
prompt,
|
||||
code,
|
||||
model,
|
||||
/*code_mode_only*/ false,
|
||||
/*non_prefixed_mcp_tool_names*/ false,
|
||||
McpToolLoading::Direct,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -349,10 +354,17 @@ async fn run_code_mode_turn_with_rmcp_mode(
|
||||
"test-gpt-5.1-codex",
|
||||
code_mode_only,
|
||||
/*non_prefixed_mcp_tool_names*/ false,
|
||||
McpToolLoading::Direct,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
enum McpToolLoading {
|
||||
Direct,
|
||||
Deferred,
|
||||
}
|
||||
|
||||
async fn run_code_mode_turn_with_rmcp_config(
|
||||
server: &MockServer,
|
||||
prompt: &str,
|
||||
@@ -360,6 +372,7 @@ async fn run_code_mode_turn_with_rmcp_config(
|
||||
model: &'static str,
|
||||
code_mode_only: bool,
|
||||
non_prefixed_mcp_tool_names: bool,
|
||||
tool_loading: McpToolLoading,
|
||||
) -> Result<(TestCodex, ResponseMock)> {
|
||||
let rmcp_test_server_bin = stdio_server_bin()?;
|
||||
let mut builder = test_codex().with_model(model).with_config(move |config| {
|
||||
@@ -371,6 +384,21 @@ async fn run_code_mode_turn_with_rmcp_config(
|
||||
if non_prefixed_mcp_tool_names {
|
||||
let _ = config.features.enable(Feature::NonPrefixedMcpToolNames);
|
||||
}
|
||||
if tool_loading == McpToolLoading::Deferred {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ToolSearchAlwaysDeferMcpTools)
|
||||
.expect("test config should allow feature update");
|
||||
let mut model_catalog = bundled_models_response()
|
||||
.unwrap_or_else(|err| panic!("bundled models.json should parse: {err}"));
|
||||
let model = model_catalog
|
||||
.models
|
||||
.iter_mut()
|
||||
.find(|model_info| model_info.slug == model)
|
||||
.unwrap_or_else(|| panic!("{model} exists in bundled models.json"));
|
||||
model.supports_search_tool = true;
|
||||
config.model_catalog = Some(model_catalog);
|
||||
}
|
||||
|
||||
let mut servers = config.mcp_servers.get().clone();
|
||||
servers.insert(
|
||||
@@ -3019,6 +3047,7 @@ text(JSON.stringify({
|
||||
"test-gpt-5.1-codex",
|
||||
/*code_mode_only*/ false,
|
||||
/*non_prefixed_mcp_tool_names*/ true,
|
||||
McpToolLoading::Direct,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -3299,6 +3328,60 @@ text(JSON.stringify(tool));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn code_mode_appends_server_instructions_to_deferred_mcp_tool_descriptions() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let code = r#"
|
||||
const tool = ALL_TOOLS.find(
|
||||
({ name }) => name === "mcp__rmcp__echo"
|
||||
);
|
||||
text(JSON.stringify(tool));
|
||||
"#;
|
||||
|
||||
let (_test, second_mock) = run_code_mode_turn_with_rmcp_config(
|
||||
&server,
|
||||
"use exec to inspect ALL_TOOLS",
|
||||
code,
|
||||
"gpt-5.4",
|
||||
/*code_mode_only*/ true,
|
||||
/*non_prefixed_mcp_tool_names*/ false,
|
||||
McpToolLoading::Deferred,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let req = second_mock.single_request();
|
||||
let (output, success) = custom_tool_output_body_and_success(&req, "call-1");
|
||||
assert_ne!(
|
||||
success,
|
||||
Some(false),
|
||||
"exec ALL_TOOLS MCP lookup failed unexpectedly: {output}"
|
||||
);
|
||||
|
||||
let parsed: Value = serde_json::from_str(
|
||||
&custom_tool_output_last_non_empty_text(&req, "call-1")
|
||||
.expect("exec ALL_TOOLS lookup should emit JSON"),
|
||||
)?;
|
||||
assert_eq!(
|
||||
parsed,
|
||||
serde_json::json!({
|
||||
"name": "mcp__rmcp__echo",
|
||||
"description": concat!(
|
||||
"Echo back the provided message and include environment data.\n\n",
|
||||
"Use these tools to exercise the rmcp test server.\n\n",
|
||||
"exec tool declaration:\n",
|
||||
"```ts\n",
|
||||
"declare const tools: { mcp__rmcp__echo(args: { env_var?: string; message: string; }): ",
|
||||
"Promise<CallToolResult<{ echo: string; env: string | null; }>>; };\n",
|
||||
"```",
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn code_mode_can_call_hidden_dynamic_tools() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
Reference in New Issue
Block a user