mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Expose selecte namespaces as direct model tools (#28825)
## Why Som tools, such as history and notes, must remain top-level when MCP deferral is enabled while staying unavailable through code-mode `exec`. ## What changed - Added `features.code_mode.direct_only_tool_namespaces`. - Classified matching MCP tools as `DirectModelOnly`. - Kept those tools top-level in `code_mode_only`. - Excluded them from `tool_search` deferral and the nested `exec` surface. - Updated the generated config schema. ## Validation - `code_mode_only_exposes_direct_model_only_mcp_namespaces` - `load_config_resolves_code_mode_config`
This commit is contained in:
committed by
GitHub
Unverified
parent
ac3fe64100
commit
78a9e169bb
@@ -362,6 +362,13 @@
|
||||
"CodeModeConfigToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"direct_only_tool_namespaces": {
|
||||
"description": "Exact tool namespaces to expose only as direct model tools. These tools bypass deferral, remain top-level in code-mode-only sessions, and are omitted from the nested code-mode tool surface.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -438,6 +438,7 @@ async fn load_config_resolves_code_mode_config() -> std::io::Result<()> {
|
||||
[features.code_mode]
|
||||
enabled = true
|
||||
excluded_tool_namespaces = ["mcp__codex_apps", "multi_agent_v1"]
|
||||
direct_only_tool_namespaces = ["mcp__history", "mcp__notes"]
|
||||
"#,
|
||||
)
|
||||
.expect("TOML deserialization should succeed");
|
||||
@@ -452,6 +453,10 @@ excluded_tool_namespaces = ["mcp__codex_apps", "multi_agent_v1"]
|
||||
config.code_mode.excluded_tool_namespaces,
|
||||
vec!["mcp__codex_apps".to_string(), "multi_agent_v1".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
config.code_mode.direct_only_tool_namespaces,
|
||||
vec!["mcp__history".to_string(), "mcp__notes".to_string()]
|
||||
);
|
||||
assert!(config.features.enabled(Feature::CodeMode));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1061,6 +1061,7 @@ pub struct Config {
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct CodeModeConfig {
|
||||
pub excluded_tool_namespaces: Vec<String>,
|
||||
pub direct_only_tool_namespaces: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
@@ -2404,6 +2405,10 @@ fn resolve_code_mode_config(config_toml: &ConfigToml) -> CodeModeConfig {
|
||||
.and_then(|config| config.excluded_tool_namespaces.as_ref())
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
direct_only_tool_namespaces: base
|
||||
.and_then(|config| config.direct_only_tool_namespaces.as_ref())
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,9 @@ impl DynamicToolHandler {
|
||||
namespace.map(|namespace| namespace.name.clone()),
|
||||
tool.name.clone(),
|
||||
);
|
||||
let output_tool = dynamic_tool_to_responses_api_tool(tool).ok()?;
|
||||
let mut output_tool = dynamic_tool_to_responses_api_tool(tool).ok()?;
|
||||
// Exposure controls deferral; tool search restores this marker for deferred results.
|
||||
output_tool.defer_loading = None;
|
||||
let spec = match namespace {
|
||||
Some(namespace) => ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: namespace.name.clone(),
|
||||
|
||||
@@ -192,11 +192,41 @@ fn build_tool_specs_and_registry(
|
||||
};
|
||||
let mut planned_tools = PlannedTools::default();
|
||||
add_tool_sources(&context, &mut planned_tools);
|
||||
apply_direct_model_only_namespace_overrides(turn_context, &mut planned_tools);
|
||||
append_tool_search_executor(&context, &mut planned_tools);
|
||||
prepend_code_mode_executors(&context, &mut planned_tools);
|
||||
build_model_visible_specs_and_registry(turn_context, planned_tools)
|
||||
}
|
||||
|
||||
fn apply_direct_model_only_namespace_overrides(
|
||||
turn_context: &TurnContext,
|
||||
planned_tools: &mut PlannedTools,
|
||||
) {
|
||||
for runtime in &mut planned_tools.runtimes {
|
||||
let configured = runtime
|
||||
.tool_name()
|
||||
.namespace
|
||||
.as_ref()
|
||||
.is_some_and(|namespace| {
|
||||
turn_context
|
||||
.config
|
||||
.code_mode
|
||||
.direct_only_tool_namespaces
|
||||
.contains(namespace)
|
||||
});
|
||||
match runtime.exposure() {
|
||||
ToolExposure::Direct | ToolExposure::Deferred if configured => {
|
||||
*runtime =
|
||||
override_tool_exposure(Arc::clone(runtime), ToolExposure::DirectModelOnly);
|
||||
}
|
||||
ToolExposure::Direct
|
||||
| ToolExposure::Deferred
|
||||
| ToolExposure::DirectModelOnly
|
||||
| ToolExposure::Hidden => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
fn build_model_visible_specs_and_registry(
|
||||
turn_context: &TurnContext,
|
||||
|
||||
@@ -1011,6 +1011,48 @@ async fn code_mode_only_exposes_code_executor_and_hides_nested_tools() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn code_mode_only_exposes_configured_dynamic_namespace_directly() {
|
||||
let plan = probe_with(
|
||||
|turn| {
|
||||
set_features(turn, &[Feature::CodeMode, Feature::CodeModeOnly]);
|
||||
turn.model_info.supports_search_tool = true;
|
||||
update_config(turn, |config| {
|
||||
config.code_mode.direct_only_tool_namespaces = vec!["direct_only".to_string()];
|
||||
});
|
||||
},
|
||||
ToolPlanInputs {
|
||||
dynamic_tools: vec![dynamic_tool(
|
||||
Some("direct_only"),
|
||||
"lookup",
|
||||
/*defer_loading*/ true,
|
||||
)],
|
||||
..ToolPlanInputs::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
plan.assert_visible_contains(&[
|
||||
codex_code_mode::PUBLIC_TOOL_NAME,
|
||||
codex_code_mode::WAIT_TOOL_NAME,
|
||||
"direct_only",
|
||||
]);
|
||||
plan.assert_visible_lacks(&["tool_search"]);
|
||||
assert_eq!(
|
||||
plan.exposure(&ToolName::namespaced("direct_only", "lookup").to_string()),
|
||||
ToolExposure::DirectModelOnly
|
||||
);
|
||||
let ToolSpec::Namespace(namespace) = plan.visible_spec("direct_only") else {
|
||||
panic!("expected direct-only namespace spec");
|
||||
};
|
||||
let ResponsesApiNamespaceTool::Function(tool) = &namespace.tools[0];
|
||||
assert_eq!(tool.defer_loading, None);
|
||||
let ToolSpec::Freeform(exec) = plan.visible_spec(codex_code_mode::PUBLIC_TOOL_NAME) else {
|
||||
panic!("expected code mode exec tool");
|
||||
};
|
||||
assert!(!exec.description.contains("direct_only_lookup(args:"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn excluded_deferred_namespaces_do_not_enable_nested_tool_guidance() {
|
||||
let plan = probe_with(
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
use anyhow::Result;
|
||||
use codex_features::Feature;
|
||||
use core_test_support::apps_test_server::AppsTestServer;
|
||||
use core_test_support::apps_test_server::SEARCH_CALENDAR_CREATE_TOOL;
|
||||
use core_test_support::apps_test_server::SEARCH_CALENDAR_NAMESPACE;
|
||||
use core_test_support::apps_test_server::search_capable_apps_builder;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::namespace_child_tool;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use serde_json::Value;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn code_mode_only_exposes_direct_model_only_mcp_namespaces() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let apps_server = AppsTestServer::mount_searchable(&server).await?;
|
||||
let response = responses::mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = search_capable_apps_builder(apps_server.chatgpt_base_url.clone())
|
||||
.with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodeModeOnly)
|
||||
.expect("test config should allow feature update");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ToolSearchAlwaysDeferMcpTools)
|
||||
.expect("test config should allow feature update");
|
||||
config.code_mode.direct_only_tool_namespaces =
|
||||
vec![SEARCH_CALENDAR_NAMESPACE.to_string()];
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
test.submit_turn("inspect directly exposed MCP tools")
|
||||
.await?;
|
||||
let body = response.single_request().body_json();
|
||||
let tools = body
|
||||
.get("tools")
|
||||
.and_then(Value::as_array)
|
||||
.expect("request should contain tools");
|
||||
|
||||
assert!(
|
||||
namespace_child_tool(
|
||||
&body,
|
||||
SEARCH_CALENDAR_NAMESPACE,
|
||||
SEARCH_CALENDAR_CREATE_TOOL,
|
||||
)
|
||||
.is_some(),
|
||||
"configured MCP namespace should remain top-level: {body}"
|
||||
);
|
||||
assert!(
|
||||
!tools.iter().any(|tool| {
|
||||
tool.get("name")
|
||||
.or_else(|| tool.get("type"))
|
||||
.and_then(Value::as_str)
|
||||
== Some("tool_search")
|
||||
}),
|
||||
"configured MCP namespace should not be deferred: {body}"
|
||||
);
|
||||
let exec_description = tools.iter().find_map(|tool| {
|
||||
(tool.get("name").and_then(Value::as_str) == Some("exec"))
|
||||
.then(|| tool.get("description").and_then(Value::as_str))
|
||||
.flatten()
|
||||
});
|
||||
assert!(
|
||||
exec_description.is_some_and(|description| {
|
||||
!description.contains("mcp__codex_apps__calendar_create_event(args:")
|
||||
}),
|
||||
"direct-model-only MCP namespace should not be available through exec: {body}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -65,6 +65,7 @@ mod image_rollout;
|
||||
mod items;
|
||||
mod json_result;
|
||||
mod live_cli;
|
||||
mod mcp_tool_exposure;
|
||||
mod mcp_turn_metadata;
|
||||
mod model_overrides;
|
||||
mod model_runtime_selectors;
|
||||
|
||||
@@ -12,6 +12,11 @@ pub struct CodeModeConfigToml {
|
||||
/// Exact tool namespaces to omit from the code-mode nested tool surface.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_tool_namespaces: Option<Vec<String>>,
|
||||
/// Exact tool namespaces to expose only as direct model tools.
|
||||
/// These tools bypass deferral, remain top-level in code-mode-only sessions, and are omitted
|
||||
/// from the nested code-mode tool surface.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub direct_only_tool_namespaces: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl FeatureConfig for CodeModeConfigToml {
|
||||
|
||||
Reference in New Issue
Block a user