mirror of
https://github.com/openai/codex.git
synced 2026-06-02 03:11:59 +00:00
Compare commits
2 Commits
fcoury/mul
...
dev/sayan/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fe2e420ab | ||
|
|
7041a830d1 |
@@ -571,6 +571,12 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"namespaceDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -8245,6 +8245,12 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"namespaceDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -4614,6 +4614,12 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"namespaceDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -81,6 +81,12 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"namespaceDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
|
||||
export type DynamicToolSpec = { namespace?: string, name: string, description: string, inputSchema: JsonValue, deferLoading?: boolean, };
|
||||
export type DynamicToolSpec = { namespace?: string, namespaceDescription?: string, name: string, description: string, inputSchema: JsonValue, deferLoading?: boolean, };
|
||||
|
||||
@@ -3297,6 +3297,8 @@ fn dynamic_tool_response_serializes_text_and_image_content_items() {
|
||||
#[test]
|
||||
fn dynamic_tool_spec_deserializes_defer_loading() {
|
||||
let value = json!({
|
||||
"namespace": "tickets",
|
||||
"namespaceDescription": "Read and update tickets.",
|
||||
"name": "lookup_ticket",
|
||||
"description": "Fetch a ticket",
|
||||
"inputSchema": {
|
||||
@@ -3313,7 +3315,8 @@ fn dynamic_tool_spec_deserializes_defer_loading() {
|
||||
assert_eq!(
|
||||
actual,
|
||||
DynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace: Some("tickets".to_string()),
|
||||
namespace_description: Some("Read and update tickets.".to_string()),
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "Fetch a ticket".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
@@ -42,6 +42,8 @@ pub enum ThreadStartSource {
|
||||
pub struct DynamicToolSpec {
|
||||
#[ts(optional)]
|
||||
pub namespace: Option<String>,
|
||||
#[ts(optional)]
|
||||
pub namespace_description: Option<String>,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: JsonValue,
|
||||
@@ -53,6 +55,7 @@ pub struct DynamicToolSpec {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DynamicToolSpecDe {
|
||||
namespace: Option<String>,
|
||||
namespace_description: Option<String>,
|
||||
name: String,
|
||||
description: String,
|
||||
input_schema: JsonValue,
|
||||
@@ -67,6 +70,7 @@ impl<'de> Deserialize<'de> for DynamicToolSpec {
|
||||
{
|
||||
let DynamicToolSpecDe {
|
||||
namespace,
|
||||
namespace_description,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
@@ -76,6 +80,7 @@ impl<'de> Deserialize<'de> for DynamicToolSpec {
|
||||
|
||||
Ok(Self {
|
||||
namespace,
|
||||
namespace_description,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
|
||||
@@ -1407,6 +1407,7 @@ Dynamic tool identifiers follow the same constraints as Responses function tools
|
||||
- `name` must match `^[a-zA-Z0-9_-]+$` and be between 1 and 128 characters.
|
||||
- `namespace`, when present, must match `^[a-zA-Z0-9_-]+$` and be between 1 and 64 characters.
|
||||
- `namespace` must not collide with reserved Responses runtime namespaces such as `functions`, `multi_tool_use`, `file_search`, `web`, `browser`, `image_gen`, `computer`, `container`, `terminal`, `python`, `python_user_visible`, `api_tool`, `tool_search`, or `submodel_delegator`.
|
||||
- `namespaceDescription`, when present on a namespaced tool, is surfaced with that namespace and, for deferred tools, in `tool_search` discovery metadata. Without it, Codex generates a generic namespace description.
|
||||
|
||||
Each dynamic tool may set `deferLoading`. When omitted, it defaults to `false`. Set it to `true` to keep the tool registered and callable by runtime features such as `code_mode`, while excluding it from the model-facing tool list sent on ordinary turns. When `tool_search` is available, deferred dynamic tools are searchable and can be exposed by a matching search result.
|
||||
|
||||
|
||||
@@ -1079,6 +1079,7 @@ impl ThreadRequestProcessor {
|
||||
.into_iter()
|
||||
.map(|tool| CoreDynamicToolSpec {
|
||||
namespace: tool.namespace,
|
||||
namespace_description: tool.namespace_description,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
input_schema: tool.input_schema,
|
||||
|
||||
@@ -92,6 +92,7 @@ mod thread_processor_behavior_tests {
|
||||
fn validate_dynamic_tools_rejects_unsupported_input_schema() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({"type": "null"}),
|
||||
@@ -105,6 +106,7 @@ mod thread_processor_behavior_tests {
|
||||
fn validate_dynamic_tools_accepts_sanitizable_input_schema() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
// Missing `type` is common; core sanitizes these to a supported schema.
|
||||
@@ -118,6 +120,7 @@ mod thread_processor_behavior_tests {
|
||||
fn validate_dynamic_tools_accepts_nullable_field_schema() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -138,6 +141,7 @@ mod thread_processor_behavior_tests {
|
||||
let tools = vec![
|
||||
ApiDynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -149,6 +153,7 @@ mod thread_processor_behavior_tests {
|
||||
},
|
||||
ApiDynamicToolSpec {
|
||||
namespace: Some("other_app".to_string()),
|
||||
namespace_description: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -166,6 +171,7 @@ mod thread_processor_behavior_tests {
|
||||
fn validate_dynamic_tools_accepts_responses_compatible_identifiers() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("Codex-App_2".to_string()),
|
||||
namespace_description: None,
|
||||
name: "lookup-ticket_2".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -183,6 +189,7 @@ mod thread_processor_behavior_tests {
|
||||
let tools = vec![
|
||||
ApiDynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -194,6 +201,7 @@ mod thread_processor_behavior_tests {
|
||||
},
|
||||
ApiDynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -251,6 +259,7 @@ mod thread_processor_behavior_tests {
|
||||
fn validate_dynamic_tools_rejects_empty_namespace() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("".to_string()),
|
||||
namespace_description: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -269,6 +278,7 @@ mod thread_processor_behavior_tests {
|
||||
fn validate_dynamic_tools_rejects_reserved_namespace() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("mcp__server__".to_string()),
|
||||
namespace_description: None,
|
||||
name: "my_tool".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -287,6 +297,7 @@ mod thread_processor_behavior_tests {
|
||||
fn validate_dynamic_tools_rejects_name_not_supported_by_responses() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: "lookup.ticket".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -308,6 +319,7 @@ mod thread_processor_behavior_tests {
|
||||
fn validate_dynamic_tools_rejects_namespace_not_supported_by_responses() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("codex.app".to_string()),
|
||||
namespace_description: None,
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -330,6 +342,7 @@ mod thread_processor_behavior_tests {
|
||||
let long_name = "a".repeat(129);
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: long_name.clone(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -349,6 +362,7 @@ mod thread_processor_behavior_tests {
|
||||
let long_namespace = "a".repeat(65);
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some(long_namespace.clone()),
|
||||
namespace_description: None,
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -367,6 +381,7 @@ mod thread_processor_behavior_tests {
|
||||
fn validate_dynamic_tools_rejects_reserved_responses_namespace() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("functions".to_string()),
|
||||
namespace_description: None,
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
@@ -65,6 +65,7 @@ async fn thread_start_injects_dynamic_tools_into_model_requests() -> Result<()>
|
||||
});
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: "demo_tool".to_string(),
|
||||
description: "Demo dynamic tool".to_string(),
|
||||
input_schema: input_schema.clone(),
|
||||
@@ -139,6 +140,7 @@ async fn thread_start_keeps_hidden_dynamic_tools_out_of_model_requests() -> Resu
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: None,
|
||||
name: "hidden_tool".to_string(),
|
||||
description: "Hidden dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -211,6 +213,7 @@ async fn thread_start_rejects_hidden_dynamic_tools_without_namespace() -> Result
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: "hidden_tool".to_string(),
|
||||
description: "Hidden dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -251,6 +254,7 @@ async fn thread_start_rejects_dynamic_tools_not_supported_by_responses() -> Resu
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: Some("codex.app".to_string()),
|
||||
namespace_description: None,
|
||||
name: "lookup.ticket".to_string(),
|
||||
description: "Invalid dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -316,6 +320,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: Some(tool_namespace.to_string()),
|
||||
namespace_description: None,
|
||||
name: tool_name.to_string(),
|
||||
description: "Demo dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -491,6 +496,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: tool_name.to_string(),
|
||||
description: "Demo dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
@@ -128,6 +128,7 @@ async fn thread_unsubscribe_during_turn_keeps_turn_running() -> Result<()> {
|
||||
model: Some("mock-model".to_string()),
|
||||
dynamic_tools: Some(vec![DynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: tool_name.to_string(),
|
||||
description: "Deterministic wait tool".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
@@ -22,7 +22,6 @@ use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchSourceInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::default_namespace_description;
|
||||
use codex_tools::dynamic_tool_to_responses_api_tool;
|
||||
use serde_json::Value;
|
||||
use std::time::Instant;
|
||||
@@ -32,6 +31,7 @@ use tracing::warn;
|
||||
pub struct DynamicToolHandler {
|
||||
tool_name: ToolName,
|
||||
spec: ToolSpec,
|
||||
search_source_info: ToolSearchSourceInfo,
|
||||
exposure: ToolExposure,
|
||||
search_text: String,
|
||||
}
|
||||
@@ -40,17 +40,47 @@ impl DynamicToolHandler {
|
||||
pub fn new(tool: &DynamicToolSpec) -> Option<Self> {
|
||||
let tool_name = ToolName::new(tool.namespace.clone(), tool.name.clone());
|
||||
let output_tool = dynamic_tool_to_responses_api_tool(tool).ok()?;
|
||||
let spec = match tool.namespace.as_ref() {
|
||||
Some(namespace) => ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: namespace.clone(),
|
||||
description: default_namespace_description(namespace),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(output_tool)],
|
||||
}),
|
||||
None => ToolSpec::Function(output_tool),
|
||||
let (spec, search_source_info) = match tool.namespace.as_ref() {
|
||||
Some(namespace) => {
|
||||
let custom_description = tool
|
||||
.namespace_description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|description| !description.is_empty());
|
||||
let description = custom_description.unwrap_or_default().to_string();
|
||||
let search_source_info = custom_description.map_or_else(
|
||||
|| ToolSearchSourceInfo {
|
||||
name: "Dynamic tools".to_string(),
|
||||
description: Some(
|
||||
"Tools provided by the current Codex thread.".to_string(),
|
||||
),
|
||||
},
|
||||
|description| ToolSearchSourceInfo {
|
||||
name: namespace.clone(),
|
||||
description: Some(description.to_string()),
|
||||
},
|
||||
);
|
||||
(
|
||||
ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: namespace.clone(),
|
||||
description,
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(output_tool)],
|
||||
}),
|
||||
search_source_info,
|
||||
)
|
||||
}
|
||||
None => (
|
||||
ToolSpec::Function(output_tool),
|
||||
ToolSearchSourceInfo {
|
||||
name: "Dynamic tools".to_string(),
|
||||
description: Some("Tools provided by the current Codex thread.".to_string()),
|
||||
},
|
||||
),
|
||||
};
|
||||
Some(Self {
|
||||
tool_name,
|
||||
spec,
|
||||
search_source_info,
|
||||
exposure: if tool.defer_loading {
|
||||
ToolExposure::Deferred
|
||||
} else {
|
||||
@@ -131,10 +161,7 @@ impl CoreToolRuntime for DynamicToolHandler {
|
||||
ToolSearchInfo::from_spec(
|
||||
self.search_text.clone(),
|
||||
self.spec(),
|
||||
Some(ToolSearchSourceInfo {
|
||||
name: "Dynamic tools".to_string(),
|
||||
description: Some("Tools provided by the current Codex thread.".to_string()),
|
||||
}),
|
||||
Some(self.search_source_info.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -228,6 +255,14 @@ fn build_dynamic_search_text(tool: &DynamicToolSpec) -> String {
|
||||
];
|
||||
if let Some(namespace) = &tool.namespace {
|
||||
parts.push(namespace.clone());
|
||||
if let Some(namespace_description) = tool
|
||||
.namespace_description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|description| !description.is_empty())
|
||||
{
|
||||
parts.push(namespace_description.to_string());
|
||||
}
|
||||
}
|
||||
parts.extend(schema_properties);
|
||||
parts.join(" ")
|
||||
|
||||
@@ -7,6 +7,7 @@ use serde_json::json;
|
||||
fn search_info_uses_dynamic_tool_metadata_and_parameter_names() {
|
||||
let handler = DynamicToolHandler::new(&DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: Some("Create and update Codex automations.".to_string()),
|
||||
name: "automation_update".to_string(),
|
||||
description: "Create or update automations.".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -24,13 +25,13 @@ fn search_info_uses_dynamic_tool_metadata_and_parameter_names() {
|
||||
|
||||
assert_eq!(
|
||||
search_info.entry.search_text,
|
||||
"automation_update automation update Create or update automations. codex_app mode timezone"
|
||||
"automation_update automation update Create or update automations. codex_app Create and update Codex automations. mode timezone"
|
||||
);
|
||||
assert_eq!(
|
||||
search_info.source_info,
|
||||
Some(ToolSearchSourceInfo {
|
||||
name: "Dynamic tools".to_string(),
|
||||
description: Some("Tools provided by the current Codex thread.".to_string()),
|
||||
name: "codex_app".to_string(),
|
||||
description: Some("Create and update Codex automations.".to_string()),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,20 +149,35 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mixed_search_results_coalesce_mcp_namespaces() {
|
||||
let dynamic_tools = [DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "automation_update".to_string(),
|
||||
description: "Create, update, view, or delete recurring automations.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": { "type": "string" },
|
||||
},
|
||||
"required": ["mode"],
|
||||
"additionalProperties": false,
|
||||
}),
|
||||
defer_loading: true,
|
||||
}];
|
||||
let dynamic_tools = [
|
||||
DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: None,
|
||||
name: "automation_list".to_string(),
|
||||
description: "List recurring automations.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false,
|
||||
}),
|
||||
defer_loading: true,
|
||||
},
|
||||
DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: Some("Create and update Codex automations.".to_string()),
|
||||
name: "automation_update".to_string(),
|
||||
description: "Create, update, view, or delete recurring automations.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": { "type": "string" },
|
||||
},
|
||||
"required": ["mode"],
|
||||
"additionalProperties": false,
|
||||
}),
|
||||
defer_loading: true,
|
||||
},
|
||||
];
|
||||
let mcp_tools = [
|
||||
tool_info("calendar", "create_event", "Create events"),
|
||||
tool_info("calendar", "list_events", "List events"),
|
||||
@@ -186,6 +201,7 @@ mod tests {
|
||||
let results = [
|
||||
&handler.entries[0],
|
||||
&handler.entries[2],
|
||||
&handler.entries[3],
|
||||
&handler.entries[1],
|
||||
];
|
||||
|
||||
@@ -228,23 +244,37 @@ mod tests {
|
||||
}),
|
||||
LoadableToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "codex_app".to_string(),
|
||||
description: "Tools in the codex_app namespace.".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "automation_update".to_string(),
|
||||
description: "Create, update, view, or delete recurring automations."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: Some(true),
|
||||
parameters: codex_tools::JsonSchema::object(
|
||||
std::collections::BTreeMap::from([(
|
||||
"mode".to_string(),
|
||||
codex_tools::JsonSchema::string(/*description*/ None),
|
||||
)]),
|
||||
Some(vec!["mode".to_string()]),
|
||||
Some(false.into()),
|
||||
),
|
||||
output_schema: None,
|
||||
})],
|
||||
description: "Create and update Codex automations.".to_string(),
|
||||
tools: vec![
|
||||
ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "automation_list".to_string(),
|
||||
description: "List recurring automations.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: Some(true),
|
||||
parameters: codex_tools::JsonSchema::object(
|
||||
Default::default(),
|
||||
/*required*/ None,
|
||||
Some(false.into()),
|
||||
),
|
||||
output_schema: None,
|
||||
}),
|
||||
ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "automation_update".to_string(),
|
||||
description: "Create, update, view, or delete recurring automations."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: Some(true),
|
||||
parameters: codex_tools::JsonSchema::object(
|
||||
std::collections::BTreeMap::from([(
|
||||
"mode".to_string(),
|
||||
codex_tools::JsonSchema::string(/*description*/ None),
|
||||
)]),
|
||||
Some(vec!["mode".to_string()]),
|
||||
Some(false.into()),
|
||||
),
|
||||
output_schema: None,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -252,6 +252,7 @@ async fn specs_filter_deferred_dynamic_tools() -> anyhow::Result<()> {
|
||||
let dynamic_tools = vec![
|
||||
DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: None,
|
||||
name: hidden_tool.to_string(),
|
||||
description: "Hidden until discovered.".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -263,6 +264,7 @@ async fn specs_filter_deferred_dynamic_tools() -> anyhow::Result<()> {
|
||||
},
|
||||
DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: None,
|
||||
name: visible_tool.to_string(),
|
||||
description: "Visible immediately.".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
@@ -494,6 +494,11 @@ fn code_mode_namespace_descriptions(
|
||||
entry.description = namespace.description.clone();
|
||||
}
|
||||
}
|
||||
for namespace in namespace_descriptions.values_mut() {
|
||||
if namespace.description.trim().is_empty() {
|
||||
namespace.description = default_namespace_description(&namespace.name);
|
||||
}
|
||||
}
|
||||
namespace_descriptions
|
||||
}
|
||||
|
||||
|
||||
@@ -334,6 +334,7 @@ fn invalid_mcp_tool(server: &str, namespace: &str, name: &str) -> ToolInfo {
|
||||
fn dynamic_tool(namespace: Option<&str>, name: &str, defer_loading: bool) -> DynamicToolSpec {
|
||||
DynamicToolSpec {
|
||||
namespace: namespace.map(str::to_string),
|
||||
namespace_description: None,
|
||||
name: name.to_string(),
|
||||
description: format!("{name} dynamic tool"),
|
||||
input_schema: json!({
|
||||
@@ -702,6 +703,10 @@ async fn code_mode_only_exposes_code_executor_and_hides_nested_tools() {
|
||||
plain.namespace_function_names("codex_app"),
|
||||
&["lookup".to_string()]
|
||||
);
|
||||
let ToolSpec::Namespace(namespace) = plain.visible_spec("codex_app") else {
|
||||
panic!("expected namespaced dynamic tool");
|
||||
};
|
||||
assert_eq!(namespace.description, "Tools in the codex_app namespace.");
|
||||
plain.assert_visible_lacks(&[
|
||||
codex_code_mode::PUBLIC_TOOL_NAME,
|
||||
codex_code_mode::WAIT_TOOL_NAME,
|
||||
@@ -729,6 +734,40 @@ async fn code_mode_only_exposes_code_executor_and_hides_nested_tools() {
|
||||
code_mode_only.namespace_function_names("codex_app"),
|
||||
Vec::<String>::new().as_slice()
|
||||
);
|
||||
let ToolSpec::Freeform(exec) = code_mode_only.visible_spec(codex_code_mode::PUBLIC_TOOL_NAME)
|
||||
else {
|
||||
panic!("expected code mode executor");
|
||||
};
|
||||
assert!(
|
||||
exec.description
|
||||
.contains("## codex_app\nTools in the codex_app namespace.")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dynamic_namespaces_prefer_a_later_supplied_description() {
|
||||
let mut described_tool =
|
||||
dynamic_tool(Some("codex_app"), "update", /*defer_loading*/ false);
|
||||
described_tool.namespace_description = Some("Create and update Codex automations.".to_string());
|
||||
let plan = probe_with(
|
||||
|_| {},
|
||||
ToolPlanInputs {
|
||||
dynamic_tools: vec![
|
||||
dynamic_tool(Some("codex_app"), "list", /*defer_loading*/ false),
|
||||
described_tool,
|
||||
],
|
||||
..ToolPlanInputs::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let ToolSpec::Namespace(namespace) = plan.visible_spec("codex_app") else {
|
||||
panic!("expected namespaced dynamic tools");
|
||||
};
|
||||
assert_eq!(
|
||||
namespace.description,
|
||||
"Create and update Codex automations."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -2,7 +2,6 @@ use codex_tools::LoadableToolSpec;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolSearchSourceInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::default_namespace_description;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ToolSearchEntry {
|
||||
@@ -29,9 +28,6 @@ impl ToolSearchInfo {
|
||||
LoadableToolSpec::Function(tool)
|
||||
}
|
||||
ToolSpec::Namespace(mut namespace) => {
|
||||
if namespace.description.trim().is_empty() {
|
||||
namespace.description = default_namespace_description(&namespace.name);
|
||||
}
|
||||
for tool in &mut namespace.tools {
|
||||
let ResponsesApiNamespaceTool::Function(tool) = tool;
|
||||
tool.defer_loading = Some(true);
|
||||
|
||||
@@ -2928,6 +2928,7 @@ async fn code_mode_can_call_hidden_dynamic_tools() -> Result<()> {
|
||||
base_test.config.clone(),
|
||||
vec![DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: None,
|
||||
name: "hidden_dynamic_tool".to_string(),
|
||||
description: "A hidden dynamic tool.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
|
||||
@@ -1048,6 +1048,7 @@ async fn remote_compact_filters_deferred_dynamic_tools() -> Result<()> {
|
||||
let dynamic_tools = vec![
|
||||
DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: None,
|
||||
name: hidden_tool.to_string(),
|
||||
description: "Hidden until discovered.".to_string(),
|
||||
input_schema: input_schema.clone(),
|
||||
@@ -1055,6 +1056,7 @@ async fn remote_compact_filters_deferred_dynamic_tools() -> Result<()> {
|
||||
},
|
||||
DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: None,
|
||||
name: visible_tool.to_string(),
|
||||
description: "Visible immediately.".to_string(),
|
||||
input_schema,
|
||||
|
||||
@@ -788,6 +788,7 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
|
||||
let dynamic_call_id = "dyn-search-call-1";
|
||||
let tool_name = "automation_update";
|
||||
let tool_description = "Create, update, view, or delete recurring automations.";
|
||||
let namespace_description = "Create and update Codex automations.";
|
||||
let tool_args = json!({ "mode": "create" });
|
||||
let tool_call_arguments = serde_json::to_string(&tool_args)?;
|
||||
let mock = mount_sse_sequence(
|
||||
@@ -837,6 +838,7 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
|
||||
});
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: Some(namespace_description.to_string()),
|
||||
name: tool_name.to_string(),
|
||||
description: tool_description.to_string(),
|
||||
input_schema: input_schema.clone(),
|
||||
@@ -915,6 +917,11 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
|
||||
!first_request_tools.iter().any(|name| name == tool_name),
|
||||
"deferred dynamic tool should be hidden before search: {first_request_tools:?}"
|
||||
);
|
||||
assert!(
|
||||
tool_search_description(&first_request_body)
|
||||
.is_some_and(|description| description.contains(namespace_description)),
|
||||
"tool_search should advertise the dynamic namespace description"
|
||||
);
|
||||
|
||||
let tools = tool_search_output_tools(&requests[1], search_call_id);
|
||||
assert_eq!(
|
||||
@@ -922,7 +929,7 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
|
||||
vec![json!({
|
||||
"type": "namespace",
|
||||
"name": "codex_app",
|
||||
"description": "Tools in the codex_app namespace.",
|
||||
"description": namespace_description,
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"name": tool_name,
|
||||
@@ -1459,6 +1466,7 @@ async fn tool_search_matches_dynamic_tools_by_name_description_namespace_and_sch
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: Some("orbit_ops".to_string()),
|
||||
namespace_description: None,
|
||||
name: "quasar_ping_beacon".to_string(),
|
||||
description: "Trigger the saffron metronome workflow for reminder follow-ups.".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
@@ -105,6 +105,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
|
||||
let dynamic_tools = vec![
|
||||
DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
namespace_description: Some("Look up geographic data.".to_string()),
|
||||
name: "geo_lookup".to_string(),
|
||||
description: "lookup a city".to_string(),
|
||||
input_schema: json!({
|
||||
@@ -116,6 +117,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
|
||||
},
|
||||
DynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: "weather_lookup".to_string(),
|
||||
description: "lookup weather".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
@@ -10,6 +10,8 @@ use ts_rs::TS;
|
||||
pub struct DynamicToolSpec {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub namespace: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub namespace_description: Option<String>,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: JsonValue,
|
||||
@@ -51,6 +53,7 @@ pub enum DynamicToolCallOutputContentItem {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DynamicToolSpecDe {
|
||||
namespace: Option<String>,
|
||||
namespace_description: Option<String>,
|
||||
name: String,
|
||||
description: String,
|
||||
input_schema: JsonValue,
|
||||
@@ -65,6 +68,7 @@ impl<'de> Deserialize<'de> for DynamicToolSpec {
|
||||
{
|
||||
let DynamicToolSpecDe {
|
||||
namespace,
|
||||
namespace_description,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
@@ -74,6 +78,7 @@ impl<'de> Deserialize<'de> for DynamicToolSpec {
|
||||
|
||||
Ok(Self {
|
||||
namespace,
|
||||
namespace_description,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
@@ -92,6 +97,8 @@ mod tests {
|
||||
#[test]
|
||||
fn dynamic_tool_spec_deserializes_defer_loading() {
|
||||
let value = json!({
|
||||
"namespace": "tickets",
|
||||
"namespaceDescription": "Read and update tickets.",
|
||||
"name": "lookup_ticket",
|
||||
"description": "Fetch a ticket",
|
||||
"inputSchema": {
|
||||
@@ -108,7 +115,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
actual,
|
||||
DynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace: Some("tickets".to_string()),
|
||||
namespace_description: Some("Read and update tickets.".to_string()),
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "Fetch a ticket".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE thread_dynamic_tools
|
||||
ADD COLUMN namespace_description TEXT;
|
||||
@@ -81,7 +81,7 @@ WHERE id = ? AND preview = ''
|
||||
) -> anyhow::Result<Option<Vec<DynamicToolSpec>>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT namespace, name, description, input_schema, defer_loading
|
||||
SELECT namespace, namespace_description, name, description, input_schema, defer_loading
|
||||
FROM thread_dynamic_tools
|
||||
WHERE thread_id = ?
|
||||
ORDER BY position ASC
|
||||
@@ -99,6 +99,7 @@ ORDER BY position ASC
|
||||
let input_schema = serde_json::from_str::<Value>(input_schema.as_str())?;
|
||||
tools.push(DynamicToolSpec {
|
||||
namespace: row.try_get("namespace")?,
|
||||
namespace_description: row.try_get("namespace_description")?,
|
||||
name: row.try_get("name")?,
|
||||
description: row.try_get("description")?,
|
||||
input_schema,
|
||||
@@ -839,17 +840,19 @@ INSERT INTO thread_dynamic_tools (
|
||||
thread_id,
|
||||
position,
|
||||
namespace,
|
||||
namespace_description,
|
||||
name,
|
||||
description,
|
||||
input_schema,
|
||||
defer_loading
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(thread_id, position) DO NOTHING
|
||||
"#,
|
||||
)
|
||||
.bind(thread_id.as_str())
|
||||
.bind(position)
|
||||
.bind(tool.namespace.as_deref())
|
||||
.bind(tool.namespace_description.as_deref())
|
||||
.bind(tool.name.as_str())
|
||||
.bind(tool.description.as_str())
|
||||
.bind(input_schema)
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::collections::BTreeMap;
|
||||
fn parse_dynamic_tool_sanitizes_input_schema() {
|
||||
let tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "Fetch a ticket".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
@@ -41,6 +42,7 @@ fn parse_dynamic_tool_sanitizes_input_schema() {
|
||||
fn parse_dynamic_tool_preserves_defer_loading() {
|
||||
let tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "Fetch a ticket".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
|
||||
@@ -94,6 +94,11 @@ pub fn coalesce_loadable_tool_specs(
|
||||
LoadableToolSpec::Function(_) | LoadableToolSpec::Namespace(_) => None,
|
||||
})
|
||||
{
|
||||
if existing_namespace.description.trim().is_empty()
|
||||
&& !namespace.description.trim().is_empty()
|
||||
{
|
||||
existing_namespace.description = namespace.description;
|
||||
}
|
||||
existing_namespace.tools.append(&mut namespace.tools);
|
||||
} else {
|
||||
coalesced_specs.push(LoadableToolSpec::Namespace(namespace));
|
||||
@@ -101,6 +106,14 @@ pub fn coalesce_loadable_tool_specs(
|
||||
}
|
||||
}
|
||||
}
|
||||
for spec in &mut coalesced_specs {
|
||||
let LoadableToolSpec::Namespace(namespace) = spec else {
|
||||
continue;
|
||||
};
|
||||
if namespace.description.trim().is_empty() {
|
||||
namespace.description = default_namespace_description(&namespace.name);
|
||||
}
|
||||
}
|
||||
coalesced_specs
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ fn tool_definition_to_responses_api_tool_omits_false_defer_loading() {
|
||||
fn dynamic_tool_to_responses_api_tool_preserves_defer_loading() {
|
||||
let tool = DynamicToolSpec {
|
||||
namespace: None,
|
||||
namespace_description: None,
|
||||
name: "lookup_order".to_string(),
|
||||
description: "Look up an order".to_string(),
|
||||
input_schema: json!({
|
||||
|
||||
Reference in New Issue
Block a user