feat: first pass on clb tool (#8930)

This commit is contained in:
jif-oai
2026-01-09 11:54:05 +00:00
committed by GitHub
parent 24d6e0114f
commit 568b938c80
5 changed files with 338 additions and 1 deletions

View File

@@ -5,6 +5,8 @@ use crate::features::Features;
use crate::tools::handlers::PLAN_TOOL;
use crate::tools::handlers::apply_patch::create_apply_patch_freeform_tool;
use crate::tools::handlers::apply_patch::create_apply_patch_json_tool;
use crate::tools::handlers::collab::DEFAULT_WAIT_TIMEOUT_MS;
use crate::tools::handlers::collab::MAX_WAIT_TIMEOUT_MS;
use crate::tools::registry::ToolRegistryBuilder;
use codex_protocol::openai_models::ApplyPatchToolType;
use codex_protocol::openai_models::ConfigShellToolType;
@@ -22,6 +24,7 @@ pub(crate) struct ToolsConfig {
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
pub web_search_request: bool,
pub web_search_cached: bool,
pub collab_tools: bool,
pub experimental_supported_tools: Vec<String>,
}
@@ -39,6 +42,7 @@ impl ToolsConfig {
let include_apply_patch_tool = features.enabled(Feature::ApplyPatchFreeform);
let include_web_search_request = features.enabled(Feature::WebSearchRequest);
let include_web_search_cached = features.enabled(Feature::WebSearchCached);
let include_collab_tools = features.enabled(Feature::Collab);
let shell_type = if !features.enabled(Feature::ShellTool) {
ConfigShellToolType::Disabled
@@ -70,6 +74,7 @@ impl ToolsConfig {
apply_patch_tool_type,
web_search_request: include_web_search_request,
web_search_cached: include_web_search_cached,
collab_tools: include_collab_tools,
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
}
}
@@ -416,6 +421,104 @@ fn create_view_image_tool() -> ToolSpec {
})
}
fn create_spawn_agent_tool() -> ToolSpec {
let mut properties = BTreeMap::new();
properties.insert(
"message".to_string(),
JsonSchema::String {
description: Some("Initial message to send to the new agent.".to_string()),
},
);
ToolSpec::Function(ResponsesApiTool {
name: "spawn_agent".to_string(),
description: "Spawn a new agent and return its id.".to_string(),
strict: false,
parameters: JsonSchema::Object {
properties,
required: Some(vec!["message".to_string()]),
additional_properties: Some(false.into()),
},
})
}
fn create_send_input_tool() -> ToolSpec {
let mut properties = BTreeMap::new();
properties.insert(
"id".to_string(),
JsonSchema::String {
description: Some("Identifier of the agent to message.".to_string()),
},
);
properties.insert(
"message".to_string(),
JsonSchema::String {
description: Some("Message to send to the agent.".to_string()),
},
);
ToolSpec::Function(ResponsesApiTool {
name: "send_input".to_string(),
description: "Send a message to an existing agent.".to_string(),
strict: false,
parameters: JsonSchema::Object {
properties,
required: Some(vec!["id".to_string(), "message".to_string()]),
additional_properties: Some(false.into()),
},
})
}
fn create_wait_tool() -> ToolSpec {
let mut properties = BTreeMap::new();
properties.insert(
"id".to_string(),
JsonSchema::String {
description: Some("Identifier of the agent to wait on.".to_string()),
},
);
properties.insert(
"timeout_ms".to_string(),
JsonSchema::Number {
description: Some(format!(
"Optional timeout in milliseconds. Defaults to {DEFAULT_WAIT_TIMEOUT_MS} and max {MAX_WAIT_TIMEOUT_MS}."
)),
},
);
ToolSpec::Function(ResponsesApiTool {
name: "wait".to_string(),
description: "Wait for an agent and return its status.".to_string(),
strict: false,
parameters: JsonSchema::Object {
properties,
required: Some(vec!["id".to_string()]),
additional_properties: Some(false.into()),
},
})
}
fn create_close_agent_tool() -> ToolSpec {
let mut properties = BTreeMap::new();
properties.insert(
"id".to_string(),
JsonSchema::String {
description: Some("Identifier of the agent to close.".to_string()),
},
);
ToolSpec::Function(ResponsesApiTool {
name: "close_agent".to_string(),
description: "Close an agent and return its last known status.".to_string(),
strict: false,
parameters: JsonSchema::Object {
properties,
required: Some(vec!["id".to_string()]),
additional_properties: Some(false.into()),
},
})
}
fn create_test_sync_tool() -> ToolSpec {
let mut properties = BTreeMap::new();
properties.insert(
@@ -981,6 +1084,7 @@ pub(crate) fn build_specs(
mcp_tools: Option<HashMap<String, mcp_types::Tool>>,
) -> ToolRegistryBuilder {
use crate::tools::handlers::ApplyPatchHandler;
use crate::tools::handlers::CollabHandler;
use crate::tools::handlers::GrepFilesHandler;
use crate::tools::handlers::ListDirHandler;
use crate::tools::handlers::McpHandler;
@@ -1107,6 +1211,18 @@ pub(crate) fn build_specs(
builder.push_spec_with_parallel_support(create_view_image_tool(), true);
builder.register_handler("view_image", view_image_handler);
if config.collab_tools {
let collab_handler = Arc::new(CollabHandler);
builder.push_spec(create_spawn_agent_tool());
builder.push_spec(create_send_input_tool());
builder.push_spec(create_wait_tool());
builder.push_spec(create_close_agent_tool());
builder.register_handler("spawn_agent", collab_handler.clone());
builder.register_handler("send_input", collab_handler.clone());
builder.register_handler("wait", collab_handler.clone());
builder.register_handler("close_agent", collab_handler);
}
if let Some(mcp_tools) = mcp_tools {
let mut entries: Vec<(String, mcp_types::Tool)> = mcp_tools.into_iter().collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
@@ -1286,6 +1402,23 @@ mod tests {
}
}
#[test]
fn test_build_specs_collab_tools_enabled() {
let config = test_config();
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::Collab);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
});
let (tools, _) = build_specs(&tools_config, None).build();
assert_contains_tool_names(
&tools,
&["spawn_agent", "send_input", "wait", "close_agent"],
);
}
fn assert_model_tools(model_slug: &str, features: &Features, expected_tools: &[&str]) {
let config = test_config();
let model_info = ModelsManager::construct_model_info_offline(model_slug, &config);